WebRTC PeerConnection » Tutorial ® Muaz Khan

HOME © Muaz Khan . @WebRTCWeb . Github . Latest issues . What's New?

Explains how to

  1. use WebRTC peer connection API
  2. write one-to-one video sharing application
  3. use socket.io or websockets for signaling

Suggestions

  1. If you're newcomer, newbie or beginner; you're suggested to try RTCMultiConnection.js or DataChannel.js libraries.
  2. Another recommended tutorial is: How to use RTCPeerConnection.js? (v1.5)

WebRTC PeerConnection API

  1. getUserMedia API to attach local media stream (webcam/microphone)
  2. Offer/Answer model to establish connection between two users
  3. ICE Server (STUN/TURN) to pass firewalls and NATs
  4. Signaling server to share offer/answer messages; or ice candidates among users

An PeerConnection object can be initialized like this:

var connection = new [webkit|moz]RTCPeerConnection(
    'ice-servers', 
    'optional-arguments'
);
  1. You can suggest one ore more ICE servers using 1st parameter. It is an array of ICE servers.
  2. You can suggest for stuff like "open data connection" or "prefer DTLS/SRTP" using 2nd parameter

Here is a simple example to create offer:

var connection = new [webkit|moz]RTCPeerConnection(
    'ice-servers', 
    'optional-arguments'
);

connection.createOffer(getOfferSDP, onfailure, sdpConstraints);
function getOfferSDP(offerSDP) {
    connection.setLocalDescription(offerSDP [, successCallback, failureCallback]);
    
    console.log('offer sdp', offerSDP.sdp);
    console.log('type',      offerSDP.type);
};

Here is a simple example to create answer:

var connection = new [webkit|moz]RTCPeerConnection(
    'ice-servers', 
    'optional-arguments'
);

// "setRemoteDescription" is quickly called for answerer
var remoteSessionDescription = new RTCSessionDescription(offerSDP);
connection.setRemoteDescription(remoteSessionDescription, successCallback, failureCallback);

connection.createAnswer(getAnswerSDP, onfailure, sdpConstraints);
function getAnswerSDP(answerSDP) {
    connection.setLocalDescription(answerSDP);
    
    console.log('answer sdp', answerSDP.sdp);
    console.log('type',       answerSDP.type);
};

You created both offer and answer; now complete the handshake:

// "setRemoteDescription" is called "later" for offerer
// it will complete the offer/answer handshake

var remoteSessionDescription = new RTCSessionDescription(answerSDP);
connection.setRemoteDescription(remoteSessionDescription, successCallback, failureCallback);

A working example with WebSockets

  1. A websocket connection will be opened for both users
  2. 1st user will create offer; and share with 2nd user via websocket connection
  3. 2nd user will create answer; and share with 1st user via websocket connection
  4. Both users will share ICE candidates too; with each other; accordingly

So, 1st step is to open websocket connection:

var socket       = new WebSocket('ws://domain.com:80/');
socket.onopen    = function() {};
socket.onmessage = function() {};

Assuming that there is a "Start Peer Connection" button; when offerer will click it; we will create offer on his side; and share with other user:

<button id="start-peer-connection">Start Peer Connection</button>

// javascript code
var button = document.getElementById('start-peer-connection');
button.onclick = function() {
    this.disabled = true;

    var peer = new [webkit|moz]RTCPeerConnection(iceServers, optional);    
    peer.createOffer(function(offerSDP) {
        peer.setLocalDescription(offerSDP);
        socket.send({
            targetUser: 'target-user-id',
            offerSDP: offerSDP
        });
    }, onfailure, sdpConstraints);
};

Defining "iceServers" and "optional" variables:

var STUN = {
    urls: 'stun:stun.l.google.com:19302'
};

var TURN = {
    urls: 'turn:turn.bistri.com:80',
    credential: 'homeo',
    username: 'homeo'
};

var iceServers = {
   iceServers: [STUN, TURN]
};

// DTLS/SRTP is preferred on chrome
// to interop with Firefox
// which supports them by default

var DtlsSrtpKeyAgreement = {
   DtlsSrtpKeyAgreement: true
};

var optional = {
   optional: [DtlsSrtpKeyAgreement]
};

Here is a list of events and methods can be used with RTCPeerConnection object:

  1. onicecandidate
  2. onaddstream
  3. oniceconnectionstatechange
  4. onsignalingstatechange
  5. onremovestream
  6. addStream
  7. removeStream
  8. close
  9. + many others

Each method and event has its own importance; however, for the sake of simplicity, we will try only "onicecandidate" and "onaddstream":

  1. PeerConnection on chrome performs ICE trickling process to track list of all available ICE candidates for current user; a browsers usually make UDP requests to ICE server (i.e. STUN or TURN) to gather ICE candidates will be used to traverse the NAT of current user. "onicecandidate" event is fired for each trickled ICE candidate.
  2. "onaddstream" is fired to return remote media stream attached by the other user.

ICE trackling is not necessary; even it is not part of WebRTC specification. Alternatives will be explained later in this document.

peer.onaddstream = function(mediaStream) {
    video.src = webkitURL.createObjectURL(mediaStream);
};

peer.onicecandidate = function(event) {
    var candidate = event.candidate;
    if(candidate) {
        socket.send({
            targetUser: 'target-user-id',
            candidate: candidate
        });
    }
};

A media-stream can be attached using "addStream" method:

// it is suggested to use gumadapter.js instead:
// https://github.com/muaz-khan/gumadapter

navigator.webkitGetUserMedia(MediaConstraints, OnMediaSuccess, OnMediaError);
var MediaConstraints = {
    audio: true,
    video: true
};

function OnMediaError(error) {
    console.error(error);
}

function OnMediaSuccess(mediaStream) {
    peer.addStream(mediaStream);
}

Remeber, chrome now supports attachment of multiple media streams:

peer.addStream(audioStream);
peer.addStream(videoStream);
peer.addStream(screenCapturingStream);

It will fire "onaddstream" multiple times according to number of media streams attached. Duplicate streams attachment is not allowed.

Putting above all together:

<button id="start-peer-connection">Start Peer Connection</button>

// javascript code
var button = document.getElementById('start-peer-connection');
button.onclick = function() {
    this.disabled = true;
    
    // it is suggested to use gumadapter.js instead:
    // https://github.com/muaz-khan/gumadapter
    navigator.webkitGetUserMedia(MediaConstraints, OnMediaSuccess, OnMediaError);
    var MediaConstraints = {
        audio: true,
        video: true
    };

    function OnMediaError(error) {
        console.error(error);
    }

    function OnMediaSuccess(mediaStream) {
        var peer = new [webkit|moz]RTCPeerConnection(iceServers, optional);
        
        peer.addStream(mediaStream);
        
        peer.onaddstream = function(mediaStream) {
            video.src = webkitURL.createObjectURL(mediaStream);
        };

        peer.onicecandidate = function(event) {
            var candidate = event.candidate;
            if(candidate) {
                socket.send({
                    targetUser: 'target-user-id',
                    candidate: candidate
                });
            }
        };
        
        peer.createOffer(function(offerSDP) {
            peer.setLocalDescription(offerSDP, successCallback, failureCallback);
            socket.send({
                targetUser: 'target-user-id',
                offerSDP: offerSDP
            });
        }, onfailure, sdpConstraints);
    }
};

var STUN = {
    urls: 'stun:stun.l.google.com:19302'
};

var TURN = {
    urls: 'turn:turn.bistri.com:80',
    credential: 'homeo',
    username: 'homeo'
};

var iceServers = {
   iceServers: [STUN, TURN]
};

// DTLS/SRTP is preferred on chrome
// to interop with Firefox
// which supports them by default

var DtlsSrtpKeyAgreement = {
   DtlsSrtpKeyAgreement: true
};

var optional = {
   optional: [DtlsSrtpKeyAgreement]
};

OK. Offer is completed. Now, it is time to create answer.

  1. Create answer only when you've offer
  2. "setRemoteDescription" should be called earlier before creating answer
socket.onmessage =  function(e) {
    var data = e.data;
    if(data.targetUser !== self && data.offerSDP) {
        createAnswer(offerSDP);
    }
};
function createAnswer(offerSDP) {
    // it is suggested to use gumadapter.js instead:
    // https://github.com/muaz-khan/gumadapter
    navigator.webkitGetUserMedia(MediaConstraints, OnMediaSuccess, OnMediaError);
    var MediaConstraints = {
        audio: true,
        video: true
    };

    function OnMediaError(error) {
        console.error(error);
    }

    function OnMediaSuccess(mediaStream) {
        var peer = new [webkit|moz]RTCPeerConnection(iceServers, optional);
        
        peer.addStream(mediaStream);
        
        peer.onaddstream = function(mediaStream) {
            video.src = webkitURL.createObjectURL(mediaStream);
        };

        peer.onicecandidate = function(event) {
            var candidate = event.candidate;
            if(candidate) {
                socket.send({
                    targetUser: 'target-user-id',
                    candidate: candidate
                });
            }
        };
        
        // remote-descriptions should be set earlier
        // using offer-sdp provided by the offerer
        var remoteDescription = new RTCSessionDescription(offerSDP);
        peer.setRemoteDescription(remoteDescription, successCallback, failureCallback);
		
        peer.createAnswer(function(answerSDP) {
            peer.setLocalDescription(answerSDP, successCallback, failureCallback);
            socket.send({
                targetUser: 'target-user-id',
                answerSDP: answerSDP
            });
        }, onfailure, sdpConstraints);
    }
};

Answer is also completed. Now, second last step is to set remote descriptions for offerer; same as we did for answerer:

socket.onmessage =  function(e) {
    var data = e.data;
    if(data.targetUser !== self && data.answerSDP) {
        // completing the handshake; this code is for offerer
        var remoteDescription = new RTCSessionDescription(answerSDP);
        peer.setRemoteDescription(remoteDescription, successCallback, failureCallback);
    }
};

Now, last step is to "add ICE candidate" for each peer. It works like this:

  1. Offerer gathers ICE candidates using ICE trickling process
  2. Offerer sends those candidates to answerer
  3. Answerer adds those candidates using "addIceCandidate" method
  4. Answerer also gathers its own ICE candidates using same ICE trickling process
  5. Answerer also sends his candidates to offerer
  6. Offerer also adds those candidates using "addIceCandidate" method

Again, ICE trickling is not "officially" included in WebRTC specification; so, it is chrome-only feature. Firefox merges all ice candidates in session descriptions. You can merge candidates in offerer/answer sdp on chrome too; see next section.

socket.onmessage =  function(e) {
    var data = e.data;
    if(data.targetUser !== self && data.candidate) {
        var candidate     = data.candidate.candidate;
        var sdpMLineIndex = data.candidate.sdpMLineIndex;
		
        peer.addIceCandidate(new [moz]RTCIceCandidate({
            sdpMLineIndex: sdpMLineIndex,
            candidate    : candidate
        }), successCallback, failureCallback);
    }
};

How to merge ice candidates?

  1. Detect "null" candidate in "onicecandidate" event
  2. Detect "iceGatheringState == 'complete'" in "ongatheringchange"
peer.onicecandidate =  function(e) {
    var candidate = e.candidate;
    // typeof candidate == 'undefined'
    // !candidate -or- !!candidate == false
	
    if(typeof candidate == 'undefined') {
        send_SDP();
    }
};

peer.ongatheringchange =  function(e) {
    if (e.currentTarget &&
        e.currentTarget.iceGatheringState === 'complete') {
        send_SDP();
    }
};

function send_SDP() {
    socket.send({
        targetUser: 'target-user-id',
        sdp       :  peer.localDescription
    });
}

How to use socket.io for signaling?

var socket = io.connect('http://domain.com:80/');
socket.on('message', function(data) {
    if(data.offerSDP)  {}
    if(data.answerSDP) {}
    if(data.candidate) {}
});

You can use either "send" or "emit" method to send the data. However, it is recommended to override "send" method like this:

socket.send = function(data) {
    socket.emit('message', data);
});

// Now you can send "any-kind" of data like this:
socket.send('string');
socket.send([array]);
socket.send({
    targetUser: 'target-user-id',
    sdp       : 'offerSDP || answerSDP'
});

sdpConstraints?

// for Chrome:
var sdpConstraints = {
    optional: [],
    mandatory: {
        OfferToReceiveAudio: true,
        OfferToReceiveVideo: true
    }
};

// for Firefox:
var sdpConstraints = {
    OfferToReceiveAudio: true,
    OfferToReceiveVideo: true
};

Demos? Examples?

  1. https://www.webrtc-experiment.com/socket.io/
  2. https://www.webrtc-experiment.com/websocket/

Simplest Demo using socket.io

// http://cdn.webrtc-experiment.com/socket.io/PeerConnection.js

var offerer = new PeerConnection('http://domain:port', 'message', 'offerer');
offerer.onStreamAdded = function(e) {
   document.body.appendChild(e.mediaElement);
};
var answerer = new PeerConnection('http://domain:port', 'message', 'answerer');
answerer.onStreamAdded = function(e) {
   document.body.appendChild(e.mediaElement);
};
answerer.sendParticipationRequest('offerer');

Latest Updates

Feedback

Enter your email too; if you want "direct" reply!