Professional Documents
Culture Documents
WEBRTC VIDEO CHAT 1 by Felix Hagspiel's Blog PDF
WEBRTC VIDEO CHAT 1 by Felix Hagspiel's Blog PDF
1/12
11/12/2014
WebRTC can be annoying - especially the error-hunt. But the more frustrated you get, the happier
you will be when your friend's videochat finally pops up in your browser :)
Note: To test the application on one computer you can open one regular and one private
tab in Chrome (press CTRL+N) to simulate two individual persons.
The application exists of 3 files:
private/server.js
public/client.js
public/index.html
The Server
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
2/12
11/12/2014
Then check if node and npm is installed by typing these commands (if they are installed you should see
a version number on the console):
node -v
npm -v
Then we have to create a few new folders where we can store our project files (of course you can do that
also by using an FTP-Client and just upload the package):
mkdir /var/www/virtual/yourusername/webrtc
cd /var/www/virtual/yourusername/webrtc
mkdir public
mkdir private
/public is the folder where we store all the HTML, JS and CSS. In /private we will store the
nodeserver file. It is important that the private-folder is outside your /html-folder! Now go into the
private-folder, create a file called server.js and open it inside the nano-editor in the terminal:
cd private
touch server.js
nano server.js
Then copy all the code from the server.js-file you downloaded before and paste it inside the terminal.
Now exit the editor (you can exit it by pressing
CTRL+X
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
3/12
11/12/2014
to exit at the bottom of your terminal), it will ask you then to save the file, press
enter
The uuid-module is a random number generator and will be used to create your room's ID.
Now let's try to start the server:
node server.js
your server is up and running. For now we will start it manually, which means that you have to restart it
everytime it crashes or you logout, which is better for debugging purposes. Later you can read here on
how to start a daemon which automatically starts the server.
Note: As node.js is written in JavaScript, the whole server will be down for all users when an error
occurs, even if the error happens just on one single connection!
The following function is called each time someone connects to the server
wsServer.on('request', function(request) { ... });
It stays opened until the connection is closed. Inside it you can see
connection.on('message', function(message) { ... });
which will be executed each time we call the connection.send()-function on the client side. Inside it we
try to parse the message to JSON and handle the different kind of message-types. The different kinds
relevant for the server are roomCreated and offer . If the type does not match those types the message
will be delivered to the partner who is inside the same room.
createRoom
This is the first message to the server before a room can be joined. This is executed whenever someone
presses the "create room"-button. First, a unique ID is created which will identify the room:
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
4/12
11/12/2014
creatorConnection
in the
rooms
rooms[roomId] = {
creatorConnection: connection,
partnerConnection: false,
}
Next we create the response-message and send it as JSON to the creator of the room:
var data = {
type: 'roomCreated',
payload: roomId
};
return send(rooms[roomId].creatorConnection, data);
Note: As this is a full-duplex permanent connection, we do not have to wait for request from the user
like you have to when using regular HTTP. We can send messages to the user whenever we want. This
is one of the big advantages of websockets.
And voil, the room is created.
Now let's see what happens when someone wants to join our room.
offer
When a user wants to enter our room, he has to have the room-id first, so you have to pass it to him
somehow (IM, email, phone, etc ...). Of course you could show all available room-ID's on the loginpage, but then everyone could join your room, not just your friend. So after he clicked on the join room button, the server receives a message with the type offer . It checks then if a room by that ID is present
and if the partnerConnection is empty. If not it will send an error to the user. If it is free the connection
will be stored as partnerConnection inside the rooms-object:
if(rooms[data.roomId].partnerConnection) {
// send error to user
var data = {
type: 'error',
payload: 'room is already full'
};
return send(connection, data);
}
// this refers to the current connection-object of the user
rooms[data.roomId].partnerConnection = this;
Because the rooms-object is above the scope of the wsServer.on('request', function(request) { ... }); function, every connected user can access it. This makes it possible to share connection data and
messages between the users. This means we can inform the creator of the room that someone wants to
join:
// we just pass on the data-object we received from the partner who wants to join
return send(rooms[data.roomId].creatorConnection, data);
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
5/12
11/12/2014
default
After that we have both creator- and partner-connection. Now each message will be just transferred to
the opposite member of the room without any special action taken:
default:
if(this === rooms[data.roomId].partnerConnection) {
return send(rooms[data.roomId].creatorConnection, data);
}
return send(rooms[data.roomId].partnerConnection, data);
break;
That's basically everything we need. Of course you can do a lot more error-handling and store more
userdata, but for this simple example it is enough.
The View
In this part we create the view which is represented by the index.html -file inside our /public -folder. I
just added some basic CSS-styling. You can style it as you wish, of course. Just be aware that this is
going to be a single-page application, so we have to hide/show some parts of it via JavaScript (I
accomplish this by adding and removing the active -class via JavaScript).
As before I will not explain the parts of the index-file which I think are self-explaining or which have
already been commented.
The two video -elements will represent the video- and audiostreams once the connection is established.
Note that the ownVideo -element is muted. This is important because otherwise you will hear your own
voice all the time. The autoplay-parameter ensures that the video plays even if we do not call the
.play() -function ourself (at least in Chrome, in FF you still have to to that). Now to the JavaScript here we create a new WebRTC-object which we will define in the client.js-file later (This is explained in
the client-part of this tutorial). Then we execute the connectToSocket() -function which establishes the
connection to the websocket-server (needless to say that you have to put in your server-name and the
related port):
var WebRTC = new WebRTC();
WebRTC.connectToSocket('ws://yourservername.uberspace.de:63949');
Then we add a custom eventlistener which is used to communicate between the view and the WebRTCobject (In the next part of the tutorial you will see where and when it is fired):
document.addEventListener('socketEvent', function(socketEvent){ ... });
Next we add the clickhandlers for the "create room"- and "join room"-button. When clicked, we try to
get the user's media-stream of microphone and camera and add it as source to the ownVideo -element.
This is done by the success -function which will be executed if you click "allow" on the browser's
request for permission to access your microphone and camera. After that we call the createRoom() respectively joinRoom() -function of the WebRTC-object:
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
6/12
11/12/2014
// get media-stream
var success = function(myStream){
// set videostream on ownVideo-object
ownVideo.src = URL.createObjectURL(myStream);
// join a room
WebRTC.joinRoom(roomidInput.value);
};
WebRTC.getMedia({audio: true, video: true},success);
Now we have to create the file on the server. As before, you can also use your FTP-Client instead.
cd /var/www/virtual/yourusername/webrtc/public
touch index.html
nano index.html
Now we paste all content from the downloaded index.html-file inside the terminal and save it like we did
when creating the server.js-file. As we have no domain associated with our VM yet and the public-folder
is outside the html-folder, we have to create a symlink to be able to access it from outside:
ln -s /var/www/virtual/yourusername/webrtc/public
/var/www/virtual/yourusername/yourusername.yourservername.uberspace.de
If you enter then http://yourusername.yourservername.uberspace.de inside your Browser, you should see
the login-screen. If not there is most likely something wrong with the symlink. If you have a 'www.'
infront of the domainname it will not work as you have to create a second symlink for that.
The Client
Now we come to the WebRTC part. As we have to communicate with the view via eventlisteners we
have to take short looks at the index.html -file. Note also that I am talking about two different kind of
connections in the following text - the websocket-connection to the server and the peer-to-peerconnection to your partner. I will not explain too much about the websocket-connection as it is selfexplaining and similar to the things I explained when creating the server. What I will explain later on is
the stuff that happens inside .onmessage() . In this part the offerer is the one who wants to join a room
and the answerer is the creator of the room. Do as we did before and create a file client.js inside your
/public -folder. At this point, the application should already work.
So now I will explain what happens when someone clicks on the join room -button (we assume that
someone already created a room as described in the server-part of this tutorial).
Offer
First, we try to get the audio- and videostream of the person and add it to our
happens in the index.html :
ownVideo
-element. This
// get media-stream
var success = function(myStream){
// set videostream on ownVideo-object
ownVideo.src = URL.createObjectURL(myStream);
// join a room
WebRTC.joinRoom(roomidInput.value);
};
WebRTC.getMedia({audio: true, video: true},success);
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
7/12
11/12/2014
Inside the client.js -file the function .getMedia() is called. We pass some constraints to the function as
well as a success-callback. The constraints tell the browser what it should be asking for, in this case for
audio AND video. There is an webapplication on chromium.org which generates the constraints based
on your input. You can read more about constraints here.
// set default constraints if none passed
if(!constraints) {
constraints = {audio: true, video: true};
}
After some prefix-checking for the different browsers we call the real getUserMedia() which receives the
contraints and a function for success and for fail (Note: This is not the success function we passed from
the index.html . That function will be called inside the getUserMedia() -function). At this point the
browser will ask you for permission to access the microphone and camera. If you can provide a secured
connection the user of your site is able to save the permission, otherwise he will be asked every time. If
you hit allow, the first callback receives the stream which we save in the myStream -variable for later
usage:
// call getUserMedia
getUserMedia(constraints, function (stream) {
// set stream
myStream = stream;
Okay, now we got the mediastream and saved it. Next we execute the
inside the view and pass the stream:
success()
-function we declared
// call success-callback
if(success) {
success(myStream);
}
The last function passed to getUserMedia() is a fail-callback. There we just print out the error if
something went wrong or the user denied the access. Here is the full documentation of getUserMedia().
Inside the success() -function (you have to look at your view again) we set the stream as sourceattribute of the ownVideo -element which should immediately play the video because we set the autoplay
-attribute on the video-element:
// get media-stream
var success = function(myStream){
// set videostream on ownVideo-object
ownVideo.src = URL.createObjectURL(myStream);
// join a room
WebRTC.joinRoom(roomidInput.value);
};
The URL.createObjectURL(myStream) creates a URL out of the stream-object, representing your local media
stream. Here you can read more about it.
For an working WebRTC-connection we need to create an offer, then answer the offer and then
complete the handshake. So the .joinRoom() -function saves the room-ID and then executes
'createOffer()':
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
8/12
11/12/2014
// connect to a room
this.joinRoom = function(id){
roomId = id;
createOffer();
};
The STUN-server is needed to setup the peer-connection because of NAT (if you want to know more
about why we need STUN-server read this article). In this case we use one of Google's STUN-servers.
Now we add our media-stream to the connection (it is important to do that at this point and not after the
connection was established):
// add media-stream to peerconnection
peerConnection.addStream(myStream);
Then we declare some basic functions which handle different kind of events. Lets start with
onaddstream() which will be fired when we receive the videostream of the other guy. In this example we
create a new custom event and dispatch it. We use this to communicate with the view, because we have
to set the stream we just received to the otherVideo -element:
// other side added stream to peerconnection
peerConnection.onaddstream = function(e) {
console.log('stream added');
otherStream = e.stream;
// fire event
socketEvent.eventType = 'streamAdded';
document.dispatchEvent(socketEvent);
};
You can see what happens next when you take a look at the switch-case-statement inside the
'socketEvent'-listener in the index.html . Note: This procedure is the same for createAnser() and
createOffer() . You can put it in an extra function to reduce code, but I wrote it each time for better
understanding.
Now we will handle ICE-candidates. ICE-candidates are used to exchange network information like
adresses and ports. The function 'onicecandidate()' is called asynchronous when there are own
candidates available. So when we set up the offer, there will be some candidates created immediately.
What we have to do is to send each of those candidates to the creator of the room where they are stored
until we need them:
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
9/12
11/12/2014
The server passes the message to the creator of the room, who then saves it by executing
setIceCandidate() inside the switch-case-statement of the websocket's .onmessage() -function. It is
important that we do not add the candidates to the connection until we received the session-descrition
(more to that later) of the other guy and added it to the RTCPeerConnection-object! So if the variable
otherSDP is false, we store the candidates in an array. Otherwise we directly add it to the connection:
// set or save the icecandidates
var setIceCandidates = function(iceCandidate) {
// push icecandidate to array if no SDP of other guys is available
if(!otherSDP) {
othersCandidates.push(iceCandidate);
}
// add icecandidates immediately if remoteDescription is set
if(otherSDP &&
iceCandidate.candidate &&
iceCandidate.candidate !== null ) {
peerConnection.addIceCandidate(createRTCIceCandidate(iceCandidate.candidate));
}
};
Now we have set all eventlisteners on the offerer's side and can create the actual offer. When we call
.createOffer() on the peer-connection we receive a session-description-object which we set as the local
description on the peer-connection. After that we send a message to the room-creator telling him that we
want to establish a connection with him. The payload of the message is our SDP, which he needs to
accept the offer.
// we actually create the offer
peerConnection.createOffer(function(SDP){
// set our SDP as local description
peerConnection.setLocalDescription(mySDP);
console.log('sending offer to: '+roomId);
// send SDP to other guy
var data = {
type: 'offer',
roomId: roomId,
payload: SDP
};
sendToServer(data);
});
Answer
On the answerer side (who is the creator of the room in this case) we receive the message containing the
SDP. We save the SDP and exectue createAnswer() , which looks a lot like the createOffer() -function
before:
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
10/12
11/12/2014
The next steps are nearly the same like before (the onaddstream() - and onicecandidate() -functions are
exactly the same like those before), except that we have to set the other guy's SDP as remote-description
of the peer-connection right after we added our stream:
// create an answer for an received offer var createAnswer = function() { // create new peer-object if(
typeof(RTCPeerConnection) === 'function') { peerConnection = new RTCPeerConnection(peerConfig);
} else if( typeof(webkitRTCPeerConnection) === 'function') { peerConnection = new
webkitRTCPeerConnection(peerConfig); }
// add media-stream to peerconnection
peerConnection.addStream(myStream);
// set remote-description
peerConnection.setRemoteDescription(createRTCSessionDescription(otherSDP));
...
The function createRTCSessionDescription() is used to handle prefixes. Now we have an endpoint for our
peer-connection we can connect to and we can create the actual answer. First we have to set our SDP as
local description:
// we create the answer
peerConnection.createAnswer(function(SDP){
// set our SDP as local description
peerConnection.setLocalDescription(SDP);
And now comes the point where we add the ICE-candidates (which we have received and stored
asynchronous before) to the connection:
// add other guy's ice-candidates to connection
for (var i = 0; i < othersCandidates.length; i++) {
if (othersCandidates[i].candidate) {
peerConnection.addIceCandidate(ceateRTCIceCandidate(othersCandidates[i].candidate));
}
}
The function ceateRTCIceCandidate() handles prefixes and creates a new RTCIceCandidate-Object and
returns it. Then we can send a message to offerer containing our SDP:
// send SDP to other guy
var data = {
type: 'answer',
roomId: roomId,
payload: SDP
};
sendToServer(data);
11/12
11/12/2014
And now we are back on the offerer's side. Here we store the SDP and call the function
handshakeDone()
Inside the 'handshakeDone()'-function we set the SDP of the answerer as remote description and add his
ICE-candidates to the peer-connection:
peerConnection.setRemoteDescription(createRTCSessionDescription(otherSDP));
// add other guy's ice-candidates to connection
for (var i = 0; i < othersCandidates.length; i++) {
if (othersCandidates[i].candidate) {
peerConnection.addIceCandidate(ceateRTCIceCandidate(othersCandidates[i].candidate));
}
}
Now the peer-connection should be up and running and the .onaddstream() -eventhandlers should also
have been called. But we cannot see anything yet on the offerer's side because the room-section is
hidden. That's why need to fire an event which tells the view that the connection is ready:
// fire event
socketEvent.eventType = 'p2pConnectionReady';
document.dispatchEvent(socketEvent);
Go back to overview
e-mail | twitter
felixhagspiel.de
B My Blog
Privacy
http://blog.felixhagspiel.de/index.php/posts/create-your-own-videochat-application-with-html-and-javascript
12/12