Delphi WebRTC: 두 애플리케이션 사이의 오디오와 비디오, 데이터

서로 다른 두 네트워크에 있는 두 Delphi 애플리케이션이 채팅 채널과 마이크 스트림, 카메라 스트림을 직접 주고받아요. 중간에 미디어 서버도 없고, 프로세스 안에 브라우저를 심지도 않고, JavaScript 브리지도 없어요. 이 페이지는 첫 시그널링 메시지부터 첫 디코딩된 오디오 프레임까지 작업 전체를 따라가요. 실제 배포되는 소스에 있는 API만 써서요.

SDP offer와 answer
ICE, STUN, TURN
SCTP 데이터 채널
Opus와 VP8 미디어 트랙
DTLS-SRTP로 암호화
브라우저나 WebView 없이

실제로 일어나야 하는 일

WebRTC는 하나의 이름을 쓰고 있는 네 가지 별개의 문제예요. 그중 미디어에 관한 건 하나뿐이고, 그게 제일 쉬워요. 풀어야 하는 순서대로 네 가지를 소개할게요.

1. 세션 기술하기

한쪽이 offer를 만들어요. 어떤 미디어를 보내려 하는지, 어떤 코덱을 쓰는지, 제시할 인증서의 지문은 무엇인지, 어떤 ICE 자격 증명을 쓸지 적은 텍스트 문서(SDP)예요. 다른 쪽은 그중 받아들일 부분만 담아 answer로 답해요. CreateOfferCreateAnswer가 그 문서를 만들고, SetRemoteDescription이 그것을 받아들여요.

2. 상대편에 전달하기

WebRTC는 offer가 상대 피어에 어떻게 닿는지를 일부러 정하지 않아요. 그 통로를 시그널링이라고 하고, 그건 여러분의 몫이에요. 양방향으로 수백 바이트짜리 텍스트면 되니까 작은 릴레이로 가는 WebSocket 연결이면 충분하고, sgcWebSockets가 그 양쪽 절반을 이미 제공해요.

3. NAT를 통과하는 경로 찾기

두 피어 모두 자기 공인 주소를 모르고, 보통은 둘 다 라우터 뒤에 있어요. ICE는 피어가 도달 가능할 만한 주소를 전부 모아서 나오는 대로 시그널링 채널로 보내고, 통하는 조합이 나올 때까지 짝마다 탐색해요. STUN은 공인 주소를 찾아주고, 직접 연결이 전혀 되지 않을 때는 TURN이 중계를 제공해요.

4. 바이트 실어 나르기

후보 짝 하나가 선정되면 그 위에서 DTLS 핸드셰이크가 이뤄지고, 그다음부터는 모든 것이 암호화돼요. 데이터 채널은 그 DTLS 전송 위의 SCTP이고, 오디오나 비디오 트랙은 그 위의 SRTP예요. 둘은 하나의 연결과 하나의 열린 포트를 함께 써요.

WebRTC 서버라는 게 있나요?

미디어 경로에는 없어요. 그게 핵심이에요. 두 애플리케이션이 서로를 찾고 나면 오디오와 비디오, 데이터는 둘 사이를 직접 오가요. 여러분이 운영하는 어떤 것도 페이로드를 보지 않고, 사용자가 통화에 쓰는 시간에 맞춰 확장해야 할 것도 없어요.

그래도 그림 안에는 서버가 두 개 있어요. 자주 혼동되니 각각이 무슨 일을 하는지 정확히 짚어두면 좋아요.

시그널링 서버는 여러분의 것이에요. 통화가 시작되기 전에 두 피어 사이로 텍스트 메시지 몇 개를 중계하고 나면 조용해져요. 미디어는 결코 보지 않아요. 이 안내에서는 TsgcWebSocketServer 위에 올린 Delphi 코드 열다섯 줄이에요.

STUN과 TURN 서버는 WebRTC 때문이 아니라 NAT 때문에 존재해요. STUN 서버는 "이 패킷은 어떤 공인 주소에서 왔는가"라는 질문 하나에만 답하고 그게 전부예요. TURN 서버는 다른 방법으로는 서로에게 닿을 수 없는 짝을 위해 패킷을 중계해요. 그래서 미디어를 실어 나르는 유일한 조각이고, 그것도 필요한 통화에서만 그래요. 공개 STUN 서버는 무료로 많이 있고 TURN은 직접 운영해야 하는데, 별도 데몬을 돌리고 싶지 않다면 sgcWebSockets Enterprise에 STUN 서버TURN 서버 컴포넌트가 모두 들어 있어요.

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

에디션과 유닛, 플랫폼

피어 연결과 미디어 엔진은 라이선스 단계가 서로 달라요. 코드를 쓰기 전에 이걸 정리해 두는 게 좋아요. 그러지 않으면 컴파일러가 API의 절반을 아예 보지 못하거든요.

하려는 일필요한 것어디에 들어 있는지
공인 주소를 알아내는 STUN 클라이언트 TsgcSTUNClient sgcWebSockets Standard 이상
직접 STUN 서버 운영하기 TsgcSTUNServer sgcWebSockets Professional 이상
WebSocket 시그널링 채널 중계하기 TsgcWebSocketServer 서버 절반은 sgcWebSockets Professional 이상이에요. 클라이언트 절반인 TsgcWebSocketClientStandard부터 제공돼요.
ICE, TURN 클라이언트와 서버, 그리고 피어 연결 컴포넌트 자체 TsgcICEClient, TsgcTURNClient, TsgcTURNServer, TsgcRTCPeerConnection sgcWebSockets Enterprise
Offer와 answer, 데이터 채널, 오디오와 비디오 트랙 CreateOffer, CreateAnswer, SetRemoteDescription, AddIceCandidate, CreateDataChannel, AddTrack Enterprise 위에 얹는 sgcWebRTC 팩이에요. All-Access에도 포함돼요.

컴파일러의 말로 보는, 이렇게 나뉜 이유

sgcVer.inc의 Enterprise 블록이 SGC_ICE, SGC_DTLS, SGC_RTCPEERCONNECTION, SGC_TURN을 정의해요. 그게 TsgcRTCPeerConnection을 팔레트에 올리고 ICE와 TURN 전송을 붙여줘요.

이 페이지가 실제로 다루는 내용은 그보다 한 단계 더 안쪽에 있어요. SGC_SDP, SGC_SCTP, SGC_DATACHANNEL, SGC_RTP, SGC_SRTP를 정의하는 건 SGC_PACK_WEBRTC이고, 이건 ICE와 DTLS, 피어 연결이 이미 있는지 먼저 확인해요. 수동 시그널링 메서드와 데이터 채널 API, 미디어 API는 각각 {$IFDEF SGC_SDP}, {$IFDEF SGC_DATACHANNEL}, {$IFDEF SGC_RTP} 안에 들어 있어서, Enterprise만으로는 컴파일되지 않아요.

CreateOffer가 해석되지 않는다면 바로 그것 때문이에요. 제품에 들어 있는 데이터 채널 데모는 조용히 실패하는 대신 그 사실을 분명히 알려줘요.

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}

두 애플리케이션 모두에 필요한 유닛

sgcP2PTsgcRTCPeerConnection을 공개하고 핸들러 타입을 다시 내보내는 배럴 유닛이에요. 컴포넌트를 선언하는 데는 이것으로 충분하지만, 열거형 상수는 그 타입을 선언한 유닛에서 오기 때문에 rtctkAudiocctAudioOpus를 쓸 때는 그 유닛도 함께 넣어야 해요.

이 안내에 나오는 두 애플리케이션은 같은 프로그램에서 다른 버튼을 누른 것일 뿐이에요. 아래 내용은 둘 다에 들어가요.

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

시그널링 채널

메시지 종류 셋, 단순한 릴레이 하나면 돼요. 모든 WebRTC 튜토리얼이 손짓만 하고 넘어가는 부분이자, 실제로 직접 작성해야 하는 부분이에요.

브로커가 아니라 릴레이

시그널링 서버는 자기가 전달하는 내용을 단 한 바이트도 이해할 필요가 없어요. 한 피어가 보낸 텍스트를 받아 다른 피어에게 건네줄 뿐이죠. Broadcast에는 이미 연결 Guid를 받는 Exclude 매개변수가 있어서 "보낸 사람만 빼고 모두에게 보내기"는 한 줄이에요.

그 정도로 단순하게 두세요. 릴레이가 SDP를 파싱하기 시작하는 순간, 코덱이 바뀔 때마다 갱신해야 하는 컴포넌트가 되고, 브라우저로 가는 통화를 중계하지 못하게 돼요.

운영 환경에서는 통화 두 건이 겹치지 않도록 방 식별자를 기준으로 릴레이를 나누고, TLS 뒤에 두게 될 거예요. TsgcWebSocketServer는 라이브러리의 나머지와 똑같은 TLSOptions, Authentication, WatchDog 인터페이스를 갖고 있어요.

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;

같은 채널의 피어 쪽

각 애플리케이션은 그 릴레이로 TsgcWebSocketClient를 열고 세 단어짜리 어휘, 즉 offer, answer, candidate만 써요. 직렬화는 같은 라이브러리의 TsgcJSON이 맡으니 의존성이 늘어나지 않아요.

분기가 어디로 가는지 눈여겨보세요. 들어온 offer는 원격 기술로 설정한 뒤 곧바로 답해요. 들어온 answer는 설정만 하고요. 들어온 candidateAddIceCandidate로 넘기는데, 기술보다 먼저 올 수도 나중에 올 수도 있어요. 그게 바로 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;

피어 연결 만들기

두 애플리케이션에서 동일해요. 전체 교환에서 유일하게 비대칭인 것은 어느 쪽이 통화 버튼을 누르느냐뿐이에요.

설정, 그리고 중요한 이벤트들

RTCOptions.ICEServers는 W3C의 iceServers 목록이에요. AddURLstun: 또는 turn: URL을 받아 스킴에서 종류와 호스트, 포트, TLS 플래그를 채워 넣고, TURN이라면 사용자 이름과 자격 증명도 선택적으로 받아요.

RTCOptions.DTLS의 기본값은 False예요. 그대로 두면 암호화도 없고 SRTP 키 재료도 없어서 미디어가 동작하지 않아요. CreateDataChannel은 이 값을 대신 켜줘요. 데이터 채널은 DTLS 위의 SCTP이고 DTLS를 끈 상태로는 유효한 설정이 없기 때문이에요. AddTrack은 켜주지 않으니 미디어를 추가할 때는 직접 설정하세요.

인증서 파일은 필요 없어요. RTCOptions.DTLSOptions.CertFile을 비워 두면 컴포넌트마다 한 번씩 메모리 안에서 자체 서명 인증서가 생성돼요. 그게 바로 WebRTC 모델이에요. 신원은 체인이 아니라 SDP의 a=fingerprint 줄에 걸려 있어요. 지문이 없는 원격 기술은 아무것과나 핸드셰이크하도록 허용되는 대신 거부돼요.

아래의 모든 이벤트는 워커 스레드, 즉 ICE나 네트워크, 타이머 스레드에서 발생해요. 메인 스레드에서는 절대 발생하지 않아요. 컨트롤을 건드리기 전에 TThread.Queue로 넘기세요.

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;

로컬 기술 공개하기

OnLocalDescription은 종류, 즉 'offer''answer' 문자열과 SDP 자체를 건네줘요. 두 피어가 같은 핸들러를 쓰고, 그 핸들러가 하는 일은 하나예요. 그걸 시그널링 채널에 올리는 거죠.

SDP는 이미 완성된 상태로 도착해요. CreateOffer는 먼저 ICE 후보를 모으면서 RTCOptions.GatheringTimeout 밀리초, 기본값 3000까지 기다리고, 새 후보 없이 GatheringIdleTimeout 밀리초, 기본값 500이 지나면 일찍 끝내요. 이게 trickle을 쓰지 않는 경로예요.

TrickleICE를 True로 설정하면 기술이 즉시 나가고 후보가 그 뒤를 따라가요. 그럴 일은 드물어요. RTCOptions.TrickleICEAuto가 기본값 True라서, 모든 브라우저가 그렇듯 원격 기술에 a=ice-options:trickle이 있으면 컴포넌트가 알아서 전환하고 수집 대기 시간을 낭비하지 않아요.

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 후보, STUN과 TURN

피어 투 피어 연결이 실패하는 지점이자, 그 실패를 읽어내기가 가장 어려운 지점이에요. 후보는 세 종류이고, 각각 존재하는 이유가 있어요.

host

컴퓨터가 자기 자신에게서 볼 수 있는 주소로, 네트워크 인터페이스마다 하나씩 있어요. 공짜이고 즉시 얻을 수 있으며, 두 애플리케이션이 같은 LAN이나 같은 VPN에 있다면 이것으로 충분해요. 두 Delphi 애플리케이션이 늘 한 사무실 안에서만 돈다면 host 후보만 있으면 되고 STUN은 아예 건너뛰어도 돼요.

srflx, server reflexive

STUN 서버가 본, 피어의 패킷이 도착한 공인 주소예요. 평범한 가정용 라우터 뒤에 있는 두 피어를 직접 대화하게 만들어 주는 게 이것이고, 실제 연결의 대다수를 감당해요. 비용은 STUN 서버로 가는 왕복 한 번뿐이고, 그 뒤로는 트래픽이 그 서버를 거치지 않아요.

relay

피어로 전달해 주는 TURN 서버상의 주소예요. 대칭형 NAT, 제한이 엄격한 회사 방화벽, 일부 이동통신사에서 필요해요. 통화의 모든 바이트가 여러분의 TURN 서버를 지나가므로 비싼 경로이고, 다른 방법이 없을 때만 물러나는 선택지예요.

후보를 흘려보내기

OnIceCandidate는 후보가 발견될 때마다 한 번씩, 후보 줄과 그것의 sdpMid, sdpMLineIndex와 함께 발생해요. 이 세 필드가 바로 브라우저 API가 기대하는 것이라서, 상대편이 Delphi든 Chrome이든 같은 JSON이 통해요.

하나가 나올 때마다 바로 보내세요. 기다리지도, 묶어 보내지도 마세요. 원격 기술보다 먼저 도착한 후보는 보관됐다가 기술이 도착할 때 적용되므로 순서는 여러분이 신경 쓸 문제가 아니에요.

짝이 최종 선정되면 SelectedLocalCandidateSelectedRemoteCandidate가 어느 두 주소가 이겼는지 알려줘요. 그 로그 한 줄이면 "이 통화가 왜 내 TURN 서버를 지나가지"라는 질문에 무엇보다 빠르게 답할 수 있어요.

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 추가하기, 그리고 잊으면 안 되는 스위치

ICEServersturn: 항목은 자체 호스트와 포트, 사용자 이름, 자격 증명을 담고 있고, 할당은 그 값을 써요. 추가하는 건 AddURL 한 번 더면 돼요.

함정은 반대 방향에 있어요. RTCOptions.ICE.TURN의 기본값이 True인데, 서버 목록에 TURN 항목이 하나도 없으면 수집이 RTCOptions.ICE의 단일 서버로 돌아가요. 그 호스트의 기본값은 127.0.0.1, 포트는 3478이고요. 그래서 STUN URL만 설정한 피어도 localhost를 상대로 TURN 할당을 시도하고, 실패하고, 그 사실을 보고해요. 결함이라기보다 잡음이지만 로그에서는 심각해 보이고 엉뚱한 곳을 뒤지게 만들어요. 실제로 TURN 서버를 갖추기 전까지는 RTCOptions.ICE.TURN := False로 두세요.

RTCOptions.ICE.STUN도 똑같이 동작하고 기본값 역시 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;

ICE 클라이언트 레퍼런스 직접 TURN 서버 운영하기

데이터 채널

두 애플리케이션 사이로 텍스트와 바이너리를 주고받아요. 신뢰성은 채널마다 고를 수 있고요. 보통 가장 먼저 동작시키게 되는 것이고, 전송 전체가 잘 되는지를 증명해 줘요.

채널 열기, 그리고 상대편 채널 받기

CreateDataChannel을 호출한 피어는 객체를 즉시 돌려받아요. 호출하지 않은 피어는 OnDataChannel을 통해 같은 채널을 받고요. 두 곳 모두에 핸들러를 붙이세요. 세션 중 어느 시점에든 어느 쪽이든 채널을 열 수 있으니까요.

채널은 만든 즉시 쓸 수 있는 게 아니에요. SCTP 연관이 올라오고 DTLS 역할이 정해질 때까지 Id는 할당되지 않은 채로 있고, 열리지 않은 동안 Send는 False를 반환해요. OnOpen을 기다리세요.

신뢰성은 생성 시점에 정해져요. 기본값은 순서가 보장되고 완전히 신뢰할 수 있는, TCP 같은 채널이에요. 순서 없는 전달을 원하면 aOrdered = False를 넘기고, 부분 신뢰성을 원하면 aMaxRetransmitsaMaxPacketLifeTime을 넘기세요. 위치 갱신처럼 늦게 오는 패킷이 잃어버린 패킷보다 나쁜 경우에 부분 신뢰성이 적합해요.

채널을 해제하지 마세요. 소유자는 피어 연결이에요. Close가 종료를 시작하고, 상대편이 동의하면 OnClose가 발생해요.

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;

보내기, 그리고 넘치게 하지 않기

Send는 문자열을, SendBytesTBytes를 받아요. 채널이 열려 있지 않으면 둘 다 예외를 던지는 대신 False를 반환해요. 그래서 정리 중에 보낸 요청은 워커 스레드의 예외가 아니라 반환된 False가 돼요.

MaxMessageSize상대 피어가 받겠다고 밝힌 최대 메시지 크기예요. 그 피어 기술의 a=max-message-size 속성에서 읽어요. 그 한도를 넘는 메시지는 회선에 올라가는 대신 로컬에서 거부돼요. 회선에 올라가면 연관 전체가 중단되면서 다른 채널까지 함께 끊기거든요. 0이면 어떤 기술도 한도를 명시하지 않았다는 뜻이고 검사는 꺼져 있어요.

BufferedAmount는 이 스트림을 위해 SCTP에 쌓여 있고 아직 확인되지 않은 바이트 수예요. 파일을 스트리밍할 때 이 값을 지켜보세요. 임계치를 넘을 때까지 보내다가, 기가바이트를 메모리에 쌓아 두는 대신 빠질 때까지 기다리면 돼요.

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;

오디오와 비디오 트랙

트랙은 그림이 담긴 데이터 채널이 아니에요. 전송도 다르고, 실패 양상도 다르고, 코드도 달라요. 대부분이 처음에 헷갈리는 구분이에요.

 데이터 채널미디어 트랙
무엇으로 운반되나 DTLS 위의 SCTP (RFC 8831) 같은 DTLS 전송 위의 SRTP (RFC 3711)
전달 방식 완전 신뢰 + 순서 보장부터 보내고 잊기까지 여러분이 골라요 설계상 늘 손실을 허용해요. 늦는 게 잃는 것보다 나쁘니 무한정 재전송하지 않아요
작업 단위 메시지예요. 언제 보낼지는 여러분이 정해요 시계예요. 오디오는 20ms 프레임으로, 비디오는 프레임 레이트에 맞춰 들어가요
여는 방법 세션 중 언제든 CreateDataChannel AddTrack, 공개하려면 새 offer가 필요해요
받는 경로 OnDataChannel, 이어서 채널 자신의 OnMessage OnTrack, 이어서 트랙의 OnAudioOnVideoFrame
DTLS를 켜야 하나 네. CreateDataChannel이 대신 설정해 줘요 네. 그런데 AddTrack은 설정해 주지 않아요. RTCOptions.DTLS를 직접 설정하세요
어디에 쓰나 채팅, 파일 전송, 원격 제어, 게임 상태, 텔레메트리 마이크, 카메라, 화면 공유, 타임라인이 있는 모든 것

마이크 보내기

AddTrack은 종류와 코덱을 받아 TsgcRTCTrack을 반환해요. 오디오 코덱은 cctAudioOpus, cctAudioPCMU, cctAudioPCMA이고, 비디오 코덱은 cctVideoVP8, cctVideoVP9, cctVideoH264, cctVideoJPEG예요.

캡처는 별도 객체예요. 플랫폼 마이크를 아예 쓰고 싶지 않을 수도 있으니까요. sgcCreateAudioCapture는 코드가 컴파일된 플랫폼에 맞는 구현을 만들어요. Windows에서는 waveIn, Linux에서는 ALSA, Android에서는 AudioRecord, iOS와 macOS에서는 VoiceProcessingIO Audio Unit이죠. 그래서 코드 어디에서도 플랫폼 클래스를 직접 이름으로 부르지 않아요. 구현이 없는 타깃에서는 nil을 반환하니 결과를 확인하세요.

SendPCM은 인코더의 샘플레이트와 채널 수에 맞춘 16비트 부호 있는 인터리브 PCM을 원해요. Opus는 48000Hz, G.711은 8000Hz예요. 캡처 소스는 실제로 내보내는 값을 AudioSampleRate, AudioChannels, AudioFrameDurationMs로 공개하니 짐작하지 말고 확인하면 돼요.

세션이 이미 올라온 뒤에 트랙을 추가하면 협상 필요 플래그가 서고 OnNegotiationNeeded가 발생해요. 공개하려면 CreateOffer를 다시 호출하면 되고, 재offer는 전송을 건드리지 않고 만들어져요.

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;

상대편이 보낸 것 재생하기

OnTrack은 원격 기술이 미디어 라인을 하나 가져올 때마다 한 번씩 발생해요. 넘겨주는 트랙의 소유자는 피어 연결이므로 이벤트만 연결하고 절대 해제하지 마세요.

오디오는 디코더가 만들어 낸 샘플레이트와 채널 수 그대로, 디코딩된 PCM으로 OnAudio를 통해 도착해요. sgcCreateAudioRenderer가 만든 TsgcMediaRenderer에 그대로 넘기면 돼요. 장치를 그 형식으로 열 수 없었다면 렌더러가 형식을 변환해 줘요.

비디오는 디코딩된 TsgcVideoFrame으로 OnVideoFrame을 통해 도착해요. Data에 원시 픽셀이 있고 Width, Height, Format, Stride가 함께 와요. 형식은 vffI420, vffNV12, vffRGB24, vffRGBA32, vffBGR24, vffBGRA32이고, BGR 두 가지는 Windows GDI 바이트 순서를 따라요. 그래서 vffBGR24 프레임을 비트맵에 옮기는 건 변환이 아니라 메모리 복사예요.

패킷 손실로 프레임이 깨진 채 도착하면 RequestKeyFrame으로 보낸 쪽에 새 프레임을 요청하세요.

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;

카메라, Windows에서

오디오 캡처는 지원하는 모든 플랫폼에 구현이 있어서 팩토리 뒤로 추상화돼 있어요. 비디오 캡처는 그렇지 않아서 플랫폼 클래스를 직접 이름으로 불러야 해요. Windows에서는 sgcP2P_MediaCapture_WinTsgcVideoCapture_Win이고, Video for Windows를 구동하며 기반 클래스가 선언한 것과 같은 OnVideoCapture 이벤트로 프레임을 전달해요.

이웃 유닛인 sgcP2P_ScreenCapture_Win에는 TsgcScreenCapture_WinTsgcWindowCapture_Win이 있고, 둘 다 TsgcMediaCaptureSource의 자손이에요. 그래서 화면 공유도 생성자만 다를 뿐 같은 세 줄이면 돼요.

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;

미리 알아둘 만한 실패들

피어 투 피어 연결은 오류를 전혀 남기지 않는 방식으로 실패해요. 그래서 어려운 거예요. 가장 자주 나오는 것들을 모았어요.

미디어가 무음인데 오류도 없어요

RTCOptions.DTLS가 False예요. 그게 기본값이고, CreateDataChannel은 켜주지만 AddTrack은 켜주지 않아요. 그래서 미디어만 싣는 세션은 DTLS 핸드셰이크를 아예 하지 않고, 따라서 SRTP 키도 받지 못해요. 직접 설정하세요.

요청한 적 없는 TURN 오류

RTCOptions.ICE.TURN의 기본값은 True이고, 서버 목록에 TURN 항목이 없으면 127.0.0.1:3478로 돌아가요. 진짜로 TURN 서버를 갖추기 전까지는 False로 두세요. 그러지 않으면 지금 문제와 아무 상관 없는 할당 실패로 로그가 가득 차요.

원격 기술이 거부돼요

a=fingerprint가 없는 기술은 곧바로 거절되고 OnError로 보고돼요. WebRTC 신뢰 모델에서 그 줄만이 피어를 인증하기 때문에, 그것 없는 기술을 받아들이면 아무 인증서로나 핸드셰이크가 완료될 수 있어요.

이벤트 핸들러에서 나는 액세스 위반

피어 연결과 데이터 채널, 트랙의 모든 이벤트는 워커 스레드, 즉 ICE나 네트워크, SCTP, 틱 스레드에서 발생해요. 메인 스레드에서는 절대 발생하지 않고요. 거기서 VCL이나 FMX 컨트롤을 직접 건드리면 동작이 정의돼 있지 않아요. TThread.Queue로 감싸세요.

양쪽이 동시에 재offer해요

그걸 glare라고 하고, W3C의 perfect negotiation 규칙으로 해결해요. 무례한 쪽, 즉 Polite = False인 피어는 자기 offer를 지키고 들어온 offer는 OnError로 보고해요. 정중한 쪽은 자기 것을 되돌리고 답해요. 한 세션의 두 피어가 모두 정중해서는 안 돼요.

큰 메시지 하나가 모든 채널을 끊어요

피어의 a=max-message-size를 넘는 메시지는 SCTP 연관 전체를 중단시키고 다른 데이터 채널까지 함께 끊어요. SendSendBytesMaxMessageSize를 확인해서 대신 로컬에서 거부해요. 큰 페이로드는 직접 나눠 보내세요.

연결이 시작되는 데 3초가 걸려요

그건 trickle을 쓰지 않을 때의 대기 시간인 RTCOptions.GatheringTimeout이에요. 후보가 더 오지 않으면 GatheringIdleTimeout이 일찍 끝내주고, 원격 기술이 trickle을 알리면 TrickleICEAuto가 trickle 모드로 전환해요. LAN에서는 두 대기 시간을 모두 낮추세요.

오디오가 빠르거나 느리거나 깨져요

캡처 장치와 인코더 사이의 샘플레이트나 채널 수가 어긋난 거예요. Opus는 48000Hz, G.711은 8000Hz로 협상돼요. 장치가 요청한 값을 지켰다고 짐작하지 말고 캡처 소스에서 AudioSampleRateAudioChannels를 다시 읽어 보세요.

Delphi WebRTC, 자주 묻는 질문

두 애플리케이션을 피어 투 피어로 연결하기 전에 개발자들이 묻는 것들이에요.

RTL에도 없고 VCL을 통해서도 없어요. TsgcRTCPeerConnection이 W3C 피어 연결 인터페이스를 Object Pascal로 직접 구현한 것이에요. CreateOffer, CreateAnswer, SetLocalDescription, SetRemoteDescription, AddIceCandidate, CreateDataChannel, AddTrack을 제공하고 그 아래에 ICE와 DTLS, SCTP, SRTP가 있어요. 프로세스 안에 내장 Chromium도, TWebBrowser도, JavaScript 브리지도 없어요.
통화가 시작되기 전에 두 피어 사이로 수백 바이트짜리 텍스트를 실어 나를 무언가는 필요해요. 아직 서로에게 닿는 법을 모르니까요. 그게 시그널링이고, WebSocket 릴레이일 수도, 이미 쓰고 있는 메시지 큐일 수도, REST 엔드포인트일 수도, 데모라면 복사해서 붙여넣기일 수도 있어요. offer와 answer, ICE 후보가 오가고 나면 미디어와 데이터는 두 애플리케이션 사이를 직접 오가고 시그널링 채널은 닫아도 돼요. TURN 중계가 유일하게 통하는 경로로 밝혀진 경우가 아니라면 미디어 경로에 서버는 없어요.
거의 언제나 WebSocket이에요. 양방향이고, 피어가 폴링하지 않아도 서버가 들어온 offer를 밀어줄 수 있으니까요. 이 페이지에서는 TsgcWebSocketServerTsgcWebSocketClient로 열다섯 줄쯤 되는 릴레이를 만들어요. Broadcast에 보낸 쪽의 Connection.Guid를 제외 대상으로 넘겨 모든 메시지를 상대 피어로 중계하죠. 릴레이를 아예 직접 쓰고 싶지 않다면, sgcWebSockets에 미리 만들어진 시그널링 프로토콜 컴포넌트 TsgcWSPServer_RTCPeerConnection도 들어 있어요. RTCOptions.WebSocketGatherCandidates를 통해 교환을 대신 진행해 줘요.
두 애플리케이션이 같은 LAN이나 같은 VPN에 있다면 둘 다 필요 없어요. host 후보가 이미 닿을 수 있는 주소를 알려주니까요. 평범한 라우터 뒤의 서로 다른 네트워크에 있다면 STUN이 필요해요. 각 피어에게 자기 패킷이 어떤 공인 주소에서 온 것처럼 보이는지 알려주고, 실제 연결의 대부분을 감당해요. TURN이 필요한 건 직접 짝짓기가 전혀 되지 않을 때예요. 대칭형 NAT, 제한이 엄격한 회사 방화벽, 일부 이동통신사가 그렇죠. TURN은 통화의 모든 바이트를 중계하므로 기본값이 아니라 비싼 대비책이에요. 둘 다 RTCOptions.ICEServers에 넣어 두면 ICE가 실제로 연결되는 것 중 가장 저렴한 짝을 골라요.
데이터 채널은 DTLS 위의 SCTP이고 메시지를 옮겨요. 신뢰성은 여러분이 골라요. TCP처럼 순서가 보장되고 완전히 신뢰할 수 있게 하거나, 늦은 패킷이 쓸모없는 경우를 위해 순서 없이 재전송 횟수나 수명 한도를 두거나요. 미디어 트랙은 같은 DTLS 전송 위의 SRTP이고 타임라인을 옮겨요. 오디오는 20ms 프레임으로, 비디오는 프레임 레이트에 맞춰 흐르고, 설계상 늘 손실을 허용해요. 채팅과 파일 전송, 원격 제어, 게임 상태에는 데이터 채널을, 마이크와 카메라, 화면에는 트랙을 쓰세요. 둘은 하나의 연결과 하나의 열린 포트를 함께 써요.
두 단계예요. Enterprise가 SGC_ICE, SGC_DTLS, SGC_TURN, SGC_RTCPEERCONNECTION을 정의하고, 그게 TsgcRTCPeerConnection, TsgcICEClient, TsgcTURNClient, TsgcTURNServer를 팔레트에 올려요. offer와 answer API, 데이터 채널, 미디어 트랙은 SGC_SDP, SGC_DATACHANNEL, SGC_RTP 뒤에 가려져 있는데, 이 셋을 정의하는 건 SGC_PACK_WEBRTC뿐이에요. 그게 sgcWebRTC 애드온이고 All-Access에도 포함돼요. 그 아래로는 STUN 클라이언트가 Standard에, STUN 서버와 WebSocket 서버 컴포넌트가 Professional에 들어 있어요.
네, 그리고 이 페이지의 내용은 하나도 달라지지 않아요. SDP는 표준이고, 후보 줄은 브라우저 API가 기대하는 것과 같은 sdpMidsdpMLineIndex를 담고 있으며, offer와 answer 상태 기계는 RFC 8829를 따라요. 그래서 릴레이가 전달하는 JSON이 양방향 모두에서 수정 없이 통해요. 브라우저는 첫 밀리초부터 후보를 흘려보내는데, 그게 바로 RTCOptions.TrickleICEAuto가 감지하는 것이에요. 원격 기술에서 a=ice-options:trickle을 보면 수집 대기 시간을 다 쓰는 대신 즉시 답해요.
언제나 워커 스레드예요. OnLocalDescription, OnIceCandidate, OnConnectionStateChange, OnTrack, OnError는 ICE나 네트워크, 타이머 스레드에서 도착해요. 데이터 채널 이벤트는 SCTP 연관을 구동하는 스레드에서, 트랙의 오디오와 비디오 이벤트는 네트워크나 틱 스레드에서 도착하고요. 어느 것도 메인 스레드로 대신 넘겨주지 않으니, 컨트롤을 건드리는 코드는 TThread.Queue로 감싸세요. 제품에 들어 있는 데모가 그렇게 하고 있어요.
아니요. RTCOptions.DTLSOptions.CertFile을 비워 두면 자체 서명 인증서와 키가 컴포넌트마다 한 번씩 메모리에서 생성되고 모든 피어에 재사용돼요. 그 지문이 로컬 기술의 a=fingerprint 속성으로 공개되고, 그것이 상대편에게 여러분을 인증해 줘요. 그게 WebRTC 신뢰 모델이에요. 체인은 결코 검증되지 않고, 시그널링 채널로 전달된 지문이 기준점이 돼요. 안정적인 신원이 필요하다면 CertFileKeyFile을 직접 준비한 PEM 파일로 지정해도 돼요.
선정된 후보 짝 위에서 DTLS 핸드셰이크가 이뤄지고 SRTP 키를 유도하기 때문에, 연결 위의 모든 RTP와 RTCP, SCTP 패킷이 암호화돼요. 데이터 채널에서는 끌 방법이 없어요. CreateDataChannelRTCOptions.DTLS를 무조건 True로 설정하거든요. RTCDataChannel은 정의상 DTLS 위의 SCTP이고, 그것 없이는 유효한 설정이 존재하지 않기 때문이에요.
P2P와 WebRTC 유닛은 Delphi 7부터 RAD Studio 13까지 모든 런타임 패키지와, 대응하는 C++ Builder 패키지에 들어 있어요. 오디오 캡처와 재생은 Windows, Linux, Android, iOS, macOS용 플랫폼 구현이 있어서 다섯 플랫폼 모두에서 팩토리 함수가 동작하는 객체를 돌려줘요. 비디오 캡처는 예외예요. 팩토리로 만들지 않고 플랫폼마다 이름을 직접 지정하는데, Windows에서는 TsgcVideoCapture_Win이고, 화면 공유에는 TsgcScreenCapture_WinTsgcWindowCapture_Win이 함께 있어요.
네, 그게 재협상이에요. 이미 성립된 세션에서 AddTrack을 호출하면 협상이 필요하다고 표시되고 OnNegotiationNeeded가 발생해요. CreateOffer를 다시 호출하면 재offer가 동기적으로 만들어지는데, 새 ICE 수집도 없고 새 DTLS나 SCTP 핸드셰이크도 없어서 전송이 끊기지 않아요. 기존 미디어 라인은 위치와 mid를 그대로 유지하고, 새 라인이 끝에 추가돼요. RemoveTrack은 반대 방향으로 똑같이 동작해요. 미디어 라인은 남고 recvonly나 inactive로 다시 공개돼요.

컴포넌트 레퍼런스와 기술 문서

이 페이지가 쓰는 조각마다 각자의 레퍼런스 페이지가 있고, 대부분은 속성과 메서드, 이벤트 전체 목록을 담은 별도의 기술 PDF도 있어요.

TsgcRTCPeerConnection

이 페이지 전체가 다루는 컴포넌트예요. Offer와 answer, ICE, DTLS, SCTP 데이터 채널, RTP 미디어를 한 클래스에 담았어요.

컴포넌트 페이지 →

sgcWebRTC

미디어 엔진 팩이에요. SDP, SCTP, RTP, SRTP, Opus와 G.711 오디오, VP8과 H.264 비디오, 대역폭 추정을 담고 있어요.

제품 페이지 →

기능 상세

코덱별, 플랫폼별로 미디어 엔진이 무엇을 하고 각 인코더가 어디서 오는지 정리했어요.

기능 보기 →

ICE 클라이언트

후보 수집과 검사 목록, 선정, ICE 서버 컬렉션까지, 피어 연결 아래의 계층을 다뤄요.

컴포넌트 페이지 →

STUN 클라이언트와 서버

바인딩 요청과 재전송 옵션, 그리고 직접 운영하고 싶을 때 쓸 서버 컴포넌트를 다뤄요.

STUN 클라이언트 →

TURN 클라이언트와 서버

할당과 권한, 채널 바인드, 그리고 중계가 필요한 통화를 위한 TURN 서버 컴포넌트를 다뤄요.

TURN 서버 →

모든 P2P 컴포넌트

UDP, STUN, TURN, ICE, RTCPeerConnection까지 피어 투 피어 계열 전체를 한 색인에 모았어요.

P2P 둘러보기 →

Delphi WebRTC 개요

sgcWebSockets 안의 WebRTC를 라이브러리 수준에서 조망해요. 시그널링 프로토콜 컴포넌트와 데모 목록도 함께 있어요.

자세히 알아보기 →

어떤 에디션이 필요한가요?

WebRTC 말고도 따져볼 것이 있을 때 볼, 기능별 전체 에디션 매트릭스예요.

에디션 비교 →

다른 사용 사례

이 페이지는 Delphi 사용 사례 중 하나예요. 각 페이지가 하나의 작업을 처음부터 끝까지 다뤄요. 지금까지 나온 다른 페이지는 Delphi에서 LLM 호출하기OAuth2와 PKCE로 사용자 로그인 처리하기예요.

전체 사용 사례 →
RTCPeerConnection 기술 문서 (PDF) 피어 연결 컴포넌트만을 위한 속성과 메서드, 이벤트, 코드 예제를 담았어요.
ICE 클라이언트 기술 문서 (PDF) 후보 수집과 검사 목록, ICE 서버 컬렉션을 자세히 다뤄요.
TURN 클라이언트 기술 문서 (PDF) 할당과 권한, 채널 바인드를 다뤄요. relay 후보를 위해 ICE가 구동하는 클라이언트예요.
STUN 클라이언트 기술 문서 (PDF) 바인딩 요청과 재전송을 다뤄요. 자기 공인 주소를 알아내는 가장 저렴한 방법이에요.
데모 프로젝트 Demos\35.P2P\05.RTCPeerConnection과 Demos\35.P2P\06.DataChannel이 패키지 안에 들어 있어요.

각 단계 뒤에 있는 사양

저희 말을 믿기보다 컴포넌트가 무엇을 구현하는지 직접 읽고 싶을 때 볼 1차 자료예요.

RFC 8829, JSEP

CreateOfferCreateAnswer, SetRemoteDescription 뒤에 있는 offer와 answer 상태 기계예요. 재협상과 롤백도 포함해요.

RFC 읽기 →

RFC 8445, ICE

후보 수집과 우선순위, 검사 목록, 선정을 다뤄요. 연결이 어떤 때는 1초가 걸리고 어떤 때는 실패하는 이유예요.

RFC 읽기 →

RFC 8489와 RFC 8656

STUN과 TURN이에요. 바인딩 요청이 무엇을 묻는지, 할당에 어떤 대가가 따르는지 다뤄요.

RFC 읽기 →

RFC 8831과 RFC 8832

SCTP 위의 WebRTC 데이터 채널, 그리고 DTLS 역할에 따라 스트림 id를 배정하는 DCEP open 핸드셰이크를 다뤄요.

RFC 읽기 →

RFC 8122, SDP 지문

a=fingerprint가 왜 피어 연결의 신원인지, 그것 없는 기술이 왜 거부되는지 다뤄요.

RFC 읽기 →

WebRTC 1.0 (W3C)

이 컴포넌트가 그대로 옮긴 API예요. Polite가 유래한 perfect negotiation도 포함해요.

사양 읽기 →

여러분의 애플리케이션 두 개를 통화에 올려보세요

체험판을 내려받아 RTCPeerConnection과 DataChannel 데모를 서로 붙여 실행해 보고, 같은 것을 여러분의 프로젝트에 넣어 보세요.