ComunicWeb/assets/js/components/calls/callWindow.js

830 lines
19 KiB
JavaScript
Raw Normal View History

2019-01-24 13:40:36 +00:00
/**
* 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,
2019-01-25 14:24:32 +00:00
/**
* @type {String}
*/
localPeerID: undefined,
/**
* @type {Boolean}
*/
2019-01-24 13:40:36 +00:00
open: true,
2019-01-25 14:24:32 +00:00
2019-01-24 13:50:03 +00:00
window: {},
2019-01-25 14:24:32 +00:00
streams: {},
2019-01-26 08:52:40 +00:00
/**
* @type {MediaStream}
*/
localStream: undefined,
2019-01-26 10:40:39 +00:00
/**
* @type {HTMLVideoElement}
*/
localStreamVideo: undefined,
2019-01-25 14:24:32 +00:00
/**
* @type {SignalExchangerClient}
*/
signalClient: undefined
2019-01-24 13:40:36 +00:00
};
2019-01-25 17:30:01 +00:00
/**
* Initialize utilities
*/
2019-01-26 08:52:40 +00:00
2019-01-25 17:30:01 +00:00
/**
* 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;
}
2019-01-26 08:52:40 +00:00
/**
* 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;
}
2019-01-26 10:40:39 +00:00
/**
* 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";
}
2019-01-25 17:30:01 +00:00
/**
* We have to begin to draw conversation UI
*/
2019-01-24 13:40:36 +00:00
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: "<i class='fa fa-phone'>"
});
//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: "<i class='fa fa-times'></i>"
});
2019-01-24 13:50:03 +00:00
//Make close button lives
call.close = function(){
call.open = false;
callContainer.remove();
2019-01-25 14:24:32 +00:00
//Close sockets connections too
ComunicWeb.components.calls.callWindow.stop(call);
2019-01-24 13:50:03 +00:00
}
call.window.closeButton.addEventListener("click", function(){
2019-01-26 10:11:25 +00:00
//Check if the call is in full screen mode
if(IsFullScreen())
RequestFullScreen(null)
else
call.close();
2019-01-24 13:50:03 +00:00
});
2019-01-24 13:40:36 +00:00
//Get information about related conversation to get the name of the call
2019-01-25 09:13:30 +00:00
ComunicWeb.components.conversations.utils.getNameForID(info.conversation_id, function(name){
2019-01-24 13:40:36 +00:00
2019-01-25 09:13:30 +00:00
if(!name)
2019-01-24 13:40:36 +00:00
return notify("Could not get information about related conversation!", "danger");
2019-01-25 09:13:30 +00:00
call.setTitle(name);
2019-01-24 13:40:36 +00:00
2019-01-25 09:13:30 +00:00
});
2019-01-24 13:40:36 +00:00
2019-01-25 14:34:40 +00:00
//Call box body
call.window.body = createElem2({
appendTo: callContainer,
type: "div",
class: "call-window-body"
});
2019-01-25 17:49:50 +00:00
//Call videos target
call.window.videosTarget = createElem2({
appendTo: call.window.body,
type: "div",
class: "streams-target"
});
2019-01-25 14:34:40 +00:00
2019-01-25 17:30:01 +00:00
2019-01-26 08:52:40 +00:00
2019-01-25 14:34:40 +00:00
/**
* Create loading message area
*/
call.window.loadingMessageContainer = createElem2({
2019-01-25 18:03:00 +00:00
insertBefore: call.window.body.firstChild,
2019-01-25 14:34:40 +00:00
type: "div",
class: "loading-message-container",
innerHTML: "<i class='fa fa-clock-o'></i>"
});
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){
2019-01-25 17:30:01 +00:00
call.setLoadingMessageVisibility(true);
2019-01-25 14:34:40 +00:00
call.window.loadingMessageContent.innerHTML = message;
}
2019-01-26 08:52:40 +00:00
//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 = [
2019-01-26 10:40:39 +00:00
//Show current user camera
{
icon: "fa-eye",
selected: true,
onclick: function(btn){
call.setLocalStreamVisibility(!call.isLocalStreamVisible());
togglButtonSelectedStatus(btn, call.isLocalStreamVisible());
}
},
2019-01-26 08:52:40 +00:00
//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);
}
2019-01-26 09:57:58 +00:00
},
//Full screen button
{
icon: "fa-expand",
selected: false,
onclick: function(btn){
RequestFullScreen(callContainer);
setTimeout(function(){
togglButtonSelectedStatus(btn, IsFullScreen());
}, 1000);
}
2019-01-26 08:52:40 +00:00
}
];
//Add buttons
buttonsList.forEach(function(button){
var buttonEl = createElem2({
appendTo: call.window.footer,
type: "div",
class: "call-option-button",
innerHTML: "<i class='fa " + button.icon + "'></i>"
});
//Add button optionnal class
if(button.class)
buttonEl.className += " " + button.class;
buttonEl.addEventListener("click", function(){
button.onclick(buttonEl);
});
togglButtonSelectedStatus(buttonEl, button.selected);
});
2019-01-26 09:23:57 +00:00
/**
* Make the call window draggable
*/
2019-01-26 10:11:25 +00:00
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
2019-01-26 09:23:57 +00:00
{
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
call.window.title.onmousedown = function(e){
e = e || window.event;
e.preventDefault();
2019-01-26 10:11:25 +00:00
//Check if the window is currently in full screen mode
if(IsFullScreen())
return;
2019-01-26 09:23:57 +00:00
//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";
2019-01-26 10:11:25 +00:00
checkWindowMinPosition();
2019-01-26 09:23:57 +00:00
}
function closeDragElement(){
//Stop moving when mouse button is released
document.onmouseup = null;
document.onmousemove = null;
}
}
2019-01-26 10:11:25 +00:00
window.addEventListener("resize", function(){
checkWindowMinPosition();
});
2019-01-26 09:23:57 +00:00
2019-01-24 13:40:36 +00:00
//Load user media
call.setLoadingMessage("Waiting for your microphone and camera...");
2019-01-24 13:50:03 +00:00
2019-01-25 17:30:01 +00:00
ComunicWeb.components.calls.userMedia.get().then(function(stream){
//Check if connection has already been closed
if(!call.open)
return;
2019-01-24 13:50:03 +00:00
2019-01-25 17:49:50 +00:00
call.localStream = stream;
2019-01-24 13:50:03 +00:00
2019-01-25 17:30:01 +00:00
//Initialize signaling server connection
ComunicWeb.components.calls.callWindow.initializeConnectionToSignalingServer(call);
2019-01-25 14:24:32 +00:00
2019-01-25 18:03:00 +00:00
//Add local stream to the list of visible stream
2019-01-26 10:40:39 +00:00
call.localStreamVideo = ComunicWeb.components.calls.callWindow.addVideoStream(call, true, stream);
2019-01-25 18:03:00 +00:00
//Mark as connecting
call.setLoadingMessage("Connecting...");
2019-01-25 14:24:32 +00:00
return true;
2019-01-25 17:30:01 +00:00
}).catch(function(e){
2019-01-24 13:50:03 +00:00
console.error("Get user media error: ", e);
call.setLoadingMessageVisibility(false);
return notify("Could not get your microphone and camera!", "danger");
});
2019-01-24 13:40:36 +00:00
2019-01-25 17:30:01 +00:00
/**
* Start to automaticaly refresh information about the call
*/
var interval = setInterval(function(){
2019-01-26 10:53:43 +00:00
//Check if call is not visible anymore
if(!callContainer.isConnected){
call.close();
return;
}
2019-01-25 17:30:01 +00:00
if(!call.open)
return clearInterval(interval);
ComunicWeb.components.calls.callWindow.refreshInfo(call);
2019-01-25 14:24:32 +00:00
2019-01-25 17:30:01 +00:00
}, 4000);
},
/**
* Initialize connection to signaling server
*
* @param {Object} call Information about the call
*/
initializeConnectionToSignalingServer: function(call) {
2019-01-25 14:24:32 +00:00
//Get current user call ID
call.info.members.forEach(function(member){
if(member.userID == userID())
call.localPeerID = member.user_call_id;
});
2019-01-25 17:30:01 +00:00
//Create client instance and connect to server
2019-01-25 14:24:32 +00:00
var config = ComunicWeb.components.calls.controller.getConfig();
call.signalClient = new SignalExchangerClient(
config.signal_server_name,
config.signal_server_port,
call.localPeerID
);
2019-01-25 17:30:01 +00:00
/**
* 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);
}
2019-01-25 14:24:32 +00:00
},
/**
* 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) {
2019-01-25 17:30:01 +00:00
//Check if we are connected to signaling server and we have got local
//streams
2019-01-25 17:49:50 +00:00
if(!call.signalClient || !call.signalClient.isConnected() || !call.localStream)
2019-01-25 14:24:32 +00:00
return;
//Check if all other members rejected call
var allDisconnected = true;
2019-01-25 14:24:32 +00:00
call.info.members.forEach(function(member){
if(member.status != "rejected" && member.status != "hang_up" && member.userID != userID())
allDisconnected = false;
2019-01-25 14:24:32 +00:00
});
//Check if all call peer rejected the call
if(allDisconnected){
call.setLoadingMessage("Conversation terminated.");
2019-01-25 14:24:32 +00:00
setTimeout(function(){
call.close();
}, 5000);
2019-01-25 14:24:32 +00:00
return;
}
2019-01-25 17:30:01 +00:00
//Process the connection to each accepted member
call.info.members.forEach(function(member){
//Ignores all not accepted connection
2019-01-25 18:18:12 +00:00
if(member.userID == userID() || member.status !== "accepted")
2019-01-25 17:30:01 +00:00
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,
2019-01-25 17:49:50 +00:00
stream: call.localStream,
2019-01-25 17:30:01 +00:00
trickle: false,
config: {
'iceServers': [
{ urls: config.stun_server },
{"url": config.turn_server,
"credential": config.turn_username,
"username": config.turn_password}
]
}
});
peerConnection.peer = peer;
2019-01-25 17:49:50 +00:00
//Add a function to remove connection
peerConnection.removePeerConnection = function(){
2019-01-25 17:30:01 +00:00
peer.destroy();
delete call.streams["peer-" + member.userID];
2019-01-25 17:49:50 +00:00
if(peerConnection.video)
peerConnection.video.remove();
2019-01-25 17:30:01 +00:00
}
peer.on("error", function(err){
console.error("Peer error !", err, member);
2019-01-25 17:49:50 +00:00
peerConnection.removePeerConnection();
2019-01-25 17:30:01 +00:00
});
peer.on("signal", function(data){
console.log('SIGNAL', JSON.stringify(data));
call.signalClient.sendSignal(member.user_call_id, JSON.stringify(data));
});
peer.on("message", message => {
console.log("Message from remote peer: " + message);
});
peer.on("close", function(){
2019-01-25 17:49:50 +00:00
peerConnection.removePeerConnection();
2019-01-25 17:30:01 +00:00
});
peer.on("stream", function(stream){
2019-01-25 17:49:50 +00:00
ComunicWeb.components.calls.callWindow.streamAvailable(call, member, stream);
2019-01-25 17:30:01 +00:00
});
//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));
},
/**
2019-01-25 17:49:50 +00:00
* This method is called when a remote stream becomes available
2019-01-25 17:30:01 +00:00
*
* @param {Object} call Information about remote call
2019-01-25 17:49:50 +00:00
* @param {String} member Information about target member
* @param {MediaStream} stream Remote stream available
2019-01-25 17:30:01 +00:00
*/
2019-01-25 17:49:50 +00:00
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);
2019-01-25 17:30:01 +00:00
2019-01-25 14:24:32 +00:00
},
2019-01-25 17:49:50 +00:00
/**
* 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}
*/
let 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;
},
2019-01-25 14:24:32 +00:00
/**
* 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
2019-01-26 07:15:17 +00:00
if(call.signalClient && call.signalClient.isConnected())
2019-01-25 14:24:32 +00:00
call.signalClient.close();
2019-01-25 17:49:50 +00:00
//Close all socket connections
for (var key in call.streams) {
if (call.streams.hasOwnProperty(key)) {
var element = call.streams[key];
element.removePeerConnection();
}
}
2019-01-26 10:53:43 +00:00
//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(){});
2019-01-25 14:24:32 +00:00
}
2019-01-24 13:40:36 +00:00
}