WebRTC Walk Through

GET https://example.org/chatrooms/foo
HTTP/1.1 200 OK
<video id='remote'></video>
<video id='self'></video>
var webcamP = navigator.mediaDevices.getUserMedia({video:true, audio: true})
    .then(showSelfVideo);

var showSelfVideo = function (stream) {
  var self = document.getElementById('self');
  self.srcObj = stream;
  self.play();
};

Permissions

navigator.mediaDevices.getUserMedia({…})
   .then(showSelfVideo).catch(handleError);

equivalent to

navigator.getUserMedia({…}, showSelfVideo,
   handleError);

Promises

Constraints

getUserMedia({video: true, audio: true})
{video: { width:  {min: 640, ideal: 1280},
          height: {min: 400, ideal: 720}},
 audio: { echoCancellation: false }}

Constraints

function showSelfVideo(stream) {…}

stream is a MediaStream object:

Sources of MediaStream

MediaStream semantics

MediaStream tracks

Capabilities

GET https://example.org/chatrooms/foo
var signalingChannel = new WebSocket("wss://example.org/signaling");
document.getElementById("call").addEventListener("click", startCall);

var pc;
var startCall = function () {
  pc = new RTCPeerConnection();
  pc.createOffer().then(startSignalling);
};

var startSignalling = function (offer) {
    pc.setLocalDescription(offer);
    signalingChannel.send(offer);
};

Signaling

Signaling functions

If both peers have public IP addresses, finding a direct path between them only requires exchanging these IP addresses

If one or both peers are behind firewalls or NAT, they don’t know how to reach one another:

STUN

If STUN works, then the peers will be able to exchange media directly via their discovered IP addresses

If STUN is not enough (e.g. UDP traffic is restricted), we have one last option: using an authorized relay for UPD traffic.

TURN

ICE

var pc = new RTCPeerConnection(
{ iceServers: [
    {url: "stun:stun.example.com:19302"},
    {url: "turns:turn.example.com:45522", username: "foo", credential: "bar"}
  ]
);
pc.addEventListener("icecandidate", sendOverSignalingChannel);

⚠ Private IP

Understanding each other

Codecs
  • browsers negotiate to find common codecs
  • for audio and for video
  • to ensure the negotiation succeeds, we need “mandatory to implement” (MTI) codecs

Audio MTI Codecs

Video Codecs

Negotiations

Negotiations

  • send this description to the peer and search common ground
  • exchange of “offers” and “answers”
  • Javascript Session Establishment Protocol
  • SDP

    var pc = new RTCPeerConnection(iceConfiguration);
    
    pc.createOffer().then(function (offer) {
      // offer.sdp has SDP representation of preferences and capabilities
      // in theory, the SDP can be tweaked here before being accepted
      pc.setLocalDescription(offer);
      // setLocalDescription also triggers ICE candidate gathering
    
      // We send our offer to the peer
      signalingChannel.send(offer);
    });
    
    pc.addEventListener("icecandidate", function (e) {
      signalingChannel.send({type: "icecandidate", candidate: e.candidate});
    });
    

    addTrack

    webcamP.then(setupCall);
    
    var setupCall = function(stream) {
      pc.addTrack(stream.getAudioTracks()[0]);
      pc.addTrack(stream.getVideoTracks()[0]);
      // as tracks of a new type have been added, the browser knows
      // negotiation is needed
      // → "negotationneeded" event triggered
    };
    pc.addEventListener("negotiationneeded", function () {
      pc.createOffer().then(function(offer) {
        // …
        signalingChannel.send(offer);
      });
    });

    ⚠ SDP as API

    signalingChannel.addEventListener("message", function (e) {
      if (!pc) {
        pc = new RTCPeerConnection(iceConfiguration);
        navigator.mediaDevices.getUserMedia({audio: true, video: true})
           .then(function(stream) { pc.addTrack(…);});
      }
      var msg = JSON.parse(e.data);
      switch(msg.type) {
        case "icecandidate": pc.addIceCandidate(new RTCIceCandidate(msg.candidate)); break;
        case "offer": pc.setRemoteDescription(msg)
                            .then(pc.createAnswer)
                            .then(pc.setLocalDescription)
                            .then(signalingChannel.send);
                      break;
        case "answer": pc.setRemoteDescription(msg);
      }
    });
    var remote = document.getElementById("remote");
    pc.addEventListener("track", function (track) {
      if (!remote.srcObj) {
        remote.srcObj = new MediaStream();
      }
      remote.srcObj.addTrack(track);
    });

    Media transmission

    var channel = pc.createDataChannel("filetransfer");
    channel.addEventListener("open"), function () {
      startFileTransfer();
    });
    var fileData = [];
    channel.addEventListener("message"), function(e) {
      fileData.push(e.data);
    });

    Data Channels

    navigator.mediaDevices.getOutputMedia({video: true})
           .then(displayLocally).then(sendToPeer);

    Screen Capture

    <select class=audio></select>
    var outputDevices = navigator.mediaDevices.enumerateDevices()
         .filter(function(d) { return d.kind === "audiooutput";});
    
    var audioSelectorUI = document.querySelector("select.audio");
    
    outputDevices.forEach(function (o) {
      var opt = document.createElement("option");
      opt.appendChild(document.createTextNode(o.label));
      document.querySelector("select.audio").appendChild(opt);
    });
    
    document.querySelector("select.audio").addEventListener("change", function (e) {
      remote.setSinkId(outputDevices[this.selectedIndex].deviceId);
    });
    

    Trust model

    Extra security

    navigator.mediaDevices.getUserMedia({video: true,
        peerIdentity: "[email protected]"});
    // the stream obtained from there can only be transmitted via
    // a peer connection constructed with
    var pc = new RTCConfiguration({peerIdentity: "[email protected]"});
    
    // This assumes the browser has been configured to know how to contact
    // the identity provider who can verify that Bob is the Bob Alice knows
    // The app can also help:
    pc.setIdentityProvider("example.net", "default", "alice");