公私問わず`mediasoup`をここ半年くらいずっと触ってて、ドキュメントだけでなく中のコードもそれなりに読み通してる身として。
mediasoupとは
OSSで公開されてる、Node.jsから利用できるWebRTCのSFUです。
SFUを簡単に説明すると、WebRTCの接続をクライアント同士で行うのではなく、共通のサーバーでリレーさせて実現する仕組み。
- サーバーにだけ送信すれば良いのでクライアント側の負荷も下がる
- クライアントは受け取りたいメディアだけを選択して受け取れる
- サーバー側で録音したりメディアの二次加工ができる
などなど、トポロジとしてのP2Pが必要でないなら、WebRTCのユースケースのほとんどはSFUありきなのでは?と思ったりもする・・。
もちろん片手で数えられるくらいの人数がビデオチャットするくらいなら、SFUなしでもやれんことはないけど。
コードとリポジトリ
コードは、メディアを取り回す部分はC++で書かれてて、APIがNode.jsつまりJavaScriptで呼び出せるデザインになってます。
我々ユーザーがさわるのはNode.jsのAPIだけなので、JavaScriptさえかければ簡単にメディアサーバーが作れてしまいます。
(程度はあれど)WebRTCの実装はなかなかに大変で、これがOSSで公開されてるのは本当にありがたい!
最新のメジャーバージョンは3で、サーバーの実装はこのリポジトリ。
そのメディアサーバーに接続するクライアントSDKのラインナップは次のとおり。
- https://github.com/versatica/mediasoup-client
- ブラウザ用のJavaScriptのSDK
- 機能がいちばん豊富
- https://github.com/versatica/libmediasoupclient
- https://github.com/versatica/mediasoup-client-aiortc
- PythonのWebRTC実装である`aiortc`をラップしており、Node.jsから利用できる
- `mediasoup-client`のコアとして使うイメージ
- つい最近できた + `aiortc`側の実装に引きずられてる部分がある
- PythonのWebRTC実装である`aiortc`をラップしており、Node.jsから利用できる
基本的にはこの中のどれかを使うことになります。(というか使わない道は用意されてない)
まずドキュメントを読もう
ありとあらゆる角度からフォローされてて読む価値ありなので、最終的には全ページ読んだほうがいいです!
ドキュメント読まずにリポジトリにIssue立てる人とかが結構いて、中の人も困ってるので。
それでもわからない場合はGitHubではなくDiscourseに書く運用になってるのでそちらへ。
まあそうならないための一助としてのこの記事だったりもするけど・・。
考え方
生のWebRTCのJavaScriptのAPIのことはいったん忘れてください。
いわゆるPub/Subモデルでメディアをやり取りできるというのが、`mediasoup`を使う時の考え方として大事です。
- `video`を送信するだけの人もいれば
- `audio`を受信するだけの人もいる
- どれを受信するかはその人の自由であり
- それはサーバーサイドでも同じ
というように、自由度が高く1つずつ積み上げて組み合わせて作っていくイメージでやるとよいです。
概念: サーバーサイド
いわゆる`mediasoup`語と、主な登場人物たちの紹介です。
まずはサーバーサイドから。
- Worker
- Router
- Transport
- Producer
- Consumer
他にももう少しだけあるけど、最低限だとこれだけ。
数が多い!って思うかもしれないけど、柔軟なAPIとはそういうものなので・・。
Worker
https://mediasoup.org/documentation/v3/mediasoup/api/#Worker
- C++のサブプロセスとして、Node.jsのプロセスの配下で動く
- CPUコアの単位で用意するのが最効率
- 当たり前だがどんなシーンでも最低1つは必要
- Node.jsのサービスを立てるときに用意しておくイメージ
Router
https://mediasoup.org/documentation/v3/mediasoup/api/#Router
- `Worker`から作られるメディアのハブで、端的に言うとルームみたいなもの
- `Router`に向けてメディアを送信し、`Router`からメディアを購読するデザイン
- SFUはトランスコードを行わないので、`Router`ごとにコーデックに関する指定をすることになる
- このルームではH264を使う、このルームは音声のみなど指定したい場合はココで縛る
Transport
https://mediasoup.org/documentation/v3/mediasoup/api/#Transport
- `Router`から作られる、接続しようとするクライアントとの橋みたいなもの
- 送信するために1つ、受信するために1つ用意することになる
- 種類がいくつかある
- ブラウザがWebRTCで接続するなら`WebRtcTransport`
- 同じくサーバーサイドでRTPを流したいなどの場合は、`PlainTransport`
- ICEやDTLSの状態を持つことになる層で、コードを書くとき一番よく触ることになる
- `RTCPeerConnection`みたいなもの
Producer
https://mediasoup.org/documentation/v3/mediasoup/api/#Producer
- メディアを送信することを、produceすると言う
- `Router`に属する`Transport`上でproduceすると、`Producer`ができる
- クライアントとしては`RTCRtpSender`みたいなもの
- メディアではなくデータ(DataChannel)の場合は、`DataProducer`を作ることになる
Consumer
https://mediasoup.org/documentation/v3/mediasoup/api/#Consumer
- メディアを受信することを、consumeすると言う
- `Router`に属する`Transport`上でconsumeすると、`Consumer`ができる
- クライアントとしては`RTCRtpReceiver`みたいなもの
- メディアではなくデータ(DataChannel)の場合は、`DataConsumer`を作ることになる
- `Producer`のIDを指定して作る
概念: クライアントサイド
続いてクライアントのSDK側。
- Device
- Transport
- Producer
- Consumer
基本的には、サーバーサイドのそれとクライアントサイドのそれは1:1で紐づくものなので、新たな登場人物はそんなにないです。
Device
https://mediasoup.org/documentation/v3/mediasoup-client/api/#Device
- クライアントもといユーザーエージェントそのもの
- ブラウザによって実装が違うので、そこを吸収するために必要
- `Router`の設定をロードして使う
Transport
https://mediasoup.org/documentation/v3/mediasoup-client/api/#Transport
- `Device`によって作られる
- `RTCPeerConnection`そのもの
Producer
https://mediasoup.org/documentation/v3/mediasoup-client/api/#Producer
- `Transport`によって作られる
- データの場合は`DataProducer`
Consumer
https://mediasoup.org/documentation/v3/mediasoup-client/api/#Consumer
- `Transport`によって作られる
- データの場合は`DataConsumer`
シグナリング
ここまで説明した登場人物を作るAPIは用意されてるものの、どのタイミングでどれを作るかなどは完全にユーザー依存です。
サーバーサイドの`Worker`や、クライアントサイドの`Device`は事前に作っておけるけど、それ以外はクライアントとサーバー間で調整が必要になる。
それをWebSocketやRESTでよしなにやることを、シグナリングするという。
`mediasoup`はシグナリングにまったく関与しないし、何でやってもよいということになってます。
`Router`のコーデックの設定などをUIで動的に指定したいかもしれないし。
`Transport`と`Producer`と`Consumer`に関しては、サーバーサイドで先に作って、それと対になるものをクライアントで作ることになる点だけ注意。
つまり、次の流れに沿って処理をすることになるということ。
- クライアント: `Transport`を作りたい旨をシグナリング
- サーバー: `Transport`をどこかの`Router`に作る
- サーバー: 作った`Transport`の情報をクライアントにシグナリング
- クライアント: その情報を使って`Transport`を作る
`Producer`を作るときも、`Consumer`を作るときも同じような流れになる。
どういうコードを書くかももちろん自由。
最初に送信用と受信用の`Transport`2つ分をサーバーで用意しておくもよし、逐一シグナリングするもよし、ベストプラクティスはアプリケーションの要件次第です。
まとめ
というわけで今記事では、`mediasoup`についての基本的な概念について手厚くお送りしました。
個人的に`mediasoup`を使う上での最初の壁は、この登場人物の多さと役割分担をしっかり理解できるかだと思ってます。
最低限のSFUの実装を用意して、クライアントAとBが互いに1種類のメディアを送受信したい場合は、こんな感じのリソースツリーになるはず。
# サーバー - Worker1 - Router1 - SendTransportForA - ProducerForA - SendTransportForB - ProducerForB - RecvTransportForA - ConsumerForA - RecvTransportForB - ConsumerForB
# クライアント - Device - SendTransport - Producer - RecvTransport - Consumer
"送信と受信は分けて考えられる"ということに気付けるかどうかがミソ。
あと、videoとaudioを送る場合、`Producer`は2つ必要だけども`Transport`は送信用のやつ1つでOKってところ。
実装も段階を分けて1stepずつやっていくとわかりやすいです。
ともあれ、これさえわかってればコードリーディングも捗るはず!