WebRTC Soraの新機能・BundleIDを使ってみる 〜画面共有時の帯域を節約〜
こんにちは、テリーです。リモートワークが定着し、ビデオ会議中に話し手のPC画面を共有する使い方も当たり前になりました。プロジェクト管理ツールの画面や、スライド資料の画面を共有するケースは昔から使われていますが、最近はゲーム画面や、収録済みのビデオ映像をビデオ会議参加者に共有するケースも増えてきています。PC画面の共有は解像度が高く、かつ微細な文字が多いため、本来かなりヘビーな処理ですが、フレームレートを自動調整することで画質を優先し、ビットレートとCPU使用率を低く抑えつつ、見やすいようにエンコードされているようです。今回はビデオ会議における画面共有を深堀りします。
なお、本記事では、音声や映像およびデータのリアルタイムな配信を実現するソフトウェアである、WebRTC SFU Soraを使用しています。Soraについての詳細は、Soraの公式サイトをご覧ください。
対象読者
- WebRTC SFU Soraを使用している人
- WebRTC SFU Soraの使用を検討している人
動作確認環境
本記事は以下の環境にて動作を確認しています。
- Sora 2022.1.1
- Windows 11 Home 21H2
- Chrome 105.0.5195.102
画面共有アプリケーションの問題点
さて、ビデオ会議中の画面共有時のWebRTC接続状況を考えてみましょう。話し手のPCからは2つの映像を配信します。2つの映像とは、話し手自身の顔を映しているカメラ映像と、話し手のパソコン画面をスクリーンキャプチャしている映像です。話し手のPCは自分以外の聞き手全員の映像を受信します。会議に参加している人数が自身を含めて4人の場合、上りが2件、下りが3件です。N人の場合、上りが2件、下りがN-1件です。
一方聞き手のPCでは、配信しているのは聞き手自身の顔を映しているカメラ映像のみです。聞き手のPCは自分以外の聞き手全員の映像と、話し手の顔映像と、話し手の画面共有映像を受信します。会議に参加している人数が自身を含めて4人の場合、上りが1件、下りが4件です。N人の場合、上りが1件、下りがN件です。
Soraのmultistream機能を使って実装します。Soraでは1接続につき映像は1つのみなので、画面共有をするためには2接続する必要があります。カメラ映像をsendrecv、画面共有映像をsendonlyで接続します。その場合、上図とは異なり、下図のようになります。なんと話し手のPCの画面共有映像を、話し手自身が受信しています。1台のPCから2接続した場合、お互いが独立した配信であるため、映像の種類が聞き手の映像なのか、共有画面の映像なのか、プログラムでは区別することが難しく、両方を受信することになります。これでは話し手のPCが使用する回線が無駄に使用され、また受信した自身の画面共有映像をデコードするためにCPUの余裕もなくなります。
BundleID機能登場
Sora 2022.1.0からBundleIDという機能が追加されました。Soraのリリースノートにはこのように書かれています。
複数のコネクションを同じ端末から接続する際、それぞれのコネクションで同一の bundle_id を指定すると、 同一の bundle_id を指定した接続からの音声や映像、メッセージングを受信しなくなります。
すばらしい! まさに画面共有配信のための機能です。この機能を使うと、前述の図1の構成にできそうです。
実際にBundleIDを指定して無駄な接続と通信が起こらないことを確認してみましょう。BundleID機能を使用するためにはsora.confで下記の行を明記する必要があります。設定が完了したらSoraを再起動します。
signaling_bundle_id = true
まずは従来のプログラム通り、BundleIDを指定しない場合を確認します。カメラ映像とデスクトップ画面映像の2つを同じチャンネルに接続します。
const signalingUrl = 'wss://sorahostname/signaling';
const channelId = 'Sora';
const debug = false;
const sora = Sora.connection(signalingUrl, debug);
const options = {
multistream: true,
}
const sendrecv = sora.sendrecv(channelId, null, options);
const sendonly = sora.sendonly(channelId, null, options);
聞き手が0人の状態で、話し手のPCのWindowsのTask Managerでネットワーク使用量を見るとこのようになっています。送信も受信も約3Mbps使用しています。
次に、Soraとの接続オプションに bundleId プロパティを追加します。スペルミスすると機能しないので大文字小文字アンダースコアに気を付けます。
const signalingUrl = 'wss://sorahostname/signaling';
const channelId = 'Sora';
const debug = false;
const sora = Sora.connection(signalingUrl, debug);
const bundleId = Math.random().toString(36).substring(2);
const options = {
multistream: true,
bundleId: bundleId
}
const sendrecv = sora.sendrecv(channelId, null, options);
const sendonly = sora.sendonly(channelId, null, options);
配信用PCのWindowsのTask Managerで使用量を見るとこのようになっています。下りの帯域使用量が明らかに違います。前述の図1や図2で説明した、下りの帯域の無駄がなくなったことが読み取れます。
共有画面映像の特別扱い
画面共有している映像を会議参加者全員の顔映像と区別して大きく表示したいケースがあります。表示位置を区別するだけでなく、ビットレートの優劣もつけたくなるでしょう。
映像を区別するには大きく3点を修正します。trackイベントとremovetrackイベントとnotifyイベントです。
要所を解説します。
配信者側では、soraのsignaling urlに対して2つ接続します。顔映像の方は双方向のsendrecv、画面映像の方は片方向のsendonlyで接続します。bundleIdは乱数やアカウントID等を用いて、他のユーザーと重複しない値を指定します。
const signalingUrl = 'wss://sorahostname/signaling';
const channelId = 'Sora';
const debug = false;
const sora = Sora.connection(signalingUrl, debug);
const bundleId = Math.random().toString(36).substring(2);
const options = {
multistream: true,
bundleId: bundleId
}
const sendrecv = sora.sendrecv(channelId, null, options);
ブラウザ同士でアプリケーション固有の値を伝える場合はsignalingNotifyMetadataプロパティを使います。この接続が画面共有映像であることを全員に伝えるために{screen:true}という値を指定します。これは本サンプル固有の決めごとです。
const screenOptions = {
multistream: true,
bundleId: bundleId,
signalingNotifyMetadata: {screen:true}
}
const sendonly = sora.sendonly(channelId, null, screenOptions);
次にイベント処理です。
Soraの接続を受信し、ビデオ画面を表示する場合、trackイベントでvideoタグとevent.streams[0]を関連付け再生開始する処理が一般的ですが、trackイベントのタイミングでは接続相手の情報が手に入りません。
そこでtrackイベントではMediaStreamオブジェクトを連想配列に格納しておき、その直後に来るnotifyイベントのevent_typeプロパティの値が"connection.created"のときに、接続相手の情報に応じてビデオ表示方法を仕分けします。
同様にremovetrackイベントで行われる処理の代わりとして、notifyイベントのevent_typeの値が"connection.destroyed"のときに行います。
自身の接続前にすでに同じチャンネルに向けて接続している人の一覧はevent.dataリストで手に入ります。
const remoteVideoTracks = {};
sendrecv.on('track', (event) => {
const stream = event.streams[0];
remoteVideoTracks[stream.id]=stream;
});
sendrecv.on('notify', (event) => {
if(event.event_type=='connection.created'){
addVideo(event);
event.data?.forEach(conn=>{
addVideo(conn);
});
}
else if(event.event_type=='connection.destroyed'){
removeVideo(event);
}
});
trackイベントと異なり、自身の接続のときにもnotifyイベントが発生するため、connection_idが自身の値の場合スキップします。
接続ごとのmetadataを確認し、screenプロパティがtrueとなっていれば表示方法を特別扱いします。screenプロパティが存在しない場合はaddRemoteVideo関数(省略)でvideoタグを生成します。
function addVideo(conn){
if(conn.connection_id == sendrecv.connectionId){
return;
}
const stream = remoteVideoTracks[conn.connection_id];
if(!stream){
console.error('notify', 'stream not found', conn.connection_id);
return;
}
if(conn.metadata?.screen){
document.querySelector('#screen').srcObject = stream;
} else {
addRemoteVideo(conn.connection_id, stream);
}
}
function removeVideo(conn){
const stream = remoteVideoTracks[conn.connection_id];
if(!stream){
console.warn('notify', 'stream not found', conn.connection_id);
return;
}
stream.getTracks().forEach(track => track.stop());
if(conn.metadata?.screen){
document.querySelector('#screen').srcObject = null;
} else {
removeRemoteVideo(conn.connection_id);
}
delete remoteVideoTracks[conn.connection_id];
}
期待通り動作していることを確認するために、解像度、フレームレート、ビットレートをリアルタイムに表示するサンプルを作成しました。顔映像を100kbps、画面共有映像を3Mbpsとして接続しました。
まとめ
Sora 2022.1.0の新機能・BundleIDと、それを使用した画面共有映像アプリの様子をご紹介しました。BundleID機能を用いることで、高画質広帯域映像の受信を1つ減らすことができ、話し手のPCの負荷を大きく減らすことができます。また、Soraで受信した映像を区別する方法を用いると、画面のレイアウトの自由度が格段に増します。ぜひ挑戦してみてください。
有料サービスのご紹介
WebRTCを利用した会員制ライブ配信サービスを短期間で導入したい方、大規模配信のためのサーバ構築・インフラ運用を専門家に任せたい方は「ImageFlux Live Streaming」をご検討下さい。ポスプロ会社様向けに動画編集のコラボレーションシステムとして利用された実績があります。WebRTC SFUという技術を用いて、配信映像をサーバに中継させる仕組みです。1対多の会員制映像配信に特に強みがあり、初期の利用コストが低価格に抑えられます。