ComunicWeb/assets/js/components/calls/window.js

1274 lines
27 KiB
JavaScript
Raw Permalink Normal View History

2020-04-10 11:18:26 +00:00
/**
* Calls window
*
* @author Pierre Hubert
*/
class CallWindow extends CustomEvents {
2020-04-10 11:18:26 +00:00
/**
* Create a new call window
*
* @param {Conversation} conv Information about the target conversation
*/
constructor(conv) {
super()
2020-04-11 12:18:27 +00:00
// Initialize variables
2020-04-10 14:07:05 +00:00
this.conv = conv;
2021-03-06 14:15:03 +00:00
this.callID = conv.id;
2020-04-13 12:27:11 +00:00
this.allowVideo = conv.can_have_video_call;
2020-04-11 12:18:27 +00:00
2020-04-11 12:59:48 +00:00
/** @type {Map<number, Peer>} */
this.peersEls = new Map()
2020-04-13 16:22:18 +00:00
/** @type {Map<number, SimplePeer>} */
this.streamsEls = new Map()
2020-04-11 12:18:27 +00:00
/** @type {Map<number, HTMLVideoElement>} */
this.videoEls = new Map()
/** @type {Map<number, AudioContext>} */
this.audioContexts = new Map()
2021-01-30 18:44:39 +00:00
this.blurBackground = false;
2020-04-11 12:18:27 +00:00
2020-04-10 11:18:26 +00:00
this.construct(conv);
}
async construct(conv) {
2020-04-10 14:07:05 +00:00
try {
// Check if calls target exists or not
if(!byId("callsTarget"))
createElem2({
appendTo: byId("wrapper"),
type: "div",
id: "callsTarget",
})
this.conv = conv;
this.rootEl = createElem2({
appendTo: byId("callsTarget"),
2020-04-10 11:18:26 +00:00
type: "div",
2020-04-10 14:07:05 +00:00
class: "call-window"
2020-04-10 11:18:26 +00:00
})
2020-04-13 10:11:38 +00:00
if(!this.conv.can_have_video_call)
this.rootEl.classList.add("audio-only")
2020-04-10 14:07:05 +00:00
// Construct head
this.windowHead = createElem2({
appendTo: this.rootEl,
type: "div",
class: "head",
innerHTML: "<i class='fa fa-phone'></i>" +
2020-04-13 12:55:22 +00:00
"<span class='title'>"+ await getConvName(conv) + "</span>" +
2020-04-10 14:07:05 +00:00
" <span class='pull-right'></span>"
})
2020-04-10 11:18:26 +00:00
2020-04-14 16:20:37 +00:00
// Add counter
this.timeEl = createElem2({
insertBefore: this.windowHead.querySelector(".pull-right"),
type: "span",
class: "time",
innerHTML: "00:00:00"
})
2020-04-10 14:07:05 +00:00
// Close button
this.closeButton = createElem2({
appendTo: this.windowHead.querySelector(".pull-right"),
type: "a",
innerHTML: "<i class='fa fa-times'></i>",
onclick: () => this.Close()
})
2020-04-10 11:18:26 +00:00
2020-04-14 16:20:37 +00:00
// Make counter lives
this.callDuration = 0;
const interval = setInterval(() => {
if(!this.timeEl.isConnected)
clearInterval(interval)
this.callDuration++;
this.timeEl.innerHTML = rpad(Math.floor(this.callDuration/3600), 2, 0) + ":"
+ rpad(Math.floor((this.callDuration/60)%60), 2, 0) + ":"
+ rpad(this.callDuration%60, 2, 0)
}, 1000);
2020-04-10 14:07:05 +00:00
this.makeWindowDraggable();
2020-04-10 14:55:31 +00:00
// Create members area
this.membersArea = createElem2({
appendTo: this.rootEl,
type: "div",
class: "members-area"
})
2020-04-14 17:06:15 +00:00
// Add message area
this.messageArea = createElem2({
appendTo: this.rootEl,
type: "div",
class: "messages-area"
})
2020-04-10 14:55:31 +00:00
2020-04-11 12:18:27 +00:00
// Create videos area
this.videosArea = createElem2({
appendTo: this.rootEl,
type: "div",
class: "videos-area"
})
2020-04-12 17:02:09 +00:00
2020-04-12 16:52:56 +00:00
// Contruct bottom area
const bottomArea = createElem2({
appendTo: this.rootEl,
type: "div",
class: "window-bottom"
})
2020-04-12 17:02:09 +00:00
/**
* @param {HTMLElement} btn
* @param {boolean} selected
*/
const setButtonSelected = (btn, selected) => {
if(selected)
btn.classList.add("selected")
else
btn.classList.remove("selected")
}
2020-04-12 16:52:56 +00:00
// Display the list of buttons
const buttonsList = [
2020-04-13 08:31:21 +00:00
// Toggle current user camera visibility
2020-04-13 08:25:39 +00:00
{
icon: "fa-eye",
2020-04-13 08:31:21 +00:00
selected: false,
label: "toggle-camera-visibility",
2020-04-13 12:27:11 +00:00
needVideo: true,
2020-04-13 08:25:39 +00:00
onclick: (btn) => {
setButtonSelected(btn, this.toggleMainStreamVisibility())
}
},
2020-04-13 06:41:23 +00:00
// Audio button
{
icon: "fa-microphone",
label: "mic",
selected: false,
onclick: () => {
this.toggleStream(false)
}
},
2020-04-12 16:52:56 +00:00
// Hang up button
{
icon: "fa-phone",
class: "hang-up-button",
selected: false,
onclick: () => {
this.Close(true)
}
2020-04-12 17:02:09 +00:00
},
2020-04-13 06:41:23 +00:00
// Video button
{
icon: "fa-video-camera",
label: "camera",
selected: false,
2020-04-13 12:27:11 +00:00
needVideo: true,
2020-04-13 06:41:23 +00:00
onclick: () => {
this.toggleStream(true)
}
},
2020-04-13 14:30:13 +00:00
// Submenu button
{
subMenu: true,
icon: "fa-ellipsis-v",
selected: true,
label: "submenu",
onclick: () => {}
},
]
// Sub-menu entries
const menuEntries = [
2020-04-13 14:54:07 +00:00
// Full screen button
2020-04-12 17:02:09 +00:00
{
icon: "fa-expand",
2020-04-13 14:30:13 +00:00
text: "Toggle fullscreen",
2020-04-13 12:27:11 +00:00
needVideo: true,
2020-04-13 14:30:13 +00:00
onclick: () => {
2020-04-12 17:02:09 +00:00
RequestFullScreen(this.rootEl);
}
2020-04-13 06:41:23 +00:00
},
2020-04-13 14:30:13 +00:00
2020-04-13 14:54:07 +00:00
// Share screen button
{
icon: "fa-tv",
text: "Share screen",
needVideo: true,
onclick: () => {
this.startStreaming(true, true)
}
},
2021-01-30 18:44:39 +00:00
// Blur background
{
icon: "fa-paint-brush",
text: "Toggle blur background",
needVideo: true,
onclick: () => {
this.toggleBlurBackground()
2021-01-30 18:44:39 +00:00
}
},
2020-04-13 14:54:07 +00:00
// Share camera button
{
icon: "fa-video-camera",
text: "Share webcam",
needVideo: true,
onclick: () => {
this.startStreaming(true, false)
}
},
2020-04-13 16:47:28 +00:00
// Record streams
{
icon: "fa-save",
text: "Start / Stop recording",
onclick: () => {
this.startRecording()
}
},
2020-04-13 17:12:43 +00:00
// Stop streaming
{
icon: "fa-stop",
text: "Stop streaming",
onclick: () => {
this.closeMainPeer()
}
}
2020-04-12 16:52:56 +00:00
]
//Add buttons
buttonsList.forEach((button) => {
2020-04-13 12:27:11 +00:00
if(button.needVideo && !this.allowVideo)
return;
2020-04-13 14:30:13 +00:00
2020-04-12 16:52:56 +00:00
const buttonEl = createElem2({
appendTo: bottomArea,
type: "div",
innerHTML: "<i class='fa " + button.icon + "'></i>"
});
2020-04-13 06:41:23 +00:00
buttonEl.setAttribute("data-label", button.label)
2020-04-12 16:52:56 +00:00
//Add button optionnal class
if(button.class)
buttonEl.classList.add(button.class);
buttonEl.addEventListener("click", () => {
button.onclick(buttonEl);
});
setButtonSelected(buttonEl, button.selected)
});
2020-04-10 14:55:31 +00:00
2020-04-13 06:41:23 +00:00
/**
* Refresh buttons state
*/
this.refreshButtonsState = () => {
// Microphone button
setButtonSelected(
bottomArea.querySelector("[data-label=\"mic\"]"),
this.mainStream && this.mainStream.getAudioTracks()[0].enabled
)
// Video button
2020-04-13 16:49:02 +00:00
if(this.allowVideo)
setButtonSelected(
bottomArea.querySelector("[data-label=\"camera\"]"),
this.mainStream && this.mainStream.getVideoTracks().length > 0 &&
this.mainStream.getVideoTracks()[0].enabled
)
2020-04-13 06:41:23 +00:00
}
2020-04-13 08:31:21 +00:00
this.on("localVideo", () => {
setButtonSelected(bottomArea.querySelector("[data-label=\"toggle-camera-visibility\"]"), true)
})
2020-04-12 17:02:09 +00:00
2020-04-13 14:30:13 +00:00
// Process sub menu
const menu = bottomArea.querySelector("[data-label=\"submenu\"]");
menu.classList.add("dropup");
const menuButton = menu.firstChild;
menuButton.classList.add("dropdown-toggle");
menuButton.setAttribute("data-toggle", "dropdown")
const menuEntriesTarget = createElem2({
appendTo: menu,
type: "ul",
class: "dropdown-menu"
})
// Parse list of menu entries
for(const entry of menuEntries) {
2020-04-13 14:48:58 +00:00
if(entry.needVideo && !this.allowVideo)
continue
2020-04-13 14:30:13 +00:00
const a = createElem2({
appendTo: menuEntriesTarget,
type: "li",
innerHTML: "<a></a>"
}).firstChild;
// Add icon
createElem2({
appendTo: a,
type: "i",
class: "fa " + entry.icon,
})
// Add label
a.innerHTML += entry.text
a.addEventListener("click", () => entry.onclick())
}
2020-04-13 09:18:04 +00:00
// Check for anchors
this.CheckNewTargetForWindow()
2020-04-12 17:02:09 +00:00
2020-04-10 14:07:05 +00:00
// Join the call
await ws("calls/join", {
2021-03-06 14:15:03 +00:00
convID: this.conv.id
2020-04-10 14:07:05 +00:00
})
2020-04-11 07:13:54 +00:00
// Get call configuration
this.callsConfig = await ws("calls/config");
2020-04-10 14:55:31 +00:00
// Get the list of members of the call
const currMembersList = await ws("calls/members", {
2021-03-06 14:15:03 +00:00
callID: this.conv.id
2020-04-10 14:55:31 +00:00
})
2020-04-12 16:06:29 +00:00
// Apply this list of user
for(const user of currMembersList)
await this.AddMember(user.userID)
2020-04-12 16:06:29 +00:00
2020-04-11 12:50:37 +00:00
// Start to connect to ready pears
for(const user of currMembersList)
if(user.userID != userID() && user.ready)
await this.PeerReady(user.userID)
2020-04-14 17:18:19 +00:00
// Show helper notice
this.on("closedMainPeer", () => {
// Show appropriate message
this.setMessage("Click on <i class='fa fa-microphone'></i> to start to share audio"+
2020-04-14 17:06:15 +00:00
(this.allowVideo ? " or on <i class='fa fa-video-camera'></i> to start sharing your camera" : "") + ".");
2020-04-14 17:18:19 +00:00
})
this.emitEvent("closedMainPeer")
2020-04-11 07:13:54 +00:00
2020-04-10 14:07:05 +00:00
} catch(e) {
console.error(e)
notify("Could not initialize call!", "danger");
}
2020-04-10 12:03:28 +00:00
}
2020-04-13 09:18:04 +00:00
/**
* Check if current call window can be applied somewhere on the screen
*/
CheckNewTargetForWindow() {
const target = byId("target-for-video-call-"+this.callID)
this.rootEl.remove()
if(target) {
target.appendChild(this.rootEl)
this.rootEl.classList.add("embedded")
}
else {
byId("callsTarget").appendChild(this.rootEl)
this.rootEl.classList.remove("embedded")
}
}
2020-04-12 16:38:41 +00:00
/**
* Check if this conversation window is open or not
*
* @returns {boolean}
*/
get isOpen() {
return this.rootEl.isConnected
}
2020-04-10 12:03:28 +00:00
/**
* Make the call window draggable
*/
makeWindowDraggable() {
const checkWindowMinPosition = () => {
if(window.innerHeight < this.rootEl.style.top.replace("px", ""))
this.rootEl.style.top = "0px";
if(window.innerWidth < this.rootEl.style.left.replace("px", ""))
this.rootEl.style.left = "0px";
if(this.rootEl.style.left.replace("px", "") < 0)
this.rootEl.style.left = "0px";
if(this.rootEl.style.top.replace("px", "") < 49)
this.rootEl.style.top = "50px";
}
//Enable dragging
{
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
this.windowHead.addEventListener("mousedown", (e) => {
e = e || window.event;
e.preventDefault();
//Check if the window is currently in full screen mode
if(IsFullScreen())
return;
//get the mouse cursor position at startup
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
});
const elementDrag = (e) => {
e = e || window.event;
e.preventDefault();
//Calculate new cursor position
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
//Set element new position
this.rootEl.style.top = (this.rootEl.offsetTop - pos2) + "px";
this.rootEl.style.left = (this.rootEl.offsetLeft - pos1) + "px";
checkWindowMinPosition();
}
const closeDragElement = () => {
//Stop moving when mouse button is released
document.onmouseup = null;
document.onmousemove = null;
}
}
window.addEventListener("resize", () => {
checkWindowMinPosition();
});
2020-04-10 11:18:26 +00:00
}
/**
* Close this window & cancel the call
*
* @param {boolean} propagate Set to true to propagate
* the event
*/
2020-04-10 14:07:05 +00:00
async Close(propagate = true) {
this.rootEl.remove();
2020-05-04 11:32:04 +00:00
// Stop recording
if(this.isRecording)
this.startRecording();
2020-04-10 14:07:05 +00:00
// Leave the call
2020-04-10 14:15:52 +00:00
if(UserWebSocket.IsConnected)
await ws("calls/leave", {
2021-03-06 14:15:03 +00:00
convID: this.conv.id
2020-04-10 14:15:52 +00:00
})
2020-04-10 14:07:05 +00:00
2020-04-11 12:50:37 +00:00
2020-04-12 13:31:32 +00:00
if(this.mainPeer) {
this.closeMainPeer()
2020-04-12 13:31:32 +00:00
}
2020-04-11 16:03:18 +00:00
// Destroy peer connections
for(const el of this.peersEls)
el[1].destroy()
2020-04-11 12:50:37 +00:00
if(propagate)
2020-04-10 11:57:43 +00:00
this.emitEvent("close");
}
2020-04-10 14:55:31 +00:00
2020-04-14 17:06:15 +00:00
/**
* Display a new message for the window
*
* @param {String} msg New message / null to remove
*/
setMessage(msg) {
if(msg == null) {
this.messageArea.style.display = "none"
}
else {
this.messageArea.style.display = "block";
this.messageArea.innerHTML = msg;
}
}
2020-04-10 14:55:31 +00:00
/**
* Add a member to this call
*
* @param {number} userID The ID of the target member
*/
async AddMember(userID) {
// Apply user information
const el = createElem2({
2020-04-10 14:55:31 +00:00
appendTo: this.membersArea,
type: "span",
innerHTML: (await user(userID)).fullName
});
el.setAttribute("data-call-member-name-id", userID)
}
2020-04-12 16:06:29 +00:00
/**
* Get the name element of a member
*
* @param {number} userID The ID of the user to get
* @return {HTMLElement|null}
*/
getMemberNameEl(userID) {
return this.membersArea.querySelector("[data-call-member-name-id=\""+userID+"\"]");
}
2020-04-13 07:26:15 +00:00
/**
* Remove the video element of a specific user
*
* @param {number} peerID Target peer ID
*/
removeVideoElement(peerID) {
const el = this.videoEls.get(peerID);
this.videoEls.delete(peerID)
2020-04-13 17:12:43 +00:00
if(el) {
el.pause()
el.parentNode.remove()
}
const ctx = this.audioContexts.get(peerID);
this.audioContexts.delete(peerID);
if (ctx) {
2021-01-23 19:46:31 +00:00
// The delay is here to ensure context has been initialized
// to make sure state update event is correctly propagated
setTimeout(() => ctx.close(), 100);
}
2020-04-13 07:26:15 +00:00
}
/**
2020-04-12 13:31:32 +00:00
* Remove a member connection
*
2020-04-12 13:31:32 +00:00
* @param {number} userID Target user ID
*/
2020-04-12 13:31:32 +00:00
async RemoveMemberConnection(userID) {
2020-04-12 16:06:29 +00:00
const el = this.getMemberNameEl(userID)
if(el)
el.classList.remove("ready")
2020-04-11 12:18:27 +00:00
// Remove video (if any)
if(this.videoEls.has(userID)) {
2020-04-13 07:26:15 +00:00
this.removeVideoElement(userID)
2020-04-11 12:18:27 +00:00
}
2020-04-11 12:59:48 +00:00
// Remove peer connection (if any)
if(this.peersEls.has(userID)) {
this.peersEls.get(userID).destroy()
this.peersEls.delete(userID)
}
2020-04-12 13:31:32 +00:00
2020-04-13 16:22:18 +00:00
// Remove associated stream
if(this.streamsEls.has(userID)) {
this.streamsEls.delete(userID)
}
2020-04-12 13:31:32 +00:00
}
/**
* Remove a user from a call
*
* @param {number} userID The ID of the target user
*/
async RemoveMember(userID) {
// Remove user name
2020-04-12 16:06:29 +00:00
const el = this.getMemberNameEl(userID)
2020-04-12 13:31:32 +00:00
if(el)
el.remove()
this.RemoveMemberConnection(userID);
2020-04-10 14:55:31 +00:00
}
2020-04-11 07:13:54 +00:00
/**
* Get call configuration
*/
callConfig() {
return {
iceServers: this.callsConfig.iceServers.map((e) => {return {urls: e}})
};
}
/**
* Toggle blur background mode
*/
toggleBlurBackground() {
this.blurBackground = !this.blurBackground;
// Check if background blur network is loaded
if(this.blurBackground)
notify("Please stop and start streaming again to apply modification!");
}
2020-04-13 06:41:23 +00:00
/**
* Toggle stream state
*
* @param {boolean} isVideo
*/
async toggleStream(isVideo) {
2020-04-14 17:06:15 +00:00
2020-04-13 06:41:23 +00:00
if(isVideo && !this.conv.can_have_video_call) {
2020-04-13 10:11:38 +00:00
notify("Video calls can not be done on this conversations!", "danger")
2020-04-13 06:41:23 +00:00
return;
}
const hasAudio = (this.mainPeer && !this.mainPeer.destroyed) === true;
const hasVideo = (this.mainPeer && !this.mainPeer.destroyed && this.mainStream && this.mainStream.getVideoTracks().length > 0) === true;
// Check if current stream is not enough
if(hasAudio && isVideo && !hasVideo) {
this.closeMainPeer()
}
2020-04-13 06:41:23 +00:00
// Check if we have to start stream or just to mute them
if(!hasAudio || (isVideo && !hasVideo)) {
try {
await this.startStreaming(isVideo)
} catch(e) {
notify("Could not start streaming ! (did you block access to your camera / microphone ?)", "danger")
console.error(e)
}
2020-04-13 06:41:23 +00:00
}
// Toggle mute
else {
// Video
if(isVideo) {
this.mainStream.getVideoTracks()[0].enabled = !this.mainStream.getVideoTracks()[0].enabled
}
// Audio
else {
this.mainStream.getAudioTracks()[0].enabled = !this.mainStream.getAudioTracks()[0].enabled
}
}
this.refreshButtonsState()
}
2020-04-13 08:25:39 +00:00
/**
* Toggle current peer stream visibility
*
* @return {boolean} New state
*/
toggleMainStreamVisibility() {
const el = this.videoEls.get(userID())
if(!el || el.nodeName !== "VIDEO")
2020-04-13 08:31:21 +00:00
return false;
2020-04-13 08:25:39 +00:00
// Show again element
if(el.parentNode.style.display == "none") {
el.parentNode.style.display = ""
return true
}
// Hide element
else {
el.parentNode.style.display = "none"
return false
}
}
2020-04-11 07:13:54 +00:00
/**
2020-04-12 15:45:10 +00:00
* Add audio / video stream to the user
2020-04-11 07:13:54 +00:00
*
2020-04-11 12:18:27 +00:00
* @param {number} peerID Remove peer ID
* @param {boolean} muted True to mute video
* @param {MediaStream} stream Target stream
2020-04-11 07:13:54 +00:00
*/
async applyStream(peerID, muted, stream) {
2021-01-30 19:23:55 +00:00
2020-04-12 16:06:29 +00:00
// Remove any previous video stream
if(this.videoEls.has(peerID)) {
2020-04-13 07:26:15 +00:00
this.removeVideoElement(peerID)
2020-04-12 16:06:29 +00:00
}
2020-04-13 07:47:21 +00:00
const isVideo = stream.getVideoTracks().length > 0;
2020-04-13 07:26:15 +00:00
const videoContainer = createElem2({
appendTo: this.videosArea,
type: "div",
2020-04-13 07:47:21 +00:00
class: isVideo ? "video" : undefined
2020-04-13 07:26:15 +00:00
})
// Apply video
2020-04-13 07:47:21 +00:00
const videoEl = document.createElement(isVideo ? "video" : "audio");
2020-04-13 07:26:15 +00:00
videoContainer.appendChild(videoEl)
2020-04-11 12:18:27 +00:00
videoEl.muted = muted;
2020-04-11 07:13:54 +00:00
2020-04-11 12:18:27 +00:00
videoEl.srcObject = stream
2020-04-14 06:47:47 +00:00
// Fix Chrome exception: DOMException: play() failed because the user didn't interact with the document first.
try {
await videoEl.play()
} catch(e) {
console.error("Caught play() error", e)
notify("Please click anywhere on the page to resume video call");
// Wait for user interaction before trying again
document.addEventListener("click", () => {
if(videoEl.isConnected)
videoEl.play()
}, {
once: true
})
}
// Request fullscreen on double click
videoEl.addEventListener("dblclick", () => {
RequestFullScreen(this.rootEl);
})
2020-04-14 06:47:47 +00:00
2020-04-11 12:18:27 +00:00
2021-01-23 19:46:31 +00:00
// Setup audio context to determine whether the person is talking or not
const audioContext = new AudioContext();
const gain_node = audioContext.createGain();
gain_node.connect(audioContext.destination);
2021-01-23 20:00:41 +00:00
// Prevent echo
gain_node.disconnect(0)
const script_processor_analysis_node = audioContext.createScriptProcessor(2048, 1, 1);
script_processor_analysis_node.connect(gain_node);
const microphone_stream = audioContext.createMediaStreamSource(stream);
microphone_stream.connect(gain_node)
const analyzer_node = audioContext.createAnalyser();
analyzer_node.smoothingTimeConstant = 0
2021-01-23 19:46:31 +00:00
analyzer_node.fftSize = 4096
analyzer_node.connect(script_processor_analysis_node);
microphone_stream.connect(analyzer_node)
const freq_data = new Uint8Array(analyzer_node.frequencyBinCount)
2021-01-23 19:56:07 +00:00
const memberEl = this.getMemberNameEl(peerID);
let callsCount = 0;
script_processor_analysis_node.onaudioprocess = (e) => {
// Do not update count each time
callsCount++;
if(callsCount < 5)
return;
callsCount = 0;
2021-01-23 19:56:07 +00:00
analyzer_node.getByteFrequencyData(freq_data);
let count = 0;
let sum = 0;
for(let val = 0; val < 50 && val < freq_data.length; val++)
{
sum += freq_data[val];
count++;
}
const avg = sum/count;
2021-01-23 19:56:07 +00:00
if(avg > 50)
{
2021-01-23 19:56:07 +00:00
memberEl.classList.add("talking")
videoEl.classList.add("talking")
}
2021-01-23 19:56:07 +00:00
else
{
2021-01-23 19:56:07 +00:00
memberEl.classList.remove("talking");
videoEl.classList.remove("talking")
}
2021-01-23 19:56:07 +00:00
}
2021-01-23 19:46:31 +00:00
audioContext.addEventListener("statechange", e => {
if (audioContext.state == "closed")
{
console.info("Release audio analysis ressources for peer " + peerID);
gain_node.disconnect();
script_processor_analysis_node.disconnect();
microphone_stream.disconnect();
analyzer_node.disconnect();
2021-01-23 19:56:07 +00:00
memberEl.classList.remove("talking")
videoEl.classList.remove("talking")
2021-01-23 19:46:31 +00:00
}
})
2020-04-11 12:18:27 +00:00
this.videoEls.set(peerID, videoEl)
this.audioContexts.set(peerID, audioContext)
2020-04-13 08:05:21 +00:00
if(isVideo) {
2020-04-13 08:31:21 +00:00
// Show user name
const userName = (await user(peerID)).fullName
videoEl.title = userName
}
2020-04-13 08:31:21 +00:00
if(isVideo && peerID == userID()) {
// Emit an event
this.emitEvent("localVideo")
}
2020-04-11 07:13:54 +00:00
}
2020-04-11 12:59:48 +00:00
/**
* Send a signal back to the proxy
*
* @param {Number} peerID Target peer ID
* @param {data} data The signal to send
*/
async SendSignal(peerID, data) {
const type = data.hasOwnProperty("sdp") ? "SDP" : "CANDIDATE";
await ws("calls/signal", {
callID: this.callID,
peerID: peerID,
type: type,
data: type == "SDP" ? JSON.stringify(data) : JSON.stringify(data.candidate)
})
}
2020-04-11 07:13:54 +00:00
/**
* Start to send this client audio & video
2020-04-13 06:41:23 +00:00
*
* @param {boolean} includeVideo
2020-04-13 14:48:58 +00:00
* @param {boolean} shareScreen
2020-04-11 07:13:54 +00:00
*/
2020-04-13 14:48:58 +00:00
async startStreaming(includeVideo, shareScreen = false) {
2020-04-11 07:13:54 +00:00
2020-04-13 14:58:43 +00:00
// Close any previous connection
2020-04-14 17:18:19 +00:00
await this.closeMainPeer();
2020-04-13 14:58:43 +00:00
2020-04-14 17:18:19 +00:00
this.setMessage(null)
2020-04-13 14:48:58 +00:00
2020-04-14 17:18:19 +00:00
let stream;
2020-04-13 14:48:58 +00:00
// Get user screen
if(includeVideo && shareScreen) {
stream = await requestUserScreen(true)
2020-04-13 14:51:09 +00:00
// Ask for audio separatly
2020-04-13 14:48:58 +00:00
const second_stream = await navigator.mediaDevices.getUserMedia({
audio: true
})
stream.addTrack(second_stream.getAudioTracks()[0])
}
// Use regular webcam
else {
// First, query user media
stream = await navigator.mediaDevices.getUserMedia({
2020-04-13 14:51:09 +00:00
video: this.conv.can_have_video_call && includeVideo,
2020-04-13 14:48:58 +00:00
audio: true,
})
}
2020-04-13 06:41:23 +00:00
this.mainStream = stream;
2020-04-11 07:13:54 +00:00
2020-04-14 17:18:19 +00:00
2020-04-13 06:59:11 +00:00
if(includeVideo)
stream.getVideoTracks()[0].applyConstraints({
width: {max: 320},
height: {max: 240},
frameRate: {max: 24}
})
2020-04-12 16:41:42 +00:00
// Check if the window was closed in the mean time
if(!this.isOpen)
return
2021-01-30 18:13:21 +00:00
2021-01-30 18:21:39 +00:00
// If streaming video stream, allow to blur background
if(includeVideo && this.blurBackground)
2021-01-30 18:13:21 +00:00
{
// Create capture
const videoTarget = document.createElement("video");
2021-01-30 18:21:39 +00:00
videoTarget.muted = true;
2021-01-30 18:13:21 +00:00
videoTarget.srcObject = stream;
videoTarget.play()
const canvasTarget = document.createElement("canvas");
2021-01-30 18:24:08 +00:00
2021-01-30 18:44:39 +00:00
// Mandatory to initialize context
const canvas = canvasTarget.getContext("2d");
// Wait for video to be ready
await new Promise((res, rej) => videoTarget.addEventListener("loadeddata", e => res(), {once: true}));
const videoTrack = this.mainStream.getVideoTracks()[0];
// Fix video & canvas size
2021-01-30 19:23:55 +00:00
videoTarget.width = 320;
videoTarget.height = 240;
2021-01-30 18:44:39 +00:00
canvasTarget.width = videoTarget.width;
canvasTarget.height = videoTarget.height;
// Process images
(async () => {
try {
2021-01-30 18:13:21 +00:00
while(videoTrack.readyState == "live")
{
if (this.blurBackground) {
// Load network if required
if (!this.backgroundDetectionNetwork)
{
2021-01-31 04:53:35 +00:00
await includeJS(ComunicConfig.assetsURL + "3rdparty/tfjs/tfjs-1.2.min.js");
await includeJS(ComunicConfig.assetsURL + "3rdparty/tensorflow-models/body-pix-2.0.js");
this.backgroundDetectionNetwork = await bodyPix.load({
multiplier: 0.75,
stride: 32,
quantBytes: 4
});
2021-02-12 15:57:49 +00:00
2021-01-30 18:44:39 +00:00
}
const segmentation = await this.backgroundDetectionNetwork.segmentPerson(videoTarget);
const backgroundBlurAmount = 6;
const edgeBlurAmount = 2;
const flipHorizontal = true;
bodyPix.drawBokehEffect(
canvasTarget, videoTarget, segmentation, backgroundBlurAmount,
edgeBlurAmount, flipHorizontal);
2021-02-12 15:57:49 +00:00
await new Promise((res, rej) => setTimeout(() => res(), 1));
}
else {
canvas.drawImage(videoTarget, 0, 0, videoTarget.width, videoTarget.height);
2021-01-30 19:16:50 +00:00
await new Promise((res, rej) => setTimeout(() => res(), 1000 / videoTrack.getSettings().frameRate));
2021-01-30 18:13:21 +00:00
}
2021-02-12 15:57:49 +00:00
2021-01-30 18:13:21 +00:00
}
}
catch(e)
{
console.error("Failure", e);
2021-01-30 19:28:29 +00:00
notify("Failed to process local video!", "error");
}
})();
2021-01-30 18:13:21 +00:00
2021-01-30 18:44:39 +00:00
stream = canvasTarget.captureStream(24);
2021-01-30 18:13:21 +00:00
stream.addTrack(this.mainStream.getAudioTracks()[0]);
}
2020-04-14 17:18:19 +00:00
2020-04-11 12:18:27 +00:00
// Show user video
await this.applyStream(userID(), true, stream)
2020-04-13 14:58:43 +00:00
this.refreshButtonsState()
2020-04-11 12:18:27 +00:00
2020-04-11 07:13:54 +00:00
this.mainPeer = new SimplePeer({
initiator: true,
trickle: true, // Allow exchange of multiple ice candidates
stream: stream,
config: this.callConfig()
})
2020-04-11 07:43:27 +00:00
// Forward signals
2020-04-11 07:13:54 +00:00
this.mainPeer.on("signal", data => {
2020-04-11 12:59:48 +00:00
this.SendSignal(userID(), data)
2020-04-11 07:13:54 +00:00
})
2020-04-11 07:43:27 +00:00
// Return errors
this.mainPeer.on("error", err => {
console.error("Peer error!", err);
notify("An error occured while trying to connect!", "danger", 5)
});
2020-04-11 12:05:29 +00:00
2020-04-11 12:30:16 +00:00
this.mainPeer.on("connect", () => {
console.info("Connected to remote peer!")
this.getMemberNameEl(userID()).classList.add("ready");
setTimeout(() => {
// Add a little delay before notifying other peers in order to let the tracks be received by the proxy
if(this.mainPeer && !this.mainPeer.destroyed)
ws("calls/mark_ready", {
callID: this.callID
})
}, 2000);
2020-04-11 12:30:16 +00:00
})
2020-04-11 12:05:29 +00:00
this.mainPeer.on("message", message => {
console.log("Message from remote peer: " + message);
});
this.mainPeer.on("stream", stream => {
console.log("mainPeer stream", stream)
alert("Stream on main peer!!!")
});
2020-04-12 13:31:32 +00:00
2020-04-12 16:06:29 +00:00
/*
DO NOT DO THIS !!! On configuration change it would close
the call window...
2020-04-12 13:31:32 +00:00
this.mainPeer.on("close", () => {
console.log("Connection to main peer was closed.")
if(this.mainPeer)
2020-04-12 14:16:42 +00:00
this.Close(false);
2020-04-12 16:06:29 +00:00
});*/
2020-04-11 12:05:29 +00:00
}
/**
* Close main peer connection
*/
async closeMainPeer() {
// Remove ready attribute
2021-01-23 19:48:33 +00:00
const memberEl = this.getMemberNameEl(userID());
if (memberEl)
memberEl.classList.remove("ready");
// Close peer connection
if(this.mainPeer) {
this.mainPeer.destroy();
delete this.mainPeer;
}
// Release user media
if(this.mainStream) {
this.mainStream.getTracks().forEach(e => e.stop())
delete this.mainStream
}
2020-04-13 17:12:43 +00:00
this.removeVideoElement(userID())
this.refreshButtonsState()
2020-04-14 07:20:57 +00:00
// Propagate information
try {
await ws("calls/stop_streaming", {
callID: this.callID
})
} catch(e) {
console.log("Failed to notify of streaming stop", e)
}
2020-04-14 17:18:19 +00:00
this.emitEvent("closedMainPeer")
}
2020-04-11 12:59:48 +00:00
/**
* Start to receive video from remote peer
*
* @param {number} peerID Target peer ID
*/
2020-04-11 12:50:37 +00:00
async PeerReady(peerID) {
2020-04-12 16:06:29 +00:00
// Remove any previous connection
if(this.peersEls.has(peerID)) {
this.peersEls.get(peerID).destroy()
}
// Mark the peer as ready
const el = this.getMemberNameEl(peerID)
if(el)
el.classList.add("ready")
2020-04-11 12:59:48 +00:00
const peer = new SimplePeer({
2020-04-11 16:21:20 +00:00
initiator: false,
2020-04-11 12:59:48 +00:00
trickle: true, // Allow exchange of multiple ice candidates
2020-04-11 14:34:05 +00:00
config: this.callConfig(),
2020-04-11 12:59:48 +00:00
})
2020-04-11 14:34:05 +00:00
this.peersEls.set(peerID, peer)
2020-04-11 12:59:48 +00:00
peer.on("signal", data => this.SendSignal(peerID, data))
peer.on("error", err => {
console.error("Peer error! (peer: " + peerID + ")", err);
notify("An error occured while trying to to a peer !", "danger", 5)
});
peer.on("connect", () => {
console.info("Connected to a remote peer ("+peerID+") !")
})
peer.on("message", message => {
console.log("Message from remote peer: " + message);
});
peer.on("stream", stream => {
2020-04-11 14:37:50 +00:00
console.log("Got remote peer stream", stream)
2020-04-12 16:06:29 +00:00
2020-04-13 16:22:18 +00:00
this.streamsEls.set(peerID, stream)
2020-04-13 07:47:21 +00:00
this.applyStream(peerID, false, stream)
2020-04-11 12:59:48 +00:00
});
2020-04-11 16:21:20 +00:00
2020-04-12 13:31:32 +00:00
peer.on("close", () => {
console.info("Connection to peer " + peerID + " closed");
this.RemoveMemberConnection(peerID)
})
2020-04-11 16:21:20 +00:00
// Request an offer from proxy
await ws("calls/request_offer", {
callID: this.callID,
peerID: peerID,
})
2020-04-11 12:50:37 +00:00
}
2020-04-11 12:05:29 +00:00
/**
* Handles new signals
*
* @param {Number} peerID Target peer ID
* @param {any} data Signal data
*/
NewSignal(peerID, data) {
2020-04-11 14:34:05 +00:00
if(peerID == userID()) {
2020-04-11 12:28:37 +00:00
if(this.mainPeer)
this.mainPeer.signal(data)
2020-04-11 14:34:05 +00:00
}
2020-04-11 12:59:48 +00:00
else if(this.peersEls.has(peerID)) {
this.peersEls.get(peerID).signal(data)
}
2020-04-11 12:05:29 +00:00
2020-04-11 07:13:54 +00:00
}
2020-04-13 16:47:28 +00:00
2020-05-04 11:32:04 +00:00
/**
* Check out whether we are currently recording video or not
*/
get isRecording() {
return this.hasOwnProperty("recorder");
}
2020-04-13 16:47:28 +00:00
/**
* Start / stop recording the streams
*/
startRecording() {
const onDataAvailable = blob => {
console.info("New record available", blob)
// = GET URL = const url = URL.createObjectURL(blob)
// Save file
saveAs(blob, new Date().getTime() + ".webm")
}
// Start recording
2020-05-04 11:32:04 +00:00
if(!this.isRecording) {
2020-04-13 16:47:28 +00:00
// Determine the list of streams to save
const streams = []
if(this.mainStream)
streams.push(this.mainStream)
this.streamsEls.forEach(v => streams.push(v))
// Create & start recorder
this.recorder = new MultiStreamRecorder(streams);
this.recorder.ondataavailable = onDataAvailable
this.recorder.start(30*60*1000); // Ask for save every 30min
2020-04-13 17:07:12 +00:00
// Add notice
this.recordLabel = createElem2({
2020-04-14 06:29:11 +00:00
insertBefore: this.videosArea,
type: "div",
2020-04-13 17:07:12 +00:00
class: "record-label",
innerHTML: "Recording"
});
createElem2({
appendTo: this.recordLabel,
type: "a",
innerHTML: "STOP",
onclick: () => this.startRecording()
})
2020-04-13 16:47:28 +00:00
}
// Stop recording
else {
this.recorder.stop(onDataAvailable)
delete this.recorder
2020-04-13 17:07:12 +00:00
// Remove notice
this.recordLabel.remove()
2020-04-13 16:47:28 +00:00
}
}
2020-04-10 11:18:26 +00:00
}