これは何?
前回記事 の続き。これまでは、現実的ではなかった WebRTC P2P での live 配信。WebCodecs と WebRTC Insertable Streams 使ったら「お、これは使えるかも!?」だったので、それの紹介記事。
WebCodecs + WebRTC Insertable Streams 使った Scalable live w/ P2P のアーキテクチャ説明
まず、前回を振り返ると、WebRTC Insertable Streams を使い、以下のアーキテクチャで動作させてみました。
要は、P2Pで対抗ごとにまともにエンコーダ回すと、その処理で CPU なりが厳しくなっちゃうんで、2本目以降はダミー映像エンコードにして、 Insertable Streams で 1本目のまともにエンコードした映像データに差し替える・・・・というもの。
詳細は、前回記事を参照いただければですが、こちらの問題は、「差し替えるオリジナルのデコード済み映像データの keyframe 制御ができないため、受信側でまともに映像がみれない」というものでした。結局エンコーダは、WebRTCまかせでアンタッチャブルなんでなんともできない(涙
そこで、今回は下図に示す WebCodecs を用いたアーキテクチャに変更しました。
一番の違いは、ソースの映像を WebCodecs を使って、コントローラブルにエンコード可能としたこと。なので、WebRTCにはすべてダミー映像 (videoの track.enabled = false
に設定したストリームオブジェクト) をくわせています。
そんで、 Insertable Streams で、ダミー映像をエンコードしたものが毎フレームごとに取得できるので、それを WebCodecs でエンコードしたものに差し替えるという戦略です。考え方としては、非常にシンプル。
エンコードの際、映像データの場合、 keyframe 制御が非常に重要になります。ここで、WebRTCでは、新規に視聴参加者が来たり、映像データのロスが検出されると、同図中の「エンコーダ by WebRTC」にその情報が渡され、keyframe としてエンコードがなされます(あくまで、ダミー映像ですが)。すると、 Insertable Streams ではこのとき chunk.type
の値が key
となるため
if( chunk.type === "key" ) {
// WebCodecs に keyframe 要求を送る
}
と、そのプロパティをチェックし、 WebCodecs の keyframe 制御を行うことで、いい感じで映像配信ができる・・・という形です。
コードの説明
ソースコード全体は https://github.com/kokutele/kokutele-live で公開しています。
以下では、あくまで、ポイントだけ。動作原理は、先に示したとおりとなっており、詳細はスニペット中のコメントを参照ください。
送信側
まず、 WebCodecs によるエンコードの部分を抜粋したのが以下のコード
start():void {
// MediaStream オブジェクトから Video Track を取り出す
const [track]:Array<MediaStreamTrack> = this._stream.getVideoTracks()
// WebCodecs の定義。エンコードが行われる都度、 'chunk' イベントを発生する。
const videoEncoder = new window.VideoEncoder({
output: chunk => {
this.emit('chunk', chunk)
},
error: err => {
this.emit('error', err)
}
})
videoEncoder.configure({
codec: 'vp8',
width: 640,
height: 480,
framerate: 30
})
// 映像トラックの読み込みを開始し、逐次 `encode()` によりエンコード処理を
// 行う
const videoReader = new window.VideoTrackReader(track)
let idx = 0
const interval = 10 * 30 // 10 sec
videoReader.start( frame => {
// keyframe 制御の部分。10秒に一回の periodical 制御とともに、
// プロパティ `_reqKeyFrame` が `true` に設定されたときも、
// keyframe 生成を行う
const _reqKeyFrame = this._reqKeyFrame || !(idx++ % interval)
videoEncoder.encode(frame, {keyFrame: _reqKeyFrame})
this._reqKeyFrame = false
})
}
これを、 Insertable Streams でダミー映像のものと差し替えます。
transform: (chunk, controller) => {
const kind = chunk instanceof window.RTCEncodedVideoFrame ? 'video' : 'audio'
// ダミー映像の chunk.type が `key` だったら、WebCodecsで、keyframe 生成
if( kind === "video" && chunk.type === "key") {
this._encoder.reqKeyFrame = true
}
// エンコードデータの差し替え処理
if( kind === "video") {
// replace media data
const _chunks = chunksMap.get(this._receiver)
const _chunk = (_chunks && _chunks.length > 0) && _chunks[idx++]
if( _chunk ) {
if( typeof _chunk === 'object' && _chunk.data ) {
chunk.data = _chunk.data
}
}
controller.enqueue( chunk )
} else if( kind==="audio" ) {
controller.enqueue( chunk )
}
});
受信側
普通に、WebRTCで受信したものを処理するだけで、Insertable Streams や WebCodecs 的な特別な処理は不要です。
パフォーマンステスト
後述のサンプル Web app を使い、簡易的なパフォーマンステストを行いました。結果が上図の通り。同一端末内で複数ブラウザを立ち上げ、一つが送信側、それ以外が受信側・・・という簡易なもの。あくまで、参考として御覧ください。
同時視聴数の増加に伴い、送信側 Chrome の CPU 使用率があがっていきますが、通常のP2P配信(赤線)と、今回のスケーラブル配信(青色)とで明らかにパフォーマンスメリットが得られています。
ここで、同時接続 6 で通常の P2P 配信で CPU 使用率が下がっていますが、こちら、CPU処理的に厳しくなったためか、送信エンコードのレートを下げているためで、あくまで目視ですが 1.6Mbps -> 600kbps 程度に下げていました。なので、通常の P2P 配信だと配信限界は 5 ぐらいかな・・・ということになります(さすがに、この数では使えない)
一方、WebCodecsの場合は、通常時で 1Mbps の値となっていたのですが、こちら全く変わらず。データとしてきちんととっていないですが、20弱に試しにあげても、問題なく配信できていました。たぶん、もっと行くんじゃないかな。100のオーダーは厳しそうだけど(端末性能に依存するところですが)
サンプル Web app
で、スケーラブル P2P live 配信試せます。配信側では
にアクセスいただき、
"start sender" をクリックすると、下部のほうに視聴側のURLが払い出されるため、こちらにアクセスすることでリモート視聴できます。
予想に難くなく、 "enable scalable" のチェックを外せば、通常 P2P 配信モードになります(Chrome M87以降で、 chrome://flags/#enable-experimental-web-platform-features
の enable を忘れずに)
終わりに
WebCodecs + WebRTC Insertable Streams で、「完全サーバレス」で小規模ライブ配信が可能であることを示しました。仲間内とか、社内とかだと数十ぐらいの配信で十分だったりするケース結構多いので、割と使い勝手あるのかな?と思います(かなり荒く作ってるんで、もーちっとコードの改善必要ですがww
こちらのサンプルApp、「単なる配信以上の何か」も入っているため、謎のモノクロ映像も表示されていますが、とりあえず今は気にしないでくださいw(この辺については、近日中に僕の個人ブログで記事を書く予定)