// 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: [],
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);
VideoChat.localICECandidates[uuid] = []; // initialise uuid with empty array
VideoChat.connected[uuid] = false;
// Set up a new RTCPeerConnection using the token's iceServers.
VideoChat.peerConnections[uuid] = new RTCPeerConnection({
iceServers: token.iceServers,
});
// Add the local video stream to the peerConnection.
VideoChat.localStream.getTracks().forEach(function (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 = function(u) {VideoChat.onIceCandidate(u, uuid)};
VideoChat.peerConnections[uuid].onaddstream = function(u) {VideoChat.onAddStream(u, uuid)};
// 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, uuid) {
logIt("onIceCandidate");
if (event.candidate) {
logIt(
`<<< Received local ICE candidate from STUN/TURN server (${event.candidate.address}) associated with UUID (${uuid})`
);
if (VideoChat.connected[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[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[uuid].forEach((candidate) => {
logIt(`>>> Sending local ICE candidate (${candidate.address})`);
// Send ice candidate over websocket
VideoChat.socket.emit("candidate", JSON.stringify(candidate), roomHash, uuid);
});
},
// Called when a stream is added to the peer connection
onAddStream: function (event, uuid) {
// if(VideoChat.counter > 4) {
// VideoChat.socket.disconnect();
// }else{
// VideoChat.counter++;
// }
logIt("onAddStream <<< Received new stream from remote. Adding it..." + event);
// Create new remote video source in wrapper
// Create a