これは何?
なんとも、ミスリーディングなタイトルなんですが・・・・WebCodecs 試してみたら、NW環境(ロスやら遅延やら)の影響で映像品質どう変わるの?が簡単に確認できるツールがあっという間にできちゃったんで、それの紹介記事です。
WebCodecsって何?
ブラウザでコーデックを自由に使えるようになるAPIです。最近、ようやく試せるようになりました(ChromeM86以降ですが、M87:今のChromiumで使うのがいいと思います。とりあえず、M86でこのデモ動かすとすぐに止まりますw)。登場背景や API の使い方など詳細は https://blog.jxck.io/entries/2020-09-01/webcodecs-webtransport-chat.html をご覧ください。
早速サイトの紹介
サイトURLは、 https://conf.kokutele.com/labs/web-codecs/
上の例は、遅延が1秒でパケロスが定常的に10%発生した時をエミュレートしたもの(コーデックはvp8)。右側が送信側、左側が受信側。
想像に難くなく画面上部の start
ボタンをクリックくると開始。エミュレートできるのは
- sendPLI : パケロス時にPLI (keyframe要求)を送るかどうか
- ignore sync : パケロス発生時に keyframe 受信するまでデコードを止めるかどうか(これは興味本位で付けただけです:なんか動いてない気がしないでもないw)
- emulated delay : 遅延のエミュレート値(msec)
- emulated packet lost ratio : パケロスのエミュレート値
ってところです。映像流している間も、こちらのパラメータは変更可能。
映像って、パケロス起きると、途端にブロックノイズ発生するんで、keyframe 映像が来るまでもじゃもじゃって感じになります。なんで、遅延を増やすと結構悪影響あるなーとか。最近、低遅延での配信はやってますが、遅延増やすと体験的にどうなるんだろう???とか簡単に確認できます。
ノリで、単に WebCodecs 試してみよーで作ったサイトなんですが、できあがってみたら何気に便利っぽいんで公開することにしましたw(開発時間5時間程度なんで、バグとかはご容赦を)
コードの紹介
WebCodecのコーディング法は、冒頭のブログを参照いただくとして、以下ではポイントのみ
コード全体は https://github.com/kokutele/kokutele/blob/master/public/labs/web-codecs/script.js に置いてありますので、併せてご覧ください(vanilla JSで書いてます)。
エンコーダサイド(送信側)
let reqKeyFrame = false
videoReader.start( frame => {
const _reqKeyFrame = reqKeyFrame || !(idx++ % interval)
videoEncoder.encode(frame, {keyFrame: _reqKeyFrame})
reqKeyFrame = false
})
const videoEncoder = new VideoEncoder({
output: chunk => {
// Emulate packet lost
const lost = Math.random() < packetLostRatio
seq++
if( !lost ) {
send( seq, chunk)
} else {
console.info("packet lost by emulator", packetLostRatio)
}
},
error: console.error
})
videoReader.start()
が、エンコーディング開始要求です。デコーダサイドでパケロス検知すると、 reqKeyFrame
を true
にするようになっているため、この時は videoEncoder.encode()
の keyFrame
パラメータを true
にすることで、keyFrameを生成しています(あと、定期的:今だと30秒おきにも生成してます)。
videoEncoder
のコンストラクタで、エンコード済みデータをどう処理するかってのを書く感じ。ここで、パラメータに応じて send()
のコールを止めることで、パケロスをエミュレート。あと、受信側でシーケンス番号的なものが無いとパケロスを検知できないので、それも付与して送ってる感じです(rtpのヘッダ付与相当の処理)。
デコーダサイド(受信側)
let prev = 0, synched = true
const send = async (seqNum, chunk) => {
if( seqNum != prev + 1 ) {
// when packet lost is detected
if(sendPLI) {
setTimeout( e => {
reqKeyFrame = true
}, delay)
}
synched = false
prev = seqNum
} else {
prev = seqNum
if( chunk.type === "key" ) synched = true
await new Promise( r => setTimeout(r, delay))
if( ignoreSync ) {
videoDecoder.decode(chunk)
} else if(synched) {
videoDecoder.decode(chunk)
}
}
}
受信側ですね。色々書いてますが、シーケンス番号を確認して、飛んでいたらロスと判断して前述の reqKeyFrame
を true
にしてます。セットする時に setTimeout()
を使って、それが delay されて送信側に届く・・・という動作をエミュレートしています。
また、通常時も同様に setTimeout()
を使って delay をエミュレート。その後 videoDecoder.decode()
を呼んで、デコーダにデータを渡しています(この辺、ちょっとしっちゃかめっちゃかのコードとなっていますが、試しに書いたコードなんでご容赦を)。
デコード後に、どうやって画面に表示しているかは、ソースコード全体を確認下さい。
終わりに
これまで、この手のことをやろうとすると、エンコーダとデコーダをセットアップして、中間にNWエミュレータ入れて・・・とかやらないとならないんでホント面倒でしたが、Web Codecs のおかげで local loop back で簡単に確認できるようになりました。
もちろん、こちらのコードだと retransmission とか FEC とか入ってないので、あれですが、その辺を追加していくのもさほど難しくないと思います(そこまで試すエンジニアであれば)。映像系の developer の方は、自分が考えたアルゴリズムがどう体感的に働くかとか簡単にチェックできると思いますので、よろしければ、こちらのコードを参考に try していただければと思います。
あ、あと。現状 WebCodecs があまり安定していなく、長時間(10分間ぐらいかな・・・・)流しっぱなしにすると OS フリーズしましたw 僕の環境の場合。できたてホヤホヤのAPIですので、そこは温かい目で見ていただければなぁと思います(たぶん、すぐに治るでしょうが :p