// Vars 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 textarea"); const pipVideo = document.getElementById("remote-video"); var VideoChat = { connected: false, willInitiateCall: false, localICECandidates: [], socket: io(), remoteVideo: document.getElementById("remote-video"), localVideo: document.getElementById("local-video"), recognition: undefined, // 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); $("#local-video-text").text("Drag Me"); setTimeout(() => $("#local-video-text").fadeOut(), 5000); }) .catch((error) => { logIt(error); logIt( "Failed to get local webcam video, check webcam privacy settings" ); setTimeout(VideoChat.requestMediaStream, 1000); }); }, // The onMediaStream function receives the media stream as an argument. onMediaStream: function (stream) { logIt("onMediaStream"); VideoChat.localStream = stream; // Add the stream as video's srcObject. Snackbar.show({ text: "Share this URL with a friend to get started", actionText: "Copy Link", width: "355px", pos: "top-center", actionTextColor: "#8688ff", duration: 500000, backgroundColor: "#16171a", onActionClick: function (element) { var copyContent = window.location.href; $('') .val(copyContent) .appendTo("body") .select(); document.execCommand("copy"); var toRemove = document.querySelector("#some-element"); toRemove.parentNode.removeChild(toRemove); $(element).css("opacity", 0); //Set opacity of element to 0 to close Snackbar }, }); VideoChat.localVideo.srcObject = stream; // Now we're ready to join the chat room. VideoChat.socket.emit("join", roomHash); VideoChat.socket.on("full", chatRoomFull); VideoChat.socket.on("offer", VideoChat.onOffer); VideoChat.socket.on("ready", VideoChat.readyToCall); VideoChat.socket.on( "willInitiateCall", () => (VideoChat.willInitiateCall = true) ); }, // When we are ready to call, enable the Call button. readyToCall: function (event) { logIt("readyToCall"); if (VideoChat.willInitiateCall) { logIt("Initiating call"); VideoChat.startCall(); } }, // Set up a callback to run when we have the ephemeral token to use Twilio's TURN server. startCall: function (event) { logIt("startCall"); logIt(">>> Sending token request..."); VideoChat.socket.on("token", VideoChat.onToken(VideoChat.createOffer)); VideoChat.socket.emit("token", roomHash); }, // When we receive the ephemeral token back from the server. onToken: function (callback) { logIt("onToken"); return function (token) { logIt("<<< Received token"); // Set up a new RTCPeerConnection using the token's iceServers. VideoChat.peerConnection = new RTCPeerConnection({ iceServers: token.iceServers, }); // Add the local video stream to the peerConnection. VideoChat.localStream.getTracks().forEach(function (track) { VideoChat.peerConnection.addTrack(track, VideoChat.localStream); }); // Set up callbacks for the connection generating iceCandidates or // receiving the remote media stream. VideoChat.peerConnection.onicecandidate = VideoChat.onIceCandidate; VideoChat.peerConnection.onaddstream = VideoChat.onAddStream; // Set up listeners on the socket for candidates or answers being passed // over the socket connection. VideoChat.socket.on("candidate", VideoChat.onCandidate); VideoChat.socket.on("answer", VideoChat.onAnswer); VideoChat.socket.on("requestToggleCaptions", () => toggleSendCaptions()); VideoChat.socket.on("recieveCaptions", (captions) => recieveCaptions(captions) ); callback(); }; }, // 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) { logIt("onCandidate"); rtcCandidate = new RTCIceCandidate(JSON.parse(candidate)); logIt( `<<< Received remote ICE candidate (${rtcCandidate.address} - ${rtcCandidate.relatedAddress})` ); VideoChat.peerConnection.addIceCandidate(rtcCandidate); }, // Create an offer that contains the media capabilities of the browser. createOffer: function () { logIt("createOffer"); logIt(">>> Creating offer..."); VideoChat.peerConnection.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.peerConnection.setLocalDescription(offer); VideoChat.socket.emit("offer", JSON.stringify(offer), roomHash); }, 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) { logIt("createAnswer"); return function () { logIt(">>> Creating answer..."); rtcOffer = new RTCSessionDescription(JSON.parse(offer)); VideoChat.peerConnection.setRemoteDescription(rtcOffer); VideoChat.peerConnection.createAnswer( function (answer) { VideoChat.peerConnection.setLocalDescription(answer); VideoChat.socket.emit("answer", JSON.stringify(answer), roomHash); }, 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) { logIt("onOffer"); logIt("<<< Received offer"); VideoChat.socket.on( "token", VideoChat.onToken(VideoChat.createAnswer(offer)) ); VideoChat.socket.emit("token", roomHash); }, // When an answer is received, add it to the peerConnection as the remote description. onAnswer: function (answer) { logIt("onAnswer"); logIt("<<< Received answer"); var rtcAnswer = new RTCSessionDescription(JSON.parse(answer)); VideoChat.peerConnection.setRemoteDescription(rtcAnswer); VideoChat.localICECandidates.forEach((candidate) => { // The caller now knows that the callee is ready to accept new ICE candidates, so sending the buffer over logIt(`>>> Sending local ICE candidate (${candidate.address})`); VideoChat.socket.emit("candidate", JSON.stringify(candidate), roomHash); }); // Reset the buffer of local ICE candidates. This is not really needed, but it's good practice VideoChat.localICECandidates = []; }, onAddStream: function (event) { logIt("onAddStream"); logIt("<<< Received new stream from remote. Adding it..."); VideoChat.remoteVideo.srcObject = event.stream; Snackbar.close(); VideoChat.remoteVideo.style.background = "none"; VideoChat.connected = true; $("#remote-video-text").fadeOut(); $("#local-video-text").fadeOut(); var timesRun = 0; var interval = setInterval(function () { timesRun += 1; if (timesRun === 20) { clearInterval(interval); } rePositionLocalVideo(); }, 300); }, }; function getBrowserName() { var name = "Unknown"; if (window.navigator.userAgent.indexOf("MSIE") !== -1) { } else if (window.navigator.userAgent.indexOf("Firefox") !== -1) { name = "Firefox"; } else if (window.navigator.userAgent.indexOf("Opera") !== -1) { name = "Opera"; } else if (window.navigator.userAgent.indexOf("Chrome") !== -1) { name = "Chrome"; } else if (window.navigator.userAgent.indexOf("Safari") !== -1) { name = "Safari"; } return name; } function logIt(message, error) { console.log(message); } function chatRoomFull() { alert( "Chat room is full. Check to make sure you don't have multiple open tabs, or try with a new room link" ); window.location.href = "/newroom"; } function rePositionLocalVideo() { var bounds = $("#remote-video").position(); bounds.top += 10; bounds.left += 10; $("#moveable").css(bounds); } // // Fullscreen // function openFullscreen() { var elem = document.getElementById("remote-video"); if (!isFullscreen) { isFullscreen = true; if (elem.requestFullscreen) { elem.requestFullscreen(); } else if (elem.mozRequestFullScreen) { /* Firefox */ elem.mozRequestFullScreen(); } else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */ elem.webkitRequestFullscreen(); } else if (elem.msRequestFullscreen) { /* IE/Edge */ elem.msRequestFullscreen(); } } else { isFullscreen = false; if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { /* Firefox */ document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */ document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { /* IE/Edge */ document.msExitFullscreen(); } } } // // End Fullscreen // // // Mute microphone // function muteMicrophone() { var muted = VideoChat.localStream.getAudioTracks()[0].enabled; VideoChat.localStream.getAudioTracks()[0].enabled = !muted; const micIcon = document.getElementById("mic-icon"); const micText = document.getElementById("mic-text"); if (muted) { micIcon.classList.remove("fa-microphone"); micIcon.classList.add("fa-microphone-slash"); micText.innerText = "Unmute"; } else { micIcon.classList.add("fa-microphone"); micIcon.classList.remove("fa-microphone-slash"); micText.innerText = "Mute"; } } // // End Mute microphone // // // Pause Video // function pauseVideo() { var paused = VideoChat.localStream.getVideoTracks()[0].enabled; VideoChat.localStream.getVideoTracks()[0].enabled = !paused; const micIcon = document.getElementById("video-icon"); const micText = document.getElementById("video-text"); if (paused) { micIcon.classList.remove("fa-video"); micIcon.classList.add("fa-video-slash"); micText.innerText = "Unpause Video"; } else { micIcon.classList.add("fa-video"); micIcon.classList.remove("fa-video-slash"); micText.innerText = "Pause Video"; } } // // End pause Video // // // Swap camera / screen share // function swap() { if (!VideoChat.connected) { alert("You must join a call before you can share your screen."); return; } const swapIcon = document.getElementById("swap-icon"); const swapText = document.getElementById("swap-text"); if (mode === "camera") { navigator.mediaDevices .getDisplayMedia({ video: true, audio: true, }) .then(function (stream) { mode = "screen"; swapIcon.classList.remove("fa-desktop"); swapIcon.classList.add("fa-camera"); swapText.innerText = "Share Webcam"; switchStreamHelper(stream); }); } else { VideoChat.localVideo.srcObject.getTracks().forEach((track) => track.stop()); navigator.mediaDevices .getUserMedia({ video: true, audio: true, }) .then(function (stream) { mode = "camera"; swapIcon.classList.remove("fa-camera"); swapIcon.classList.add("fa-desktop"); swapText.innerText = "Share Screen"; switchStreamHelper(stream); }); } } function switchStreamHelper(stream) { let videoTrack = stream.getVideoTracks()[0]; if (VideoChat.connected) { var sender = VideoChat.peerConnection.getSenders().find(function (s) { return s.track.kind === videoTrack.kind; }); sender.replaceTrack(videoTrack); } VideoChat.localStream = videoTrack; VideoChat.localVideo.srcObject = stream; } // // End swap camera / screen share // // // Live caption // function requestToggleCaptions() { if (!VideoChat.connected) { alert("You must be connected to a peer to use Live Caption"); return; } if (receivingCaptions) { $("#remote-video-text").text("").fadeOut(); $("#caption-text").text("Start Live Caption"); receivingCaptions = false; } else { alert( "This is an expirimental feature. Live transcription requires the other user to have chrome." ); $("#caption-text").text("End Live Caption"); receivingCaptions = true; } VideoChat.socket.emit("requestToggleCaptions", roomHash); } function toggleSendCaptions() { if (sendingCaptions) { sendingCaptions = false; VideoChat.recognition.stop(); } else { startSpeech(); sendingCaptions = true; } } function startSpeech() { try { var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; var recognition = new SpeechRecognition(); VideoChat.recognition = recognition; } catch (e) { sendingCaptions = false; logIt(e); logIt("error importing speech library"); VideoChat.socket.emit("sendCaptions", "notusingchrome", roomHash); return; } // If false, the recording will stop after a few seconds of silence. // When true, the silence period is longer (about 15 seconds), // allowing us to keep recording even when the user pauses. recognition.continuous = true; recognition.interimResults = true; // recognition.maxAlternatives = 3; var finalTranscript; recognition.onresult = (event) => { let interimTranscript = ""; for (let i = event.resultIndex, len = event.results.length; i < len; i++) { let transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; VideoChat.socket.emit("sendCaptions", interimTranscript, roomHash); // console.log(interimTranscript); } } }; recognition.onstart = function () { console.log("recording on"); }; recognition.onspeechend = function () { console.log("on speech end"); }; recognition.onerror = function (event) { if (event.error === "no-speech") { console.log("no speech detected"); } }; recognition.onend = function () { console.log("on end"); console.log(sendingCaptions); if (sendingCaptions) { startSpeech(); } else { VideoChat.recognition.stop(); } }; recognition.start(); } function recieveCaptions(captions) { $("#remote-video-text").text("").fadeIn(); if (!receivingCaptions) { $("#remote-video-text").text("").fadeOut(); } if (captions === "notusingchrome") { alert("Other caller must be using chrome for this feature to work"); receivingCaptions = false; $("#remote-video-text").text("").fadeOut(); $("#caption-text").text("Start Live Caption"); return; } if (captions.length > 100) { $("#remote-video-text").text(captions.substr(captions.length - 199)); } else { $("#remote-video-text").text(captions); } } // // End Live caption // // // Chat // var socket = VideoChat.socket; chatInput.addEventListener("keypress", function (event) { if (event.keyCode === 13) { event.preventDefault(); socket.emit("chat message", chatInput.value, roomHash); $(".chat-messages").append( '
' + chatInput.value + "
" ); $("#chat-zone").scrollTop($("#chat-zone")[0].scrollHeight); chatInput.value = ""; } }); socket.on("chat message", function (msg) { $(".chat-messages").append( '
' + msg + "
" ); $("#chat-zone").scrollTop($("#chat-zone")[0].scrollHeight); if ($("#entire-chat").is(":hidden")) { toggleChat(); } }); function toggleChat() { var entireChat = $("#entire-chat"); var chatIcon = document.getElementById("chat-icon"); var chatText = $("#chat-text"); if (entireChat.is(":visible")) { entireChat.fadeOut(); chatText.text("Show Chat"); chatIcon.classList.remove("fa-comment-slash"); chatIcon.classList.add("fa-comment"); } else { entireChat.fadeIn(); chatText.text("Hide Chat"); chatIcon.classList.remove("fa-comment"); chatIcon.classList.add("fa-comment-slash"); } } // // End chat // // //Picture in picture // function togglePictureInPicture() { if ("pictureInPictureEnabled" in document) { if (document.pictureInPictureElement) { document.exitPictureInPicture().catch((error) => { logIt("Error exiting pip."); logIt(error); }); } else { pipVideo.requestPictureInPicture().catch((error) => { logIt("Error entering pip."); logIt(error); }); } } else { alert( "Picture in picture is not supported in your browser. Consider using Chrome." ); } } pipVideo.addEventListener("enterpictureinpicture", () => { $("#pip-text").text("Exit Picture in Picture"); }); pipVideo.addEventListener("leavepictureinpicture", () => { $("#pip-text").text("Enter Picture in Picture"); }); // //Picture in picture // function startUp() { // strip url parameters if (url.indexOf("?") > -1) { window.location.href = url.split("?")[0]; } // if no chat room provided go back to newroom if (window.location.pathname === "/room") { window.location.href = "/landing/newroom"; } // Redirect unsupported browsers if ( !isWebRTCSupported || browserName === "Safari" || browserName === "MSIE" ) { alert( "Your browser doesn't support Neon Chat. Please use Chrome or Firefox." ); window.location.href = "/"; } document.title = "Neon Chat - " + url.substring(url.lastIndexOf("/") + 1); // get webcam on load VideoChat.requestMediaStream(); $("#remote-video-text").text("").fadeOut(); $("#moveable").draggable({ containment: "window" }); $(".HoverState").hide(); $("#entire-chat").hide(); // // Show hide button labels on hover // $(document).ready(function () { $(".hoverButton").mouseover(function () { $(".HoverState").hide(); $(this).next().show(); }); $(".hoverButton").mouseout(function () { $(".HoverState").hide(); }); }); // // End show hide button labels on hover // // // Fade out / show UI on mouse move // var timedelay = 1; function delayCheck() { if (timedelay === 5) { $(".multi-button").fadeOut(); $("#header").fadeOut(); timedelay = 1; } timedelay = timedelay + 1; } $(document).mousemove(function () { $(".multi-button").fadeIn(); $("#header").fadeIn(); timedelay = 1; clearInterval(_delay); _delay = setInterval(delayCheck, 500); }); _delay = setInterval(delayCheck, 500); // // End Fade out / show UI on mouse move // // Show accept webcam snackbar Snackbar.show({ text: "Please allow microphone and webcam access", actionText: "Show Me How", width: "455px", pos: "top-right", actionTextColor: "#8688ff", duration: 50000, backgroundColor: "#292B32", onActionClick: function (element) { window.open( "https://getacclaim.zendesk.com/hc/en-us/articles/360001547832-Setting-the-default-camera-on-your-browser", "_blank" ); }, }); } startUp();