// Vars var isMuted; var videoIsPaused; var dataChanel = null; 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"); var VideoChat = { connected: false, counter: 0, willInitiateCall: false, localICECandidates: [], socket: io(), remoteVideoWrapper: document.getElementById("wrapper"), localVideo: document.getElementById("local-video"), peerConnections: {}, recognition: undefined, answerCreatedUUIDs: [], // HACKY workaround: currently, onAnswer seems to be triggering twice and we don't know why // 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); // Receive its own uuid VideoChat.socket.on("uuid", (uuid) => (VideoChat.uuid = uuid)); // Add listeners to the websocket VideoChat.socket.on("full", chatRoomFull); VideoChat.socket.on("offer", VideoChat.onOffer); VideoChat.socket.on("ready", VideoChat.readyToCall); 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) ); }, // When we are ready to call, enable the Call button. readyToCall: function (event) { logIt("readyToCall"); }, call: function (uuid, room) { logIt("Initiating call"); VideoChat.startCall(uuid); }, // Set up a callback to run when we have the ephemeral token to use Twilio's TURN server. startCall: function (uuid) { VideoChat.socket.on("token", VideoChat.establishConnection(uuid, function(a) {VideoChat.createOffer(a);})); VideoChat.socket.emit("token", roomHash, uuid); }, establishConnection: function (correctUuid, callback) { return function(token, uuid) { if (correctUuid != uuid) { return; } console.log("establishing connection to", uuid); // Set up a new RTCPeerConnection using the token's iceServers. console.log(VideoChat.peerConnections) VideoChat.peerConnections[uuid] = new RTCPeerConnection({ iceServers: token.iceServers, }); // Add the local video stream to the peerConnection. VideoChat.localStream.getTracks().forEach(function (track) { console.log("local stream: ", track); VideoChat.peerConnections[uuid].addTrack(track, VideoChat.localStream); }); // Add general purpose data channel to peer connection, // used for text chats, captions, and toggling sending captions dataChanel = VideoChat.peerConnections[uuid].createDataChannel("chat", { negotiated: true, // both peers must have same id id: 0, }); // Called when dataChannel is successfully opened dataChanel.onopen = function (event) { logIt("dataChannel opened"); }; // Handle different dataChannel types dataChanel.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); } else if (dataType === "cap:") { recieveCaptions(cleanedMessage); } else if (dataType === "tog:") { toggleSendCaptions(); } }; // Set up callbacks for the connection generating iceCandidates or // receiving the remote media stream. VideoChat.peerConnections[uuid].onicecandidate = VideoChat.onIceCandidate; VideoChat.peerConnections[uuid].onaddstream = VideoChat.onAddStream; // Called when there is a change in connection state VideoChat.peerConnections[uuid].oniceconnectionstatechange = function (event) { switch (VideoChat.peerConnections[uuid].iceConnectionState) { case "connected": logIt("connected"); // Once connected we no longer have a need for the signaling server, so disconnect VideoChat.socket.off("token"); VideoChat.socket.off("offer"); break; case "disconnected": logIt("disconnected"); 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) { logIt("onIceCandidate"); if (event.candidate) { logIt( `<<< Received local ICE candidate from STUN/TURN server (${event.candidate.address})` ); if (VideoChat.connected) { logIt(`>>> Sending local ICE candidate (${event.candidate.address})`); VideoChat.socket.emit( "candidate", JSON.stringify(event.candidate), roomHash ); } 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.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[uuid].addIceCandidate(rtcCandidate); }, // Create an offer that contains the media capabilities of the browser. createOffer: function (uuid) { console.log(">>> Creating offer to UUID: ", uuid, ". my UUID is", VideoChat.socket.id); VideoChat.peerConnections[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[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 both 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)); console.log("createAnswer: setting remote description of " + uuid + " on " + VideoChat.socket.id); VideoChat.peerConnections[uuid].setRemoteDescription(rtcOffer); VideoChat.peerConnections[uuid].createAnswer( function (answer) { VideoChat.peerConnections[uuid].setLocalDescription(answer); console.log(">>> Creating answer to UUID: ", uuid, ". my UUID is", VideoChat.socket.id); 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.createOffer(a);})); 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) { console.log("onAnswer <<< Received answer", uuid, ". my UUID is", VideoChat.socket.id); // if (!VideoChat.answerCreatedUUIDs.includes(uuid)) { VideoChat.answerCreatedUUIDs.push(uuid); // logIt("onAnswer <<< Received answer" + ""); var rtcAnswer = new RTCSessionDescription(JSON.parse(answer)); // Set remote description of RTCSession console.log("onAnswer: setting remote description of " + uuid + " on " + VideoChat.socket.id); VideoChat.peerConnections[uuid].setRemoteDescription(rtcAnswer); // The caller now knows that the callee is ready to accept new ICE candidates, so sending the buffer over VideoChat.localICECandidates.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 = []; // } else { // console.log("attemped to run onAnswer on UUID ", uuid, " which already happened"); // } }, // Called when a stream is added to the peer connection onAddStream: function (event) { // if(VideoChat.counter > 4) { // VideoChat.socket.disconnect(); // }else{ // VideoChat.counter++; // } logIt("onAddStream <<< Received new stream from remote. Adding it..."); // Create new remote video source in wrapper // Create a