// 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,
borderColor: `hsl(${Math.floor(Math.random() * 360)}, 70%, 80%)`,
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.borderColor = uuidToColor(VideoChat.socket.id);
VideoChat.localVideo.srcObject = stream;
VideoChat.localVideo.style.border = `3px solid ${VideoChat.borderColor}`;
// Now we're ready to join the chat room.
VideoChat.socket.emit("join", roomHash);
// Add listeners to the websocket
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);
},
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, VideoChat.peerColors[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":
logIt("disconnected - UUID " + uuid);
VideoChat.remoteVideoWrapper.removeChild(
document.querySelectorAll(`[uuid="${uuid}"]`)[0]
);
VideoChat.connected.delete(uuid);
VideoChat.peerConnections.delete(uuid);
dataChannel.delete(uuid);
if (VideoChat.peerConnections.size === 0) {
displayWaitingCaption();
}
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