brly.github.io
codec
28 Nov 2018

続 Javascriptでライブカメラっぽいアプリケーションを作ったメモ

https://qiita.com/brly__/items/25cd423c348cbc302b4a

は書かれてからそれなりに時間の経った記事にもかかわらず、いまだに いいね・ブックマークされるのですが、その度になんとなく申し訳ない気持ちになるので続きを書きます。

というのも今は諸事情があって、多少なり動画圧縮・動画配信に関する一定の知識が付いたこともあり、 上記の記事だけで満足してほしくない・もう少しマシな記事をかけるぞ、という自負があるので続きを書きます。

もともとの話を簡単にまとめてみると

こんな感じでしょうか。この記事が褒められることがあるとすれば、「動画の作り方」くらいでしょう。 参考にさせて頂いてる丁寧な記事へのリンクなどを用意し、動画とはパラパラ漫画のようなもの、であることを説明している点ですかね。 それ以外は大したことない気がします。すいません。単純に、この記事を書いた時に知識が無かっただけの話なのですが。

しかも javascript と題している割にはコードも一切出てきませんし…。 配信するにあたって Web カメラ持ちパソコンで「ブラウザアクセス」するだけ、ってのは手軽で良かったですかね。

圧縮効率がわるい

自分が使っていた時も趣味プロジェクトの範囲でしたし、上記の記事を読んで万が一にも商用で検討している人はいないと思いますが、 とにかく説明していたやり方は圧縮効率は悪いです。画像の伝送に多大な帯域を使用します。携帯回線ならあっという間に上限に到達してしまうかも知れません。

本文からやり方を読み取ってみると

とあり、 Uint8Array と宣言しているのは大きさが 3 となる、それぞれ RGB を示す Uint8Array とするとします。

この時のかすかな記憶として、「差分フレームがたいしてサイズが縮まなかったな…」という記憶があります。 対して、toDataURL の方は完全フレームながらも「以外と圧縮されるな…」という感じでした。 これは、完全フレームが生データではなく何らかの圧縮が効いているものと考えられるのですが、すいません、仕組みはわかりません。

ただ少なくともピクセルあたり 3ch/8bit = 3byte のサイズではなかったと認識しています。 一方、差分フレームの方は差分ピクセル毎にピクセルのインデックスと差分ピクセル値の3つの値を保持するようにしていた記憶があります。 差分ピクセル値の値域は 8bit なので大きさとしては 1ch あたり 1byte で十分ですが、 画面上のどこの差分かを示すインデックスの情報については 0~255 では足りない可能性があります。もしかしたら 16, 32 bit あたりを使っていたかも知れません。 ここでは 32 bit だったと仮定しましょう。

すると、1ピクセルあたりの差分を表示するために必要なデータ長は 32 + 32 + 8 * 3 bit = 88 bit = 11 byte にもなります。 8bit/3ch の 1px が 24bit = 3byte かかるのに対して、インデックスとやらで 8byte もかかっています。もし、画面全体の差分情報が必要だとすると、生データと比較しても 3 倍近くの容量が必要になります。これは厳しいです。

じゃあどうやるのが圧縮率がいいのか、普段見ている動画はどうやって圧縮されているのか気になるところです。

身の回りの圧縮コーデック

よく聞くのは H.264 とか H.265 とか AV1 とか、になるでしょうか。

これらの実際の処理をちょっと見てみるとわかるのは、上で書いたみたいに 1px ずつ処理されることはない、ということです。 H.264 を例に出すと、例えば基本の処理単位は 16x16 ピクセルのグリッドとしていて、その中をさらに分割したりして最小では 4x4 を最小の処理単位としています。 どういうことかというと、1 px 毎に処理するのでなく 4x4 とか 16x16 毎に処理して、処理単位にインデックスのような情報を付随させるほうが結果として処理ビットを節約できるのです。

例えば、縦 1px で 横 4px のピクセル群があったとして、1フレーム後に右に 5px 動いたとしましょう。

1フレーム目「□□□□■■■■■」
2フレーム目「■■■■■□□□□」

移動した部分を表現するためには、先程のやり方だと 44 byte (11byte * 4px) とか、かかるわけですが、そうではなく「□□□□」の 1x4 が全体として右に 5px 動いた、という風に記述できると 44 byte もかからずできそうですよね。なんとなくですけど。

さて、これをやるためにはこの 4x4 などの「処理単位」ごとにどういう風に動いたのかな、と推定する必要があるわけですが、 この推定処理を「動き推定」、で推定したピクセルで補完することは 動き補償 とか呼ばれていて、 推定をするには実際に総当たりとか、それの近似計算とか、いろいろ組み合わせてエンコーダが頑張っています。

これが動画エンコード (ソフトウェア) の計算が重たい理由のうちの一つです。

これだけでもだいぶビットをケチれそうなのですが、さらにもう一つ頑張っているのが DCT です。

https://people.xiph.org/~xiphmont/demo/daala/demo1.shtml

上記のデモページの中断にある「DCT」の部分がわかりやすくておすすめです。 DCT することでかなり情報が削れる (ほとんどが 0 になっている) のでほんまか?という感じですが…。 DCT は情報を周波数成分に変換する、と言われてます。ピクセルの平均的な色は「低周波」へ、そうでなくまばらな色なんかは「高周波」成分に変換されます(ざっくりとした説明…)。

この DCT をどこに適用するのかというと、先程の文脈で言うところの「差分ピクセル」に使っていきます。 最小処理単位はもはや 1px ではなく 4x4 とかになっているので、差分ピクセルとしての情報もそれだけの大きさ(行列みたいなもの)になっているのですが、そこに DCT を適用します。 うまくいけば、デモページのように数字は行列の左上に集中してそれ以外は 0 となり、伝送すべき情報が減らせることでしょう。

それで、動き補償のことは MC とか呼ばれているんですが、身の回りのおおよそのコーデックはこの MC と DCT をベースとした仕組みに乗っかった形になっています。 じゃあ新しいコーデックは何を頑張っているのかというと、扱える処理単位の種類を増やしたり (16x16, 4x4 だけでなく 32x32 とか 64x64 など)、 DCT をした後に、情報を欠落させる、不可逆圧縮となる「量子化」の処理を適用したり、さらに算術符号などを利用したエントロピー符号化を用いたりして 1 bit でも削ろうと頑張っています。

処理単位が増えて、例えば 16x16 を4つ使って伝送していたものが 32x32 の1つで済むようになると、処理単位にかかるオーバーヘッド分の ビットがケチれそうだな、というのは直感的に理解できると思います。 ただ、その分エンコーダは利用するすべてのパターンのエンコードを試して良いものを選ぶというプロセスを取る必要があるため、 必然的にエンコード処理が重たくなります。

仕組みはなんとなくわかった、さて

ざっくりここまで読むと、普通のコーデックは MC/DCT のおかげで最初に述べたような 1px ごとの処理より圧縮できるんだな、ということはわかりました。

じゃあ、最初にあげていた javascript で作っていたの仕組みのところになんとか入れるプランを検討してみましょう。 ところで、実際にエンコーダのライブラリを書くのは結構たいへんです。たとえば pdf 版に限り H.264 は規格書が無料で読めますが、実装は大変なので試すなら すでに広く実装されている OSS なりを使うのが楽です。ffmpeg はたくさんのコーデックを内包していて、便利なやつですね。

さて、元々の仕組みだと、web カメラにアクセスするブラウザ内部で差分フレームの情報を計算させていました。 これは単純ではありますが、エンコーダの仕事をしていたと言っていいでしょう。

これは、現段階ではそんなにいいやり方でない気がします。というのも、動画エンコードというタスクはとても CPU 資源的に高価なタスクなので、 javascript の上で動かすにはいささか富豪的に見えます。Web アプリケーションが好きだったり、Web Assembly を推している皆様には申し訳ないですが、 それでもこの意見は変わりません。もうちょっと未来になって Web Assembly がより高速になったら現実的なプランとなるかも知れません。

そこで、もし、なるべく簡単に、いわゆる普通のコーデックを使うように仕組みを変更するならば、以下のように変更します。

もしくは、上で否定していたブラウザエンコードをやる方法もあります。C/C++ のコードを emscripten などで js に変換して、ffmpeg をブラウザ上で動かすようにして、 backend.example.com に rtmp を受け付けて変換して配信する ffmpeg を起動し、配信者側からはその rtmp を受け付けているところに向けてブラウザ上の ffmpeg で Web カメラを入力として送信するなど… いろいろやり方はあります。

これらはあくまで趣味プロジェクトレベル、のやり方の話です。

まぁ、でも

現実のコーデックを使うと、とても圧縮効率を高められますが、「めんどくさ」「2,3秒に1回画像が更新できれば問題ないよ」という人は Qiita の記事のままのやり方で大丈夫でしょう。

ただ、どうやったら圧縮効率が高まるのか、どうやって圧縮されているのか知っておくのは良い事だと思います。