Tag Archives: JavaScript

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.

Transkription: Anrufe als Emails erhalten

Für die natürliche Nutzung eines Systems ist Spracherkennung in vielen Szenarien eine Option. Ganz egal ob auf Schlüsselwörter gelauscht oder vordefinierten Grammatiken zum Einsatz kommen. Daneben gibt es auch noch die Transkription, welche gesprochene Sprache als lesbaren Text darstellt. Der Developer Garden bietet hier gleich mehrere Lösungen, wie unter http://www.wolter.biz/?p=1368 beschrieben. Neu ist, dass für die Transkription zum einen der in dem Artikel erwähnte Trick nicht mehr notwendig ist und dass zum anderen nun mehr als 40 Sprachen, darunter auch Deutsch, unterstützt werden:

  • English (AUS): en_AU
  • English (UK): en_GB
  • English (US): en_US
  • Arabic (Egypt): ar_EG
  • Arabic (Saudi): ar_SA
  • Arabic (UAE): ar_AE
  • Cantonese Chinese: zh_HK
  • Catalan (Spain): ca_ES
  • Croatian: hr_HR
  • Czech: cs_CZ
  • Danish: da_DK
  • Dutch: nl_NL
  • Finnish: fi_FI
  • French (CAN): fr_CA
  • French (EU): fr_FR
  • German: de_DE
  • Greek: el_GR
  • Hebrew: he_IL
  • Hungarian: hu_HU
  • Indonesian: id_ID
  • Italian: it_IT
  • Japanese: ja_JP
  • Korean: ko_KR
  • Malay: ms_MY
  • Mandarin Chinese: cn_MA
  • Taiwanese Mandarin: zh_TW
  • Norwegian: no_NO
  • Polish: pl_PL
  • Portuguese (BR): pt_BR
  • Portuguese (EU): pt_PT
  • Romanian: ro_RO
  • Russian: ru_RU
  • Slovak: sk_SK
  • Spanish (EU): es_ES
  • Spanish (MX): es_MX
  • Spanish (US): es_US
  • Swedish: sv_SE
  • Thai: th_TH
  • Turkish: tr_TR
  • Ukranian: uk_UA
  • Vietnamese: vi_VN

Die Programmierung gestaltet sich wie gewohnt sehr einfach. In der Scripting API von Tropo sieht das dann mit Angabe des transcriptionLanguage Parameters so aus:

1
2
3
4
5
6
7
8
record("Sag uns bitte, wie es Dir geht!", {
   voice: "Katrin",    
   beep: true,    
   maxTime: 600,
   transcriptionID: currentCall.callerID,
   transcriptionLanguage: "de_DE",   
   transcriptionOutURI: "mailto:me@mymail.com"
});

Nur eine Rufnummer, aber viele Empfänger

Das Weiterleiten einer zentralen Rufnummer an mehrere Empfänger geht mit Telekom Tropo und ein wenig JavaScript ganz einfach. Beispielsweise für eine einfache Art von Support- oder Business-Kontakt auf einer Website, bei der der Mitarbeiter, der gerade Zeit hat, den Anruf entgegen nimmt. Dafür sollte aber sichergestellt werden, dass die Rufnummer des Anrufers in jedem Fall gemerkt wird. Eine einfache Lösung bietet das folgende JavaScript für die Scripting API von Telekom Tropo. So ist kein weiterer Aufwand in Form von Hosting oder einer Datenbank oder so notwendig. Selbst das Merken der Rufnummer erledigt dieses Skript ohne weitere Hilfe, indem einfach eine SMS mit den Details gesendet wird.

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
var callerID = "+" + currentCall.callerID;
var calledID = "+" + currentCall.calledID;
 
// All receiver get the call transferred, but the first one wins
var transferToID = ["+4912345", "+493012345"];
var sendNotificationToID = "+4912345";
 
// Notify at least one person in case nobody answers the call
message("Received voice call to Tropo business contact (" + calledID + ") from " + callerID, {
    to:sendNotificationToID,
    network:"SMS"
});
 
say("Welcome to Developer Garden!", {
   voice: "Simon"
});
say("Herzlich Willkommen!", {
   voice: "Katrin"
});
say("For support or general questions please go to our website.", {
   voice: "Simon"
});
say("Nutzen sie fuer allgemeine Fragen bitte unsere Website.", {
   voice: "Katrin"
});
say("Dont hang up, you will be connected!", {
   voice: "Simon"
});
say("Bitte legen sie nicht auf, sie werden verbunden!", {
   voice: "Katrin"
});
 
transfer(transferToID, {
   callerID: calledID,
   playvalue: "http://www.phono.com/audio/holdmusic.mp3" /* only in case there is no ring tone */,
   answerOnMedia: true,
   voice: "Simon"
});