Back-End-Dienste für WebRTC-App erstellen

Was ist Signalisierung?

Signalisieren ist der Prozess der Koordination der Kommunikation. Damit eine WebRTC-App einen Anruf einrichten kann, müssen ihre Clients die folgenden Informationen austauschen:

  • Sitzungssteuerungsnachrichten, mit denen die Kommunikation geöffnet oder geschlossen wird
  • Fehlermeldungen
  • Medienmetadaten wie Codecs, Codec-Einstellungen, Bandbreite und Medientypen
  • Schlüsseldaten, die zum Herstellen sicherer Verbindungen verwendet werden
  • Netzwerkdaten wie die IP-Adresse und der Port eines Hosts, wie sie von außen gesehen werden

Für diesen Signalisierungsprozess benötigen Clients eine Möglichkeit, Nachrichten hin und her zu senden. Dieser Mechanismus wird von den WebRTC APIs nicht implementiert. Sie müssen es selbst erstellen. Später in diesem Artikel erfahren Sie, wie Sie einen Signalisierungsdienst erstellen. Zuerst aber ein wenig Kontext.

Warum wird die Signalisierung nicht von WebRTC definiert?

Um Redundanz zu vermeiden und die Kompatibilität mit etablierten Technologien zu maximieren, werden Signalisierungsmethoden und ‑protokolle nicht durch WebRTC-Standards spezifiziert. Dieser Ansatz wird vom JavaScript Session Establishment Protocol (JSEP) beschrieben:

Die Architektur von JSEP verhindert auch, dass ein Browser den Status speichern muss, d. h. als Signalisierungs-Zustandsmaschine fungieren muss. Das wäre problematisch, wenn beispielsweise Signaldaten jedes Mal verloren gingen, wenn eine Seite neu geladen wird. Stattdessen kann der Signalisierungsstatus auf einem Server gespeichert werden.

JSEP-Architekturdiagramm
JSEP-Architektur

JSEP erfordert den Austausch zwischen Peers von offer und answer, den oben genannten Medienmetadaten. Angebote und Antworten werden im SDP-Format (Session Description Protocol) übertragen und sehen so aus:

v=0
o=- 7614219274584779017 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126
c=IN IP4 0.0.0.0
a=rtcp:1 IN IP4 0.0.0.0
a=ice-ufrag:W2TGCZw2NZHuwlnf
a=ice-pwd:xdQEccP40E+P0L5qTyzDgfmW
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=mid:audio
a=rtcp-mux
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQe
a=rtpmap:111 opus/48000/2

Du möchtest wissen, was all diese SDP-Fachbegriffe eigentlich bedeuten? Sehen Sie sich die Beispiele der Internet Engineering Task Force (IETF) an.

WebRTC ist so konzipiert, dass das Angebot oder die Antwort durch Bearbeiten der Werte im SDP-Text angepasst werden kann, bevor es als lokale oder Remote-Beschreibung festgelegt wird. Mit der preferAudioCodec()-Funktion in appr.tc können beispielsweise der Standard-Codec und die Standardbitrate festgelegt werden. SDP ist mit JavaScript etwas mühsam zu manipulieren und es wird diskutiert, ob zukünftige Versionen von WebRTC stattdessen JSON verwenden sollten. Es gibt jedoch einige Vorteile, bei SDP zu bleiben.

RTCPeerConnection API und Signalisierung: Angebot, Antwort und Kandidat

RTCPeerConnection ist die API, die von WebRTC-Apps verwendet wird, um eine Verbindung zwischen Peers herzustellen und Audio- und Videoinhalte zu übertragen.

Für die Initialisierung dieses Prozesses hat RTCPeerConnection zwei Aufgaben:

  • Ermitteln Sie die lokalen Medienbedingungen, z. B. Auflösung und Codec-Funktionen. Das sind die Metadaten, die für den Angebots- und Antwortmechanismus verwendet werden.
  • Potenzielle Netzwerkadressen für den Host der App abrufen, sogenannte Kandidaten.

Sobald diese lokalen Daten ermittelt wurden, müssen sie über einen Signalmechanismus mit dem Remote-Peer ausgetauscht werden.

Angenommen, Alice versucht, Eve anzurufen. Hier ist der vollständige Mechanismus für Angebote und Antworten in allen Details:

  1. Alice erstellt ein RTCPeerConnection-Objekt.
  2. Alice erstellt mit der Methode RTCPeerConnection createOffer() ein Angebot (eine SDP-Sitzungsbeschreibung).
  3. Alice ruft setLocalDescription() mit ihrem Angebot an.
  4. Alice stringisiert das Angebot und sendet es mit einem Signalmechanismus an Eve.
  5. Eva ruft setRemoteDescription() mit dem Angebot von Alice an, damit ihr RTCPeerConnection über die Einrichtung von Alice Bescheid weiß.
  6. Eva ruft createAnswer() auf und dem Erfolgs-Callback wird eine lokale Sitzungsbeschreibung übergeben – Evas Antwort.
  7. Eva legt ihre Antwort als lokale Beschreibung fest, indem sie setLocalDescription() aufruft.
  8. Eva sendet dann ihre stringierte Antwort über den Signalmechanismus an Alice.
  9. Alice verwendet setRemoteDescription(), um die Antwort von Eve als Beschreibung der Remotesitzung festzulegen.

Alice und Eve müssen außerdem Netzwerkinformationen austauschen. Der Ausdruck „Kandidaten finden“ bezieht sich auf das Finden von Netzwerkschnittstellen und ‑ports mit dem ICE-Framework.

  1. Alice erstellt ein RTCPeerConnection-Objekt mit einem onicecandidate-Handler.
  2. Der Handler wird aufgerufen, wenn Netzwerkkandidaten verfügbar werden.
  3. Im Handler sendet Alice stringisierte Kandidatendaten über ihren Signalisierungskanal an Eve.
  4. Wenn Eva eine Kandidatennachricht von Alice erhält, ruft sie addIceCandidate() auf, um den Kandidaten der Beschreibung des Remote-Peers hinzuzufügen.

JSEP unterstützt das ICE-Kandidaten-Trickling, mit dem der Anrufer dem Angerufenen nach dem ersten Angebot nach und nach Kandidaten zur Verfügung stellen kann. Der Angerufene kann dann mit dem Anruf beginnen und eine Verbindung herstellen, ohne auf alle Kandidaten zu warten.

WebRTC für Signalisierung codieren

Das folgende Code-Snippet ist ein W3C-Codebeispiel, das den gesamten Signalisierungsprozess zusammenfasst. Der Code setzt einen Signalmechanismus voraus, SignalingChannel. Signale werden später ausführlicher behandelt.

// handles JSON.stringify/parse
const signaling = new SignalingChannel();
const constraints = {audio: true, video: true};
const configuration = {iceServers: [{urls: 'stun:stun.example.org'}]};
const pc = new RTCPeerConnection(configuration);

// Send any ice candidates to the other peer.
pc.onicecandidate = ({candidate}) => signaling.send({candidate});

// Let the "negotiationneeded" event trigger offer generation.
pc.onnegotiationneeded = async () => {
  try {
    await pc.setLocalDescription(await pc.createOffer());
    // send the offer to the other peer
    signaling.send({desc: pc.localDescription});
  } catch (err) {
    console.error(err);
  }
};

// After remote track media arrives, show it in remote video element.
pc.ontrack = (event) => {
  // Don't set srcObject again if it is already set.
  if (remoteView.srcObject) return;
  remoteView.srcObject = event.streams[0];
};

// Call start() to initiate.
async function start() {
  try {
    // Get local stream, show it in self-view, and add it to be sent.
    const stream =
      await navigator.mediaDevices.getUserMedia(constraints);
    stream.getTracks().forEach((track) =>
      pc.addTrack(track, stream));
    selfView.srcObject = stream;
  } catch (err) {
    console.error(err);
  }
}

signaling.onmessage = async ({desc, candidate}) => {
  try {
    if (desc) {
      // If you get an offer, you need to reply with an answer.
      if (desc.type === 'offer') {
        await pc.setRemoteDescription(desc);
        const stream =
          await navigator.mediaDevices.getUserMedia(constraints);
        stream.getTracks().forEach((track) =>
          pc.addTrack(track, stream));
        await pc.setLocalDescription(await pc.createAnswer());
        signaling.send({desc: pc.localDescription});
      } else if (desc.type === 'answer') {
        await pc.setRemoteDescription(desc);
      } else {
        console.log('Unsupported SDP type.');
      }
    } else if (candidate) {
      await pc.addIceCandidate(candidate);
    }
  } catch (err) {
    console.error(err);
  }
};

Eine Demo der Prozesse „Angebot/Antwort“ und „Kandidatenaustausch“ finden Sie unter simpl.info RTCPeerConnection. Im Konsolenprotokoll finden Sie ein Beispiel für einen einseitigen Videochat. Wenn Sie mehr benötigen, laden Sie einen vollständigen Dump der WebRTC-Signalisierung und -Statistiken von der Seite about://webrtc-internals in Google Chrome oder von der Seite opera://webrtc-internals in Opera herunter.

Peer-Erkennung

Das ist eine elegante Art zu fragen: „Wie finde ich jemanden, mit dem ich sprechen kann?“

Für Telefonanrufe gibt es Telefonnummern und Verzeichnisse. Für Online-Videochats und ‑Messaging benötigen Sie Identitäts- und Anwesenheitsverwaltungssysteme sowie eine Möglichkeit für Nutzer, Sitzungen zu starten. WebRTC-Apps benötigen eine Möglichkeit, mit der Clients sich gegenseitig signalisieren können, dass sie einen Anruf starten oder daran teilnehmen möchten.

Peer-Erkennungsmechanismen werden nicht von WebRTC definiert und die Optionen werden hier nicht behandelt. Das kann so einfach sein wie das Senden einer URL per E-Mail oder Nachricht. Bei Videochat-Apps wie Talky, tawk.to und Browser Meeting können Sie andere Nutzer zu einem Anruf einladen, indem Sie einen benutzerdefinierten Link teilen. Der Entwickler Chris Ball hat einen interessanten serverlosen WebRTC-Test entwickelt, mit dem WebRTC-Anrufteilnehmer Metadaten über beliebige Messaging-Dienste wie IM, E-Mail oder Brieftaube austauschen können.

Wie können Sie einen Signalisierungsdienst erstellen?

Signalisierungsprotokolle und -mechanismen werden nicht durch WebRTC-Standards definiert. Unabhängig von Ihrer Entscheidung benötigen Sie einen Intermediärserver, um Signalisierungsnachrichten und App-Daten zwischen Clients auszutauschen. Leider kann eine Webanwendung nicht einfach ins Internet rufen: „Verbinde mich mit meinem Freund!“

Glücklicherweise sind Signalisierungsnachrichten klein und werden meistens zu Beginn eines Anrufs ausgetauscht. Bei Tests mit appr.tc für eine Videochat-Sitzung wurden insgesamt etwa 30–45 Nachrichten vom Signalisierungsdienst verarbeitet, mit einer Gesamtgröße von etwa 10 KB.

WebRTC-Signalisierungsdienste sind nicht nur relativ anspruchslos in Bezug auf die Bandbreite, sondern verbrauchen auch nicht viel Verarbeitungs- oder Arbeitsspeicher, da sie nur Nachrichten weiterleiten und nur eine kleine Menge an Sitzungsstatusdaten speichern müssen, z. B. welche Clients verbunden sind.

Push-Nachrichten vom Server an den Client

Ein Nachrichtendienst für die Signalisierung muss bidirektional sein: Client zu Server und Server zu Client. Die bidirektionale Kommunikation verstößt gegen das HTTP-Client/Server-Anfrage-/Antwortmodell. Im Laufe der Jahre wurden jedoch verschiedene Hacks wie Long Polling entwickelt, um Daten von einem Dienst, der auf einem Webserver ausgeführt wird, an eine Webanwendung zu senden, die in einem Browser ausgeführt wird.

In letzter Zeit wurde die EventSource API weitgehend implementiert. Dadurch werden serverseitig gesendete Ereignisse aktiviert, also Daten, die von einem Webserver über HTTP an einen Browserclient gesendet werden. EventSource ist für Einweg-Messaging konzipiert, kann aber in Kombination mit XHR verwendet werden, um einen Dienst zum Austausch von Signalisierungsnachrichten zu erstellen. Ein Signalisierungsdienst übergibt eine Nachricht von einem Anrufer, die per XHR-Anfrage gesendet wird, indem er sie über EventSource an den Angerufenen sendet.

WebSocket ist eine natürlichere Lösung, die für die Vollduplex-Client-Server-Kommunikation entwickelt wurde – Nachrichten können gleichzeitig in beide Richtungen fließen. Ein Vorteil eines Signalisierungsdienstes, der mit reinen WebSockets oder servergesendeten Ereignissen (EventSource) erstellt wurde, ist, dass das Backend für diese APIs in einer Vielzahl von Web-Frameworks implementiert werden kann, die für die meisten Webhosting-Pakete für Sprachen wie PHP, Python und Ruby üblich sind.

Alle modernen Browser außer Opera Mini unterstützen WebSocket. Noch wichtiger ist, dass alle Browser, die WebRTC unterstützen, auch WebSocket unterstützen, sowohl auf dem Computer als auch auf Mobilgeräten. TLS sollte für alle Verbindungen verwendet werden, damit Nachrichten nicht unverschlüsselt abgefangen werden können und um Probleme mit dem Proxy-Durchlauf zu reduzieren. Weitere Informationen zu WebSocket und Proxy-Durchlauf finden Sie im Kapitel zu WebRTC in High Performance Browser Networking von Ilya Grigorik.

Es ist auch möglich, die Signalisierung zu verarbeiten, indem WebRTC-Clients einen Messaging-Server wiederholt über Ajax abfragen. Dies führt jedoch zu vielen redundanten Netzwerkanfragen, was besonders für Mobilgeräte problematisch ist. Auch nach der Einrichtung einer Sitzung müssen Peers bei Änderungen oder Sitzungsabbrüchen durch andere Peers nach Signalisierungsnachrichten suchen. Im Beispiel der WebRTC Book-App wird diese Option mit einigen Optimierungen für die Abfragehäufigkeit verwendet.

Skalierungssignale

Obwohl ein Signalisierungsdienst relativ wenig Bandbreite und CPU pro Client verbraucht, müssen Signalisierungsserver für eine beliebte App möglicherweise viele Nachrichten von verschiedenen Standorten mit hoher Gleichzeitigkeit verarbeiten. WebRTC-Apps mit hohem Traffic benötigen Signalisierungsserver, die eine erhebliche Last bewältigen können. Sie gehen hier nicht näher darauf ein, aber es gibt eine Reihe von Optionen für leistungsstarke Nachrichten mit hohem Volumen, darunter:

  • Extensible Messaging and Presence Protocol (XMPP), ursprünglich als Jabber bekannt – ein für Instant Messaging entwickeltes Protokoll, das für die Signalisierung verwendet werden kann. Zu den Serverimplementierungen gehören ejabberd und Openfire. JavaScript-Clients wie Strophe.js verwenden BOSH, um bidirektionales Streaming zu emulieren. Aus verschiedenen Gründen ist BOSH jedoch möglicherweise nicht so effizient wie WebSocket und kann aus denselben Gründen nicht gut skaliert werden.) (Nebenbei: Jingle ist eine XMPP-Erweiterung, die Sprach- und Videoanrufe ermöglicht. Das WebRTC-Projekt verwendet Netzwerk- und Transportkomponenten aus der libjingle-Bibliothek, einer C++-Implementierung von Jingle.

  • Open-Source-Bibliotheken wie ZeroMQ (von TokBox für den Rumour-Dienst verwendet) und OpenMQ (NullMQ wendet ZeroMQ-Konzepte auf Webplattformen mit dem STOMP-Protokoll über WebSocket an.)

  • Kommerzielle Cloud-Messaging-Plattformen, die WebSocket verwenden (können aber auf Long Polling zurückgreifen), z. B. Pusher, Kaazing und PubNub (PubNub hat auch eine API für WebRTC).

  • Kommerzielle WebRTC-Plattformen wie vLine

Der Leitfaden zu Echtzeit-Webtechnologien von Entwickler Phil Leggetter enthält eine umfassende Liste von Messaging-Diensten und ‑Bibliotheken.

Signaldienst mit Socket.io auf Node erstellen

Im Folgenden finden Sie den Code für eine einfache Webanwendung, die einen Signalisierungsdienst verwendet, der mit Socket.io auf Node erstellt wurde. Das Design von Socket.io macht es einfach, einen Dienst zum Austausch von Nachrichten zu erstellen. Socket.io eignet sich aufgrund des integrierten Konzepts von Räumen besonders für die WebRTC-Signalisierung. Dieses Beispiel ist nicht für die Skalierung als Signaldienst in Produktionsqualität vorgesehen, sondern ist für eine relativ kleine Anzahl von Nutzern leicht verständlich.

Socket.io verwendet WebSocket mit Fallbacks: AJAX-Long-Polling, AJAX-Multipart-Streaming, Forever-Iframe und JSONP-Polling. Es wurde auf verschiedene Backends portiert, ist aber vielleicht am besten für seine Node-Version bekannt, die in diesem Beispiel verwendet wird.

In diesem Beispiel wird kein WebRTC verwendet. Es soll nur zeigen, wie Signalisierung in eine Webanwendung eingebunden wird. Im Konsolenprotokoll sehen Sie, was passiert, wenn Clients einem Raum beitreten und Nachrichten austauschen. In diesem WebRTC-Codelab finden Sie eine detaillierte Anleitung dazu, wie Sie diese Funktion in eine vollständige WebRTC-Videochat-App einbinden.

Hier ist der Client index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>WebRTC client</title>
  </head>
  <body>
    <script src='/socket.io/socket.io.js'></script>
    <script src='js/main.js'></script>
  </body>
</html>

Hier ist die JavaScript-Datei main.js, auf die im Client verwiesen wird:

const isInitiator;

room = prompt('Enter room name:');

const socket = io.connect();

if (room !== '') {
  console.log('Joining room ' + room);
  socket.emit('create or join', room);
}

socket.on('full', (room) => {
  console.log('Room ' + room + ' is full');
});

socket.on('empty', (room) => {
  isInitiator = true;
  console.log('Room ' + room + ' is empty');
});

socket.on('join', (room) => {
  console.log('Making request to join room ' + room);
  console.log('You are the initiator!');
});

socket.on('log', (array) => {
  console.log.apply(console, array);
});

Hier ist die vollständige Server-App:

const static = require('node-static');
const http = require('http');
const file = new(static.Server)();
const app = http.createServer(function (req, res) {
  file.serve(req, res);
}).listen(2013);

const io = require('socket.io').listen(app);

io.sockets.on('connection', (socket) => {

  // Convenience function to log server messages to the client
  function log(){
    const array = ['>>> Message from server: '];
    for (const i = 0; i < arguments.length; i++) {
      array.push(arguments[i]);
    }
      socket.emit('log', array);
  }

  socket.on('message', (message) => {
    log('Got message:', message);
    // For a real app, would be room only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', (room) => {
    const numClients = io.sockets.clients(room).length;

    log('Room ' + room + ' has ' + numClients + ' client(s)');
    log('Request to create or join room ' + room);

    if (numClients === 0){
      socket.join(room);
      socket.emit('created', room);
    } else if (numClients === 1) {
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room);
    } else { // max two clients
      socket.emit('full', room);
    }
    socket.emit('emit(): client ' + socket.id +
      ' joined room ' + room);
    socket.broadcast.emit('broadcast(): client ' + socket.id +
      ' joined room ' + room);

  });

});

(Sie müssen dazu nichts über node-static lernen. In diesem Beispiel wird es zufällig verwendet.)

Wenn Sie diese App auf dem Localhost ausführen möchten, müssen Sie Node, Socket.IO und node-static installiert haben. Node kann unter Node.js heruntergeladen werden. Die Installation ist einfach und schnell. Um Socket.IO und node-static zu installieren, führen Sie den Node Package Manager über ein Terminal in Ihrem App-Verzeichnis aus:

npm install socket.io
npm install node-static

Führen Sie den folgenden Befehl in einem Terminal in Ihrem App-Verzeichnis aus, um den Server zu starten:

node server.js

Öffnen Sie localhost:2013 in Ihrem Browser. Öffnen Sie in einem beliebigen Browser einen neuen Tab oder ein neues Fenster und öffnen Sie localhost:2013 noch einmal. In der Konsole sehen Sie, was passiert. In Chrome und Opera können Sie über die Google Chrome-Entwicklertools mit Ctrl+Shift+J (oder Command+Option+J auf einem Mac) auf die Console zugreifen.

Unabhängig davon, welchen Ansatz Sie für die Signalisierung wählen, müssen Ihr Backend und Ihre Client-App mindestens Dienste ähnlich wie in diesem Beispiel bereitstellen.

Fallstricke bei der Signalisierung

  • RTCPeerConnection beginnt erst mit der Suche nach Kandidaten, wenn setLocalDescription() aufgerufen wird. Dies ist im IETF-Entwurf für JSEP vorgeschrieben.
  • Nutzen Sie Trickle ICE. Rufen Sie addIceCandidate() an, sobald die Bewerber eintreffen.

Vorgefertigte Signalisierungsserver

Wenn Sie keinen eigenen Signalisierungsserver erstellen möchten, gibt es mehrere WebRTC-Signalisierungsserver, die wie im vorherigen Beispiel Socket.IO verwenden und in WebRTC-Client-JavaScript-Bibliotheken eingebunden sind:

  • webRTC.io ist eine der ersten Abstraktionsbibliotheken für WebRTC.
  • Signalmaster ist ein Signalisierungsserver, der für die Verwendung mit der JavaScript-Clientbibliothek SimpleWebRTC entwickelt wurde.

Wenn Sie keinen Code schreiben möchten, sind vollständige kommerzielle WebRTC-Plattformen von Unternehmen wie vLine, OpenTok und Asterisk verfügbar.

Zur Information: Ericsson hat in den frühen Tagen von WebRTC einen Signalisierungsserver mit PHP auf Apache entwickelt. Dieser Ansatz ist mittlerweile etwas veraltet, aber es lohnt sich, sich den Code anzusehen, wenn Sie etwas Ähnliches in Betracht ziehen.

Signalsicherheit

„Sicherheit ist die Kunst, nichts zu tun.“

Salman Rushdie

Die Verschlüsselung ist für alle WebRTC-Komponenten erforderlich.

Signalmechanismen werden jedoch nicht durch WebRTC-Standards definiert. Daher liegt es an Ihnen, die Signalisierung zu sichern. Wenn es einem Angreifer gelingt, das Signal zu manipulieren, kann er Sitzungen beenden, Verbindungen umleiten und Inhalte aufzeichnen, ändern oder einschleusen.

Der wichtigste Faktor für die Sicherheit der Signalisierung ist die Verwendung sicherer Protokolle wie HTTPS und WSS (z. B. TLS), die dafür sorgen, dass Nachrichten nicht unverschlüsselt abgefangen werden können. Achten Sie außerdem darauf, dass Sie Signalisierungsnachrichten nicht so senden, dass andere Anrufer, die denselben Signalisierungsserver verwenden, darauf zugreifen können.

Nach der Signalisierung: ICE verwenden, um NATs und Firewalls zu umgehen

Für die Metadatensignalisierung verwenden WebRTC-Apps einen Intermediärserver. Für das tatsächliche Streaming von Medien und Daten versucht RTCPeerConnection jedoch, nach der Einrichtung einer Sitzung Clients direkt oder Peer-to-Peer zu verbinden.

In einer einfacheren Welt hätte jeder WebRTC-Endpunkt eine eindeutige Adresse, die er mit anderen Peers austauschen könnte, um direkt zu kommunizieren.

Einfache Peer-to-Peer-Verbindung
Eine Welt ohne NATs und Firewalls

In der Realität befinden sich die meisten Geräte hinter einer oder mehreren NAT-Ebenen, einige haben Antivirensoftware, die bestimmte Ports und Protokolle blockiert, und viele befinden sich hinter Proxys und Unternehmensfirewalls. Eine Firewall und NAT können tatsächlich vom selben Gerät implementiert werden, z. B. von einem WLAN-Router für zu Hause.

Peers hinter NATs und Firewalls
Die reale Welt

WebRTC-Apps können das ICE-Framework verwenden, um die Komplexität der Netzwerke in der Praxis zu überwinden. Dazu muss deine App ICE-Server-URLs an RTCPeerConnection weitergeben, wie in diesem Artikel beschrieben.

ICE versucht, den besten Pfad zur Verbindung von Peers zu finden. Dabei werden alle Möglichkeiten parallel getestet und die effizienteste Option ausgewählt. ICE versucht zuerst, eine Verbindung über die Hostadresse herzustellen, die vom Betriebssystem und der Netzwerkkarte eines Geräts abgerufen wird. Wenn das fehlschlägt (was bei Geräten hinter NATs der Fall ist), erhält ICE eine externe Adresse über einen STUN-Server. Wenn das fehlschlägt, wird der Traffic über einen TURN-Relay-Server weitergeleitet.

Mit anderen Worten: Ein STUN-Server wird verwendet, um eine externe Netzwerkadresse abzurufen, und TURN-Server werden verwendet, um Traffic weiterzuleiten, wenn die direkte (Peer-to-Peer-)Verbindung fehlschlägt.

Jeder TURN-Server unterstützt STUN. Ein TURN-Server ist ein STUN-Server mit zusätzlichen integrierten Weiterleitungsfunktionen. ICE kommt auch mit den Komplexitäten von NAT-Konfigurationen zurecht. In der Praxis ist für das NAT-Hole-Punching möglicherweise mehr als nur eine öffentliche IP‑Port-Adresse erforderlich.

URLs für STUN- und/oder TURN-Server werden (optional) von einer WebRTC-App im iceServers-Konfigurationsobjekt angegeben, das das erste Argument für den RTCPeerConnection-Konstruktor ist. Für appr.tc sieht dieser Wert so aus:

{
  'iceServers': [
    {
      'urls': 'stun:stun.l.google.com:19302'
    },
    {
      'urls': 'turn:192.158.29.39:3478?transport=udp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    },
    {
      'urls': 'turn:192.158.29.39:3478?transport=tcp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    }
  ]
}

Sobald RTCPeerConnection diese Informationen hat, funktioniert ICE automatisch. RTCPeerConnection verwendet das ICE-Framework, um den besten Pfad zwischen Peers zu ermitteln, und arbeitet bei Bedarf mit STUN- und TURN-Servern zusammen.

STUN

NATs stellen einem Gerät eine IP-Adresse für die Verwendung in einem privaten lokalen Netzwerk zur Verfügung. Diese Adresse kann jedoch nicht extern verwendet werden. Ohne öffentliche Adresse können WebRTC-Peers nicht kommunizieren. Um dieses Problem zu umgehen, verwendet WebRTC STUN.

STUN-Server sind im öffentlichen Internet verfügbar und haben eine einfache Aufgabe:Sie prüfen die IP‑Port-Adresse einer eingehenden Anfrage (von einer App, die hinter einer NAT ausgeführt wird) und senden diese Adresse als Antwort zurück. Mit anderen Worten:Die App verwendet einen STUN-Server, um ihre IP-Adresse und ihren Port aus einer öffentlichen Perspektive zu ermitteln. Mit diesem Verfahren kann ein WebRTC-Peer eine öffentlich zugängliche Adresse für sich selbst abrufen und dann über einen Signalmechanismus an einen anderen Peer weitergeben, um eine direkte Verbindung herzustellen. In der Praxis funktionieren verschiedene NATs auf unterschiedliche Weise und es kann mehrere NAT-Ebenen geben, aber das Prinzip bleibt gleich.

STUN-Server müssen nicht viel tun oder sich viel merken, sodass relativ leistungsschwache STUN-Server eine große Anzahl von Anfragen verarbeiten können.

Bei den meisten WebRTC-Anrufen wird eine Verbindung über STUN hergestellt – laut Webrtcstats.com in 86% der Fälle. Bei Anrufen zwischen Peers hinter Firewalls und komplexen NAT-Konfigurationen kann dies jedoch geringer sein.

Peer-to-Peer-Verbindung über einen STUN-Server
STUN-Server zum Abrufen öffentlicher IP‑Port-Adressen verwenden

TURN

RTCPeerConnection versucht, eine direkte Kommunikation zwischen Peers über UDP einzurichten. Wenn das nicht funktioniert, wechselt RTCPeerConnection zu TCP. Wenn das fehlschlägt, können TURN-Server als Fallback verwendet werden, um Daten zwischen Endpunkten weiterzuleiten.

Zur Wiederholung: TURN wird verwendet, um Audio-, Video- und Datenstreaming zwischen Peers weiterzuleiten, nicht Signaldaten.

TURN-Server haben öffentliche Adressen und können daher von Peers kontaktiert werden, auch wenn sich die Peers hinter Firewalls oder Proxys befinden. TURN-Server haben eine konzeptionell einfache Aufgabe: einen Stream weiterleiten. Im Gegensatz zu STUN-Servern verbrauchen sie jedoch von Natur aus viel Bandbreite. Mit anderen Worten: TURN-Server müssen leistungsfähiger sein.

Peer-to-Peer-Verbindung über einen STUN-Server
Ganz oder gar nicht: STUN, TURN und Signalisierung

Dieses Diagramm zeigt TURN in Aktion. Reine STUN-Verbindungen waren nicht erfolgreich, daher verwendet jeder Peer einen TURN-Server.

STUN- und TURN-Server bereitstellen

Für Tests betreibt Google den öffentlichen STUN-Server stun.l.google.com:19302, der auch von appr.tc verwendet wird. Verwenden Sie für einen Produktions-STUN/TURN-Dienst den rfc5766-turn-server. Der Quellcode für STUN- und TURN-Server ist auf GitHub verfügbar. Dort finden Sie auch Links zu mehreren Informationsquellen zur Serverinstallation. Es ist auch ein VM-Image für Amazon Web Services verfügbar.

Ein alternativer TURN-Server ist restund, der als Quellcode und auch für AWS verfügbar ist. Hier finden Sie eine Anleitung zum Einrichten von restund in der Compute Engine.

  1. Öffnen Sie die Firewall nach Bedarf für tcp=443 und udp/tcp=3478.
  2. Erstellen Sie vier Instanzen, eine für jede öffentliche IP-Adresse, mit dem Standard-Ubuntu-12.06-Image.
  3. Richten Sie die lokale Firewallkonfiguration ein (ALLE von ALLE zulassen).
  4. Tools installieren: shell sudo apt-get install make sudo apt-get install gcc
  5. Installiere libre unter creytiv.com/re.html.
  6. Rufe restund unter creytiv.com/restund.html ab und entpacke die Datei./
  7. wget hancke.name/restund-auth.patch und mit patch -p1 < restund-auth.patch anwenden.
  8. Führen Sie make und sudo make install für „libre“ und „restund“ aus.
  9. Passen Sie restund.conf an Ihre Anforderungen an (ersetzen Sie die IP-Adressen und achten Sie darauf, dass das gleiche gemeinsame Secret verwendet wird) und kopieren Sie es in /etc.
  10. Kopieren Sie restund/etc/restund nach /etc/init.d/.
  11. Erstattung konfigurieren:
    1. Legen Sie dazu LD_LIBRARY_PATH fest.
    2. Kopieren Sie restund.conf nach /etc/restund.conf.
    3. Legen Sie für restund.conf den Wert „10“ fest. IP-Adresse.
  12. restund ausführen
  13. Test mit dem Stund-Client von einem Remote-Computer aus: ./client IP:port

Mehr als eins zu eins: WebRTC für mehrere Teilnehmer

Sehen Sie sich auch den von Justin Uberti vorgeschlagenen IETF-Standard für eine REST API für den Zugriff auf TURN-Dienste an.

Es ist leicht, sich Anwendungsfälle für das Medienstreaming vorzustellen, die über einen einfachen Einzelanruf hinausgehen. Beispiele: Videokonferenzen zwischen einer Gruppe von Kollegen oder eine öffentliche Veranstaltung mit einem Redner und Hunderten oder Millionen von Zuschauern.

Eine WebRTC-App kann mehrere RTCPeerConnections verwenden, sodass jeder Endpunkt mit jedem anderen Endpunkt in einer Mesh-Konfiguration verbunden ist. Dieser Ansatz wird von Apps wie talky.io verwendet und funktioniert für eine kleine Gruppe von Gleichaltrigen bemerkenswert gut. Darüber hinaus werden die Verarbeitung und die Bandbreitennutzung übermäßig hoch, insbesondere bei mobilen Clients.

Mesh: kleiner N-Wege-Anruf
Full-Mesh-Topologie: Alle Knoten sind miteinander verbunden

Alternativ kann eine WebRTC-App einen Endpunkt auswählen, um Streams in einer Sternkonfiguration an alle anderen zu verteilen. Es ist auch möglich, einen WebRTC-Endpunkt auf einem Server auszuführen und einen eigenen Mechanismus zur Weiterverteilung zu erstellen. Eine Beispiel-Client-App wird von webrtc.org bereitgestellt.

Seit Chrome 31 und Opera 18 kann eine MediaStream aus einer RTCPeerConnection als Eingabe für eine andere verwendet werden. Dies kann zu flexibleren Architekturen führen, da eine Webanwendung die Anrufweiterleitung verarbeiten kann, indem sie auswählt, mit welchem anderen Peer eine Verbindung hergestellt werden soll. Weitere Informationen finden Sie unter WebRTC-Beispiele: Weiterleitung von Peer-Verbindungen und WebRTC-Beispiele: Mehrere Peer-Verbindungen.

Multipoint Control Unit

Eine bessere Option für eine große Anzahl von Endpunkten ist die Verwendung einer Multipoint Control Unit (MCU). Dieser Server dient als Brücke, um Medien zwischen einer großen Anzahl von Teilnehmern zu verteilen. MCUs können mit verschiedenen Auflösungen, Codecs und Frameraten in einer Videokonferenz umgehen, Transcodierung verarbeiten, eine selektive Streamweiterleitung durchführen und Audio und Video mischen oder aufzeichnen. Bei Gruppenanrufen müssen einige Probleme berücksichtigt werden, insbesondere die Darstellung mehrerer Videoinputs und das Mischen von Audio aus mehreren Quellen. Cloud-Plattformen wie vLine versuchen ebenfalls, das Traffic-Routing zu optimieren.

Sie können ein komplettes MCU-Hardwarepaket kaufen oder selbst einrichten.

Rückansicht von Cisco MCU5300
Rückseite einer Cisco-MCU

Es gibt mehrere Open-Source-MCU-Softwareoptionen. Licode (früher Lynckia) stellt beispielsweise eine Open-Source-MCU für WebRTC bereit. OpenTok bietet Mantis.

Nicht nur Browser: VoIP, Telefone und Messaging

Durch die Standardisierung von WebRTC ist es möglich, eine Kommunikation zwischen einer WebRTC-App, die in einem Browser ausgeführt wird, und einem Gerät oder einer Plattform herzustellen, die auf einer anderen Kommunikationsplattform ausgeführt wird, z. B. einem Telefon oder einem Videokonferenzsystem.

SIP ist ein Signalisierungsprotokoll, das von VoIP- und Videokonferenzsystemen verwendet wird. Damit eine Kommunikation zwischen einer WebRTC-Web-App und einem SIP-Client wie einem Videokonferenzsystem möglich ist, benötigt WebRTC einen Proxy-Server, der die Signalisierung vermittelt. Die Signalisierung muss über das Gateway geleitet werden. Sobald die Kommunikation jedoch hergestellt wurde, kann SRTP-Traffic (Video und Audio) direkt Peer-to-Peer übertragen werden.

Das Public Switched Telephone Network (PSTN) ist das schaltungsvermittelte Netzwerk aller herkömmlichen analogen Telefone. Bei Anrufen zwischen WebRTC-Web-Apps und Telefonen muss der Traffic über ein PSTN-Gateway laufen. Ebenso benötigen WebRTC-Web-Apps einen vermittelnden XMPP-Server, um mit Jingle-Endpunkten wie IM-Clients zu kommunizieren. Jingle wurde von Google als Erweiterung von XMPP entwickelt, um Sprach- und Videoanrufe für Messaging-Dienste zu ermöglichen. Aktuelle WebRTC-Implementierungen basieren auf der C++-Bibliothek libjingle, einer Jingle-Implementierung, die ursprünglich für Google Talk entwickelt wurde.

Eine Reihe von Apps, Bibliotheken und Plattformen nutzen die Möglichkeit von WebRTC, mit der Außenwelt zu kommunizieren:

  • sipML5: ein Open-Source-JavaScript-SIP-Client
  • jsSIP: JavaScript-SIP-Bibliothek
  • Phono: Open-Source-JavaScript-Telefon-API als Plug-in
  • Zingaya: ein einbettbares Smartphone-Widget
  • Twilio: Sprach- und Messaging-Dienste
  • Uberconference: Videokonferenzen

Die sipML5-Entwickler haben auch das webrtc2sip-Gateway entwickelt. Tethr und Tropo haben ein Framework für die Katastrophenkommunikation „in einem Aktenkoffer“ mit einer OpenBTS-Zelle demonstriert, um die Kommunikation zwischen Feature Phones und Computern über WebRTC zu ermöglichen. Das ist Telefonkommunikation ohne Mobilfunkanbieter.

Weitere Informationen

Im WebRTC-Codelab finden Sie eine detaillierte Anleitung zum Erstellen einer Video- und Textchat-App mit einem Socket.io-Signalisierungsdienst, der auf Node ausgeführt wird.

WebRTC-Präsentation der Google I/O 2013 mit dem WebRTC-Tech Lead Justin Uberti

SFHTML5-Präsentation von Chris Wilson – Einführung in WebRTC-Apps

Das 350-seitige Buch WebRTC: APIs and RTCWEB Protocols of the HTML5 Real-Time Web enthält viele Details zu Daten- und Signalpfaden sowie eine Reihe detaillierter Netzwerktopologiediagramme.

WebRTC and Signaling: What Two Years Has Taught Us – Blogpost von TokBox, in dem erklärt wird, warum es eine gute Idee war, die Signalisierung aus der Spezifikation herauszunehmen

Ben Strongs A Practical Guide to Building WebRTC Apps (Ein praktischer Leitfaden zum Erstellen von WebRTC-Apps) enthält viele Informationen zu WebRTC-Topologien und ‑Infrastrukturen.

Im Kapitel zu WebRTC in Ilya Grigoriks High Performance Browser Networking werden die Architektur, Anwendungsfälle und die Leistung von WebRTC ausführlich behandelt.