Upload files

This commit is contained in:
ian ramzy 2020-03-21 17:23:46 -04:00
parent d6faf23885
commit 1ea59dcd6b
8 changed files with 1631 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
.env
.DS_Store
/.idea

View File

@ -1 +1,24 @@
decentralized-video-chat
# Video Chat
A simple video chat between two clients as an example of how to connect two browsers via WebRTC using Twilio STUN/TURN infrastructure.
Read the blog post to see how to build this: [Getting Started with WebRTC using Node.js, Socket.io and Twilios NAT Traversal Service](https://www.twilio.com/blog/2014/12/set-phasers-to-stunturn-getting-started-with-webrtc-using-node-js-socket-io-and-twilios-nat-traversal-service.html).
# Quick start
* Clone this repo
```
git clone https://github.com/philnash/video-chat.git
cd video-chat
```
* Install dependencies
```
npm install
```
* Create a `.env` file copying `.env.template`. Fill in your `Account SID` and `Auth Token` from your [Twilio console](https://www.twilio.com/console)
* Start the server
```
npm start
```
* Open two browsers on your laptop and point them `localhost:3000`. If you want to use a client on another computer/mobile, make sure you publish your server on an HTTPS connection (otherwise the camera may not work). You can use a service like [ngrok](https://ngrok.com/) for that.
* Click on the "Get Video" button on both browsers
* Click on button "Call" on one of the browser, to establish the video call

73
index.js Normal file
View File

@ -0,0 +1,73 @@
require('dotenv').config();
// Twilio init
var twilio = require('twilio')(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
var express = require('express');
var app = express();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
app.use(express.static('public'));
// When a socket connects, set up the specific listeners we will use.
io.on('connection', function(socket){
// When a client tries to join a room, only allow them if they are first or
// second in the room. Otherwise it is full.
socket.on('join', function(room){
console.log('A client joined')
var clients = io.sockets.adapter.rooms[room];
var numClients = typeof clients !== 'undefined' ? clients.length : 0;
if(numClients == 0){
socket.join(room);
socket.emit('firstin', room);
}else if(numClients == 1){
socket.join(room);
// When the client is second to join the room, both clients are ready.
console.log('Broadcasting ready message')
socket.emit('ready', room);
socket.broadcast.emit('ready', room);
}else{
console.log("room full");
socket.emit('full', room);
}
});
// When receiving the token message, use the Twilio REST API to request an
// token to get ephemeral credentials to use the TURN server.
socket.on('token', function(){
console.log('Received token request')
twilio.tokens.create(function(err, response){
if(err){
console.log(err);
}else{
// Return the token to the browser.
console.log('Token generated. Returning it to the client')
socket.emit('token', response);
}
});
});
// Relay candidate messages
socket.on('candidate', function(candidate){
console.log('Received candidate. Broadcasting...')
socket.broadcast.emit('candidate', candidate);
});
// Relay offers
socket.on('offer', function(offer){
console.log('Received offer. Broadcasting...')
socket.broadcast.emit('offer', offer);
});
// Relay answers
socket.on('answer', function(answer){
console.log('Received answer. Broadcasting...')
socket.broadcast.emit('answer', answer);
});
});
http.listen(3000, function() {
console.log("http://localhost:3000");
});

1206
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "video-chat2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^8.1.0",
"express": "^4.17.1",
"socket.io": "^2.2.0",
"twilio": "^3.34.0"
}
}

230
public/app.js Normal file
View File

@ -0,0 +1,230 @@
function logIt(message, error) {
// Print on console
console.log(message);
// Add to logs on page
let logs = document.getElementById('logs');
let tmp = document.createElement('P');
tmp.innerText = message;
if (error) {
tmp.classList.add('error');
}
logs.appendChild(tmp);
}
// var willCall = false;
var firstIn = false
// var connected = false;
// Create an object to save various objects to without polluting the global namespace.
var VideoChat = {
connected: false,
localICECandidates: [],
// Initialise our connection to the WebSocket.
socket: io(),
// 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) {
navigator.mediaDevices
.getUserMedia({video: true, audio: true})
.then(stream => {
VideoChat.onMediaStream(stream);
})
.catch(error => {
VideoChat.noMediaStream(error);
});
},
requestScreenStream: function (event) {
navigator.mediaDevices
.getDisplayMedia({video: true, audio: true})
.then(stream => {
VideoChat.onMediaStream(stream);
})
.catch(error => {
VideoChat.noMediaStream(error);
});
},
// The onMediaStream function receives the media stream as an argument.
onMediaStream: function (stream) {
VideoChat.localVideo = document.getElementById('local-video');
VideoChat.localVideo.volume = 0; // Turn the volume down to 0 to avoid echoes.
VideoChat.localStream = stream;
VideoChat.videoButton.setAttribute('disabled', 'disabled');
VideoChat.screenButton.setAttribute('disabled', 'disabled');
// Add the stream as video's srcObject.
VideoChat.localVideo.srcObject = stream;
// Now we're ready to join the chat room.
VideoChat.socket.emit('join', 'test');
VideoChat.socket.on('offer', VideoChat.onOffer);
VideoChat.socket.on('ready', VideoChat.readyToCall);
VideoChat.socket.on('firstin', () => firstIn = true);
},
// There's not much to do in this demo if there is no media stream. So let's just stop.
noMediaStream: function () {
logIt('No media stream for us.');
},
// When we are ready to call, enable the Call button.
readyToCall: function (event) {
VideoChat.callButton.removeAttribute('disabled');
if (firstIn) {
// alert("firstin is calling")
VideoChat.startCall()
}
},
// Set up a callback to run when we have the ephemeral token to use Twilio's
// TURN server.
startCall: function (event) {
logIt('>>> Sending token request...');
VideoChat.socket.on('token', VideoChat.onToken(VideoChat.createOffer));
VideoChat.socket.emit('token');
VideoChat.callButton.disabled = true
},
// When we receive the ephemeral token back from the server.
onToken: function (callback) {
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.peerConnection.addStream(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);
callback();
};
},
// When the peerConnection generates an ice candidate, send it over the socket
// to the peer.
onIceCandidate: function (event) {
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));
} 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) {
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('>>> 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));
},
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) {
return function () {
logIt('>>> Creating answer...');
VideoChat.connected = true;
rtcOffer = new RTCSessionDescription(JSON.parse(offer));
VideoChat.peerConnection.setRemoteDescription(rtcOffer);
VideoChat.peerConnection.createAnswer(
function (answer) {
console.log(answer);
VideoChat.peerConnection.setLocalDescription(answer);
VideoChat.socket.emit('answer', JSON.stringify(answer));
},
function (err) {
// Handle a 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('<<< Received offer');
VideoChat.socket.on('token', VideoChat.onToken(VideoChat.createAnswer(offer)));
VideoChat.socket.emit('token');
},
// When an answer is received, add it to the peerConnection as the remote
// description.
onAnswer: function (answer) {
logIt('<<< Received answer');
var rtcAnswer = new RTCSessionDescription(JSON.parse(answer));
VideoChat.peerConnection.setRemoteDescription(rtcAnswer);
VideoChat.connected = true;
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));
});
// Resest the buffer of local ICE candidates. This is not really needed
// in this specific client, but it's good practice
VideoChat.localICECandidates = [];
},
// When the peerConnection receives the actual media stream from the other
// browser, add it to the other video element on the page.
onAddStream: function (event) {
logIt('<<< Received new stream from remote. Adding it...');
VideoChat.remoteVideo = document.getElementById('remote-video');
VideoChat.remoteVideo.srcObject = event.stream;
// VideoChat.remoteVideo.volume = 1;
}
};
// Get the video button and add a click listener to start the getUserMedia
// process
VideoChat.videoButton = document.getElementById('get-video');
VideoChat.videoButton.addEventListener('click', VideoChat.requestMediaStream, false);
VideoChat.screenButton = document.getElementById('get-screen');
VideoChat.screenButton.addEventListener('click', VideoChat.requestScreenStream, false);
VideoChat.callButton = document.getElementById('call');
VideoChat.callButton.addEventListener('click', VideoChat.startCall, false);
// auto get media
// VideoChat.requestScreenStream();
VideoChat.requestMediaStream()

64
public/index.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Video Chat</title>
<style>
body {
background: #ececec;
display: flex;
height: 100vh;
margin: 0;
align-items: center;
padding: 0 50px;
}
video {
background: white;
margin: 0 5%;
box-sizing: border-box;
border-radius: 10px;
padding: 0;
box-shadow:rgba(118, 143, 255, 0.4) 0px 16px 24px 0px;
}
#local-video{
width:20%;
height: auto
}
#remote-video{
width:70%;
height: auto
}
div#logs p {
margin: 0;
font-family: 'Courier New', Courier, monospace;
font-size: 9px;
}
div#logs p.error {
color: red;
}
</style>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div>
<button id="get-video">Share Webcam</button>
<button id="get-screen">Share Screen</button>
<button id="call" disabled="disabled">Call</button>
<button id="will-call">Will Call</button>
</div>
<video id="remote-video" height="500" autoplay></video>
<video id="local-video" height="150" autoplay muted></video>
<p>Log messages:</p>
<div id="logs">
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/app.js"></script>
</body>
</html>

12
public/main.css Normal file
View File

@ -0,0 +1,12 @@
/*html, body {*/
/* width: 100%;*/
/* height: 100%;*/
/* margin: 0;*/
/* padding: 0;*/
/* background-color: #DCECFE;*/
/* background: -webkit-linear-gradient(-450deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, -webkit-linear-gradient(-450deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, -webkit-linear-gradient(-450deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, -webkit-linear-gradient(-450deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, -webkit-linear-gradient(-450deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, -webkit-linear-gradient(-390deg, #DCECFE 0%, #F5E1EE 100%) no-repeat;*/
/* background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.05)) repeat-y, linear-gradient(120deg, #DCECFE 0%, #F5E1EE 100%) no-repeat;*/
/* background-position: 10%, 30%, 50%, 70%, 90%, 0;*/
/* background-size: 2px auto, 2px auto, 2px auto, 2px auto, 2px auto, 100% auto;*/
/* background-attachment: fixed;*/
/*}*/