MediaRecorderAPIで作ったwebmをseekableにしたい
ブラウザ上で録画 のつづき。
これまでのあらすじ
先駆者がいるのは知ってる
https://qiita.com/legokichi/items/83871e1f034331222fd2
https://github.com/legokichi/ts-ebml
知ってるが、自分でやりたくなるやつ
最低限の実装をブックマークレットにそのまま埋め込めたらよくない?
webm
https://qiita.com/ryiwamoto/items/0ff451da6ab76b4f4064 が入口になった
EBMLのSpec https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown
プリミティブ型、VINT、不定長要素の終端ルール、EBMLHeader等の定義、編集時にpositionを変えない工夫など
Matroska https://www.matroska.org/technical/basics.html
構造図解 https://www.matroska.org/technical/diagram.html
Header + Segment > {Info+, SeekHead*, Cluster*, Cues?, Tags*, ...}
Streaming https://www.matroska.org/technical/streaming.html
不定長Segment(sizeを全ビット立てる)にするか、固定長のSegmentを連続させる
不定長の表現 https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown#unknown-data-size
固定長Segmentの連続はHeader入れるのだろうか?(EBML Stream)
https://www.matroska.org/technical/diagram.html https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown#ebml-stream
MetaSeekもCuesもなければnon-seekableと判断すべき → MediaRecorderが生成するwebmがこれ
WebMのmuxerガイドライン https://www.webmproject.org/docs/container/#muxer-guidelines
ここでいうshouldはmustとしてmuxerは取り扱うべき(SHOULD)、という温度感
Cues要素の存在がわかるようにSeekHeadを入れる
SeekHeadはトップレベル要素の目次。これがないとファイル全体からトップレベル要素を探すことになる。
SeekHeadを置く場合、Segmentの先頭に置く。SeekHeadからSeekHeadを参照して連結することもできるが、いずれにせよ全てのトップレベル要素をSeekHeadに載せなければならない。
https://www.matroska.org/technical/ordering.html では、CuesをCluster群より先に置く or SeekHeadで参照すべき、とある。
Cues要素をCluster群より前に入れる(それはそうだ)
動画のキーフレームのみを含める(サイズ節約)
キーフレームはClusterの先頭に置く(Clusterの中から目的のBlockを探すことになるからかな?CueRelativePositionをつける手もありそうだけど)
これ元々はClusterの順序保証もないのかそういえば(webmではTimestampが単調増加することを要求しているが)
demuxerガイドライン https://www.webmproject.org/docs/container/#demuxer-guidelines
Seeking will be disabled if the webm file does not have a key frame Cues element.
便利ツール https://www.matroska.org/downloads/mkvtoolnix.html
mkvtoolnix-gui がつよい。多分mkvinfoでもいいけど、こういうのはツリーを展開したりできるGUIつよい。
特定の要素のバイト列も確認できるので、デバッグにもってこい
各種出力
MediaRecorder
基本的にはChromeもFirefoxも Segment(inf) > (Info + Tracks + Cluster(inf)*) という構造
キーフレームはClusterの先頭に存在する
Firefoxは空のSeekHeadが存在する
分割された場合、Chromeは要素ヘッダの途中だろうとぶった切ってくるが、FirefoxはClusterの切れ目に揃えてるっぽい
ts-ebml: Duration, 先頭Cues, SeekHead(Info,Tracks,Cues,0)
mkclean: Duration, 先頭Cues, SeekHead(Info,Tracks,Cues,Cluster,201)
ffmpeg: Duration, 末尾Cues, SeekHead(Info,Tracks,Tags,Cues,226)
mkvtoolbox(mkvmerge): Duration, 末尾Cues, SeekHead(Info,Tracks,Cues,4096)
実験
チャンクが分割された場合、ちょうどいいところで切られるのか
A. 切られるとは限らない
https://www.w3.org/TR/media-source/#sourcebuffer-segment-parser-loop も、完全な media segment が得られていない状態を想定しているようだし(取りこぼしはどうしようもないのだろうか…?)
例の記事だとSeekHeadを追加してるだけだが、Cuesの方がよいのでは?
https://w3c.github.io/mse-byte-stream-format-webm/#webm-random-access-points を見ると、Cueは見ずにキーフレームであるSimpleBlock等だけランダムアクセスに使われる説も…?
The user agent must accept and ignore Cues or Chapters elements that follow a Cluster element. というのも気になる
ts-ebml、実行してみたら普通にCues書いてた
実験結果
※Range requests に対応していないとキューポイント知っててもシークできない
Win10の動画アプリ
Durationがない(Cuesに関わらず): バーが伸びない
Cuesがない/SeekHeadなしの末尾Cue: シークできない
Chrome
Durationがない(Cuesに関わらず): 動画長なし、バーが伸びない
最後まで読みこむとシーク可能になる
積極的な先読みはないらしく、20秒程度の動画でスロットルなしでもほぼ最後まで再生しないとシーク可能にならなかった
それまでは手前にシークすることもできない
Cuesなし/SeekHeadなし末尾Cue: クリックはできるが読み込みを待つ
先頭Cue/SeekHead+末尾Cue: どこでもシーク可能(末尾Cueの場合Cueを取りに行く)
Firefox
Durationなし(Cuesに関わらず): 読みこめた分動画長が伸びていく。先読みあり。
Cuesなし/SeekHeadなし末尾Cue: 読み込めたところまでシーク可能。先読みあり。
先頭Cue/SeekHead+末尾Cue: どこでもシーク可能(末尾Cueの場合Cueを取りに行く)
進捗
Denoでパーサ書いてみた https://gist.github.com/unarist/bf2d9d9960d900107cb27e821242be0d
DataViewでさぼれるかと思ったけど結局微妙な長さの数値読んだりできないのであれ
EBMLSchemaから定義持ってきてやるのはできたのでよき
Matroska https://github.com/ietf-wg-cellar/matroska-specification/blob/master/ebml_matroska.xml
EBML側で定義されているものはSchemaないのでSpecから手でおこした
Deno本体にはDOMParserがなかったのと、 https://deno.land/x/deno_dom はXMLに対応していなかった、が、HTMLとして読めたのでとりあえずこれで
キーフレームの判別にはEBMLに加えて SimpleBlock Structure のパースも必要 https://www.matroska.org/technical/basics.html#simpleblock-structure
2021/2/8 実験用に↑のやつで一部要素のVoid化や不定長化をできるようにしてみた
5バイト以上のreadUintが盛大にバグってたのも直した(bitwise operator が32bit想定とは…)