Delphi WebRTC: iki uygulama arasında ses, video ve veri

İki farklı ağdaki iki Delphi uygulaması, aralarında doğrudan bir sohbet kanalı, bir mikrofon akışı ve bir kamera akışı alışverişi yapıyor. Ortada medya sunucusu yok, süreçte gömülü tarayıcı yok, JavaScript köprüsü yok. Bu sayfa işin tamamını, ilk sinyalleşme mesajından ilk çözülmüş ses karesine kadar, dağıtılan kaynakta var olan API'lerle yürür.

SDP offer ve answer
ICE, STUN ve TURN
SCTP veri kanalları
Opus ve VP8 medya izleri
DTLS-SRTP ile şifreli
Tarayıcı veya WebView yok

Gerçekte neler olması gerekiyor

WebRTC, tek bir ad taşıyan dört ayrı sorundur. Bunlardan yalnızca biri medyayla ilgilidir ve kolay olanı odur. Çözmeniz gereken sırayla dördü şöyle.

1. Oturumu tanımlayın

Bir taraf bir offer kurar; hangi medyayı göndermek istediğini, hangi kodekleri konuştuğunu, sunacağı sertifikanın parmak izini ve kullanacağı ICE kimlik bilgilerini söyleyen bir metin belgesi (SDP). Diğer taraf kabul ettiği alt kümeyle yanıt verir. CreateOffer ve CreateAnswer bu belgeleri üretir, SetRemoteDescription tüketir.

2. Karşı tarafa taşıyın

WebRTC, offer'ın diğer eşe nasıl ulaşacağını bilerek söylemez. O kanala sinyalleşme denir ve iş size düşer. Her yöne birkaç yüz bayt metindir, dolayısıyla küçük bir röleye açılan bir WebSocket bağlantısı yeter ve sgcWebSockets zaten böyle bir rölenin iki yarısını da verir.

3. NAT'ların arasından bir yol bulun

Hiçbir eş kendi genel adresini bilmez ve ikisi de genellikle bir yönlendiricinin arkasındadır. ICE, bir eşin ulaşılabilir olabileceği her adresi toplar, ortaya çıktıkça sinyalleşme kanalından gönderir ve biri işleyene kadar her eşleşmeyi yoklar. STUN genel adresi bulur, TURN ise doğrudan hiçbir şey işe yaramadığında bir röle sağlar.

4. Baytları taşıyın

Bir aday çifti aday gösterildiğinde onun üzerinde bir DTLS el sıkışması çalışır ve ondan sonrası şifrelidir. Bir veri kanalı o DTLS taşıması üzerinde SCTP'dir, bir ses veya video izi ise onun üzerinde SRTP'dir. İkisi de tek bağlantıyı ve tek açık portu paylaşır.

Bir WebRTC sunucusu var mı?

Medya yolunda yok, bütün mesele de bu. İki uygulama birbirini bulduktan sonra ses, video ve veri doğrudan aralarında gider. Barındırdığınız hiçbir şey yükü görmez ve barındırdığınız hiçbir şeyin kullanıcılarınızın görüşmede geçirdiği dakika sayısıyla ölçeklenmesi gerekmez.

Yine de resimde iki sunucu var ve her birinin ne yaptığı konusunda net olmak fayda sağlar, çünkü sık sık karıştırılırlar.

Sinyalleşme sunucusu sizindir. Görüşme başlamadan önce iki eş arasında bir avuç metin mesajı iletir, sonra susar. Medyayı hiç görmez. Bu anlatımda TsgcWebSocketServer üzerine kurulmuş on beş satır Delphi'dir.

STUN ve TURN sunucusu, WebRTC yüzünden değil NAT yüzünden vardır. Bir STUN sunucusu tek bir soruyu yanıtlar, "bu paket hangi genel adresten geldi", hepsi bu. Bir TURN sunucusu, başka hiçbir yoldan birbirine ulaşamayan çiftler için paketleri röleler, dolayısıyla medya taşıyan tek parça odur ve yalnızca buna ihtiyaç duyan görüşmeler için. Genel STUN sunucuları ücretsiz ve boldur, TURN'ü ise kendiniz barındırırsınız; ayrı bir arka plan servisi çalıştırmayı tercih etmiyorsanız sgcWebSockets Enterprise hem bir STUN sunucusu hem de bir TURN sunucusu bileşeniyle gelir.

kim-kiminle-konusur.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

Sürümler, birimler ve platformlar

Eş bağlantısı ile medya motoru iki farklı lisans adımıdır. Kod yazmadan önce bunu netleştirmekte fayda var, çünkü aksi halde derleyici API'nin yarısını hiç görmez.

Ne yapmak istiyorsunuzNeye ihtiyaç duyarNereden gelir
Genel bir adresi keşfetmek için STUN istemcisi TsgcSTUNClient sgcWebSockets Standard ve üstü
Kendi STUN sunucunuzu çalıştırmak TsgcSTUNServer sgcWebSockets Professional ve üstü
Bir WebSocket sinyalleşme kanalını rölelemek TsgcWebSocketServer Sunucu yarısı için sgcWebSockets Professional ve üstü. İstemci yarısı olan TsgcWebSocketClient Standard ile gelir.
ICE, TURN istemcisi ve sunucusu ve eş bağlantısı bileşeninin kendisi TsgcICEClient, TsgcTURNClient, TsgcTURNServer, TsgcRTCPeerConnection sgcWebSockets Enterprise
Offer ve answer, veri kanalları, ses ve video izleri CreateOffer, CreateAnswer, SetRemoteDescription, AddIceCandidate, CreateDataChannel, AddTrack Enterprise üzerine sgcWebRTC paketi. All-Access içinde de yer alır.

Ayrım neden var, derleyicinin kendi diliyle

sgcVer.inc dosyasının Enterprise bloğu SGC_ICE, SGC_DTLS, SGC_RTCPEERCONNECTION ve SGC_TURN tanımlarını yapar. TsgcRTCPeerConnection bileşenini palete koyan ve ona bir ICE ve TURN taşıması veren şey budur.

Bu sayfanın asıl konusu olan her şey bir kat daha içeridedir. SGC_SDP, SGC_SCTP, SGC_DATACHANNEL, SGC_RTP ve SGC_SRTP tanımlarını yapan SGC_PACK_WEBRTC değeridir ve önce ICE, DTLS ile eş bağlantısının zaten orada olduğunu denetler. Elle sinyalleşme metotları, veri kanalı API'si ve medya API'si sırasıyla {$IFDEF SGC_SDP}, {$IFDEF SGC_DATACHANNEL} ve {$IFDEF SGC_RTP} içinde yer alır, dolayısıyla tek başına Enterprise ile derlenmezler.

CreateOffer çözümlenmiyorsa olan budur. Kutuda gelen veri kanalı demosu sessizce başarısız olmak yerine bunu açıkça söyler.

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}

İki uygulamanın da ihtiyaç duyduğu birimler

sgcP2P, TsgcRTCPeerConnection bileşenini yayımlayan ve işleyici tiplerini yeniden dışa aktaran çatı birimidir. Bileşeni bildirmeye yeter, ama sabit değerler tiplerini bildiren birimlerden gelir, dolayısıyla rtctkAudio veya cctAudioOpus yazdığınızda onları da ekleyin.

Bu anlatımdaki iki uygulama, farklı bir düğmeye basılan aynı programdır. Aşağıdaki her şey ikisine de girer.

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

Sinyalleşme kanalı

Üç mesaj türü, tek bir aptal röle. Her WebRTC anlatımının el salladığı ve sizin gerçekten yazmanız gereken kısım budur.

Bir röle, bir aracı değil

Sinyalleşme sunucusunun ilettiği şeyin tek baytını bile anlaması gerekmez. Bir eşin gönderdiği metni alır ve diğerine verir. Broadcast zaten bir bağlantı Guid'i alan bir Exclude parametresine sahiptir, dolayısıyla "gönderen dışında herkese gönder" tek satırdır.

Onu bu kadar aptal tutun. Röle SDP ayrıştırmaya başladığı anda, her kodek değiştiğinde güncellemeniz gereken bir bileşen olur ve bir görüşmeyi bir tarayıcıya röleleyemez hale gelir.

Üretimde röleyi bir oda tanımlayıcısına anahtarlar, böylece iki görüşme çakışamaz, ve onu TLS arkasına koyarsınız. TsgcWebSocketServer, kütüphanenin geri kalanıyla aynı TLSOptions, Authentication ve WatchDog yüzeyini taşır.

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;

Aynı kanalın eş tarafı

Her uygulama o röleye bir TsgcWebSocketClient açar ve üç kelimelik bir söz dağarcığı konuşur: offer, answer ve candidate. Aynı kütüphaneden TsgcJSON bunları serileştirir, dolayısıyla ek bir bağımlılık yoktur.

Dalların nereye gittiğine dikkat edin. Gelen bir offer uzak açıklama olarak ayarlanır ve hemen yanıtlanır. Gelen bir answer yalnızca ayarlanır. Gelen bir candidate ise AddIceCandidate metoduna verilir ve açıklamadan önce de sonra da gelebilir; trickle ICE'ın bütün amacı budur.

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;

Eş bağlantısını kurun

İki uygulamada da aynıdır. Bütün alışverişteki tek asimetri, hangisinin Ara düğmesine bastığıdır.

Yapılandırma ve önemli olan olaylar

RTCOptions.ICEServers, W3C'nin iceServers listesidir. AddURL bir stun: veya turn: URL'si alır ve türü, ana bilgisayarı, portu ve TLS bayrağını şemadan doldurur; TURN için isteğe bağlı bir kullanıcı adı ve kimlik bilgisiyle birlikte.

RTCOptions.DTLS varsayılan olarak False değerindedir. Öyle bırakırsanız şifreleme ve SRTP anahtar materyali olmaz, dolayısıyla medya çalışmaz. CreateDataChannel bunu sizin için açar, çünkü bir veri kanalı DTLS üzerinde SCTP'dir ve DTLS kapalıyken geçerli bir yapılandırma yoktur. AddTrack açmaz, dolayısıyla medya eklerken siz ayarlayın.

Bir sertifika dosyasına ihtiyacınız yok. RTCOptions.DTLSOptions.CertFile alanını boş bırakın, bileşen başına bir kez bellekte kendinden imzalı bir sertifika üretilir; WebRTC modeli tam olarak budur: kimlik bir zincire değil, SDP içindeki a=fingerprint satırına dayanır. Parmak izi taşımayan bir uzak açıklama, herhangi bir şeye karşı el sıkışmasına izin verilmek yerine reddedilir.

Aşağıdaki her olay bir çalışan iş parçacığında tetiklenir; ICE, ağ veya zamanlayıcı iş parçacığında, asla ana iş parçacığında değil. Bir denetime dokunmadan önce TThread.Queue ile aktarın.

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;

Yerel açıklamayı yayımlamak

OnLocalDescription size türü, yani 'offer' veya 'answer' dizesini, ve SDP'nin kendisini verir. İki eş de aynı işleyiciyi kullanır ve o tek bir şey yapar: onu sinyalleşme kanalına koyar.

SDP zaten eksiksiz gelir. CreateOffer önce ICE adaylarını toplar ve varsayılanı 3000 olan RTCOptions.GatheringTimeout milisaniyeye kadar bekler, varsayılanı 500 olan GatheringIdleTimeout milisaniye boyunca yeni aday gelmezse erken biter. Trickle olmayan yol budur.

TrickleICE değerini True yaparsanız açıklama hemen çıkar ve adaylar arkasından gelir. Buna nadiren gerek duyarsınız. RTCOptions.TrickleICEAuto varsayılan olarak True'dur, dolayısıyla uzak açıklama her tarayıcının yaptığı gibi a=ice-options:trickle dediğinde bileşen kendini o moda geçirir ve toplama zaman aşımını boşa harcamayı bırakır.

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;

ICE adayları, STUN ve TURN

Eşler arası bağlantıların başarısız olduğu ve başarısızlıkların en zor okunduğu yer burasıdır. Üç tür aday, var olmalarının üç nedeni.

host

Makinenin kendi üzerinde görebildiği bir adres, ağ arayüzü başına bir tane. Ücretsiz, anında ve iki uygulama aynı yerel ağda ya da aynı VPN üzerindeyken yeterli. İki Delphi uygulamanız yalnızca tek bir ofisin içinde çalışıyorsa host adayları size yeter ve STUN'u tamamen atlayabilirsiniz.

srflx, sunucu yansımalı

Bir STUN sunucusunun, eşin paketinin geldiğini gördüğü genel adres. Sıradan ev yönlendiricilerinin arkasındaki iki eşi doğrudan konuşturan şey budur ve gerçek bağlantıların büyük çoğunluğunu kapsar. Sonrasında hiç trafik taşımayan bir STUN sunucusuna tek bir gidiş dönüşe mal olur.

relay

Bir TURN sunucusu üzerinde, eşe ileten bir adres. Simetrik NAT, kısıtlayıcı kurumsal güvenlik duvarları ve bazı mobil operatörler için gereklidir. Görüşmenin her baytı TURN sunucunuzdan geçer, dolayısıyla pahalı yoldur ve yalnızca geri düştüğünüz yoldur.

Onları damla damla göndermek

OnIceCandidate, her aday keşfedildikçe aday satırı, sdpMid ve sdpMLineIndex değerleriyle bir kez tetiklenir. Bu üç alan tam olarak tarayıcı API'sinin beklediği alanlardır, dolayısıyla karşı taraf Delphi de olsa Chrome da olsa aynı JSON çalışır.

Her birini hemen gönderin. Beklemeyin, toplu göndermeyin. Uzak açıklamadan önce gelen bir aday tutulur ve açıklama geldiğinde uygulanır, dolayısıyla sıralama sizin sorununuz değildir.

Çift sonunda aday gösterildiğinde SelectedLocalCandidate ve SelectedRemoteCandidate size hangi iki adresin kazandığını söyler. O tek günlük satırı, "bu görüşme neden TURN sunucumdan geçiyor" sorusunu her şeyden hızlı yanıtlar.

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;

TURN eklemek ve unutmamanız gereken anahtar

ICEServers içindeki bir turn: girdisi kendi ana bilgisayarını, portunu, kullanıcı adını ve kimlik bilgisini taşır ve tahsis bunu kullanır. Eklemek bir AddURL daha demektir.

Tuzak diğer yöndedir. RTCOptions.ICE.TURN varsayılan olarak True değerindedir ve sunucu listesinde hiç TURN girdisi yokken toplama, ana bilgisayarı varsayılan olarak 127.0.0.1 ve portu 3478 olan RTCOptions.ICE içindeki tek sunucuya geri düşer. Yani yalnızca bir STUN URL'siyle yapılandırılmış bir eş yine de localhost'a karşı bir TURN tahsisi dener, başarısız olur ve bunu bildirir. Bu bir arıza değil gürültüdür, ama günlükte endişe verici görünür ve sizi yanlış yerde aramaya gönderir. Gerçekten bir TURN sunucunuz olana kadar RTCOptions.ICE.TURN := False yapın.

RTCOptions.ICE.STUN aynı şekilde davranır ve o da varsayılan olarak True'dur.

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;

ICE istemcisi referansı Kendi TURN sunucunuzu çalıştırın

Veri kanalı

İki uygulama arasında metin ve ikili veri, kanal başına seçtiğiniz güvenilirlikle. Genellikle ilk çalıştırdığınız şey budur ve bütün taşımayı kanıtlar.

Bir tane açmak ve karşı tarafınkini almak

CreateDataChannel çağıran eş nesneyi hemen geri alır. Çağırmayan eş aynı kanalı OnDataChannel üzerinden alır. İşleyicileri iki yere de bağlayın, çünkü oturumun herhangi bir noktasında iki taraf da bir kanal açabilir.

Kanal, oluşturduğunuz anda kullanılabilir değildir. SCTP ilişkisi ayağa kalkana ve DTLS rolü çözülene kadar Id değeri atanmadan kalır, kanal açık değilken Send False döndürür. OnOpen olayını bekleyin.

Güvenilirliğe oluşturma anında karar verilir. Varsayılanlar sıralı ve tam güvenilirdir, yani TCP benzeri bir kanal. Sırasız teslimat için aOrdered = False geçin, ya da kısmi güvenilirlik için bir aMaxRetransmits veya aMaxPacketLifeTime verin; konum güncellemeleri ya da geç bir paketin kayıp bir paketten kötü olduğu her şey için istediğiniz budur.

Bir kanalı serbest bırakmayın, sahibi eş bağlantısıdır. Close kapanışı başlatır ve karşı taraf kabul ettiğinde OnClose tetiklenir.

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;

Göndermek ve boğmamak

Send bir dize, SendBytes ise bir TBytes alır. Kanal açık değilken ikisi de hata fırlatmak yerine False döndürür, dolayısıyla kapanış sırasındaki bir gönderim, bir çalışan iş parçacığındaki istisna değil, döndürülen bir False'tur.

MaxMessageSize, eşin kabul ettiğini söylediği en büyük mesajdır ve açıklamasının a=max-message-size özniteliğinden okunur. Bu sınırın üzerindeki bir mesaj, hatta konularak bütün ilişkiyi iptal edip diğer her kanalı da beraberinde götürmek yerine yerelde reddedilir. Sıfır, hiçbir açıklamanın bir sınır belirtmediği ve denetimin kapalı olduğu anlamına gelir.

BufferedAmount, bu akış için SCTP'de sıraya alınmış ve henüz onaylanmamış bayt sayısıdır. Bir dosya akıtırken bunu izleyin: bir eşiği geçene kadar gönderin, sonra belleğe gigabaytlarca veri yığmak yerine boşalmasını bekleyin.

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;

Ses ve video izleri

Bir iz, içinde resim olan bir veri kanalı değildir. Farklı taşıma, farklı hata biçimleri, farklı kod. İnsanların ilk başta en çok yanlış anladığı ayrım budur.

 Veri kanalıMedya izi
Taşıyan DTLS üzerinde SCTP (RFC 8831) Aynı DTLS taşıması üzerinde SRTP (RFC 3711)
Teslimat Sizin seçiminiz, tam güvenilir ve sıralıdan gönder ve unuta kadar Tasarımı gereği her zaman kayıplı. Geç olmak kayıp olmaktan kötüdür, bu yüzden hiçbir şey sonsuza kadar yeniden iletilmez
İş birimi Bir mesaj. Ne zaman göndereceğinize siz karar verirsiniz Bir saat. Ses 20 ms'lik karelerle, video bir kare hızıyla beslenir
Şununla açılır CreateDataChannel, oturumun herhangi bir anında AddTrack, yayımlamak için yeni bir offer ister
Şununla alınır OnDataChannel, sonra kanalın kendi OnMessage olayı OnTrack, sonra izin OnAudio veya OnVideoFrame olayı
DTLS açık olmalı Evet, ve CreateDataChannel bunu sizin için ayarlar Evet, ve AddTrack ayarlamaz. RTCOptions.DTLS değerini kendiniz verin
Şunun için kullanın Sohbet, dosya aktarımı, uzaktan denetim, oyun durumu, telemetri Mikrofon, kamera, ekran paylaşımı, zaman çizgisi olan her şey

Mikrofonu göndermek

AddTrack bir tür ve bir kodek alır ve bir TsgcRTCTrack döndürür. Ses kodekleri cctAudioOpus, cctAudioPCMU ve cctAudioPCMA, video kodekleri ise cctVideoVP8, cctVideoVP9, cctVideoH264 ve cctVideoJPEG değerleridir.

Yakalama ayrı bir nesnedir, çünkü platform mikrofonunu hiç istemiyor olabilirsiniz. sgcCreateAudioCapture, kodun derlendiği platform için doğru uygulamayı kurar; Windows'ta waveIn, Linux'ta ALSA, Android'de AudioRecord, iOS ve macOS'ta bir VoiceProcessingIO Audio Unit, dolayısıyla kodunuzda hiçbir şey bir platform sınıfını adlandırmaz. Uygulaması olmayan bir hedefte nil döndürür, bu yüzden sonucu sınayın.

SendPCM, kodlayıcının hızında ve kanal sayısında 16 bit işaretli aralıklı PCM ister; bu, Opus için 48000 Hz, G.711 için 8000 Hz demektir. Yakalama kaynağı gerçekte ne verdiğini AudioSampleRate, AudioChannels ve AudioFrameDurationMs üzerinden yayımlar, böylece varsaymak yerine denetleyebilirsiniz.

Oturum zaten ayaktayken bir iz eklemek, müzakere gerekli bayrağını kurar ve OnNegotiationNeeded olayını tetikler. Yayımlamak için yeniden CreateOffer çağırın; yeni offer taşımaya dokunulmadan kurulur.

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;

Karşı tarafın gönderdiğini oynatmak

OnTrack, uzak açıklama bir medya satırı getirdiğinde her uzak medya satırı için bir kez tetiklenir. Size verdiği izin sahibi eş bağlantısıdır, dolayısıyla olaylarını bağlayın ve onu asla serbest bırakmayın.

Ses, kod çözücünün ürettiği hız ve kanal sayısıyla, çözülmüş PCM olarak OnAudio üzerinden gelir. Onu doğrudan sgcCreateAudioRenderer tarafından kurulan TsgcMediaRenderer nesnesine verin; cihaz eşleşecek şekilde açılamadığında biçimi o dönüştürür.

Video, OnVideoFrame üzerinden çözülmüş bir TsgcVideoFrame olarak gelir: Data içinde ham pikseller, yanında Width, Height, Format ve Stride. Biçimler vffI420, vffNV12, vffRGB24, vffRGBA32, vffBGR24 ve vffBGRA32 değerleridir; iki BGR biçimi Windows GDI bayt sırasını izler, dolayısıyla bir vffBGR24 karesini bir bitmap'e aktarmak bir bellek kopyasıdır, bir dönüşüm değil.

Paket kaybından sonra bozuk bir kare gelirse RequestKeyFrame göndericiden yenisini ister.

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, Windows'ta

Ses yakalama fabrikanın arkasına soyutlanmıştır, çünkü desteklenen her platformun bir uygulaması vardır. Video yakalama öyle değildir, dolayısıyla platform sınıfını siz adlandırırsınız. Windows'ta bu, sgcP2P_MediaCapture_Win biriminden gelen ve Video for Windows'u süren, kareleri temel sınıfın bildirdiği aynı OnVideoCapture olayı üzerinden veren TsgcVideoCapture_Win sınıfıdır.

Komşu birim sgcP2P_ScreenCapture_Win size TsgcScreenCapture_Win ve TsgcWindowCapture_Win sınıflarını verir; ikisi de TsgcMediaCaptureSource türevidir, dolayısıyla ekran paylaşımı farklı bir yapıcıyla aynı üç satırdır.

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;

Önceden bilmeye değer hatalar

Eşler arası bağlantılar hiç hata üretmeyen biçimlerde başarısız olur, onları zorlaştıran da budur. En sık karşılaşılanlar şunlar.

Medya sessiz ve hiçbir hata yok

RTCOptions.DTLS False. Varsayılan budur; CreateDataChannel açar ama AddTrack açmaz, dolayısıyla yalnızca medya taşıyan bir oturum hiç DTLS el sıkışması çalıştırmaz ve bu yüzden hiç SRTP anahtarı almaz. Açıkça ayarlayın.

İstemediğiniz bir TURN hatası

RTCOptions.ICE.TURN varsayılan olarak True'dur ve sunucu listesinde TURN girdisi yokken 127.0.0.1:3478 adresine geri düşer. Gerçekten bir TURN sunucunuz olana kadar False yapın, yoksa günlük sorununuzla hiç ilgisi olmayan tahsis hatalarıyla dolar.

Uzak açıklama reddediliyor

a=fingerprint satırı olmayan bir açıklama doğrudan reddedilir ve OnError üzerinden bildirilir. WebRTC güven modelinde eşin kimliğini doğrulayan tek şey o satırdır, dolayısıyla o satır olmadan bir açıklamayı kabul etmek, el sıkışmasının herhangi bir sertifikaya karşı tamamlanmasına izin vermek olurdu.

Bir olay işleyicisinde erişim ihlali

Her eş bağlantısı, veri kanalı ve iz olayı bir çalışan iş parçacığında tetiklenir; ICE, ağ, SCTP veya tik iş parçacığında, asla ana iş parçacığında değil. Bunlardan birinden doğrudan bir VCL veya FMX denetimine dokunmak tanımsızdır. TThread.Queue içine sarın.

İki taraf da aynı anda yeniden offer gönderiyor

Buna glare denir ve W3C mükemmel müzakere kuralıyla çözülür. Kibar olmayan eş, yani Polite = False olan, kendi offer'ını korur ve geleni OnError üzerinden bildirir. Kibar eş kendininkini geri alır ve yanıt verir. Bir oturumun iki eşi aynı anda kibar olmamalıdır.

Tek bir büyük mesaj her kanalı öldürüyor

Eşin a=max-message-size değerini aşan bir mesaj, bütün SCTP ilişkisini iptal eder ve diğer her veri kanalını da beraberinde götürür. Send ve SendBytes bunun yerine MaxMessageSize değerini denetler ve yerelde reddeder. Büyük yükleri kendiniz bölün.

Bağlantının başlaması üç saniye sürüyor

Bu, trickle olmayan bekleme olan RTCOptions.GatheringTimeout değeridir. Adaylar gelmeyi bıraktığında GatheringIdleTimeout onu erken bitirir ve uzak açıklama trickle duyurduğunda TrickleICEAuto trickle moduna geçer. Yerel ağda iki zaman aşımını da düşürün.

Ses hızlı, yavaş veya bozuk

Yakalama cihazı ile kodlayıcı arasında bir örnekleme hızı veya kanal sayısı uyuşmazlığı. Opus 48000 Hz'de, G.711 ise 8000 Hz'de müzakere edilir. Cihazın istediğinizi yerine getirdiğini varsaymak yerine AudioSampleRate ve AudioChannels değerlerini yakalama kaynağından geri okuyun.

Delphi WebRTC, sıkça sorulanlar

Geliştiricilerin iki uygulamayı eşler arası bağlamadan önce sorduğu sorular.

RTL'de değil, VCL üzerinden de değil. TsgcRTCPeerConnection, W3C eş bağlantısı yüzeyinin yerel bir Object Pascal uygulamasıdır: CreateOffer, CreateAnswer, SetLocalDescription, SetRemoteDescription, AddIceCandidate, CreateDataChannel ve AddTrack, altlarında ICE, DTLS, SCTP ve SRTP ile. Süreçte gömülü Chromium, TWebBrowser veya JavaScript köprüsü yoktur.
Görüşme başlamadan önce iki eş arasında birkaç yüz bayt metin taşıyabilecek bir şeye ihtiyacınız var, çünkü henüz hiçbiri diğerine nasıl ulaşacağını bilmiyor. Buna sinyalleşme denir ve bir WebSocket rölesi, mevcut bir mesaj kuyruğu, bir REST uç noktası, hatta bir demo için kopyala yapıştır olabilir. Offer, answer ve ICE adayları karşıya geçtikten sonra medya ve veri doğrudan iki uygulama arasında gider ve sinyalleşme kanalı kapanabilir. Bir TURN rölesi işleyen tek yol olarak ortaya çıkmadıkça medya yolunda hiçbir sunucu oturmaz.
Neredeyse her zaman bir WebSocket, çünkü çift yönlüdür ve sunucu gelen bir offer'ı, eş yoklama yapmadan itebilir. Bu sayfa bunu TsgcWebSocketServer ve TsgcWebSocketClient ile yaklaşık on beş satırda kurar; her mesajı Broadcast ile ve göndericinin Connection.Guid değerini dışlayarak diğer eşe röleler. sgcWebSockets ayrıca hazır bir sinyalleşme protokolü bileşeni olan TsgcWSPServer_RTCPeerConnection ile de gelir; röleyi hiç yazmak istemediğinizde alışverişi sizin için RTCOptions.WebSocket ve GatherCandidates üzerinden yürütür.
İki uygulama aynı yerel ağda ya da aynı VPN üzerindeyse hiçbirine. Host adayları zaten ulaşılabilir adresleri tanımlar. Sıradan yönlendiricilerin arkasında farklı ağlardaysalar STUN'a ihtiyacınız olur; STUN her eşe paketlerinin geldiği görünen genel adresi söyler ve bu, gerçek bağlantıların çoğunu kapsar. TURN'e ise hiçbir doğrudan eşleşme işe yaramadığında ihtiyaç duyarsınız: simetrik NAT, kısıtlayıcı kurumsal güvenlik duvarları ve bazı mobil operatörler. TURN görüşmenin her baytını röleler, dolayısıyla varsayılan değil pahalı geri düşüş yoludur. İkisini de RTCOptions.ICEServers içine ekleyin, ICE gerçekten bağlanan en ucuz çifti seçsin.
Bir veri kanalı, DTLS üzerinde SCTP'dir ve mesaj taşır; güvenilirliği siz seçersiniz: TCP gibi sıralı ve tam güvenilir, ya da geç bir paketin işe yaramadığı her şey için yeniden iletim veya ömür sınırıyla sırasız. Bir medya izi ise aynı DTLS taşıması üzerinde SRTP'dir ve bir zaman çizgisi taşır: 20 ms'lik karelerle ses, kare hızıyla video, tasarımı gereği her zaman kayıplı. Sohbet, dosya aktarımı, uzaktan denetim ve oyun durumu için veri kanalı kullanın. Mikrofon, kamera veya ekran için iz kullanın. Tek bağlantıyı ve tek açık portu paylaşırlar.
İki adım. Enterprise, SGC_ICE, SGC_DTLS, SGC_TURN ve SGC_RTCPEERCONNECTION tanımlarını yapar; TsgcRTCPeerConnection, TsgcICEClient, TsgcTURNClient ve TsgcTURNServer bileşenlerini palete koyan budur. Offer ve answer API'si, veri kanalları ve medya izleri SGC_SDP, SGC_DATACHANNEL ve SGC_RTP arkasında kapılıdır; bunları yalnızca SGC_PACK_WEBRTC tanımlar ve o da All-Access içinde de yer alan sgcWebRTC eklentisidir. Onun altında bir STUN istemcisi Standard içinde, bir STUN sunucusu ile WebSocket sunucu bileşeni ise Professional içindedir.
Evet ve bu sayfada hiçbir şey değişmez. SDP standarttır, aday satırları tarayıcı API'sinin beklediği aynı sdpMid ve sdpMLineIndex değerlerini taşır ve offer ile answer durum makinesi RFC 8829'u izler, dolayısıyla rölenizin ilettiği JSON her iki yönde de değiştirilmeden çalışır. Tarayıcılar adayları ilk milisaniyeden itibaren damla damla gönderir; RTCOptions.TrickleICEAuto tam olarak bunu algılar: uzak açıklamada a=ice-options:trickle satırını görür ve toplama zaman aşımını beklemek yerine hemen yanıt verir.
Her zaman çalışan iş parçacıklarında. OnLocalDescription, OnIceCandidate, OnConnectionStateChange, OnTrack ve OnError ICE, ağ veya zamanlayıcı iş parçacığında gelir. Veri kanalı olayları SCTP ilişkisini süren iş parçacığında gelir. İz ses ve video olayları ağ veya tik iş parçacığında gelir. Hiçbiri sizin için ana iş parçacığına aktarılmaz, dolayısıyla bir denetime dokunan her şeyi TThread.Queue içine sarın; dağıtılan demoların yaptığı da budur.
Hayır. RTCOptions.DTLSOptions.CertFile alanını boş bırakın; bileşen başına bir kez bellekte kendinden imzalı bir sertifika ve anahtar üretilir ve her eş için yeniden kullanılır. Parmak izi, yerel açıklamanın a=fingerprint özniteliği olarak yayımlanır ve karşı tarafa kimliğinizi doğrulayan şey odur. WebRTC güven modeli budur: zincir hiç doğrulanmaz, sinyalleşme kanalı üzerinden taşınan parmak izi dayanaktır. Kararlı bir kimlik istediğinizde CertFile ve KeyFile değerlerini yine de kendi PEM dosyalarınıza yöneltebilirsiniz.
Aday gösterilen çift üzerinde bir DTLS el sıkışması çalışır ve SRTP anahtarlarını türetir, dolayısıyla bağlantıdaki her RTP, RTCP ve SCTP paketi şifrelidir. Veri kanalları için bunu kapatmanın bir yolu yoktur: CreateDataChannel, RTCOptions.DTLS değerini koşulsuz olarak True yapar, çünkü bir RTCDataChannel tanımı gereği DTLS üzerinde SCTP'dir ve onsuz geçerli bir yapılandırma yoktur.
P2P ve WebRTC birimleri, Delphi 7'den RAD Studio 13'e kadar her çalışma zamanı paketinde ve eşleşen C++ Builder paketlerinde gelir. Ses yakalama ve oynatmanın Windows, Linux, Android, iOS ve macOS için platform uygulamaları vardır, dolayısıyla fabrika fonksiyonları beşinde de çalışan bir nesne döndürür. Video yakalama istisnadır: bir fabrikayla kurulmak yerine platform başına adlandırılır ve Windows'ta bu, ekran paylaşımı için TsgcScreenCapture_Win ve TsgcWindowCapture_Win sınıflarının yanında TsgcVideoCapture_Win sınıfıdır.
Evet, buna yeniden müzakere denir. Kurulmuş bir oturumda AddTrack çağırmak oturumu müzakere gerektiriyor olarak işaretler ve OnNegotiationNeeded olayını tetikler. Yeniden CreateOffer çağırın; yeni offer eşzamanlı olarak, yeni ICE toplaması olmadan ve yeni DTLS ya da SCTP el sıkışması olmadan kurulur, dolayısıyla taşıma hiç kesilmez. Var olan medya satırları konumlarını ve mid değerlerini korur, yenisi sona eklenir. RemoveTrack ters yönde aynı şekilde çalışır: medya satırı kalır ve recvonly ya da inactive olarak yeniden yayımlanır.

Bileşen referansları ve teknik belgeler

Bu sayfanın kullandığı her parçanın kendi referans sayfası vardır ve çoğunun tam özellik, metot ve olay listesini içeren bağımsız bir teknik PDF'i bulunur.

TsgcRTCPeerConnection

Bu sayfanın tamamının konusu olan bileşen. Tek sınıfta offer ve answer, ICE, DTLS, SCTP veri kanalları ve RTP medyası.

Bileşen sayfası →

sgcWebRTC

Medya motoru paketi: SDP, SCTP, RTP, SRTP, Opus ve G.711 ses, VP8 ve H.264 video, bant genişliği kestirimi.

Ürün sayfası →

Özellik dökümü

Kodek kodek ve platform platform, medya motorunun ne yaptığı ve her kodlayıcının nereden geldiği.

Özellikleri görün →

ICE istemcisi

Aday toplama, denetim listesi, aday gösterme ve ICE sunucu koleksiyonu; eş bağlantısının altındaki katman.

Bileşen sayfası →

STUN istemcisi ve sunucusu

Bağlama istekleri, yeniden iletim seçenekleri ve kendinizinkini barındırmayı tercih ettiğinizde sunucu bileşeni.

STUN istemcisi →

TURN istemcisi ve sunucusu

Tahsisler, izinler, kanal bağlamaları ve röleye ihtiyaç duyan görüşmeler için bir TURN sunucu bileşeni.

TURN sunucusu →

Tüm P2P bileşenleri

UDP, STUN, TURN, ICE ve RTCPeerConnection; bütün eşler arası aile tek bir dizinde.

P2P'ye göz atın →

Delphi WebRTC genel bakış

sgcWebSockets içinde WebRTC'nin kütüphane düzeyindeki görünümü, sinyalleşme protokolü bileşenleri ve demo listesiyle.

Daha fazla bilgi →

Hangi sürüme ihtiyacım var?

WebRTC tarttığınız tek şey olmadığında, özellik özellik tam sürüm matrisi.

Sürümleri karşılaştırın →

Diğer kullanım senaryoları

Bu sayfa, her biri tek bir işi baştan sona ele alan Delphi kullanım senaryolarından biridir. Şimdiye kadarki diğerleri Delphi'den bir LLM çağırma ve bir kullanıcıyı OAuth2 ve PKCE ile oturum açtırma.

Tüm kullanım senaryoları →
RTCPeerConnection teknik belgesi (PDF) Yalnızca eş bağlantısı bileşeni için özellikler, metotlar, olaylar ve kod örnekleri.
ICE istemcisi teknik belgesi (PDF) Aday toplama, denetim listesi ve ICE sunucu koleksiyonu ayrıntılarıyla.
TURN istemcisi teknik belgesi (PDF) Tahsisler, izinler ve kanal bağlamaları; röleli bir aday için ICE'ın sürdüğü istemci.
STUN istemcisi teknik belgesi (PDF) Bağlama istekleri ve yeniden iletim; kendi genel adresinizi öğrenmenin en ucuz yolu.
Demo projeleri Demos\35.P2P\05.RTCPeerConnection ve Demos\35.P2P\06.DataChannel paketin içinde gelir.

Her adımın arkasındaki belirtimler

Bileşenin neyi uyguladığını bizim sözümüze güvenmek yerine okumayı tercih ettiğinizde birincil kaynaklar.

RFC 8829, JSEP

CreateOffer, CreateAnswer ve SetRemoteDescription arkasındaki offer ve answer durum makinesi, yeniden müzakere ve geri alma dahil.

RFC'yi okuyun →

RFC 8445, ICE

Aday toplama, öncelik, denetim listesi ve aday gösterme. Bir bağlantının bazen bir saniye sürmesinin, bazen de başarısız olmasının nedeni.

RFC'yi okuyun →

RFC 8489 ve RFC 8656

STUN ve TURN. Bir bağlama isteğinin ne sorduğu ve bir tahsisin size neye mal olduğu.

RFC'yi okuyun →

RFC 8831 ve RFC 8832

SCTP üzerinde WebRTC veri kanalları ve akış kimliklerini DTLS rolüne göre atayan DCEP açma el sıkışması.

RFC'yi okuyun →

RFC 8122, SDP parmak izleri

a=fingerprint satırının neden bir eş bağlantısının kimliği olduğu ve o satır olmayan bir açıklamanın neden reddedildiği.

RFC'yi okuyun →

WebRTC 1.0 (W3C)

Bu bileşenin yansıttığı API, Polite değerinin geldiği yer olan mükemmel müzakere dahil.

Belirtimi okuyun →

İki uygulamanızı bir görüşmeye bağlayın

Denemeyi indirin, RTCPeerConnection ve DataChannel demolarını birbirine karşı çalıştırın, sonra aynı şeyi kendi projenizin içine kurun.