// Vars var isMuted; var videoIsPaused; const browserName = getBrowserName(); const url = window.location.href; const roomHash = url.substring(url.lastIndexOf("/") + 1).toLowerCase(); var mode = "camera"; // var isFullscreen = false; var sendingCaptions = false; var receivingCaptions = false; const isWebRTCSupported = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || window.RTCPeerConnection; // Element vars const chatInput = document.querySelector(".compose input"); const remoteVideoVanilla = document.getElementById("remote-video"); const remoteVideo = $("#remote-video"); const remoteVideosWrapper = $("#wrapper"); const captionText = $("#remote-video-text"); const localVideoText = $("#local-video-text"); const captionButtontext = $("#caption-button-text"); const entireChat = $("#entire-chat"); const chatZone = $("#chat-zone"); // Need a Map to keep track of dataChannel connecting with each peer var dataChannel = new Map(); var VideoChat = { videoEnabled: true, audioEnabled: true, connected: new Map(), localICECandidates: {}, socket: io(), remoteVideoWrapper: document.getElementById("wrapper"), localVideo: document.getElementById("local-video"), peerConnections: new Map(), recognition: undefined, borderColor: undefined, peerColors: new Map(), // Call to getUserMedia (provided by adapter.js for cross browser compatibility) // asking for access to both the video and audio streams. If the request is // accepted callback to the onMediaStream function, otherwise callback to the // noMediaStream function. requestMediaStream: function (event) { logIt("requestMediaStream"); rePositionLocalVideo(); navigator.mediaDevices .getUserMedia({ video: true, audio: true, }) .then((stream) => { VideoChat.onMediaStream(stream); localVideoText.text("Drag Me"); setTimeout(() => localVideoText.fadeOut(), 5000); }) .catch((error) => { logIt(error); logIt( "Failed to get local webcam video, check webcam privacy settings" ); // Keep trying to get user media setTimeout(VideoChat.requestMediaStream, 1000); }); }, // Called when a video stream is added to VideoChat onMediaStream: function (stream) { logIt("onMediaStream"); VideoChat.localStream = stream; // Add the stream as video's srcObject. // Now that we have webcam video sorted, prompt user to share URL Snackbar.show({ text: "Here is the join link for your call: " + url, actionText: "Copy Link", width: "750px", pos: "top-center", actionTextColor: "#616161", duration: 500000, backgroundColor: "#16171a", onActionClick: function (element) { // Copy url to clipboard, this is achieved by creating a temporary element, // adding the text we want to that element, selecting it, then deleting it var copyContent = window.location.href; $('') .val(copyContent) .appendTo("body") .select(); document.execCommand("copy"); var toRemove = document.querySelector("#some-element"); toRemove.parentNode.removeChild(toRemove); Snackbar.close(); }, }); VideoChat.localVideo.srcObject = stream; // Now we're ready to join the chat room. VideoChat.socket.emit("join", roomHash, function() { VideoChat.borderColor = hueToColor(uuidToHue(VideoChat.socket.id)); VideoChat.localVideo.style.border = `3px solid ${VideoChat.borderColor}`; }); // Add listeners to the websocket VideoChat.socket.on("leave", VideoChat.onLeave); VideoChat.socket.on("full", chatRoomFull); VideoChat.socket.on("offer", VideoChat.onOffer); VideoChat.socket.on("willInitiateCall", VideoChat.call); // Set up listeners on the socket VideoChat.socket.on("candidate", VideoChat.onCandidate); VideoChat.socket.on("answer", VideoChat.onAnswer); VideoChat.socket.on("requestToggleCaptions", () => toggleSendCaptions()); VideoChat.socket.on("recieveCaptions", (captions) => recieveCaptions(captions) ); }, call: function (uuid, room) { logIt(`call >>> Initiating call with ${uuid}...`); VideoChat.socket.on( "token", VideoChat.establishConnection(uuid, function (a) { VideoChat.createOffer(a); }) ); VideoChat.socket.emit("token", roomHash, uuid); }, onLeave: function(uuid) { logIt("disconnected - UUID " + uuid); // Remove video element VideoChat.remoteVideoWrapper.removeChild( document.querySelectorAll(`[uuid="${uuid}"]`)[0] ); // Delete connection & metadata VideoChat.connected.delete(uuid); VideoChat.peerConnections.get(uuid).close(); // This is necessary, because otherwise the RTC connection isn't closed VideoChat.peerConnections.delete(uuid); dataChannel.delete(uuid); if (VideoChat.peerConnections.size === 0) { displayWaitingCaption(); } }, establishConnection: function (correctUuid, callback) { return function (token, uuid) { if (correctUuid != uuid) { return; } logIt(`<<< Received token, connecting to ${uuid}`); // Initialise localICEcandidates for peer uuid to empty array VideoChat.localICECandidates[uuid] = []; // Initialise connection status with peer uuid to false VideoChat.connected.set(uuid, false); // Set up a new RTCPeerConnection using the token's iceServers. VideoChat.peerConnections.set( uuid, new RTCPeerConnection({ iceServers: token.iceServers, }) ); // Add the local video stream to the peerConnection. VideoChat.localStream.getTracks().forEach(function (track) { VideoChat.peerConnections .get(uuid) .addTrack(track, VideoChat.localStream); }); // Add general purpose data channel to peer connection, // used for text chats, captions, and toggling sending captions dataChannel.set( uuid, VideoChat.peerConnections.get(uuid).createDataChannel("chat", { negotiated: true, // both peers must have same id id: 0, }) ); // Handle different dataChannel types dataChannel.get(uuid).onmessage = function (event) { const receivedData = event.data; // First 4 chars represent data type const dataType = receivedData.substring(0, 4); const cleanedMessage = receivedData.slice(4); if (dataType === "mes:") { handleRecieveMessage(cleanedMessage, hueToColor(VideoChat.peerColors.get(uuid))); } else if (dataType === "cap:") { recieveCaptions(cleanedMessage); } else if (dataType === "tog:") { toggleSendCaptions(); } else if (dataType === "clr:") { setStreamColor(uuid, cleanedMessage); } }; // Called when dataChannel is successfully opened dataChannel.get(uuid).onopen = function (event) { logIt("dataChannel opened"); setStreamColor(uuid); }; // Set up callbacks for the connection generating iceCandidates or // receiving the remote media stream. Wrapping callback functions // to pass in the peer uuids. VideoChat.peerConnections.get(uuid).onicecandidate = function (event) { VideoChat.onIceCandidate(event, uuid); }; VideoChat.peerConnections.get(uuid).onaddstream = function (event) { VideoChat.onAddStream(event, uuid); }; // Called when there is a change in connection state VideoChat.peerConnections.get( uuid ).oniceconnectionstatechange = function (event) { switch (VideoChat.peerConnections.get(uuid).iceConnectionState) { case "connected": logIt("connected"); break; case "disconnected": // Remove UUID if connection to server is intact if (VideoChat.socket.connected) { VideoChat.onLeave(uuid); } else { location.reload(); } break; case "failed": logIt("failed"); // VideoChat.socket.connect // VideoChat.createOffer(); // Refresh page if connection has failed location.reload(); break; case "closed": logIt("closed"); break; } }; callback(uuid); }; }, // When the peerConnection generates an ice candidate, send it over the socket to the peer. onIceCandidate: function (event, uuid) { logIt("onIceCandidate"); if (event.candidate) { logIt( `<<< Received local ICE candidate from STUN/TURN server (${event.candidate.address}) for connection with ${uuid}` ); if (VideoChat.connected.get(uuid)) { logIt(`>>> Sending local ICE candidate (${event.candidate.address})`); VideoChat.socket.emit( "candidate", JSON.stringify(event.candidate), roomHash, uuid ); } else { // If we are not 'connected' to the other peer, we are buffering the local ICE candidates. // This most likely is happening on the "caller" side. // The peer may not have created the RTCPeerConnection yet, so we are waiting for the 'answer' // to arrive. This will signal that the peer is ready to receive signaling. VideoChat.localICECandidates[uuid].push(event.candidate); } } }, // When receiving a candidate over the socket, turn it back into a real // RTCIceCandidate and add it to the peerConnection. onCandidate: function (candidate, uuid) { // Update caption text captionText.text("Found other user... connecting"); rtcCandidate = new RTCIceCandidate(JSON.parse(candidate)); logIt( `onCandidate <<< Received remote ICE candidate (${rtcCandidate.address} - ${rtcCandidate.relatedAddress})` ); VideoChat.peerConnections.get(uuid).addIceCandidate(rtcCandidate); }, // Create an offer that contains the media capabilities of the browser. createOffer: function (uuid) { logIt(`createOffer to ${uuid} >>> Creating offer...`); VideoChat.peerConnections.get(uuid).createOffer( function (offer) { // If the offer is created successfully, set it as the local description // and send it over the socket connection to initiate the peerConnection // on the other side. VideoChat.peerConnections.get(uuid).setLocalDescription(offer); VideoChat.socket.emit("offer", JSON.stringify(offer), roomHash, uuid); }, function (err) { logIt("failed offer creation"); logIt(err, true); } ); }, // Create an answer with the media capabilities that the client and peer browsers share. // This function is called with the offer from the originating browser, which // needs to be parsed into an RTCSessionDescription and added as the remote // description to the peerConnection object. Then the answer is created in the // same manner as the offer and sent over the socket. createAnswer: function (offer, uuid) { logIt("createAnswer"); rtcOffer = new RTCSessionDescription(JSON.parse(offer)); logIt(`>>> Creating answer to ${uuid}`); VideoChat.peerConnections.get(uuid).setRemoteDescription(rtcOffer); VideoChat.peerConnections.get(uuid).createAnswer( function (answer) { VideoChat.peerConnections.get(uuid).setLocalDescription(answer); VideoChat.socket.emit("answer", JSON.stringify(answer), roomHash, uuid); }, function (err) { logIt("Failed answer creation."); logIt(err, true); } ); }, // When a browser receives an offer, set up a callback to be run when the // ephemeral token is returned from Twilio. onOffer: function (offer, uuid) { logIt("onOffer <<< Received offer"); VideoChat.socket.on( "token", VideoChat.establishConnection(uuid, function (a) { VideoChat.createAnswer(offer, a); }) ); VideoChat.socket.emit("token", roomHash, uuid); }, // When an answer is received, add it to the peerConnection as the remote description. onAnswer: function (answer, uuid) { logIt(`onAnswer <<< Received answer from ${uuid}`); var rtcAnswer = new RTCSessionDescription(JSON.parse(answer)); // Set remote description of RTCSession VideoChat.peerConnections.get(uuid).setRemoteDescription(rtcAnswer); // The caller now knows that the callee is ready to accept new ICE candidates, so sending the buffer over VideoChat.localICECandidates[uuid].forEach((candidate) => { logIt(`>>> Sending local ICE candidate (${candidate.address})`); // Send ice candidate over websocket VideoChat.socket.emit( "candidate", JSON.stringify(candidate), roomHash, uuid ); }); // Reset the buffer of local ICE candidates. This is not really needed, but it's good practice // VideoChat.localICECandidates[uuid] = []; // TESTING }, // Called when a stream is added to the peer connection onAddStream: function (event, uuid) { logIt("onAddStream <<< Received new stream from remote. Adding it..."); // Create new remote video source in wrapper // Create a