2020-04-10 11:18:26 +00:00
|
|
|
/**
|
|
|
|
* Calls window
|
|
|
|
*
|
|
|
|
* @author Pierre Hubert
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2020-04-10 11:51:36 +00:00
|
|
|
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) {
|
2020-04-10 11:51:36 +00:00
|
|
|
super()
|
2020-04-11 12:18:27 +00:00
|
|
|
|
|
|
|
// Initialize variables
|
2020-04-10 14:07:05 +00:00
|
|
|
this.conv = conv;
|
2020-04-11 07:13:54 +00:00
|
|
|
this.callID = conv.ID;
|
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-11 12:18:27 +00:00
|
|
|
/** @type {Map<number, HTMLVideoElement>} */
|
|
|
|
this.videoEls = new Map()
|
|
|
|
|
|
|
|
|
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-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>" +
|
|
|
|
await getConvName(conv) +
|
|
|
|
" <span class='pull-right'></span>"
|
|
|
|
})
|
2020-04-10 11:18:26 +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-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-11 12:18:27 +00:00
|
|
|
// Create videos area
|
|
|
|
this.videosArea = createElem2({
|
|
|
|
appendTo: this.rootEl,
|
|
|
|
type: "div",
|
|
|
|
class: "videos-area"
|
|
|
|
})
|
|
|
|
|
2020-04-10 14:55:31 +00:00
|
|
|
|
2020-04-10 14:07:05 +00:00
|
|
|
// Join the call
|
|
|
|
await ws("calls/join", {
|
|
|
|
convID: this.conv.ID
|
|
|
|
})
|
2020-04-10 11:51:36 +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", {
|
|
|
|
callID: this.conv.ID
|
|
|
|
})
|
|
|
|
|
2020-04-12 16:06:29 +00:00
|
|
|
// Apply this list of user
|
|
|
|
for(const user of currMembersList)
|
|
|
|
if(user.userID != userID())
|
|
|
|
await this.AddMember(user.userID)
|
|
|
|
|
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-11 07:13:54 +00:00
|
|
|
// Start to stream audio & video
|
|
|
|
await this.startStreaming();
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
}
|
|
|
|
|
2020-04-10 11:51:36 +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) {
|
2020-04-10 11:51:36 +00:00
|
|
|
this.rootEl.remove();
|
|
|
|
|
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", {
|
|
|
|
convID: this.conv.ID
|
|
|
|
})
|
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) {
|
2020-04-11 12:50:37 +00:00
|
|
|
this.mainPeer.destroy();
|
2020-04-12 13:31:32 +00:00
|
|
|
delete this.mainPeer;
|
|
|
|
}
|
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
|
|
|
|
2020-04-10 11:51:36 +00:00
|
|
|
if(propagate)
|
2020-04-10 11:57:43 +00:00
|
|
|
this.emitEvent("close");
|
2020-04-10 11:51:36 +00:00
|
|
|
}
|
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
|
2020-04-10 15:09:40 +00:00
|
|
|
const el = createElem2({
|
2020-04-10 14:55:31 +00:00
|
|
|
appendTo: this.membersArea,
|
|
|
|
type: "span",
|
|
|
|
innerHTML: (await user(userID)).fullName
|
|
|
|
});
|
2020-04-10 15:09:40 +00:00
|
|
|
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-10 15:09:40 +00:00
|
|
|
/**
|
2020-04-12 13:31:32 +00:00
|
|
|
* Remove a member connection
|
2020-04-10 15:09:40 +00:00
|
|
|
*
|
2020-04-12 13:31:32 +00:00
|
|
|
* @param {number} userID Target user ID
|
2020-04-10 15:09:40 +00:00
|
|
|
*/
|
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)) {
|
|
|
|
const el = this.videoEls.get(userID);
|
|
|
|
this.videoEls.delete(userID)
|
|
|
|
|
|
|
|
el.pause()
|
|
|
|
el.remove()
|
|
|
|
}
|
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
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}})
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
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
|
|
|
*/
|
2020-04-11 12:18:27 +00:00
|
|
|
addVideoStream(peerID, muted, stream) {
|
2020-04-12 15:45:10 +00:00
|
|
|
|
2020-04-12 16:06:29 +00:00
|
|
|
// Remove any previous video stream
|
|
|
|
if(this.videoEls.has(peerID)) {
|
|
|
|
this.videoEls.get(peerID).remove()
|
|
|
|
}
|
|
|
|
|
2020-04-12 15:45:10 +00:00
|
|
|
const videoEl = document.createElement(stream.getVideoTracks().length > 0 ? "video" : "audio");
|
2020-04-11 12:18:27 +00:00
|
|
|
this.videosArea.appendChild(videoEl)
|
|
|
|
|
|
|
|
videoEl.muted = muted;
|
2020-04-11 07:13:54 +00:00
|
|
|
|
2020-04-11 12:18:27 +00:00
|
|
|
videoEl.srcObject = stream
|
|
|
|
videoEl.play()
|
|
|
|
|
|
|
|
this.videoEls.set(peerID, videoEl)
|
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
|
|
|
|
*/
|
|
|
|
async startStreaming() {
|
|
|
|
|
|
|
|
// First, query user media
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
2020-04-12 15:45:10 +00:00
|
|
|
video: this.conv.can_have_video_call,
|
2020-04-11 07:13:54 +00:00
|
|
|
audio: true
|
|
|
|
})
|
|
|
|
|
2020-04-11 12:18:27 +00:00
|
|
|
// Show user video
|
|
|
|
this.addVideoStream(userID(), true, stream)
|
|
|
|
|
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!")
|
|
|
|
ws("calls/mark_ready", {
|
|
|
|
callID: this.callID
|
|
|
|
})
|
|
|
|
})
|
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
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2020-04-12 16:07:26 +00:00
|
|
|
// 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-11 14:37:50 +00:00
|
|
|
this.addVideoStream(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-12 13:31:32 +00:00
|
|
|
|
|
|
|
console.log(peer)
|
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-10 11:18:26 +00:00
|
|
|
}
|