mirror of
https://github.com/ianramzy/decentralized-video-chat.git
synced 2024-11-27 04:29:20 +08:00
Upload files
This commit is contained in:
parent
d6faf23885
commit
1ea59dcd6b
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.env
|
||||
.DS_Store
|
||||
/.idea
|
25
README.md
25
README.md
@ -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 Twilio’s 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
73
index.js
Normal 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
1206
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal 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
230
public/app.js
Normal 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
64
public/index.html
Normal 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
12
public/main.css
Normal 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;*/
|
||||
/*}*/
|
Loading…
Reference in New Issue
Block a user