/** * Single call window management * * @author Pierre HUBERT */ ComunicWeb.components.calls.callWindow = { /** * Initialize a call * * @param {Object} info Information about the call to initialize */ initCall: function(info){ /** * Initialize call object */ var call = { info: info, /** * @type {String} */ localPeerID: undefined, /** * @type {Boolean} */ open: true, /** * @type {Boolean} */ stopped: false, window: {}, streams: {}, /** * @type {MediaStream} */ localStream: undefined, /** * @type {HTMLVideoElement} */ localStreamVideo: undefined, /** * @type {SignalExchangerClient} */ signalClient: undefined }; /** * Initialize utilities */ /** * Get member information based on call ID * * @param {String} id The ID of the peer to process * @return Information about the peer / empty object * if no peer found */ call.getMemberByCallID = function(id){ var memberInfo = undefined; call.info.members.forEach(function(member){ if(member.user_call_id == id) memberInfo = member; }); return memberInfo; } /** * Check out whether local stream audio is enabled or not * * @return {Boolean} TRUE if local stream is enabled / FALSE else */ call.isLocalAudioEnabled = function(){ //Default behaviour : local stream not enabled if(!call.localStream) return true; return call.localStream.getAudioTracks()[0].enabled; } /** * Set audio mute status of local stream * * @param {Boolean} enabled New enabled status */ call.setLocalAudioEnabled = function(enabled){ if(call.localStream) call.localStream.getAudioTracks()[0].enabled = enabled; } /** * Check if the local video is enabled or not * * @return {Boolean} TRUE to mute video / FALSE else */ call.isLocalVideoEnabled = function(){ //Default behaviour : video not enabled if(!call.localStream) return true; return call.localStream.getVideoTracks()[0].enabled; } /** * Update mute status of local video stream * * @param {Boolean} enabled New mute status */ call.setLocalVideoEnabled = function(enabled){ if(call.localStream) call.localStream.getVideoTracks()[0].enabled = enabled; } /** * Set local stream video visibility * * @param {Boolean} visible TRUE to make it visible / FALSE else */ call.setLocalStreamVisibility = function(visible){ if(call.localStreamVideo) call.localStreamVideo.style.display = visible ? "block" : "none"; } /** * Get local stream visibility * * @return {Boolean} TRUE if local stream is visible / FALSE else */ call.isLocalStreamVisible = function(){ if(!call.localStreamVideo) return true; return call.localStreamVideo.style.display !== "none"; } /** * We have to begin to draw conversation UI */ var callContainer = createElem2({ appendTo: byId("callsTarget") ? byId("callsTarget") : byId("wrapper"), //If call target is not found, add call in page wrapper type: "div", class: "call-window" }); call.window.container = callContainer; //Add toolbar call.window.toolbar = createElem2({ appendTo: callContainer, type: "div", class: "call-toolbar", innerHTML: "" }); //Call title call.window.title = createElem2({ appendTo: call.window.toolbar, type: "div", class: "call-title", innerHTML: "Loading..." }); /** * Update the title of the call */ call.setTitle = function(title){ call.window.title.innerHTML = title; } //Add close button call.window.closeButton = createElem2({ appendTo: call.window.toolbar, type: "button", class: "btn btn-box-tool close-btn", innerHTML: "" }); //Make close button lives call.close = function(){ //Avoid to call this several times if(call.stopped) return; call.open = false; call.stopped = true; callContainer.remove(); //Close sockets connections too ComunicWeb.components.calls.callWindow.stop(call); } call.window.closeButton.addEventListener("click", function(){ //Check if the call is in full screen mode if(IsFullScreen()) RequestFullScreen(null) else call.close(); }); //Get information about related conversation to get the name of the call ComunicWeb.components.conversations.utils.getNameForID(info.conversation_id, function(name){ if(!name) return notify("Could not get information about related conversation!", "danger"); call.setTitle(name); }); //Call box body call.window.body = createElem2({ appendTo: callContainer, type: "div", class: "call-window-body" }); //Call videos target call.window.videosTarget = createElem2({ appendTo: call.window.body, type: "div", class: "streams-target" }); /** * Create loading message area */ call.window.loadingMessageContainer = createElem2({ insertBefore: call.window.body.firstChild, type: "div", class: "loading-message-container", innerHTML: "" }); call.window.loadingMessageContent = createElem2({ appendTo: call.window.loadingMessageContainer, type: "div", class: "message", innerHTML: "Loading..." }); /** * Set loading message visiblity * * @param {Boolean} visible TRUE to make it visible / FALSE else */ call.setLoadingMessageVisibility = function(visible){ call.window.loadingMessageContainer.style.display = visible ? "flex" : "none"; } /** * Update call loading message * * @param {String} message The new message to show to the * users */ call.setLoadingMessage = function(message){ call.setLoadingMessageVisibility(true); call.window.loadingMessageContent.innerHTML = message; } //Call footer call.window.footer = createElem2({ appendTo: callContainer, type: "div", class: "call-footer" }); /** * This function is used to toggle selection state * of one of the call toolbar button * * @param {HTMLElement} btn Target button * @param {Boolean} selected Selection state of the button */ var togglButtonSelectedStatus = function(btn, selected){ if(!selected){ while(btn.className.includes(" selected")) btn.className = btn.className.replace(" selected", ""); } else if(selected && !btn.className.includes(" selected")) btn.className += " selected"; } var buttonsList = [ //Show current user camera { icon: "fa-eye", selected: true, onclick: function(btn){ call.setLocalStreamVisibility(!call.isLocalStreamVisible()); togglButtonSelectedStatus(btn, call.isLocalStreamVisible()); } }, //Mute button { icon: "fa-microphone", selected: true, onclick: function(btn){ call.setLocalAudioEnabled(!call.isLocalAudioEnabled()); togglButtonSelectedStatus(btn, call.isLocalAudioEnabled()); } }, //Hang up button { icon: "fa-phone", class: "hang-up-button", selected: false, onclick: function(){ call.close(); } }, //Stop video button { icon: "fa-video-camera", selected: true, onclick: function(btn){ call.setLocalVideoEnabled(!call.isLocalVideoEnabled()); togglButtonSelectedStatus(btn, call.isLocalVideoEnabled()); console.log(call); } }, //Full screen button { icon: "fa-expand", selected: false, onclick: function(btn){ RequestFullScreen(callContainer); setTimeout(function(){ togglButtonSelectedStatus(btn, IsFullScreen()); }, 1000); } } ]; //Add buttons buttonsList.forEach(function(button){ var buttonEl = createElem2({ appendTo: call.window.footer, type: "div", class: "call-option-button", innerHTML: "" }); //Add button optionnal class if(button.class) buttonEl.className += " " + button.class; buttonEl.addEventListener("click", function(){ button.onclick(buttonEl); }); togglButtonSelectedStatus(buttonEl, button.selected); }); /** * Make the call window draggable */ function checkWindowMinPosition(){ if(window.innerHeight < callContainer.style.top.replace("px", "")) callContainer.style.top = "0px"; if(window.innerWidth < callContainer.style.left.replace("px", "")) callContainer.style.left = "0px"; if(callContainer.style.left.replace("px", "") < 0) callContainer.style.left = "0px"; if(callContainer.style.top.replace("px", "") < 49) callContainer.style.top = "50px"; } //Enable dragging { var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; call.window.title.onmousedown = function(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; } function 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 callContainer.style.top = (callContainer.offsetTop - pos2) + "px"; callContainer.style.left = (callContainer.offsetLeft - pos1) + "px"; checkWindowMinPosition(); } function closeDragElement(){ //Stop moving when mouse button is released document.onmouseup = null; document.onmousemove = null; } } window.addEventListener("resize", function(){ checkWindowMinPosition(); }); //Load user media call.setLoadingMessage("Waiting for your microphone and camera..."); ComunicWeb.components.calls.userMedia.get().then(function(stream){ //Check if connection has already been closed if(!call.open) return; call.localStream = stream; //Initialize signaling server connection ComunicWeb.components.calls.callWindow.initializeConnectionToSignalingServer(call); //Add local stream to the list of visible stream call.localStreamVideo = ComunicWeb.components.calls.callWindow.addVideoStream(call, true, stream); //Mark as connecting call.setLoadingMessage("Connecting..."); return true; }).catch(function(e){ console.error("Get user media error: ", e); call.setLoadingMessageVisibility(false); return notify("Could not get your microphone and camera!", "danger"); }); /** * Start to automaticaly refresh information about the call */ var interval = setInterval(function(){ //Check if call is not visible anymore if(!callContainer.isConnected){ call.close(); return; } if(!call.open) return clearInterval(interval); ComunicWeb.components.calls.callWindow.refreshInfo(call); }, 4000); }, /** * Initialize connection to signaling server * * @param {Object} call Information about the call */ initializeConnectionToSignalingServer: function(call) { //Get current user call ID call.info.members.forEach(function(member){ if(member.userID == userID()) call.localPeerID = member.user_call_id; }); //Create client instance and connect to server var config = ComunicWeb.components.calls.controller.getConfig(); call.signalClient = new SignalExchangerClient( config.signal_server_name, config.signal_server_port, call.localPeerID, config.is_signal_server_secure ); /** * Error when connecting to signaling server */ call.signalClient.onError = function(){ call.setLoadingMessage("Could not connect to signaling server!"); call.open = false; }; /** * Connection to signaling server is not supposed to close */ call.signalClient.onClosed = function(){ call.setLoadingMessage("Connection to signaling server closed!"); call.open = false; } /** * A remote peer sent a ready notice */ call.signalClient.onReadyMessage = function(peerID){ ComunicWeb.components.calls.callWindow.readyToInitialize(call, peerID); } /** * A remote peer sent a signal */ call.signalClient.onSignal = function(signal, peerID){ ComunicWeb.components.calls.callWindow.receivedSignal(call, peerID, signal); } }, /** * Refresh at a regular interval information about the call * * @param {Object} call Call Root object */ refreshInfo: function(call){ ComunicWeb.components.calls.interface.getInfo(call.info.id, function(result){ if(result.error) return notify("Could not get information about the call!", "danger"); call.info = result; ComunicWeb.components.calls.callWindow.gotNewCallInfo(call); }); }, /** * This method get called each time information about the call * are refreshed on the server * * @param {Object} call Information about the call */ gotNewCallInfo: function(call) { //Check if we are connected to signaling server and we have got local //streams if(!call.signalClient || !call.signalClient.isConnected() || !call.localStream) return; //Check if all other members rejected call var allDisconnected = ComunicWeb.components.calls.utils.hasEveryoneLeft(call.info); //Check if all call peer rejected the call if(allDisconnected){ call.setLoadingMessage("Conversation terminated."); setTimeout(function(){ call.close(); }, 5000); return; } //Process the connection to each accepted member call.info.members.forEach(function(member){ //Ignores all not accepted connection if(member.userID == userID() || member.status !== "accepted") return; //Check if we have not peer information if(!call.streams.hasOwnProperty("peer-" + member.userID)){ //If the ID of the current user is bigger than the remote //peer user ID, we wait a ready signal from him if(member.userID < userID()) return; //Else we have to create peer ComunicWeb.components.calls.callWindow.createPeerConnection(call, member, false); } }); }, /** * Create a peer connection * * @param {Object} call Information about the peer * @param {Object} member Information about the member * @param {Boolean} isInitiator Specify whether current user is the * initiator of the connection or not */ createPeerConnection: function(call, member, isInitiator){ var peerConnection = { peer: undefined }; call.streams["peer-" + member.userID] = peerConnection; //Get call configuration var config = ComunicWeb.components.calls.controller.getConfig(); //Create peer var peer = new SimplePeer({ initiator: isInitiator, stream: call.localStream, trickle: true, config: { 'iceServers': [ { urls: config.stun_server }, {"url": config.turn_server, "credential": config.turn_username, "username": config.turn_password} ] } }); peerConnection.peer = peer; //Add a function to remove connection peerConnection.removePeerConnection = function(){ peer.destroy(); delete call.streams["peer-" + member.userID]; if(peerConnection.video) peerConnection.video.remove(); } peer.on("error", function(err){ console.error("Peer error !", err, member); peerConnection.removePeerConnection(); }); peer.on("signal", function(data){ console.log('SIGNAL', JSON.stringify(data)); call.signalClient.sendSignal(member.user_call_id, JSON.stringify(data)); }); peer.on("message", function(message){ console.log("Message from remote peer: " + message); }); peer.on("close", function(){ peerConnection.removePeerConnection(); }); peer.on("stream", function(stream){ ComunicWeb.components.calls.callWindow.streamAvailable(call, member, stream); }); //If this peer does not initialize connection, inform other peer we are ready if(!isInitiator) call.signalClient.sendReadyMessage(member.user_call_id); }, /** * This method is called when a remote peers notify it is ready to * establish connection * * @param {Object} call Information about the call * @param {String} peerID Remote peer ID */ readyToInitialize: function(call, peerID){ var member = call.getMemberByCallID(peerID); if(member == undefined) return; //It the user with the smallest ID who send the ready message //else it would mess everything up if(member.userID > userID()) return; this.createPeerConnection(call, member, true); }, /** * This method is called when we received a remote signal * * @param {Object} call Information about the call * @param {String} peerID Remote peer ID * @param {String} signal Received signal */ receivedSignal: function(call, peerID, signal){ console.log("Received signal from " + peerID, signal); var member = call.getMemberByCallID(peerID); if(member == undefined) return; //Check we have got peer information if(!call.streams.hasOwnProperty("peer-" + member.userID)) return; call.streams["peer-" + member.userID].peer.signal(JSON.parse(signal)); }, /** * This method is called when a remote stream becomes available * * @param {Object} call Information about remote call * @param {String} member Information about target member * @param {MediaStream} stream Remote stream available */ streamAvailable: function(call, member, stream){ call.setLoadingMessageVisibility(false); call.streams["peer-" + member.userID].stream = stream; call.streams["peer-" + member.userID].video = this.addVideoStream(call, false, stream); }, /** * Create and set a video object for a stream * * @param {Object} call Target call * @param {Boolean} muted Specify whether audio should be muted * or not * @param {MediaStream} stream Target stream * @return {HTMLVideoElement} Generated video element */ addVideoStream: function(call, muted, stream){ /** * @type {HTMLVideoElement} */ var video = createElem2({ appendTo: call.window.videosTarget, type: "video" }); video.muted = muted; //Set target video object and play it video.srcObject = stream; video.play(); return video; }, /** * Stop the ongoing call * * @param {Object} call Information about the call */ stop: function(call){ //Remove the call from the opened list ComunicWeb.components.calls.currentList.removeCallFromList(call.info.id); //Close the connection to the server if(call.signalClient && call.signalClient.isConnected()) call.signalClient.close(); //Close all socket connections for (var key in call.streams) { if (call.streams.hasOwnProperty(key)) { var element = call.streams[key]; element.removePeerConnection(); } } //Close local stream if(call.localStream){ call.localStream.getTracks().forEach(function(track){ track.stop(); }); } //Notify server ComunicWeb.components.calls.interface.hangUp(call.info.id, function(){}); } }