Last active
November 26, 2020 14:15
-
-
Save SumindaD/334cd1a4f7b87b5960755a1e0dbb8576 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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