Echtzeitkommunikation mit WebRTC enträtselt und ohne Schnickschnack

WebRTC-Logo

WebRTC-Logo von http://www.webrtc.org/ 

Es scheint so, dass WebRTC sich zukünftig zu einem Standard für die Echtzeitkommunikation im Browser mausern könnte. Denn auch wenn WebRTC noch weit von einem Browser-übergreifenden Standard entfernt ist, reicht der aktuelle Stand durchaus schon aus, erste Gehversuche wenn nicht sogar bereits erste Anwendungen zu wagen. Für die effiziente Arbeit gibt es einen ganzen Haufen an JavaScript-Bibliotheken, welche die Komplexität und eventuelle Unzulänglichkeiten kompensieren. Nur leider ist es dann auch schwierig, die eigentliche Arbeitsweise zu verstehen und möglichen Problemen auf den Grund zu gehen. Darum dieser Artikel rund um WebRTC mal ganz ohne Schnickschnack.

Audio und Video

Viele Entwickler verstehen unter WebRTC in erster Linie das Einbinden der Webcam. Dieses gestaltet sich mit WebRTC in der Tat auch sehr einfach und gelingt im Google-Chrome z. B. mit JavaScript über navigator.webkitGetUserMedia (im Firefox heißt es aktuell navigator.mozGetUserMedia):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Stream
var senderStream;
var constraints = {audio: true, video: true};
var startButton = document.getElementById("startButton");
startButton.onclick =  start;
 
function start() {
    // Request sender stream
    console.log("request local stream");
    // Exact method varies based on browser
    navigator.webkitGetUserMedia(constraints, gotStreamSuccessCallback, errorCallback);
}
 
function gotStreamSuccessCallback(stream) {
    var senderVideo = document.getElementById("senderVideo");
    senderVideo.src = window.URL.createObjectURL(stream);
    senderStream = stream;
}
 
function errorCallback(error) {
    console.log("navigator.getUserMedia error: ", error);
}

Echtzeitkommunikation

simple-web-rtc-call-flow

Das Sequenzdiagramm aus der Spezifikation unter http://dev.w3.org/2011/webrtc/editor/webrtc.html  ist auf den ersten Blick wenig erhellend. Dabei ist der Ablauf garnicht so kompliziert…

Die Verarbeitung von Medien-Streams in Form von Audio und Video ist nur ein Aspekt von WebRTC. Denn die eigentliche Stärke ist die Echtzeitkommunikation über Browser hinweg per RTCPeerConnection. Die Arbeitsweise ist eigentlich ganz einfach:

  1. Der Sender generiert (nicht sendet, sondern generiert nur) ein Angebot (createOffer). In so einem Angebot (Session Description Protocol, kurz SDP) stehen die verfügbaren Codecs und Streams und jedes Mal, wenn sich ein Angebot ändert, muss das auch entsprechend bekannt gemacht und aktualisiert werden.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    // RTCPeerConnection WITHOUT the use of a server
    var servers = null;
    var senderPeerConnection;
    var senderIceCandidates = [];
    var callButton = document.getElementById("callButton");
    callButton.onclick = call;
     
    function call() {
     
        // create sender peer connection
        console.log("create sender peer connection");
        senderPeerConnection = new webkitRTCPeerConnection(servers);
        senderPeerConnection.onicecandidate = gotSenderIceCandidate;
     
        // add stream
        senderPeerConnection.addStream(senderStream);
     
        // create offer
        senderPeerConnection.createOffer(gotSenderDescription);
     
    }
     
    function gotSenderDescription(description){
        // console.log("offer from senderPeerConnection: \n" + description.sdp);
        senderPeerConnection.setLocalDescription(description);
        var senderDescriptionText = document.getElementById("senderDescriptionText");
        senderDescriptionText.value = description.sdp;
    }
  2. Neben diesem Angebot werden noch die eigentlichen Verbindungsmöglichkeiten für die Empfänger erzeugt. Die Verbindungsmöglichkeiten (Interactive Connectivity Establishment, kurz ICE) umfassen die möglichen IP-Adressen, Ports usw. – letzteres kann wie in diesem Beispiel sowohl lokal erzeugt oder von einem sogenannten TURN- oder STUN-Server generiert werden.
    1
    2
    3
    4
    5
    6
    7
    
    function gotSenderIceCandidate(event) {
        if (event.candidate) {
            var senderIceText = document.getElementById("senderIceText");
            senderIceCandidates.push(event.candidate);
            senderIceText.value = JSON.stringify(senderIceCandidates);
        }
    }
  3. Der Empfänger erhält das Angebot auf einem beliebigen Weg. Normalerweise über einen Server oder eine Socket-Verbindung. Aber es geht auch wie hier im Beispiel einfach per „Copy and Paste“ vom einen Browser zum anderen (oder sogar per Email). An dieser Stelle sollte die Verbindung auch auf den Empfang von Medienstreams vorbereitet werden (onaddstream).
  4. Anschließend erzeugt der Empfänger eine Antwort (createAnswer). In dieser Antwort stehen genauso wie in dem Angebot die verfügbaren Streams und Codecs (SDP).
  5. Nun werden die Verbindungsmöglichkeiten (ICE) des Senders verarbeitet. Auch diese können auf einem beliebigen Weg übertragen werden.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    var servers = null;
    var remotePeerConnection;
    var remoteIceCandidates = [];
    var answerButton = document.getElementById("answerButton");
    answerButton.onclick = answer;
     
    function answer() {
        // create remote peer connection
        console.log("create remote peer connection");
        remotePeerConnection = new webkitRTCPeerConnection(servers);
        remotePeerConnection.onicecandidate = gotRemoteIceCandidate;
        remotePeerConnection.onaddstream = gotRemoteStream;
     
        var senderDescriptionText = document.getElementById("senderDescriptionText");
        var senderDescription = new RTCSessionDescription({sdp:senderDescriptionText.value, type: "offer" });
        remotePeerConnection.setRemoteDescription(senderDescription);
        remotePeerConnection.createAnswer(gotRemoteDescription);
    }
     
    function gotRemoteStream(event) {
        console.log("gotRemoteStream stream: " + event.stream);
        var remoteVideo = document.getElementById("remoteVideo");
        remoteVideo.src = webkitURL.createObjectURL(event.stream);
    }
     
    function gotRemoteDescription(description) {
        // console.log("offer from remotePeerConnection: \n" + description.sdp);
        remotePeerConnection.setLocalDescription(description);
        var remoteDescriptionText = document.getElementById("remoteDescriptionText");
        remoteDescriptionText.value = description.sdp;
     
        // list of candidates
        var senderIceText = document.getElementById("senderIceText");
        var senderIceCandidates = JSON.parse(senderIceText.value);
        for (var i in senderIceCandidates) {
            var senderIceCandidate = new RTCIceCandidate(senderIceCandidates[i]);
            remotePeerConnection.addIceCandidate(senderIceCandidate);
        }
    }
  6. Da es sich um eine Peer-to-Peer-Verbindung (P2P) handelt, benötigt auch der Sender Verbindungsdaten (ICE), die der Empfänger nun generiert.
    1
    2
    3
    4
    5
    6
    7
    8
    
    function gotRemoteIceCandidate(event){
        if (event.candidate) {
            // console.log("gotRemoteIceCandidate candidate: " + event.candidate.candidate);
            var remoteIceText = document.getElementById("remoteIceText");
            remoteIceCandidates.push(event.candidate);
            remoteIceText.value = JSON.stringify(remoteIceCandidates);
        }
    }
  7. Jetzt müssen sowohl die Antwort und die Verbindungsdaten noch zurück zum Sender geschickt und dort dann verarbeitet werden, um eine Verbindung auszuhandeln. Um die Details kümmert sich WebRTC nun glücklicherweise selbständig.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    var handleAnswerButton = document.getElementById("handleAnswerButton");
    handleAnswerButton.onclick = handleAnswer;
     
    function handleAnswer() {
     
        var remoteDescriptionText = document.getElementById("remoteDescriptionText");
        var remoteDescription = new RTCSessionDescription({sdp: remoteDescriptionText.value, type: "answer"});
        senderPeerConnection.setRemoteDescription(remoteDescription);
     
        // list of candidates
        var remoteIceText = document.getElementById("remoteIceText");
        var remoteIceCandidates = JSON.parse(remoteIceText.value);
        for (var i in remoteIceCandidates) {
            var remoteICECandidate = new RTCIceCandidate(remoteIceCandidates[i]);
            senderPeerConnection.addIceCandidate(remoteICECandidate);
        }
     
    }

Und nun?

Klar, das hier ist nur eine Richtung und würde sich rein auf das Beobachten beschränken. Auch fehlen eine adäquate Software-Architektur sowie eine Fehlerbehandlung. Und der Austausch der Verbindungsdaten per „Copy and Paste“ ist vermutlich auch nur in wenigen Szenarien sinnvoll. Dennoch ist das ein guter Startpunkt, um Schritt-für-Schritt genau diese fehlenden Aspekte hinzuzufügen oder ggf. auf eine der zahlreichen Bibliotheken dafür zurückzugreifen – die beiden HTML-Dokumente für den Sender und den Empfänger finden sich dafür hier als möglichst einfach gehaltener Quellcode.