そろそろ私なんかでも日曜大工で遊べそうな雰囲気が漂ってきたWebRTCでの画面共有アプリを習作してみました、という例です。
なお、各種ソフトのバージョンはあえて割愛しますが、2020年5月2日に確認した内容の記事となりますので、そのような鮮度だとご承知ください。
シグナリングサーバとWebサイト部分は、node.jsで実現しています。(下記は資材の一覧です。live.puは無視してください。)
実行イメージです。
Socket.IOを使っているのですが、Socket.IOのチュートリアルのリアルタイムテキストチャットを改変(?)したミニチャット付きです。 このミニチャット部分は「シグナリング」のやりとりの簡易ログも兼ねています。
(1) WebRTCについて
要素技術・関連記述についてのリンク(WebRTC、getUserMedia、 Socket.IO(WebSocket) )
いきなりですが(私のおぼろげな理解でいろいろウソをばらまいてもイヤなので)、まずは、権威のある別サイトへのリンクをさせていただきます。
https://gist.github.com/voluntas/67e5a26915751226fdcf
↑
著者の方は、WebRTCのプロダクトを開発されている方のようです。 情報の質・量に、私なぞは圧倒されてしまうのですが、(うまい表現が見つからないので上から目線風の言い方で恐縮ですが)WebRTCとプロダクトに向き合った現場の生きた経験からくる裏付けを感じます。 著者の方のブログとtwitterを参照させていただいて、WebRTCの最新情報、最新動向など(の雰囲気)を勉強させてもらうことにしています。
いまやブラウザ標準といえるRTCPeerConnection() と getUserMedia() 。
WebSocketが楽チンに使えるSocket.IO(シグナリングに使う)。
なお、今回は、シグナリングサーバに必要になるソケット通信については、Socket.IO*1を利用させてもらっているのですが、WebSocket自体も私のようなワナビーもとっつきやすいぐらいところまで標準化されているようです。
利用ソフトなど
- Socket.IO
- node.js(シグナリングサーバ、およびSocket.IOのライブラリを稼働。次項のExpressなどの用途) → 確認したのは、v12.16.1です。
- Express(待合室ページのWebサイトのHTTPアクセス用)
- jQuery →テキストチャット周りでINPUTフォームの出し入れに利用。「画面共有」としては特に使っていませんし、本来WebRTCに必要かというとそうではありません。
なお、この類のものはhttpsだと動くけど、httpだと動かないというものが(今時当たり前ですが)チラチラあります。
よって、後述のアプリでは、オレオレですが、OpenSSLで証明書を作成して、pemディレクトリに格納している体です。
あと、STUNサーバは、Googleのものにアクセスさせてもらっています。
その他キーワード
- データチャネルとメディアチャネル → 今回はメディアチャネルしか使っていない...ハズ
- シグナリング
- SDP
- ICE
- VanillaICEとTrickleICE → 今回の事例は、(多分)VanillaICEスタイルになっているハズ。
- MCUとSFU → 今回は試してみるだけレベルなので(あとわたくしの技術力の都合で)MCU・SFUどちらでもなく、配信側が接続先分頑張る方式。
- Socket.IOでは、room Socket.IO — Rooms and Namespaces | Socket.IO という、しかけがありますが、これは使っていません。が、UIの言い回しの都合上、本サンプルアプリでは、「放送室」みたいなroom感ある名称を使っているところがあります。...がこのroomとは別物です。
- TURNサーバ (使っていません)
他にも大事なキーワードがあると思いますが、今回の例で暗黙的に関わっている部分とそうではないものの、この先の本格的な世界では次に重要そうなワードを挙げてみました。
まあ、これらをよくご存知の方は、この記事は不要だったという話になると思いますが...
(2) サンプルアプリ
では、アプリの話に入ります。
アプリの資材ファイルの構成(再掲)です。
このディレクトリ構成のまま、app.jsをnode.jsで起動して、シグナリングサーバを起動してください。
app.jsはWebサーバにもなっているので、https://起動サーバ/ でアクセスしてもらって、その後、リンクを辿っていればなんとなく分かるかな...という気がします。
なお、ここでは説明しませんが、本件を試すだけであればオレオレで良いので証明書の設定を行うか、app.js中に「httpsではなくhttpの場合は...」みたいなコメントアウトになっているロジックの方を有効にしてください。 (httpsとhttp同時両対応にはしていません。)
あと、app.jsの冒頭に、サーバの起動ポートを定義しているので、これをよろしく変更してください。
(3) サンプルアプリの説明
配信側と受信側で役割が別れた方式です。
特に、認証・認可などは行っておらず、放送室、視聴室と称したページにアクセスして、そこでボタン操作などすると共有が始まるという動作です。
仕組み上は、配信1対受信Nです。
配信を双方向にしないことで、シンプルな方式にするのと、調子が悪くなってもページのリロードでなんとか回復させられるようにしています*2。
プロダクトレベルであれば、もっとシビアなハンドリングやそもそもTrickleICEにするなどいろいろ必要だと思われますが、つながる...という意味ですとこんな感じで手元では動作しています。
私のおあそび環境だと、配信側・受信側ともに、Chrome/FIrefox/Safari/Edgeで動作しました。
ソースコード
サンプルアプリのソースコード一覧です。
public/css/chat.jsなどディレクトリ配下のものは、「publiccsschat.js」などのように、「/」を「アンダースコア3つ」に置き換えた名前で登録しています。
実現方式イメージ と補足
このアプリのシーケンス図風の実現方式イメージです。
以下、所感レベルの補足です。
◆(補足1) そんなに分かってなくてもWebRTCできた!(やってみるだけなら)
- WebRTCのおかげで、「動画」をシェアするところの仕組みは(もっとシビアな例だとともかく)各種サンプルの写経レベルで動作するようになっているのは、非常にありがたいです。 このあたりは、RTCPeerConnectionから生えているメソッドの使い方の様々な事例からこんな感じなのかなと理解・想像できるところまでこなれているようなので、ここでは説明しません*3。
◆(補足2) シグナリングとSDPとICE
- 次に、「シグナリング」部分ですが、ここまで厳密な定義もせずこの用語を使ってきました。
(といいつつ改めて定義はしないのですが、)この記事に見合ったレベルで雑にご説明すると、WebRTCでリアルタイム通信を始めるための、端末間のネゴシエーションの仕掛けことだと思えば良さそうです。
何をネゴるのかでいうと、IP上の通信なので、インターネット側から見たIPアドレスやポート、あとはストリームのようなデータの送受になるので、通信品質や動画であればフレームレートなども必要な情報としてありそうです。
どこまでがネゴシエーションの範囲かは冒頭の有識者の方のサイトなどでお調べいただくとして、規格としては、SDPやICE(ICE Candidate)といったものがこのようなネゴシエーションのために共有する情報*4を定義するものとして定められているようです。
ただ、これらも、WebRTCのAPIが拡充されているので、(実際は著名なハイレベルなライブラリのAPIを使うとして、一方で)ブラウザの生のAPIを使うとしても、使ってみるだけであれば、SDPの内容などはひとまず意識しなくても使えるようです。
なお、SDPやICEの内容は例えば次のようなスクリプトで参照できるようです。相手がいなくても素振りできるのですね。Chromeであれば、画面のキャプチャ許可をOKする*5と、コンソールに出力されるかと思います。
↓自分のSDPを見てみる例(ブラウザのJavaScriptコンソールに出力)
<!DOCTYPE html> <html lang="ja"> <body> <script> ; (async () => { const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }) const pcConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] } const peer = new RTCPeerConnection(pcConfig) stream.getTracks().forEach(track => peer.addTrack(track, stream)) peer.setLocalDescription(await peer.createOffer()) peer.onicecandidate = evt => { if (!evt.candidate) { console.log(JSON.stringify(peer.localDescription)) } else { console.log(JSON.stringify(evt.candidate)) } } })() </script> </body> </html>
◆(補足3) OfferとAnswer、VanillaICE
シグナリングは特にルール化はされてないようです。... と私も最初そのように聞いてピンときませんでしたが、実際に習作してみて確かにそうだなと思いました。たしかに、Offer SDP、Answer SDPの送受はWebRTCを利用する上でのかならずとおる道ですが、実際にこれらをやり取りするやり方は、アプリの要件そのものやWebRTC自体のアプリでの位置付けによります。このことは実際に記事にまとめるとともに実装してみてわかりました。なお、シグナリングは標準化の対象ではないもののの、SDPを互いに共有する際の通信のことを「Offer」「Answer」という呼び名でいうのが慣例のようです。
さて、このサンプルアプリでは、シグナリングは、VanillaICEと呼ばれるスタイルを意識しています。これは、TrickleICEと呼ばれる方式と対比して語られるようです。
TrickleICEは、WebRTCの通信の確立に必要な情報が少しでも得られたら通信確立のために双方でやり取りを始める方式で、よりリアルタイム性の高い要件の場合に、きめ細やかな制御ができます。 というか、きめ細やかな制御をするためのもので、もちろん、その分、いろいろ大変でしょう。
一方、VanillaICEは、最初につながるのに時間を要しても、あるいは途中回線の品質が悪化した場合などに融通がきかないかもしれませんが、この交渉を1回で済ませようというような方式といえると思います。
- ということで、このサンプルアプリでは、VanillaICEのスタイルで実現するとともに、おおよそ、上記のシーケンス図でいうところの、 「シグナリング(VanillaICEスタイル)」のようなシグナリング方式としています。 このシグナリングでは、VanillaICEのシグナリングは、必ず、配信側から出すようにしています。
シグナリングについては、そもそもWebRTCの通信をするための事前の手続きです。このシグナリングのやり取りは、シグナリングサーバを仲介して、またこのようなやり取りに適したWebSocketでの通信(このサンプルアプリではSocket.IOががんばっている)で実現しています。
◆(補足4) アプリの仕様に合わせた前捌き(図中「ご案内(アプリ独自のシグナリング)」)について
- この他、このアプリでは、VanillaICEのシグナリングを開始する前段階として、「画面共有の配信がはじまったか」、「受信側が配信動画を受け入れる気があるか」を配信側と受信側が確認できるように、WebSocketでのやりとりをしています。
図中でいう、「ご案内(アプリ独自のシグナリング)」がこれにあたります。
- このプレシグナリングというべきものがどこまで必要だったかはありますが、画面共有が始まっていないと意味がないしということで、配信側の「画面を共有ボタン」で発火させる方式としています。
画面を共有ボタンを押下すると、シグナリングサーバを介して、ぶら下がっている配信先に「NOW ON AIR」というシグナルが飛びます。
これをうけて、受信側は、「request」*6ですので、実行にあたってはどのようなプログラム内容かをご認識いただき自己責任でお願いします。
この項以上
参考にさせていただいたサイト
下記のサイトからそれぞれつまみ食いさせていただいた感じです。
*1:詳しくは分かっていませんが、ブラウザの標準化されたWebSocketの各種便利ラッパーになっているととともに、例えば、ネイティブのWebSocketを通しづらい環境の場合に、代替手段でサーバサイドからのブッシュを実現できるなど何かと便利なため。
*2:これはウリなのかというとそうではないかもしれませんが、
*3:ぼろがでないように
*4:交渉といいましたが、実際はオークションのようなものではなく、互いに候補の一覧を出して、規格に従って一番適切と思われるもので確定させる方式のようです。多分。
*5:この時点では他者に動画が垂れ流しになることはないと思います。
*6:どうせ独自なのでもっと特殊なワードにすれば良かったですが...)というシグナルを戻します。 配信側は、「request」シグナルとともにシグナリングサーバを介して配信側のIDを知るとともに、(シグナリングサーバでのSocketIDごとのemitが前提ですが)依頼のあった配信側の該当のクライアント向けに、「Offer」を投げるという、「シグナリング(VanillaICEスタイル)」フェーズに入っていく方式となっています。
なんでこうしたのという言われると、次のようなことを考慮したためです。
画面共有自体は一方向の限られた機能ですが、1対多の配信に対応する必要がある。一方、シグナリングサーバで細かい「多」との通信制御をするのは手間。
- 配信側もいきなり「WebRTC」のOfferとAnswerをやり取りし始めるのではなく、配信側が先にOfferを投げる方式にしておいた方が、(処理時間などを要しても)シンプルなコードにできる。
- 一度配信を始めている状況であれば、後から参加した受信側は、「視聴室」に入室した際に、「request」を自動送信することで、ライブ受信に特に手間なく参加できる。
SIPとかこの類の専門家の人はもっと高度なノウハウがあるのでしょうね。私はひとまず上記のように考えました。
◆(補足5) 免責事項
- サンプルアプリでは、データの更新などは行っていませんが、なんだかんだいって画面の共有などを伴うアプリ((私のスキル不足もあると思いますので、思わぬバグもあるかもしれません。また、そもそもサンプルアプリであるということもありますが、エラーハンドリングなどは意識的に省いています。 この他、処理方式からすると、それなりの数の端末が配信を待ち受けている状況で配信を始めると、配信requestが配信側に殺到することになるので、かなりきついかもしれません。