Delphi WebRTC: audio, wideo i dane między dwiema aplikacjami

Dwie aplikacje Delphi, w dwóch różnych sieciach, wymieniające bezpośrednio między sobą kanał czatu, strumień z mikrofonu i strumień z kamery. Żadnego serwera multimediów pośrodku, żadnej przeglądarki osadzonej w procesie, żadnego mostka JavaScript. Ta strona przechodzi przez całe zadanie, od pierwszej wiadomości sygnalizacyjnej do pierwszej zdekodowanej ramki dźwięku, przy użyciu API, które istnieją w dostarczanym kodzie źródłowym.

SDP offer i answer
ICE, STUN i TURN
kanały danych SCTP
ścieżki multimedialne Opus i VP8
szyfrowanie DTLS-SRTP
bez przeglądarki i bez WebView

Co naprawdę musi się wydarzyć

WebRTC to cztery osobne problemy pod jedną nazwą. Tylko jeden z nich dotyczy multimediów i jest to ten łatwy. Oto te cztery, w kolejności, w jakiej musisz je rozwiązać.

1. Opisz sesję

Jedna strona buduje offer, dokument tekstowy (SDP), który mówi, jakie multimedia chce wysyłać, jakimi kodekami się posługuje, jaki jest odcisk palca certyfikatu, który przedstawi, oraz jakich poświadczeń ICE użyje. Druga strona odpowiada podzbiorem, który akceptuje. CreateOffer i CreateAnswer tworzą te dokumenty, SetRemoteDescription je przyjmuje.

2. Przenieś go na drugą stronę

WebRTC celowo nie określa, w jaki sposób offer trafia do drugiego peera. Ten kanał nazywa się sygnalizacją i to twoje zadanie. To kilkaset bajtów tekstu w każdą stronę, więc wystarczy połączenie WebSocket z niewielkim przekaźnikiem, a sgcWebSockets daje ci już obie jego połowy.

3. Znajdź drogę przez NAT-y

Żaden z peerów nie zna swojego adresu publicznego, a oba zwykle znajdują się za routerem. ICE zbiera każdy adres, pod którym peer może być osiągalny, wysyła je kanałem sygnalizacyjnym w miarę ich pojawiania się i testuje każdą parę, aż któraś zadziała. STUN znajduje adres publiczny, TURN zapewnia przekaźnik, gdy nic bezpośredniego nie działa.

4. Przenieś bajty

Gdy jedna para kandydatów zostanie nominowana, biegnie po niej handshake DTLS, a wszystko po nim jest szyfrowane. Kanał danych to SCTP na tym transporcie DTLS, ścieżka dźwięku lub wideo to SRTP na nim. Oba dzielą to samo połączenie i ten sam otwarty port.

Czy istnieje serwer WebRTC?

Nie w ścieżce multimediów i o to właśnie chodzi. Gdy dwie aplikacje już się odnajdą, dźwięk, wideo i dane wędrują bezpośrednio między nimi. Nic, co hostujesz, nie widzi ładunku, i nic, co hostujesz, nie musi skalować się wraz z liczbą minut, które twoi użytkownicy spędzają na rozmowie.

Na obrazku wciąż są dwa serwery i warto dokładnie określić, co robi każdy z nich, ponieważ często się je myli.

Serwer sygnalizacyjny jest twój. Przekazuje garść wiadomości tekstowych między dwoma peerami, zanim rozmowa się zacznie, a potem milknie. Nigdy nie widzi multimediów. W tym przewodniku to piętnaście linii Delphi zbudowanych na TsgcWebSocketServer.

Serwer STUN i TURN istnieje z powodu NAT, a nie z powodu WebRTC. Serwer STUN odpowiada na jedno pytanie, "z jakiego adresu publicznego przyszedł ten pakiet", i to wszystko. Serwer TURN przekazuje pakiety dla tych par, które nie mogą dosięgnąć się nawzajem w żaden inny sposób, więc jest jedynym elementem przenoszącym multimedia, i to tylko dla rozmów, które tego wymagają. Publiczne serwery STUN są darmowe i liczne, TURN hostujesz sam, a sgcWebSockets Enterprise dostarcza zarówno komponent serwera STUN, jak i serwera TURN, jeśli wolisz nie uruchamiać osobnego demona.

who-talks-to-whom.txt
  App A                                App B
    |                                    |
    |---- offer / answer / candidate --->|   your signalling
    |<-------- (WebSocket relay) --------|   server, text only
    |                                    |
    |---> "what is my public address?"   |   STUN, once per
    |     (STUN binding request)         |   candidate
    |                                    |
    |====== audio, video and data ======>|   direct, encrypted,
    |<===================================|   no server involved
    |                                    |
    |== only when nothing direct works ==|   TURN relay,
    |    (relayed candidate pair)        |   your server

Edycje, moduły i platformy

Peer connection i silnik multimediów to dwa różne kroki licencyjne. Warto to sobie wyjaśnić, zanim napiszesz jakikolwiek kod, bo inaczej kompilator po prostu nie zobaczy połowy API.

Co chcesz zrobićCzego wymagaSkąd pochodzi
Klient STUN, aby odkryć adres publiczny TsgcSTUNClient sgcWebSockets Standard i wyżej
Uruchomienie własnego serwera STUN TsgcSTUNServer sgcWebSockets Professional i wyżej
Przekazywanie kanału sygnalizacyjnego WebSocket TsgcWebSocketServer sgcWebSockets Professional i wyżej dla połowy serwerowej. Połowa kliencka, TsgcWebSocketClient, przychodzi z edycją Standard.
ICE, klient i serwer TURN oraz sam komponent peer connection TsgcICEClient, TsgcTURNClient, TsgcTURNServer, TsgcRTCPeerConnection sgcWebSockets Enterprise
Offer i answer, kanały danych, ścieżki dźwięku i wideo CreateOffer, CreateAnswer, SetRemoteDescription, AddIceCandidate, CreateDataChannel, AddTrack Pakiet sgcWebRTC, na bazie Enterprise. Zawarty także w All-Access.

Dlaczego ten podział, słowami samego kompilatora

Blok Enterprise w sgcVer.inc definiuje SGC_ICE, SGC_DTLS, SGC_RTCPEERCONNECTION i SGC_TURN. To właśnie umieszcza TsgcRTCPeerConnection na palecie i daje mu transport ICE oraz TURN.

Wszystko, o czym naprawdę jest ta strona, leży o poziom głębiej. To SGC_PACK_WEBRTC definiuje SGC_SDP, SGC_SCTP, SGC_DATACHANNEL, SGC_RTP i SGC_SRTP, a najpierw sprawdza, czy ICE, DTLS i peer connection już tam są. Ręczne metody sygnalizacji, API kanału danych i API multimediów znajdują się odpowiednio wewnątrz {$IFDEF SGC_SDP}, {$IFDEF SGC_DATACHANNEL} i {$IFDEF SGC_RTP}, więc na samym Enterprise się nie kompilują.

Jeśli CreateOffer nie daje się rozwiązać, to właśnie się stało. Demo kanału danych dostarczane w pakiecie mówi o tym wprost, zamiast po cichu zawodzić.

sgcVer.inc
{$IFDEF SGC_PACK_WEBRTC} { PACK WEBRTC }
  {$IFDEF SGC_INDY_LIB}
    {$IFDEF SGC_ICE} { requires ICE + DTLS + RTCPeerConnection }
      {$IFDEF SGC_DTLS}
        {$IFDEF SGC_RTCPEERCONNECTION}
          {$DEFINE SGC_SDP}
          {$DEFINE SGC_SCTP}
          {$DEFINE SGC_DATACHANNEL}
          {$DEFINE SGC_RTP}
          {$DEFINE SGC_SRTP}
          {$DEFINE SGC_CODEC_OPUS}
          {$DEFINE SGC_CODEC_VP8}
          {$DEFINE SGC_CODEC_H264}
        {$ENDIF}
      {$ENDIF}
    {$ENDIF}
  {$ENDIF}
{$ENDIF}

Moduły potrzebne obu aplikacjom

sgcP2P to moduł zbiorczy, który publikuje TsgcRTCPeerConnection i reeksportuje typy procedur obsługi. Wystarczy do zadeklarowania komponentu, ale stałe wyliczeniowe pochodzą z modułów deklarujących ich typy, więc dołącz je również, gdy używasz nazw rtctkAudio lub cctAudioOpus.

Dwie aplikacje w tym przewodniku to ten sam program z wciśniętym innym przyciskiem. Wszystko poniżej trafia do obu.

uPeer.pas
uses
  Classes, SysUtils,
  // sgc
  sgcWebSocket,             // signalling carrier
  sgcWebSocket_Classes,     // TsgcWSConnection
  sgcJSON,                  // wraps the SDP and the candidates
  sgcP2P,                   // TsgcRTCPeerConnection
  sgcP2P_RTCPeerConnection, // TsgcRTCConnectionState
  sgcP2P_DataChannel,       // TsgcRTCDataChannel
  sgcP2P_RTC_Media,         // TsgcRTCTrack, rtctkAudio
  sgcP2P_Codec_Types,       // cctAudioOpus, TsgcVideoFrame
  sgcP2P_Media_Factory,     // sgcCreateAudioCapture
  sgcP2P_MediaCapture,      // TsgcMediaCaptureSource
  sgcP2P_MediaRenderer;     // TsgcMediaRenderer

Kanał sygnalizacyjny

Trzy rodzaje wiadomości, jeden prosty przekaźnik. To ta część, którą każdy poradnik WebRTC zbywa machnięciem ręki, i ta, którą naprawdę musisz napisać.

Przekaźnik, nie broker

Serwer sygnalizacyjny nie musi rozumieć ani jednego bajtu tego, co przekazuje. Bierze tekst wysłany przez jednego peera i podaje go drugiemu. Broadcast ma już parametr Exclude, który przyjmuje Guid połączenia, więc "wyślij do wszystkich oprócz nadawcy" to jedna linia.

Zostaw go tak prostym. W chwili, gdy przekaźnik zaczyna parsować SDP, staje się komponentem, który musisz aktualizować przy każdej zmianie kodeka, i przestaje być w stanie przekazać rozmowę do przeglądarki.

W środowisku produkcyjnym oprzesz przekaźnik na identyfikatorze pokoju, aby dwie rozmowy nie mogły się zderzyć, i umieścisz go za TLS. TsgcWebSocketServer ma ten sam zestaw TLSOptions, Authentication i WatchDog co reszta biblioteki.

uSignallingServer.pas
procedure TFormServer.Start;
begin
  FServer := TsgcWebSocketServer.Create(nil);
  FServer.Port := 5000;
  FServer.OnMessage := OnSignallingMessage;
  FServer.Active := True;
end;

procedure TFormServer.OnSignallingMessage(
  Connection: TsgcWSConnection; const Text: string);
begin
  // relay verbatim to the other peer. The server never
  // parses the SDP, so it never learns about codecs.
  FServer.Broadcast(Text, '', '', Connection.Guid);
end;

Strona peera tego samego kanału

Każda aplikacja otwiera TsgcWebSocketClient do tego przekaźnika i posługuje się trzywyrazowym słownikiem: offer, answer i candidate. TsgcJSON z tej samej biblioteki je serializuje, więc nie ma dodatkowej zależności.

Zwróć uwagę, dokąd prowadzą gałęzie. Przychodzący offer jest ustawiany jako opis zdalny i od razu otrzymuje odpowiedź. Przychodzący answer jest tylko ustawiany. Przychodzący candidate trafia do AddIceCandidate i może dotrzeć przed opisem albo po nim, o co właśnie chodzi w trickle ICE.

uPeer.pas
procedure TFormPeer.OnSignallingMessage(
  Connection: TsgcWSConnection; const Text: string);
var
  oJSON: TsgcJSON;
  vKind: string;
begin
  oJSON := TsgcJSON.Create(nil);
  try
    oJSON.Read(Text);
    vKind := oJSON.Node['kind'].Value;

    if vKind = 'offer' then
    begin
      FPeer.SetRemoteDescription('offer',
        oJSON.Node['sdp'].Value);
      FPeer.CreateAnswer;  // fires OnLocalDescription
    end
    else if vKind = 'answer' then
      FPeer.SetRemoteDescription('answer',
        oJSON.Node['sdp'].Value)
    else if vKind = 'candidate' then
      FPeer.AddIceCandidate(oJSON.Node['candidate'].Value,
        oJSON.Node['sdpMid'].Value,
        oJSON.Node['sdpMLineIndex'].Value);
  finally
    FreeAndNil(oJSON);
  end;
end;

Zbuduj peer connection

Identycznie w obu aplikacjach. Jedyna asymetria w całej wymianie to ta, która z nich wciska Zadzwoń.

Konfiguracja i zdarzenia, które mają znaczenie

RTCOptions.ICEServers to lista iceServers według W3C. AddURL przyjmuje URL stun: lub turn: i wypełnia typ, host, port oraz flagę TLS na podstawie schematu, z opcjonalną nazwą użytkownika i poświadczeniem dla TURN.

RTCOptions.DTLS domyślnie ma wartość False. Zostaw tak, a nie będzie szyfrowania ani materiału kluczy SRTP, więc multimedia nie zadziałają. CreateDataChannel włącza je za ciebie, bo kanał danych to SCTP na DTLS i nie ma poprawnej konfiguracji z wyłączonym DTLS. AddTrack tego nie robi, więc ustaw je sam, gdy dodajesz multimedia.

Nie potrzebujesz pliku certyfikatu. Zostaw RTCOptions.DTLSOptions.CertFile puste, a certyfikat samopodpisany zostanie wygenerowany w pamięci raz na komponent, co jest dokładnie modelem WebRTC: tożsamość jest zakotwiczona w linii a=fingerprint w SDP, a nie w łańcuchu certyfikatów. Opis zdalny, który nie niesie odcisku palca, jest odrzucany, zamiast pozwolić na handshake z czymkolwiek.

Każde zdarzenie poniżej uruchamia się w wątku roboczym, wątku ICE, sieciowym lub timera, nigdy w wątku głównym. Przekaż je przez TThread.Queue, zanim dotkniesz kontrolki.

uPeer.pas
procedure TFormPeer.CreatePeer;
begin
  FPeer := TsgcRTCPeerConnection.Create(nil);

  FPeer.RTCOptions.ICEServers.AddURL(
    'stun:stun.l.google.com:19302');
  FPeer.RTCOptions.ICE.STUN := True;
  FPeer.RTCOptions.ICE.TURN := False;  // no TURN server yet
  FPeer.RTCOptions.DTLS     := True;   // default is False

  FPeer.OnLocalDescription      := OnLocalDescription;
  FPeer.OnIceCandidate          := OnIceCandidate;
  FPeer.OnConnectionStateChange := OnConnectionStateChange;
  FPeer.OnDataChannel           := OnDataChannel;
  FPeer.OnTrack                 := OnTrack;
  FPeer.OnError                 := OnError;

  FSignalling := TsgcWebSocketClient.Create(nil);
  FSignalling.Host := 'signalling.example.com';
  FSignalling.Port := 5000;
  FSignalling.OnMessage := OnSignallingMessage;
  FSignalling.Active := True;
end;

Publikowanie opisu lokalnego

OnLocalDescription przekazuje ci typ, ciąg 'offer' lub 'answer', oraz samo SDP. Oba peery używają tej samej procedury obsługi, która robi jedną rzecz: wystawia to na kanał sygnalizacyjny.

SDP dociera już kompletne. CreateOffer najpierw zbiera kandydatów ICE i czeka do RTCOptions.GatheringTimeout milisekund, domyślnie 3000, kończąc wcześniej po GatheringIdleTimeout milisekundach bez nowego kandydata, domyślnie 500. To ścieżka bez trickle.

Ustaw TrickleICE na True, a opis wyjdzie natychmiast, z kandydatami podążającymi za nim. Rzadko musisz to robić. RTCOptions.TrickleICEAuto jest domyślnie True, więc gdy opis zdalny zawiera a=ice-options:trickle, co robi każda przeglądarka, komponent sam się przełącza i przestaje marnować limit czasu zbierania.

uPeer.pas
procedure TFormPeer.OnLocalDescription(Sender: TObject;
  const aType, aSDP: string);
var
  oJSON: TsgcJSON;
begin
  oJSON := TsgcJSON.Create(nil);
  try
    oJSON.AddPair('kind', aType);  // 'offer' or 'answer'
    oJSON.AddPair('sdp', aSDP);
    FSignalling.WriteData(oJSON.Text);
  finally
    FreeAndNil(oJSON);
  end;
end;

// App A only. App B answers from OnSignallingMessage.
procedure TFormPeer.btnCallClick(Sender: TObject);
begin
  FPeer.CreateDataChannel('chat');  // forces DTLS on
  FPeer.CreateOffer;
end;

Kandydaci ICE, STUN i TURN

To tutaj połączenia peer to peer zawodzą i tutaj awarie są najtrudniejsze do odczytania. Trzy rodzaje kandydatów, trzy powody ich istnienia.

host

Adres, który maszyna widzi u siebie, jeden na każdy interfejs sieciowy. Darmowy, natychmiastowy i wystarczający, gdy obie aplikacje są w tej samej sieci LAN lub tym samym VPN. Jeśli twoje dwie aplikacje Delphi działają wyłącznie w obrębie jednego biura, kandydaci typu host to wszystko, czego potrzebujesz, i możesz całkowicie pominąć STUN.

srflx, server reflexive

Kandydat srflx, czyli adres refleksyjny wobec serwera, to adres publiczny, z którego serwer STUN zobaczył nadchodzący pakiet peera. To właśnie sprawia, że dwa peery za zwykłymi domowymi routerami rozmawiają bezpośrednio, i obejmuje zdecydowaną większość rzeczywistych połączeń. Kosztuje jedną wymianę pakietów z serwerem STUN, który potem nie przenosi już żadnego ruchu.

relay

Adres na serwerze TURN, który przekazuje dane do peera. Potrzebny przy symetrycznym NAT, restrykcyjnych firewallach korporacyjnych i u niektórych operatorów komórkowych. Każdy bajt rozmowy przechodzi przez twój serwer TURN, więc jest to droga kosztowna, do której schodzisz tylko awaryjnie.

Przekazywanie ich na bieżąco

OnIceCandidate uruchamia się raz na kandydata, w momencie jego wykrycia, z linią kandydata, jego sdpMid i jego sdpMLineIndex. Te trzy pola to dokładnie to, czego oczekuje API przeglądarki, więc ten sam JSON działa niezależnie od tego, czy po drugiej stronie jest Delphi, czy Chrome.

Wysyłaj każdego natychmiast. Nie czekaj, nie grupuj. Kandydat, który dotrze przed opisem zdalnym, jest przytrzymywany i stosowany, gdy opis dotrze, więc kolejność nie jest twoim problemem.

Gdy para zostanie w końcu nominowana, SelectedLocalCandidate i SelectedRemoteCandidate mówią ci, które dwa adresy wygrały. Ta jedna linia logu odpowiada na pytanie "dlaczego ta rozmowa idzie przez mój serwer TURN" szybciej niż cokolwiek innego.

uPeer.pas
procedure TFormPeer.OnIceCandidate(Sender: TObject;
  const aCandidate, aSdpMid: string;
  aSdpMLineIndex: Integer);
var
  oJSON: TsgcJSON;
begin
  oJSON := TsgcJSON.Create(nil);
  try
    oJSON.AddPair('kind', 'candidate');
    oJSON.AddPair('candidate', aCandidate);
    oJSON.AddPair('sdpMid', aSdpMid);
    oJSON.AddPair('sdpMLineIndex', aSdpMLineIndex);
    FSignalling.WriteData(oJSON.Text);
  finally
    FreeAndNil(oJSON);
  end;
end;

procedure TFormPeer.OnConnectionStateChange(Sender: TObject;
  aState: TsgcRTCConnectionState);
begin
  // rtccsNew, rtccsGathering, rtccsConnecting, rtccsConnected,
  // rtccsDisconnected, rtccsFailed, rtccsClosed
  if aState = rtccsConnected then
    Log(FPeer.SelectedLocalCandidate + ' -> ' +
        FPeer.SelectedRemoteCandidate);
end;

Dodawanie TURN i przełącznik, o którym nie wolno zapomnieć

Wpis turn: w ICEServers niesie własny host, port, nazwę użytkownika i poświadczenie, i to właśnie wykorzystuje alokacja. Dodanie go to jedno kolejne AddURL.

Pułapka leży w drugą stronę. RTCOptions.ICE.TURN domyślnie ma wartość True, a gdy lista serwerów nie zawiera żadnego wpisu TURN, zbieranie wraca do pojedynczego serwera w RTCOptions.ICE, którego host domyślnie to 127.0.0.1, a port 3478. Więc peer skonfigurowany wyłącznie adresem STUN i tak próbuje alokacji TURN wobec localhost, zawodzi i to zgłasza. To raczej szum niż błąd, ale w logu wygląda alarmująco i wysyła cię na poszukiwania w złym miejscu. Ustaw RTCOptions.ICE.TURN := False, dopóki naprawdę nie masz serwera TURN.

RTCOptions.ICE.STUN zachowuje się tak samo i również domyślnie ma wartość True.

uPeer.pas
// STUN for the public address, TURN for the fallback relay
FPeer.RTCOptions.ICEServers.AddURL(
  'stun:stun.example.com:3478');
FPeer.RTCOptions.ICEServers.AddURL(
  'turn:turn.example.com:3478', 'user', 'secret');

FPeer.RTCOptions.ICE.STUN := True;
FPeer.RTCOptions.ICE.TURN := True;

// a stalled call is usually a candidate problem. Lower the
// gathering waits on a LAN, where there is nothing to gather.
FPeer.RTCOptions.GatheringTimeout     := 1000;
FPeer.RTCOptions.GatheringIdleTimeout := 200;

Dokumentacja klienta ICE Uruchom własny serwer TURN

Kanał danych

Tekst i dane binarne między dwiema aplikacjami, z niezawodnością wybieraną osobno dla każdego kanału. To zwykle pierwsza rzecz, którą uruchamiasz, i dowodzi ona działania całego transportu.

Otwieranie własnego i odbieranie kanału drugiej strony

Peer, który wywoła CreateDataChannel, dostaje obiekt od razu z powrotem. Peer, który tego nie zrobił, dostaje ten sam kanał przez OnDataChannel. Podepnij procedury obsługi w obu miejscach, bo każda ze stron może otworzyć kanał w dowolnym momencie sesji.

Kanał nie nadaje się do użytku w chwili utworzenia. Jego Id pozostaje nieprzypisane, dopóki asocjacja SCTP nie działa i rola DTLS nie zostanie ustalona, a Send zwraca False, dopóki kanał nie jest otwarty. Poczekaj na OnOpen.

Niezawodność jest rozstrzygana przy tworzeniu. Domyślnie kanał jest uporządkowany i w pełni niezawodny, podobny do TCP. Przekaż aOrdered = False dla dostarczania bez zachowania kolejności albo aMaxRetransmits lub aMaxPacketLifeTime dla częściowej niezawodności, czyli tego, czego chcesz przy aktualizacjach pozycji lub wszędzie tam, gdzie spóźniony pakiet jest gorszy niż zgubiony.

Nie zwalniaj kanału, jest własnością peer connection. Close rozpoczyna zamykanie, a OnClose uruchamia się, gdy druga strona się zgodzi.

uPeer.pas
// caller: ordered and reliable, the default
FChannel := FPeer.CreateDataChannel('chat');
AttachChannel(FChannel);

// unordered, give up after 3 retransmits
FState := FPeer.CreateDataChannel('state', False, 3);

// callee: the same channel arrives here
procedure TFormPeer.OnDataChannel(Sender: TObject;
  aChannel: TsgcRTCDataChannel);
begin
  AttachChannel(aChannel);
end;

procedure TFormPeer.AttachChannel(
  aChannel: TsgcRTCDataChannel);
begin
  FChannel := aChannel;
  FChannel.OnOpen          := OnChannelOpen;
  FChannel.OnMessage       := OnChannelText;
  FChannel.OnMessageBinary := OnChannelBinary;
  FChannel.OnClose         := OnChannelClose;
  FChannel.OnError         := OnChannelError;
end;

procedure TFormPeer.OnChannelText(Sender: TObject;
  const aText: string);
begin
  // fires on the SCTP thread, queue before touching a control
  TThread.Queue(nil,
    procedure
    begin
      memoChat.Lines.Add(aText);
    end);
end;

Wysyłanie bez zalewania łącza

Send przyjmuje ciąg, a SendBytes przyjmuje TBytes. Oba zwracają False, zamiast zgłaszać wyjątek, gdy kanał nie jest otwarty, więc wysyłka podczas zamykania to zwrócone False, a nie wyjątek w wątku roboczym.

MaxMessageSize to największa wiadomość, jaką peer zadeklarował, że przyjmie, odczytana z atrybutu a=max-message-size jego opisu. Wiadomość przekraczająca ten limit jest odrzucana lokalnie, zamiast trafić na łącze, gdzie przerwałaby całą asocjację i pociągnęła za sobą wszystkie pozostałe kanały. Zero oznacza, że żaden opis nie podał limitu i kontrola jest wyłączona.

BufferedAmount to liczba bajtów zakolejkowanych w SCTP dla tego strumienia i jeszcze niepotwierdzonych. Obserwuj ją, gdy przesyłasz plik strumieniowo: wysyłaj, dopóki nie przekroczy progu, a potem poczekaj, aż się opróżni, zamiast kolejkować gigabajty w pamięci.

uPeer.pas
procedure TFormPeer.btnSendClick(Sender: TObject);
begin
  if not Assigned(FChannel) then
    Exit;

  if not FChannel.Send(txtMessage.Text) then
    Log('channel not open');
end;

procedure TFormPeer.SendChunk(const aBytes: TBytes);
begin
  if (FChannel.MaxMessageSize > 0) and
     (Cardinal(Length(aBytes)) > FChannel.MaxMessageSize) then
  begin
    Log('too big for this peer, split it');
    Exit;
  end;

  if FChannel.BufferedAmount < 262144 then
    FChannel.SendBytes(aBytes);
end;

Ścieżki dźwięku i wideo

Ścieżka to nie kanał danych z obrazkami w środku. Inny transport, inne tryby awarii, inny kod. To rozróżnienie, które większość osób najpierw myli.

 Kanał danychŚcieżka multimedialna
Przenoszony przez SCTP na DTLS (RFC 8831) SRTP na tym samym transporcie DTLS (RFC 3711)
Dostarczanie Twój wybór, od w pełni niezawodnego i uporządkowanego po wyślij i zapomnij Z założenia zawsze stratne. Spóźnione jest gorsze niż zgubione, więc nic nie jest retransmitowane w nieskończoność
Jednostka pracy Wiadomość. To ty decydujesz, kiedy ją wysłać Zegar. Dźwięk podawany jest w ramkach 20 ms, wideo z określoną liczbą klatek na sekundę
Otwierany przez CreateDataChannel, w dowolnym momencie sesji AddTrack, który wymaga nowego offer, aby ją opublikować
Odbierany przez OnDataChannel, a potem własne OnMessage kanału OnTrack, a potem OnAudio lub OnVideoFrame ścieżki
Wymaga włączonego DTLS Tak, a CreateDataChannel ustawia to za ciebie Tak, a AddTrack tego nie robi. Ustaw RTCOptions.DTLS sam
Używaj do Czat, przesyłanie plików, zdalne sterowanie, stan gry, telemetria Mikrofon, kamera, udostępnianie ekranu, wszystko, co ma oś czasu

Wysyłanie dźwięku z mikrofonu

AddTrack przyjmuje rodzaj i kodek, a zwraca TsgcRTCTrack. Kodeki dźwięku to cctAudioOpus, cctAudioPCMU i cctAudioPCMA, kodeki wideo to cctVideoVP8, cctVideoVP9, cctVideoH264 i cctVideoJPEG.

Przechwytywanie to osobny obiekt, bo możesz wcale nie chcieć mikrofonu systemowego. sgcCreateAudioCapture buduje właściwą implementację dla platformy, pod którą kod został skompilowany, waveIn na Windows, ALSA na Linux, AudioRecord na Android, Audio Unit VoiceProcessingIO na iOS i macOS, więc nic w twoim kodzie nie nazywa klasy platformowej. Na platformie docelowej bez implementacji zwraca nil, więc sprawdź wynik.

SendPCM oczekuje 16-bitowego PCM ze znakiem, z przeplotem kanałów, w częstotliwości i liczbie kanałów kodera, czyli dla Opus 48000 Hz, a dla G.711 8000 Hz. Źródło przechwytywania publikuje to, co faktycznie dostarcza, przez AudioSampleRate, AudioChannels i AudioFrameDurationMs, więc możesz sprawdzić, zamiast zakładać.

Dodanie ścieżki, gdy sesja już działa, ustawia flagę potrzeby negocjacji i uruchamia OnNegotiationNeeded. Wywołaj CreateOffer ponownie, aby ją opublikować, a nowy offer zostanie zbudowany bez ruszania transportu.

uPeer.pas
procedure TFormPeer.StartCall;
begin
  FPeer.RTCOptions.DTLS := True;  // SRTP keys come from DTLS

  FAudioTrack := FPeer.AddTrack(rtctkAudio, cctAudioOpus);
  FVideoTrack := FPeer.AddTrack(rtctkVideo, cctVideoVP8);

  FCapture := sgcCreateAudioCapture;
  if Assigned(FCapture) then
  begin
    FCapture.OnAudioCapture := OnAudioCaptured;
    FCapture.Start;
    if not FCapture.Active then
      Log(FCapture.LastError);
  end;

  FRenderer := sgcCreateAudioRenderer;
  if Assigned(FRenderer) then
    FRenderer.Start;

  FPeer.CreateOffer;
end;

procedure TFormPeer.OnAudioCaptured(Sender: TObject;
  const aPCM: TBytes;
  aSampleRate, aChannels, aSamplesPerChannel: Integer);
begin
  if Assigned(FAudioTrack) then
    FAudioTrack.SendPCM(aPCM, aSamplesPerChannel);
end;

Odtwarzanie tego, co przysłała druga strona

OnTrack uruchamia się raz na każdą zdalną linię multimediów, gdy opis zdalny ją wprowadza. Ścieżka, którą ci przekazuje, należy do peer connection, więc podepnij jej zdarzenia i nigdy jej nie zwalniaj.

Dźwięk przychodzi jako zdekodowany PCM przez OnAudio, z częstotliwością i liczbą kanałów, jakie wyprodukował dekoder. Podaj go prosto do TsgcMediaRenderer zbudowanego przez sgcCreateAudioRenderer, który przekształca format, gdy urządzenia nie dało się otworzyć w pasujących parametrach.

Wideo przychodzi jako zdekodowana TsgcVideoFrame przez OnVideoFrame: surowe piksele w Data, z Width, Height, Format i Stride. Formaty to vffI420, vffNV12, vffRGB24, vffRGBA32, vffBGR24 i vffBGRA32, a te dwa BGR zachowują kolejność bajtów Windows GDI, więc przeniesienie ramki vffBGR24 do bitmapy to kopiowanie pamięci, a nie konwersja.

Jeśli ramka dotrze uszkodzona po utracie pakietów, RequestKeyFrame prosi nadawcę o nową.

uPeer.pas
procedure TFormPeer.OnTrack(Sender: TObject;
  aTrack: TsgcRTCTrack);
begin
  if aTrack.Kind = rtctkAudio then
    aTrack.OnAudio := OnRemoteAudio
  else
  begin
    FRemoteVideo := aTrack;
    aTrack.OnVideoFrame := OnRemoteVideoFrame;
  end;
  aTrack.OnEnded := OnRemoteTrackEnded;
end;

procedure TFormPeer.OnRemoteAudio(Sender: TObject;
  const aPCM: TBytes;
  aSampleRate, aChannels, aSamplesPerChannel: Integer);
begin
  if Assigned(FRenderer) then
    FRenderer.RenderAudio(aPCM, aSampleRate, aChannels,
      aSamplesPerChannel);
end;

procedure TFormPeer.OnRemoteVideoFrame(Sender: TObject;
  const aFrame: TsgcVideoFrame);
begin
  // aFrame.Data holds Width x Height pixels in aFrame.Format
  if aFrame.Format = vffBGR24 then
    BlitToBitmap(aFrame);
end;

Kamera, na Windows

Przechwytywanie dźwięku jest ukryte za fabryką, bo każda obsługiwana platforma ma implementację. Przechwytywanie wideo nie jest, więc nazywasz klasę platformową wprost. Na Windows to TsgcVideoCapture_Win z sgcP2P_MediaCapture_Win, który steruje Video for Windows i dostarcza ramki przez to samo zdarzenie OnVideoCapture, które deklaruje klasa bazowa.

Sąsiedni moduł sgcP2P_ScreenCapture_Win daje ci TsgcScreenCapture_Win i TsgcWindowCapture_Win, oba potomkowie TsgcMediaCaptureSource, więc udostępnianie ekranu to te same trzy linie z innym konstruktorem.

uPeer.pas
uses
  sgcP2P_MediaCapture_Win;   // MSWINDOWS only

procedure TFormPeer.StartCamera;
begin
  FVideoCapture := TsgcVideoCapture_Win.Create(640,
    480, 30);
  FVideoCapture.DeviceIndex := 0;
  FVideoCapture.OnVideoCapture := OnVideoCaptured;
  FVideoCapture.Start;
end;

procedure TFormPeer.OnVideoCaptured(Sender: TObject;
  const aFrame: TsgcVideoFrame);
begin
  if Assigned(FVideoTrack) then
    FVideoTrack.SendVideoFrame(aFrame);
end;

Awarie, które warto znać z wyprzedzeniem

Połączenia peer to peer zawodzą w sposób, który nie generuje żadnego błędu, i to czyni je trudnymi. Oto te, które pojawiają się najczęściej.

Multimedia milczą i nic nie zgłasza błędu

RTCOptions.DTLS ma wartość False. Taka jest wartość domyślna, CreateDataChannel ją włącza, ale AddTrack nie, więc sesja przenosząca wyłącznie multimedia nigdy nie przeprowadza handshake'u DTLS i w efekcie nigdy nie otrzymuje kluczy SRTP. Ustaw ją jawnie.

Błąd TURN, o który nie prosiłeś

RTCOptions.ICE.TURN domyślnie ma wartość True i wraca do 127.0.0.1:3478, gdy lista serwerów nie zawiera wpisu TURN. Ustaw ją na False, dopóki naprawdę nie masz serwera TURN, bo inaczej log zapełni się błędami alokacji, które nie mają nic wspólnego z twoim problemem.

Opis zdalny zostaje odrzucony

Opis bez a=fingerprint jest odrzucany od razu i zgłaszany przez OnError. W modelu zaufania WebRTC ta linia jest jedyną rzeczą uwierzytelniającą peera, więc przyjęcie opisu bez niej pozwoliłoby zakończyć handshake z dowolnym certyfikatem.

Naruszenie ochrony pamięci w procedurze obsługi zdarzenia

Każde zdarzenie peer connection, kanału danych i ścieżki uruchamia się w wątku roboczym, wątku ICE, sieciowym, SCTP lub taktującym, nigdy w wątku głównym. Dotykanie kontrolki VCL lub FMX bezpośrednio z takiego wątku jest niezdefiniowane. Opakuj to w TThread.Queue.

Obie strony jednocześnie wysyłają nowy offer

To się nazywa glare i rozwiązuje je reguła perfect negotiation z W3C. Niegrzeczny peer, Polite = False, zatrzymuje własny offer i zgłasza przychodzący przez OnError. Grzeczny peer wycofuje swój i odpowiada. Dwa peery jednej sesji nie mogą być oba grzeczne.

Jedna duża wiadomość zabija wszystkie kanały

Wiadomość przekraczająca a=max-message-size peera przerwałaby całą asocjację SCTP, pociągając za sobą wszystkie pozostałe kanały danych. Send i SendBytes sprawdzają MaxMessageSize i odrzucają ją lokalnie. Duże ładunki dziel sam.

Połączenie startuje dopiero po trzech sekundach

To RTCOptions.GatheringTimeout, czyli oczekiwanie bez trickle. GatheringIdleTimeout kończy je wcześniej, gdy kandydaci przestają napływać, a TrickleICEAuto przełącza na tryb trickle, gdy opis zdalny go ogłasza. W sieci LAN obniż oba limity czasu.

Dźwięk jest przyspieszony, zwolniony albo zniekształcony

Niezgodność częstotliwości próbkowania lub liczby kanałów między urządzeniem przechwytującym a koderem. Opus jest negocjowany przy 48000 Hz, a G.711 przy 8000 Hz. Odczytaj AudioSampleRate i AudioChannels ze źródła przechwytywania, zamiast zakładać, że urządzenie uszanowało to, o co prosiłeś.

Delphi WebRTC, najczęściej zadawane pytania

O co pytają programiści, zanim połączą dwie aplikacje peer to peer.

Nie w RTL i nie przez VCL. TsgcRTCPeerConnection to natywna implementacja w Object Pascal interfejsu peer connection według W3C: CreateOffer, CreateAnswer, SetLocalDescription, SetRemoteDescription, AddIceCandidate, CreateDataChannel i AddTrack, z ICE, DTLS, SCTP i SRTP pod spodem. W procesie nie ma osadzonego Chromium, nie ma TWebBrowser i nie ma mostka JavaScript.
Potrzebujesz czegoś, co przeniesie kilkaset bajtów tekstu między dwoma peerami, zanim rozmowa się zacznie, bo żaden z nich nie wie jeszcze, jak dosięgnąć drugiego. To sygnalizacja i może to być przekaźnik WebSocket, istniejąca kolejka wiadomości, punkt końcowy REST, a w demo nawet kopiuj i wklej. Gdy offer, answer i kandydaci ICE już się wymienią, multimedia i dane idą bezpośrednio między dwiema aplikacjami, a kanał sygnalizacyjny może się zamknąć. Żaden serwer nie stoi w ścieżce multimediów, chyba że przekaźnik TURN okazał się jedyną działającą trasą.
Prawie zawsze WebSocket, bo jest dwukierunkowy i serwer może przesłać przychodzący offer bez odpytywania przez peera. Ta strona buduje taki przekaźnik z TsgcWebSocketServer i TsgcWebSocketClient w około piętnastu liniach, przekazując każdą wiadomość drugiemu peerowi przez Broadcast z Connection.Guid nadawcy jako wykluczeniem. sgcWebSockets dostarcza też gotowy komponent protokołu sygnalizacyjnego, TsgcWSPServer_RTCPeerConnection, który prowadzi tę wymianę za ciebie przez RTCOptions.WebSocket i GatherCandidates, jeśli wolisz w ogóle nie pisać przekaźnika.
Jeśli obie aplikacje są w tej samej sieci LAN lub tym samym VPN, żadnego z nich. Kandydaci typu host już opisują osiągalne adresy. Jeśli są w różnych sieciach za zwykłymi routerami, potrzebujesz STUN, który mówi każdemu peerowi, z jakiego adresu publicznego zdają się pochodzić jego pakiety, i to obejmuje większość rzeczywistych połączeń. TURN potrzebujesz wtedy, gdy żadne bezpośrednie parowanie w ogóle nie działa: symetryczny NAT, restrykcyjne firewalle korporacyjne i niektórzy operatorzy komórkowi. TURN przekazuje każdy bajt rozmowy, więc jest kosztownym rozwiązaniem awaryjnym, a nie domyślnym. Dodaj oba do RTCOptions.ICEServers, a ICE wybierze najtańszą parę, która faktycznie się łączy.
Kanał danych to SCTP na DTLS i przenosi wiadomości, z niezawodnością, którą wybierasz: uporządkowany i w pełni niezawodny jak TCP albo nieuporządkowany, z limitem retransmisji lub czasu życia, dla wszystkiego, gdzie spóźniony pakiet jest bezużyteczny. Ścieżka multimedialna to SRTP na tym samym transporcie DTLS i przenosi oś czasu: dźwięk w ramkach 20 ms, wideo z określoną liczbą klatek, z założenia zawsze stratnie. Kanału danych używaj do czatu, przesyłania plików, zdalnego sterowania i stanu gry. Ścieżki używaj do mikrofonu, kamery lub ekranu. Dzielą jedno połączenie i jeden otwarty port.
Dwa kroki. Enterprise definiuje SGC_ICE, SGC_DTLS, SGC_TURN i SGC_RTCPEERCONNECTION, co umieszcza TsgcRTCPeerConnection, TsgcICEClient, TsgcTURNClient i TsgcTURNServer na palecie. API offer i answer, kanały danych oraz ścieżki multimedialne są zamknięte za SGC_SDP, SGC_DATACHANNEL i SGC_RTP, które definiuje wyłącznie SGC_PACK_WEBRTC, czyli dodatek sgcWebRTC, zawarty także w All-Access. Poniżej tego, klient STUN jest w edycji Standard, a serwer STUN i komponent serwera WebSocket są w Professional.
Tak i nic na tej stronie się nie zmienia. SDP jest standardowe, linie kandydatów niosą te same sdpMid i sdpMLineIndex, których oczekuje API przeglądarki, a maszyna stanów offer i answer podąża za RFC 8829, więc JSON przekazywany przez twój przekaźnik działa bez zmian w obu kierunkach. Przeglądarki wysyłają kandydatów w trybie trickle od pierwszej milisekundy, co jest dokładnie tym, co wykrywa RTCOptions.TrickleICEAuto: widzi a=ice-options:trickle w opisie zdalnym i odpowiada natychmiast, zamiast odczekiwać limit czasu zbierania.
Zawsze w wątkach roboczych. OnLocalDescription, OnIceCandidate, OnConnectionStateChange, OnTrack i OnError przychodzą w wątku ICE, sieciowym lub timera. Zdarzenia kanału danych przychodzą w wątku prowadzącym asocjację SCTP. Zdarzenia dźwięku i wideo ścieżki przychodzą w wątku sieciowym lub taktującym. Żadne z nich nie jest za ciebie przekazywane do wątku głównego, więc opakuj wszystko, co dotyka kontrolki, w TThread.Queue, tak jak robią to dostarczane dema.
Nie. Zostaw RTCOptions.DTLSOptions.CertFile puste, a certyfikat samopodpisany i klucz zostaną wygenerowane w pamięci, raz na komponent, i użyte ponownie dla każdego peera. Jego odcisk palca jest publikowany jako atrybut a=fingerprint opisu lokalnego i to właśnie uwierzytelnia cię wobec drugiej strony. Taki jest model zaufania WebRTC: łańcuch nigdy nie jest weryfikowany, kotwicą jest odcisk palca przeniesiony kanałem sygnalizacyjnym. Nadal możesz wskazać w CertFile i KeyFile własne pliki PEM, gdy chcesz mieć stałą tożsamość.
Handshake DTLS biegnie po nominowanej parze kandydatów i wyprowadza klucze SRTP, więc każdy pakiet RTP, RTCP i SCTP na tym połączeniu jest szyfrowany. Dla kanałów danych nie da się tego wyłączyć: CreateDataChannel bezwarunkowo ustawia RTCOptions.DTLS na True, ponieważ RTCDataChannel jest z definicji SCTP na DTLS i nie istnieje poprawna konfiguracja bez niego.
Moduły P2P i WebRTC są dostarczane w każdym pakiecie runtime od Delphi 7 po RAD Studio 13 oraz w odpowiadających im pakietach C++ Builder. Przechwytywanie i odtwarzanie dźwięku mają implementacje platformowe dla Windows, Linux, Android, iOS i macOS, więc funkcje fabryczne zwracają działający obiekt na wszystkich pięciu. Wyjątkiem jest przechwytywanie wideo: jest nazywane osobno dla każdej platformy, zamiast być budowane przez fabrykę, a na Windows to TsgcVideoCapture_Win, obok TsgcScreenCapture_Win i TsgcWindowCapture_Win do udostępniania ekranu.
Tak, to renegocjacja. AddTrack na ustanowionej sesji oznacza ją jako wymagającą negocjacji i uruchamia OnNegotiationNeeded. Wywołaj CreateOffer ponownie, a nowy offer zostanie zbudowany synchronicznie, bez nowego zbierania ICE i bez nowego handshake'u DTLS czy SCTP, więc transport nigdy nie jest przerywany. Istniejące linie multimediów zachowują swoją pozycję i swój mid, nowa jest dopisywana na końcu. RemoveTrack działa tak samo w drugą stronę: linia multimediów zostaje i jest publikowana ponownie jako recvonly lub inactive.

Dokumentacja komponentów i dokumenty techniczne

Każdy element używany na tej stronie ma własną stronę dokumentacji, a większość ma też osobny dokument techniczny PDF z pełną listą właściwości, metod i zdarzeń.

TsgcRTCPeerConnection

Komponent, o którym jest cała ta strona. Offer i answer, ICE, DTLS, kanały danych SCTP i multimedia RTP w jednej klasie.

Strona komponentu →

sgcWebRTC

Pakiet silnika multimediów: SDP, SCTP, RTP, SRTP, dźwięk Opus i G.711, wideo VP8 i H.264, estymacja przepustowości.

Strona produktu →

Rozbicie funkcji

Kodek po kodeku i platforma po platformie, co robi silnik multimediów i skąd pochodzi każdy koder.

Zobacz funkcje →

Klient ICE

Zbieranie kandydatów, lista kontrolna, nominacja i kolekcja serwerów ICE, warstwa pod peer connection.

Strona komponentu →

Klient i serwer STUN

Żądania binding, opcje retransmisji i komponent serwera, gdy wolisz hostować własny.

Klient STUN →

Klient i serwer TURN

Alokacje, uprawnienia, powiązania kanałów i komponent serwera TURN dla rozmów, które wymagają przekaźnika.

Serwer TURN →

Wszystkie komponenty P2P

UDP, STUN, TURN, ICE i RTCPeerConnection, cała rodzina peer to peer w jednym spisie.

Przeglądaj P2P →

Delphi WebRTC, przegląd

Spojrzenie na WebRTC w sgcWebSockets na poziomie biblioteki, z komponentami protokołu sygnalizacyjnego i listą dem.

Czytaj więcej →

Której edycji potrzebuję?

Pełna matryca edycji, funkcja po funkcji, gdy WebRTC nie jest jedyną rzeczą, którą rozważasz.

Porównaj edycje →

Inne przypadki użycia

Ta strona jest jednym z przypadków użycia Delphi, z których każdy przeprowadza jedno zadanie od początku do końca. Pozostałe jak dotąd to Wywołanie LLM z Delphi i Logowanie użytkownika za pomocą OAuth2 i PKCE.

Wszystkie przypadki użycia →
Dokument techniczny RTCPeerConnection (PDF) Właściwości, metody, zdarzenia i przykłady kodu dla samego komponentu peer connection.
Dokument techniczny klienta ICE (PDF) Zbieranie kandydatów, lista kontrolna i kolekcja serwerów ICE w szczegółach.
Dokument techniczny klienta TURN (PDF) Alokacje, uprawnienia i powiązania kanałów, klient, którym ICE steruje dla kandydata przekazywanego.
Dokument techniczny klienta STUN (PDF) Żądania binding i retransmisja, najtańszy sposób poznania własnego adresu publicznego.
Projekty demonstracyjne Demos\35.P2P\05.RTCPeerConnection i Demos\35.P2P\06.DataChannel są dostarczane w pakiecie.

Specyfikacje stojące za każdym krokiem

Źródła pierwotne, gdy wolisz przeczytać, co komponent implementuje, niż wierzyć nam na słowo.

RFC 8829, JSEP

Maszyna stanów offer i answer stojąca za CreateOffer, CreateAnswer i SetRemoteDescription, wraz z renegocjacją i wycofaniem.

Przeczytaj RFC →

RFC 8445, ICE

Zbieranie kandydatów, priorytet, lista kontrolna i nominacja. Powód, dla którego połączenie czasem zajmuje sekundę, a czasem zawodzi.

Przeczytaj RFC →

RFC 8489 i RFC 8656

STUN i TURN. O co pyta żądanie binding i ile kosztuje cię alokacja.

Przeczytaj RFC →

RFC 8831 i RFC 8832

Kanały danych WebRTC na SCTP oraz handshake otwarcia DCEP, który przypisuje identyfikatory strumieni według roli DTLS.

Przeczytaj RFC →

RFC 8122, odciski palca SDP

Dlaczego a=fingerprint jest tożsamością peer connection i dlaczego opis bez niego jest odrzucany.

Przeczytaj RFC →

WebRTC 1.0 (W3C)

API, które ten komponent odwzorowuje, wraz z perfect negotiation, skąd pochodzi Polite.

Przeczytaj specyfikację →

Połącz dwie swoje aplikacje rozmową

Pobierz wersję próbną, uruchom dema RTCPeerConnection i DataChannel przeciwko sobie, a potem zbuduj to samo we własnym projekcie.