WebCodecs の QP 設定機能によって高精度なビットレート制御を実現させる方法
この記事は、NTT Communications Advent Calendar 2023 24日目の記事です。
はじめに
先日参加してきた TPAC 2023 [1] の中で、WebCodecs API に追加された新たな機能が紹介されていました。その機能とは、フレームごとに QP (Quantization Parameter) という値を変更してエンコードできるというものです。確認してみたところ、既に Chrome 117 にて有効化 されていたので、どのような機能なのか調べ、実際に動作を確かめてみました。
本記事は、以下のような方々をターゲットとして想定しています。
- ブラウザ上で映像処理を行うことに興味・関心を持っている方
- WebRTC や WebTransport などを利用した映像伝送の最新動向を把握したい方
本記事を読むことで、WebCodecs の新機能である QP 設定について概要を理解し、その使用感や使用する際に注意すべき点が把握できます。
忙しい方向け
- QP 設定機能は既存の設定よりも低レベルで、カスタマイズ性のある機能
- この機能を使用すれば既存の設定では難しかった以下のことが実現可能
- 低ビットレート時の正確なレート制御
- 非常に高品質な映像の生成
- 大抵の場合は既存の設定で十分だが、一部のユースケースで利用されることがありそう
- さまざまな理由から、WebCodecs の動作安定や標準化完了にはまだ時間がかかるだろう
事前知識
本章では WebCodecs、エンコーダの QP 設定について簡単に解説します。
これらを既に理解している方は、読み飛ばして 次の章 へ進んでいただいて問題ありません。
WebCodecs について
WebCodecs はブラウザが実装しているエンコード・デコード機能にアクセスして使用するための API です。
WebCodecs の役割
例えば、ブラウザ上でエンコード・デコードを行うユースケースとして WebRTC があります。WebRTC ではエンコード・デコードはブラウザが自動で行なってくれるため、簡単である反面、カスタマイズ性が乏しいという課題が存在します。
一方、WebCodecs はエンコード・デコード機能のみを単体で扱うことができます。以下のような方法と組み合わせることで、WebRTCの 拡張ユースケース や映像伝送におけるその他のユースケースを実現する手段となることが期待されています。
- MOQT: WebTransport を利用し、QUIC でメディアを送信するためのプロトコル [2]
- RTPTransport: 最近策定が開始された WebRTC の新たな低レベル API
先日の私の記事の中で RTPTransport と WebCodecs の関係を解説しておりますので、ご興味ある方はぜひご一読ください。
WebCodecs の現状
WebCodecs は W3C の Media WG が標準化を進めている最中で、仕様書 は Working Draft の状態です。ブラウザによって対応状況はまちまちで、Chrome・Edge・Opera は対応していますが、Safari は映像のみ対応、Firefox は未対応の状態です。また、対応している場合でも、その実装はまだ安定していません。
以上から、WebCodecs の仕様策定が完了し、全ブラウザで標準的に使用可能になるまでには、まだまだ時間がかかるでしょう。
QP (Quantization Parameter) の役割
QP について順を追って説明します。このパートの内容は少し複雑ですが、以下の要点さえおさえていただければ問題ありません。
- 多くの映像コーデックでは量子化によってデータ量が削減されている
- 量子化レベルを調整するために QP が用意されており、大きくするほど圧縮率が上がる
映像圧縮における量子化
各映像コーデックは映像品質を保ちながら圧縮率を上げるため、いくつもの処理を採用してエンコードを行っています。コーデックごとに採用している処理が少しづつ異なっており、それによってそれぞれ特徴 (例えば、圧縮効率や処理負荷の差分) が生まれています。
それらの処理の中でも多くのコーデックに共通しているものがあります。それは、映像のフレームを小さなブロックへ分割し、予測値との差分を求め、DCT (離散コサイン変換)[3] により周波数成分へ変換し、量子化を行うというものです。
映像のエンコードにおける共通処理
ブロック分割・変換・量子化の流れは、JPEG などの画像圧縮にも採用される非常にスタンダードなものです。
量子化を行う理由
ここでは、内容を簡単にするため、画像への処理を例として説明します。
画像を周波数成分へ変換することで、画像の信号を低〜高周波成分へと分解できます。ここで、低周波成分は信号の緩やかな変化を表し、高周波成分は信号の急激な変化を表します。
多くの場合、画像の周波数成分は低周波に偏り、高周波は小さな値になりやすいという特性があります。したがって、各周波数成分を量子化することで、高周波成分のほとんどを0として扱うことができ、大幅にデータ量を削減できるようになります。
変換・量子化の流れと値の変化
ここで、図中では (b) → (c) の量子化の際、全体を均等に10で割って四捨五入していますが、実際には高周波成分になるほど大きな値で割られるように重み付けされることが多いです。
元の画像を得たい場合、 (c) を10倍し、逆 DCT をかけることで (a) に近い輝度成分を得ることができます。[4]
図の例では (a) を表現するのに 128bit 必要ですが、(c) はたった 28bit [5]で表現できます。
QP とは
QP は Quantization Parameter のことで、量子化のレベルを調整するためのパラメータです。QP は大きくなればなるほど量子化のレベルが上がってデータ量を削減できますが、その分表現力が低下するので品質が劣化します。
QP はインデックスのようなものであり、実際には Qstep という値を使用して量子化されます。
コーデックごとに QP に対応する Qstep が決められています。例えば、AVC (H.264) の場合、QP = 0 のとき Qstep = 0.625、QP = 1 のとき Qstep = 0.701... のように定められています。
QP が 6 増えるごとに Qstep は2倍になるので、Qstep の対数と QP は比例します。
AVC における QP と Qstep の関係
QP 変更時にビットレートや品質がどの程度変化するかについては、記事の 後半 で触れます。
WebCodecs のフレームごとの QP 設定機能
ここからは、本題である WebCodecs の新機能について触れていきます。
概要
Chrome 117 から VideoEncoder.configure() の config.bitrateMode
に"quantizer"
が新たに追加されました。これを指定することで、映像のフレームごとに QP を設定できるようになります。
const encoder = new VideoEncoder(init);
const encoderConfig = {
codec: "vp09.00.10.08",
width: 800,
height: 600,
bitrateMode: "quantizer", // 新たに追加
framerate: 30,
latencyMode: "realtime",
};
encoder.configure(encoderConfig);
WebCodecs は さまざまな映像コーデック に対応していますが、この設定を使用できるコーデックは AV1・VP9・AVC (H.264) の3種類です。以下のように、options.[使用するコーデック名].quantizer
に QP を設定して encode()
の引数として渡すことで、設定された QP に応じたエンコードが実行されます。
const encodeOptions = { keyFrame: false };
const qp = calculateQp(codec, frame); // ユーザ定義のQP計算
if (codec.includes("vp09")) {
encodeOptions.vp9 = { quantizer: qp }; // VP9の場合のQP設定
} else if (codec.includes("av01")) {
encodeOptions.av1 = { quantizer: qp }; // AV1の場合のQP設定
} else if (codec.includes("avc")) {
encodeOptions.avc = { quantizer: qp }; // AVCの場合のQP設定
}
encoder.encode(frame, encodeOptions);
QP の導出ロジックは開発者が自由に実装できます。QP の設定範囲は AV1・VP9 が0〜63、 AVC が0〜51と定められているため、その範囲内となるように導出する必要があります。
既存の設定との差分
これまでの既存の config.bitrateMode
として、"constant"
と "variable"
がありました。新たに追加された "quantizer"
の特徴を、これらと比較して説明します。
"constant"
設定
この設定は、一般的には CBR (Constant Bit Rate) と呼ばれるものです。
VideoEncoder.configure()
で設定された config.bitrate
を目標とし、自動で圧縮率を調整するモードです。config.bitrate
の設定は動作中に変更できますので、ある程度のビットレートの調整も可能です。
"constant"
は 仕様 の中でも実装方法について特に指定がないため、各ブラウザの実装にしたがって QP を変更しながらビットレートを調整しているはずです。"quantizer"
はそれをユーザ自身が自由に実装できるという意味で "constant"
よりも低レベルな手段ということになります。
"variable"
設定
この設定は、一般的には VBR (Variable Bit Rate) と呼ばれるものです。
映像の内容にしたがってビットレートが変更されるモードです。内容の変化が小さいシーンではビットレートが低くなり、内容の変化が大きいシーンではビットレートが高くなります。bitrateMode
に何も設定しなかった場合、デフォルトで設定が使用されます。
このモードでは、ビットレートの変化は完全に映像の内容に依ります。ビットレートをコントロールしたい場合、"constant"
や "quantizer"
を使用すべきです。
ユースケース
この機能は、W3C によって議論された内容 によると、プラットフォーム固有のビットレート制御ではなく、独自にカスタマイズしたビットレート制御を実装したい開発者のニーズを満たすものとして用意されているようです。具体的なユースケースとして以下のようなものが考えられそうです。
急な帯域の変化への対応
映像の送信中にネットワーク帯域が映像のビットレートよりも狭くなってしまうと、映像が送信できなくなってしまいます。対策として、映像の品質を下げてビットレートを帯域幅よりも低くすることで映像送信を継続できますが、変更が反映されるまでの間は映像のフリーズが続きます。
例えば、WebRTC ではビットレートが下がるまで多少時間がかかり、フリーズが発生することがあります。実例として、WebRTC WG の Chair が 提供した情報 から、WebRTC で帯域が狭まった際に映像がフリーズしてしまう様子が確認できます。
帯域が狭くなってもフレームごとに適切な QP を設定できれば、このようなフリーズを回避できるでしょう。
映像品質の維持
既存の設定ではブラウザの実装にしたがって映像の品質が制御されます。品質を指定したい場合、config.bitrateMode
を "constant"
にして config.bitrate
を設定すればある程度は対処できますが、完璧な品質維持は不可能です。
一方、QP を固定値に設定すると一定の品質を維持できます。例えば、QP を最小値の0とすることで、最高品質の映像を生成して保存できます。
QP 設定はブラウザ以外でも OBS などで使用されていますが、OBS では映像を高品質に保ちながら保存をする用途などで実際に使われることがあるようです。
動作チェック
今回は "quantizer"
と "constant"
の動作を確認できるデモを用意して検証してみました。
おおまかな処理の流れは以下のとおりです。
- 映像ファイルを読み込む
- QP を導出して設定する
- 各フレームをエンコード・デコードする
- 3の結果の映像とビットレートを表示する
- 2〜4を繰り返す
ここで、2は "quantizer"
の動作を説明していますが、"constant"
の場合は QP の代わりにビットレートをそのまま設定します。
また、今回使用した映像は こちら です。
- 解像度: 720p (1280x720)
- FPS: 30
特に補足がない場合、コーデックには VP9 を使用して検証します。
QP を固定した場合の動作
映像品質とビットレートの確認
まずは、QP を固定してエンコード・デコードし、映像の品質とビットレートがどのようになるか確認してみます。
例えば、VP9 で QP を0に固定したい場合、以下のようにすれば設定できます。
const encodeOptions = { keyFrame: false };
const qp = 0;
encodeOptions.vp9 = { quantizer: qp };
encoder.encode(frame, encodeOptions);
さまざまな QP を設定して試してみたところ、結果は以下のようになりました。
QP | キャプチャ画像 | 平均ビットレート |
---|---|---|
0 | 35Mbps | |
10 | 4Mbps | |
20 | 2Mbps | |
30 | 1Mbps | |
40 | 600kbps | |
50 | 300kbps | |
60 | 200kbps | |
63 | 150kMbps |
気がついた点と考察
筆者が気がついた点としては、以下のとおりです。
-
QP=0
のビットレートは 720p とは思えないほどに高くなる -
QP=30
くらいになると品質は十分高くなったように見える -
QP=63
はかなり品質がひどいが、QP=60
では大分ましになっているように見える
1点目について、QP=10
と比べて大幅にビットレートが上がってしまった理由を、前述 した内容に沿って考えてみます。おそらく QP が低くなることで、これまで0として扱うことができていた高周波成分を0に丸め込めなくなり、割り当てなければならないビット数が大幅に増加したことが原因だと思われます。
また、3点目については QP の説明 の際にも触れたとおり、 QP は Qstep の対数と比例の関係にあるので、QP が大きくなるにつれて Qstep の変化量が大きくなるためだと考えられます。
QP を推移させた場合の動作
続いて、変動する目標ビットレートに対して QP を推移させた場合の動作を確認します。
QP の導出方法
今回の QP の導出には、TPAC 2023 で紹介された基本的なアルゴリズムを採用しています。
const frames_to_consider = 4;
const frame_budget_bytes = (this.bitrate / this.fps) / 8;
const impact_ratio = [1.0 / 8, 1.0 / 8, 1.0 / 4, 1.0 / 2];
let chunks = this.chunks.slice(-frames_to_consider);
let normalized_chunks_size = 0;
for (let i = 0; i < frames_to_consider; i++)
normalized_chunks_size += chunks[i].byteLength * impact_ratio[i];
const diff_bytes = normalized_chunks_size - frame_budget_bytes;
const diff_ratio = diff_bytes / frame_budget_bytes;
let qp_change = 0;
// Addressing overshoot more aggressively that undershoot
// Don't change QP too much when it's already low, because ti
// changes chunk size to drastically.
if (diff_ratio > 0.6 && qp > 15) {
qp_change = 3;
} else if (diff_ratio > 0.25 && qp > 5) {
qp_change = 2;
} else if (diff_ratio > 0.04) {
//Overshoot by more than 4%
qp_change = 1;
} else if (diff_ratio < (qp < 10 ? -0.10 : -0.04)) {
// Undershoot by more than 4% (or 10% if QP is already low)
qp_change = -1;
}
new_qp = this.clamp_qp(qp + qp_change);
上記では、設定されたビットレートにおける1フレームでのビット数と、直近4フレームのビット数の加重平均との差分から QP の変更値を導出しています。
"constant"
設定との比較
config.bitrateMode
を "constant"
とし、 config.bitrate
を変動させた場合と比較します。
シナリオ1
まずは、目標ビットレートを 1Mbps から 200kps へと急激に下げた場合の実際のビットレートの推移の様子を示します。
【シナリオ1】"quantizer"
でのビットレート推移(左)と "constant"
でのビットレート推移(右)
"quantizer"
では目標ビットレートを下げた際に実際のビットレートも即座に低下し、概ね 200kbps を実現しています。
一方、"constant"
では実際のビットレートは即座に低下し始めていますが、200kbps まで下がりきっていないように見えます。目標が 1Mbps の際はかなり正確に調整できている様子を見ると、低ビットレートのときは正確性に欠ける動作となってしまうようです。
シナリオ2
続いて、目標ビットレートを 1Mbps から 20Mbps へと徐々に増加させた場合の実際のビットレート推移の様子を示します。
【シナリオ2】"quantizer"
でのビットレート推移(左)と "constant"
でのビットレート推移(右)
"quantizer"
では目標ビットレートに沿って実際のビットレートも増加を続けています。QP が低くなった 15〜20Mbps ではビットレートの変動が激しくなり、目標ビットレートからの誤差が大きくなりやすくなっている様子が確認できます。
一方、"constant"
では実際のビットレートの増加が若干遅れている様子と、13Mbps 程度で上限に達している様子が確認できます。
"quantizer"
で確認してみると、13Mbps では QP=3 程度だったため、"constant"
ではそれ以下にならないような制御が入っているようです。
シナリオ2 (AV1)
前述のとおり、ここまでの検証はすべて VP9 で行っていましたが、ふと気になったため同様の確認を AV1 でも行ってみました。
【シナリオ2 (AV1)】"quantizer"
でのビットレート推移(左)と "constant"
でのビットレート推移(右)
AV1 では"constant"
は 3Mbps 程度で上限に達しています。なお、"quantizer"
で確認したところ、3Mbps では QP=10 程度でした。
今回発覚した問題点
シーンの切り替えでのビットレート増加
今回のような固定視点の映像ではなく、 視点が切り替わる Big Buck BUNNY を使用して確認してみたところ、シーンの切り替わりでビットレートがとても大きくなってしまいました。
Big Buck BUNNY でのビットレート推移
これは、現在のアルゴリズムが1パスエンコーディング[6]であり、シーンの切り替わりによるビットレートの急増を考慮できていないためです。対策としては2パスエンコーディング[7]を実装することが挙げられます。
"quantizer"
の追加によって2パスエンコーディングの実装も以前と比べて容易になっているはずなので、機会があればトライしてみたいです。
M1 Mac で AVC 指定時のエラー発生
M1 Mac で AVC を試してみましたが、Unsupported configuration parameters.
というエラーが発生してしまいました。おそらくプラットフォーム依存のバグだと思うので、更新があれば再度確認してみようと思います。
おわりに
今回確認した結果から、フレームごとの QP 設定機能は低ビットレートで非常に精密な制御を実装可能であることを確認できました。また、非常に高品質な映像も生成できました。
これらのユースケースはかなり特殊なものと思われるので、多くの場合は "constant"
設定で事足りるように思います。ただ、以前からこの機能を望む声は一定数あったので、特定のユーザから切望されていたものだと考えられます。具体的なユースケースについては今後も探求したいです。
以上、NTT Communications Advent Calendar 2023 24日目の記事でした。
明日もお楽しみに!
参考文献
映像のエンコード
https://www.vcodex.com/video-compression-patents/
https://github.com/leandromoreira/digital_video_introduction/blob/master/README-ja.md
VP9
https://qiita.com/yohhoy/items/4f96c99f4e872880ea31
AV1
https://arxiv.org/pdf/2008.06091.pdf
-
W3C (World Wide Web Consortium) が Web ブラウザの標準化を行うために年1回開催する総会。 ↩︎
-
WebCodecs のエンコード結果をそのままペイロードに使用する LOC (Low Overhead Media Container) というフォーマットも提案されています。 ↩︎
-
実際には整数精度の直交変換などが採用され、DCT よりも高速に処理されます。 ↩︎
-
量子化はロッシー符号化なので完全な復元はできず、画像の劣化を発生させます。 ↩︎
-
ジグザグスキャン・ハフマン符号化・ランレングス符号化での処理を想定しています。 ↩︎
-
映像の内容を予測しながら圧縮率を決定するための手法です。あまり動きのない映像のエンコードに適しています。 ↩︎
-
事前に映像の内容を解析してから最適な圧縮率を割り当てる手法です。動きが多く、内容の変化が予想しにくい映像のエンコードに適しています。 ↩︎
Discussion