Skip to content

Instantly share code, notes, and snippets.

@SumindaD
Last active November 26, 2020 14:15
Show Gist options
  • Save SumindaD/334cd1a4f7b87b5960755a1e0dbb8576 to your computer and use it in GitHub Desktop.
Save SumindaD/334cd1a4f7b87b5960755a1e0dbb8576 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WebRTC Video Chat</title>
<style>
#videoContainer {
display: table;
margin: 0 auto;
}
video{
margin: 10px;
border: 2px solid #000000;
}
textarea {
width: 100%;
height: 150px;
resize: none;
box-sizing : border-box;
}
.center {
margin: auto;
width: 70%;
border: 3px solid green;
padding: 20px;
}
input, button{
width: 100%;
box-sizing : border-box;
}
video{
width: 40%;
height: 100%;
}
button {
border: 1px solid #000000;
background-color: #0099ff;
color: #ffffff;
padding: 5px 10px;
}
button:disabled,
button[disabled]{
border: 1px solid #999999;
background-color: #cccccc;
color: #666666;
}
th, td {
text-align: center;
}
</style>
</head>
<body>
</body>
<div class="center">
<button id="sendOfferButton" onclick="createAndSendOffer()">Call</button>
<button id="answerButton" onclick="createAndSendAnswer()">Answer</button>
<button id="hangUpButton" onclick="disconnectRTCPeerConnection()">Hang Up</button><br/><br/>
<input id="messageInput" type="text" size="80" placeholder="Enter message to send">
<button id="sendMessageButton" onclick="sendMessage()">Send Message</button><br/><br/>
<p style="text-align: center">Tap to Change Front/Back Camera</p>
<div id="videoContainer">
<video id="localVideo" onclick="switchMobileCamera()" muted autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
</div>
<table style="width:100%">
<tr>
<th>Outbound Video Stats</th>
<th>Inbound Video Stats</th>
</tr>
<tr>
<td><div readonly id="outBoundstats"></div></td>
<td><div readonly id="inBoundstats"></div></td>
</tr>
</table>
</div>
<br/><br/>
<textarea readonly id="chatTextArea"></textarea><br/><br/>
<textarea readonly id="logs"></textarea><br/><br/>
</div>
<script>
const webSocketConnection = "WEBSOCKET_URL";
const turnServerIPAddress = "TURN_SERVER_IP_ADDRESS";
const turnServerPort = "TURN_SERVER_PORT";
const turnServerUserName = "TURN_SERVER_USERNAME";
const turnServerPassword = "TURN_SERVER_PASSWORD";
var cameraMode = "user";
var inBoundTimestampPrev = 0;
var inBoundBytesPrev = 0;
var outBoundTimestampPrev = 0;
var outBoundBytesPrev = 0;
existingTracks = [];
var socket, localStream, connection, clientId = uuidv4(), channel;
const configuration = {
iceServers: [
{
urls: 'stun:' + turnServerIPAddress + ':' + turnServerPort
},
{
urls: 'turn:' + turnServerIPAddress + ':' + turnServerPort,
username: turnServerUserName,
credential: turnServerPassword
}
]
}
disableAllButtons();
getLocalWebCamFeed();
/*
This function creates the socket connection and WebRTC connection.
This is also responsible for changing media tracks when user switches mobile cameras (Front and back)
*/
function initiatSocketAndPeerConnection(stream){
document.getElementById("localVideo").srcObject = stream;
if(typeof socket === 'undefined'){
connectToWebSocket();
}else{
existingTracks.forEach(function (existingTrack, index) {
existingTrack.replaceTrack(localStream.getTracks()[index]);
});
}
}
function disableAllButtons(){
document.getElementById("sendOfferButton").disabled = true;
document.getElementById("answerButton").disabled = true;
document.getElementById("sendMessageButton").disabled = true;
document.getElementById("hangUpButton").disabled = true;
}
/*
Send messages via Data Channel
*/
function sendMessage(){
var messageText = document.getElementById("messageInput").value;
channel.send(JSON.stringify({
"message": messageText
}));
document.getElementById("chatTextArea").value += messageText + '\n';
}
function disconnectRTCPeerConnection(){
connection.close();
}
/*
Connect to the web socket and handle recieved messages from web sockets
*/
function connectToWebSocket(){
socket = new WebSocket(webSocketConnection);
// Create WebRTC connection only if the socket connection is successful.
socket.onopen = function(event) {
log('WebSocket Connection Open.');
createRTCPeerConnection();
};
// Handle messages recieved in socket
socket.onmessage = function(event) {
jsonData = JSON.parse(event.data);
switch (jsonData.type){
case 'candidate':
handleCandidate(jsonData.data, jsonData.id);
break;
case 'offer':
handleOffer(jsonData.data, jsonData.id);
break;
case 'answer':
handleAnswer(jsonData.data, jsonData.id);
break;
default:
break
}
};
socket.onerror = function(event) {
console.error(event);
log('WebSocket Connection Error. Make sure web socket URL is correct and web socket server is up and running at - ' + webSocketConnection);
};
socket.onclose = function(event) {
log('WebSocket Connection Closed. Please Reload the page.');
document.getElementById("sendOfferButton").disabled = true;
document.getElementById("answerButton").disabled = true;
};
}
function log(message){
document.getElementById("logs").value += message + '\n';
}
/*
Get local camera permission from user and initiate socket and WebRTC connection
*/
function getLocalWebCamFeed(){
// width: { ideal: 4096 },
// height: { ideal: 2160 }
constraints = {
audio: true,
video: {
facingMode: cameraMode,
width: { ideal: 4096 },
height: { ideal: 2160 }
}
}
navigator.getWebcam = (navigator.getUserMedia || navigator.webKitGetUserMedia || navigator.moxGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia(constraints)
.then(function (stream) {
localStream = stream;
initiatSocketAndPeerConnection(stream);
})
.catch(function (e) { log(e.name + ": " + e.message); });
}
else {
navigator.getWebcam({ audio: true, video: true },
function (stream) {
localStream = stream;
initiatSocketAndPeerConnection(stream);
},
function () { log("Web cam is not accessible.");
});
}
}
/*
This is responsible for creating an RTCPeerConnection and handle it's events.
*/
function createRTCPeerConnection(){
pushStats();
connection = new RTCPeerConnection(configuration);
// Add both video and audio tracks to the connection
for (const track of localStream.getTracks()) {
log("Sending Stream.")
existingTracks.push(connection.addTrack(track, localStream));
}
// This event handles displaying remote video and audio feed from the other peer
connection.ontrack = event => {
log("Recieved Stream.");
document.getElementById("remoteVideo").srcObject = event.streams[0];
}
// This event handles the received data channel from the other peer
connection.ondatachannel = function (event) {
log("Recieved a DataChannel.")
channel = event.channel;
setChannelEvents(channel);
document.getElementById("sendMessageButton").disabled = false;
};
// This event sends the ice candidates generated from Stun or Turn server to the Receiver over web socket
connection.onicecandidate = event => {
if (event.candidate) {
log("Sending Ice Candidate - " + event.candidate.candidate);
socket.send(JSON.stringify(
{
action: 'onMessage',
type: 'candidate',
data: event.candidate,
id: clientId
}
));
}
}
// This event logs messages and handles button state according to WebRTC connection state changes
connection.onconnectionstatechange = function(event) {
switch(connection.connectionState) {
case "connected":
log("Web RTC Peer Connection Connected.");
document.getElementById("answerButton").disabled = true;
document.getElementById("sendOfferButton").disabled = true;
document.getElementById("hangUpButton").disabled = false;
document.getElementById("sendMessageButton").disabled = false;
break;
case "disconnected":
log("Web RTC Peer Connection Disconnected. Please reload the page to reconnect.");
disableAllButtons();
break;
case "failed":
log("Web RTC Peer Connection Failed. Please reload the page to reconnect.");
console.log(event);
disableAllButtons();
break;
case "closed":
log("Web RTC Peer Connection Failed. Please reload the page to reconnect.");
disableAllButtons();
break;
default:
break;
}
}
log("Web RTC Peer Connection Created.");
document.getElementById("sendOfferButton").disabled = false;
}
/*
Creates and sends the Offer to the Receiver
Creates a Data channel for exchanging text messages
This function is invoked by the Caller
*/
function createAndSendOffer(){
if(channel){
channel.close();
}
// Create Data channel
channel = connection.createDataChannel('channel', {});
setChannelEvents(channel);
// Create Offer
connection.createOffer().then(
offer => {
log('Sent The Offer.');
// Send Offer to other peer
socket.send(JSON.stringify(
{
action: 'onMessage',
type: 'offer',
data: offer,
id: clientId
}
));
// Set Offer for negotiation
connection.setLocalDescription(offer);
},
error => {
log('Error when creating an offer.');
console.error(error);
}
);
}
/*
Creates and sends the Answer to the Caller
This function is invoked by the Receiver
*/
function createAndSendAnswer(){
// Create Answer
connection.createAnswer().then(
answer => {
log('Sent The Answer.');
// Set Answer for negotiation
connection.setLocalDescription(answer);
// Send Answer to other peer
socket.send(JSON.stringify(
{
action: 'onMessage',
type: 'answer',
data: answer,
id: clientId
}
));
},
error => {
log('Error when creating an answer.');
console.error(error);
}
);
}
/*
Accepts ICE candidates received from the Caller
*/
function handleCandidate(candidate, id){
// Avoid accepting the ice candidate if this is a message created by the current peer
if(clientId != id){
log("Adding Ice Candidate - " + candidate.candidate);
connection.addIceCandidate(new RTCIceCandidate(candidate));
}
}
/*
Accepts Offer received from the Caller
*/
function handleOffer(offer, id){
// Avoid accepting the Offer if this is a message created by the current peer
if(clientId != id){
log("Recieved The Offer.");
connection.setRemoteDescription(new RTCSessionDescription(offer));
document.getElementById("answerButton").disabled = false;
document.getElementById("sendOfferButton").disabled = true;
}
}
/*
Accetps Answer received from the Receiver
*/
function handleAnswer(answer, id){
// Avoid accepting the Answer if this is a message created by the current peer
if(clientId != id){
log("Recieved The Answer");
connection.setRemoteDescription(new RTCSessionDescription(answer));
}
}
/*
Generate a unique ID for the peer
*/
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/*
Handle Data Channel events
*/
function setChannelEvents(channel) {
channel.onmessage = function (event) {
var data = JSON.parse(event.data);
document.getElementById("chatTextArea").value += data.message + '\n';
};
channel.onerror = function (event) {
log('DataChannel Error.');
console.error(event)
};
channel.onclose = function (event) {
log('DataChannel Closed.');
disableAllButtons();
};
}
/*
Switch between front and back camera when opened in a mobile browser
*/
function switchMobileCamera(){
if (cameraMode == "user") {
cameraMode = "environment";
} else {
cameraMode = "user";
}
getLocalWebCamFeed();
}
function pushStats(){
let inBoundStatsDiv = document.getElementById("inBoundstats");
let outBoundstatsDiv = document.getElementById("outBoundstats");
window.setInterval(function() {
connection.getStats(null).then(stats => {
let inBoundBitrate;
let outBoundBitrate;
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.mediaType === 'video'){
let now = report.timestamp;
let bytes = report.bytesReceived;
if (inBoundTimestampPrev) {
inBoundBitrate = 0.125 * (8 * (bytes - inBoundBytesPrev) / (now - inBoundTimestampPrev));
inBoundBitrate = Math.floor(inBoundBitrate);
}
inBoundBytesPrev = bytes;
inBoundTimestampPrev = now;
}
else if(report.type === 'outbound-rtp' && report.mediaType === 'video'){
let now = report.timestamp;
let bytes = report.bytesSent;
if (outBoundTimestampPrev) {
outBoundBitrate = 0.125 * (8 * (bytes - outBoundBytesPrev) / (now - outBoundTimestampPrev));
outBoundBitrate = Math.floor(outBoundBitrate);
}
outBoundBytesPrev = bytes;
outBoundTimestampPrev = now;
}
if(isNaN(inBoundBitrate)){
inBoundBitrate = 0;
}
if(isNaN(outBoundBitrate)){
outBoundBitrate = 0;
}
let inboundVideoWidth = document.getElementById("remoteVideo").videoWidth;
let inboundVideoHeight = document.getElementById("remoteVideo").videoHeight;
inBoundStatsDiv.innerHTML = `<strong>Bitrate: </strong>${inBoundBitrate} KB/sec<br/><strong>Video dimensions: </strong> ${inboundVideoWidth}x${inboundVideoHeight}px<br/>`;
let outboundVideoWidth = document.getElementById("localVideo").videoWidth;
let outboundVideoHeight = document.getElementById("localVideo").videoHeight;
outBoundstatsDiv.innerHTML = `<strong>Bitrate: </strong>${outBoundBitrate} KB/sec<br/><strong>Video dimensions: </strong> ${outboundVideoWidth}x${outboundVideoHeight}px<br/>`;
});
});
}, 1000);
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment