console.lealog();

@leader22のWeb系に関する勉強めもブログですのだ

OSSのWebRTC SFU mediasoup v3のコードを読む(サーバー/C++後編)

前編はこちら。

OSSのWebRTC SFU mediasoup v3のコードを読む(サーバー/C++前編) - console.lealog();

前編のおさらい

SFUのRoomに入ったクライアントがメディアを送受信するためには、`WebRtcTransport`の確立が必要。

`WebRtcTransport`が確立できると、クライアントはサーバーのICEやDTLSの接続情報を知っている状態になる。
あとはいつでもいいのでメディアを送受信するAPIを叩けば、通信が開始できるというわけ。

送信なら`produce()`、受信なら`consume()`で、そのどちらをやった場合でも、`WebRtcTransport`に`connect()`する処理が内部的に走る。

後編では、この実際にメディアを送受信する時に呼ばれる処理の流れを見ていく。

C++のコードを読む

transport.connect

クライアントが`produce()`や`consume()`でメディアを送受信しようとしたタイミングで発火する。

そのときにC++側では`transport.connect`のメッセージが渡されて、ICEの処理やDTLSのセッション確立が行われ、メディアの送受信の準備が整う。

このイベントも`RTC/Router.cpp`からはじまり、`RTC/WebRtcTransport.cpp`へ。

RTC::WebRtcTransport
  • DTLSのFingerprintやRoleなど、クライアントの接続情報をもらう
    • それによって、自身のDTLSの振る舞いを決める
    • `DtlsTransport::SetRemoteFingerprint()`
  • その後、`WrbRtcTransport::MayRunDtlsTransport()`を実行
    • この裏でクライアントがオファーアンサーを済ませるはず
    • なので、程なくしてDTLSハンドシェイクや他の処理がはじまる
    • `DtlsTransport::Run()`へ
RTC::DtlsTransport
  • `Run(role)`
    • そのRoleから判断して、必要あればDTLSハンドシェイクをする
    • ハンドシェイクが成功すると、SRTP用の鍵を取得できるはず
  • そこまでいければDTLSのステータスが`connected`になる

そして`RTC/WebRtcTransport.cpp`へ戻る。

RTC::WebRtcTransport
  • `WebRtcTransport::OnDtlsConnected()`
    • `RTC::SrtpSession`を用意
    • 送信用と受信用の2つ
  • `RTC::Transport::Connected()`
    • RTCPのタイマーをスタート
    • 既に`Consumer`がいれば、キーフレームを要求する(親切)

ここまでくると、DTLS-SRTPのパスが通りサーバーとクライアントの間に`WebRtcTransport`が確立することになって、メディアの送受信の準備が整うことになる。

RTC::SrtpSession

これもまたあとでメディアを実際に送信するシーンで読むことになるかな?

transport.produce

クライアントが`produce()`した時に発火する。
この時点で`transport.connect`のメッセージは処理されてるはず。

`RTC/Tranport.cpp`から読み進める。
実際は`RTC/WebRtcTransport`がこれを継承してる。

RTC::Transport
  • 新たに`Producer`のIDを払い出し、インスタンスを作成
    • `RTC::Producer`
  • その`Producer`をRTPのリスナーとして追加
    • `RTC::RtpListener`
  • 一部のRTPのヘッダ拡張をチェック

クライアントからメディアが送信されてくる処理なので、RTP関連の話になるというわけ。

RTC::Producer
  • 担当する`kind`を指定される
    • audioかvideoか
  • その他に渡されるRTPのパラメータの格納
    • コーデックとかヘッダとか
  • ssrc/ridのチェック
  • キーフレームの管理をするクラスの初期化

初期化でやってるのはそれくらいで、他に主要なメソッドとしては、

  • `ReceiveRtpPacket()`
    • クライアントに紐づく`Producer`がRTPを受け取ったとき
    • RTPのストリームを構成するものとしてマーク
    • `Router`にそれを通知して、`Consumer`がいればそこに分配する
      • `RTC::Consumer::SendRtpPacket()`
  • `GetRtcp()`
    • RTCPを送るために必要な情報を集める処理
  • `ReceiveRtcpSenderReport()`
  • `RequestKeyFrame()`
    • SSRCでもってキーフレームを要求する
  • etc..

その他にもいろいろやってるけど、今回の記事ではあまり踏み込まないので割愛。

RTC::RtpListener
  • `Transport`が抱えるリスナーであり、`Producer`のマップでもあるクラス
  • ssrc/rid/midで`Producer`をそれぞれマッピングしてる

このへんの煩雑さがSFU実装の面倒ポイントの1つなのかなー。

クライアントからメディアを受信

ICEで解決されたポートに対して、UDPのパケットがいろいろ飛んできてるはず。

前編でみてた`WebRtcTransport`がその役割を担ってて、`WebRtcTransport::OnPacketRecv()`が根本。

  • 各パケットのタイプを見る
  • RTPの場合は`OnRtpDataRecv()`
  • DTLS-SRTP、ICEのTupleなどセッションが有効かチェック
  • SRTPをdecryptしてRTPを取り出し
  • RTPのヘッダ拡張を必要あれば適用
  • 送信元の`Producer`を特定
  • その`Producer`にパケットを渡す
    • さっき見てた`ReceiveRtpPacket()`へ

transport.consume

最後にメディアを受信したいときの処理を追う。

これはクライアントが`consume()`した時に発火する。
この時点で`transport.connect`のメッセージは処理されてるはず。

これも`RTC/Tranport.cpp`から。
実際は`RTC/WebRtcTransport`がこれを継承してる。

  • `Consumer`のIDを払い出して、インスタンスを作成
    • `Consumer`は3種類: `SimpleConsumer` / `SimulcastConsumer` / `PipeConsumer`
    • Encodingの指定が複数あれば`SimulcastConsumer`で、1つなら`SimpleConsumer`など
  • `Router`に対して、新たな`Consumer`ができたことを通知
    • `Router`で`consume()`したい`Producer`IDと`Consumer`を紐づけ

これで特定の`Producer`と`Consumer`が関連付けられて、メディアがクライアントからクライアントへ流れるように。

RTC::SimpleConsumer / RTC::Consumer

継承関係にあるので、根本の`RTC::Consumer`から。

  • 担当する`kind`の指定
  • RTPのパラメータやエンコーディングなどのチェック
    • 基本的には`Producer`と同じ感じ

次に`RTC::SimpleConsumer`。

  • `kind`によってRTCPのインターバルの指定を変更
  • クライアントに流すようのRTPストリームを作成
    • `Router`が`Producer`から受け取ったRTPを流してくれるときに使う

主要なメソッドは、

  • `SendRtpPacket()`
    • `Router`が`Producer`から受け取ったRTPを流してくれるときに呼ばれる
  • `ProducerNewRtpStream()`
    • 対応する`Producer`を紐づけ
  • `ReceiveNack()`
    • RTCPのNACKのパケットを受け取ったとき
  • `ReceiveKeyFrameRequest()`
    • これをきっかけに`Producer`にキーフレームを要求する
  • `ReceiveRtcpReceiverReport()`
  • etc..

などなど、受信用だけあってそれ系のメソッドがいろいろある。

クライアントにメディアを送信

ICEのTupleにパケットを送っているところを探して、逆引きしてみた。
っても起点となるのは`Producer`側でメディアの送信なので、そこから追うだけ。

  • `Producer::ReceiveRtpPacket()`
    • クライアントがメディアを送信してきた
  • `Transport::OnProducerRtpPacketReceived()`
    • パケットを受けるのは`WebRtcTransport`なので、それを`Router`に分配
  • `Router::OnTransportProducerRtpPacketReceived()`
    • `Router`はそのメディアを`consume()`してる`Consumer()`に分配
  • `SimpleConsumer::SendRtpPacket()`
    • `Consumer`は受け取ったメディアをクライアントへ送信
  • `Transport::OnConsumerSendRtpPacket()`
  • `WebRtcTransport::SendRtpPacket()`
    • SRTPにencrypt
    • SRTPのセッションが有効化チェック

これですっきり。

後編のまとめ

メディアを送受信する`Producer`と`Consumer`の処理をメインにお送りました。
これでmediasoupでメディアが送受信される一連の処理はこれでだいたい見たはずですが、まだまだ読み足りない。

いちおうこの記事で触れてないトピックも一覧にしておくと。

  • DTLS-SRTPのハンドシェイクまわりの詳細
  • ICEのTuple決定までの詳細
  • AudioLevelObserver
    • その`Router`に属するメディアの音量の統括するやつ
  • コーデックまわりの解析処理
    • VP8とH264
    • encode/decode
  • NACKやREMBなどRTCP関連
    • ファイル数としてはこのあたりが一番多い
    • 各種FBのタイプごとにファイルあったりする
  • `WebRtcTransport`以外の`Transport`
    • `PlainRtpTransport` / `PipeTransport`
  • RTPのヘッダ拡張やパラメータの取り回しの部分
  • `SimlucastConsumer`
    • およびそのあたりの実装まるっと全部
    • というかSFUの実装という意味での差別化ポイントはここにある

うーん高機能!

本当に参考になりまくるawesome work👏でした。

C++、こんな感じでコードを読む分にはいけるけど、実際に書くとなると大変そう・・という感想になりました。