やりたいこと
認証がかかったストレージのAPIからJavaScriptで動画データを取得してvideoタグで再生したかった.
FetchAPIやXHRでMP4ファイルにアクセスはできるので,これをブラウザ上で再生したいだけだけなのだけど...
- 普通にvideoタグのsrcにAPIのURLを突っ込む → リクエストヘッダ等をセットできなくて認証できない
- FetchAPIでデータを読んでblobを再生する → ファイル全体を読まないと再生開始できない(動画は数百MBある) (参考記事)
- MPEG-DASH等のFragmented MP4を読み込んでMSEで再生する → 事前にサーバ側に置くデータを変換しないと再生できない (参考記事)
意外とめんどう.
JavaScriptでFragmented MP4を作る
+------------+ +-----------+ +----------------+ +-------------+ +-----------+
| 動画データ | → | Fetch API | → | ReadableStream | → (なんか良い感じの変換) → | MediaSource | → | videoタグ |
+------------+ +-----------+ +----------------+ +-------------+ +-----------+
この(なんか良い感じの変換)
の部分が欲しい.
MediaSourceは通常のMP4ではなくて,Fragmented MP4を入れる必要がある.
良さげなライブラリが無いか探しているけど,https://github.com/gpac/mp4box.js くらいしか見当たらない.(探し足りないのかもしれないけど)
やりたいことに対してライブラリが大きすぎるのと,ダウンロードしたデータを入れるとsegmentができたときにコールバックが呼ばれる感じなので,必要な量だけStreamから読むという挙動にしにくい.
Fragmentedが付いても所詮はMP4なので簡単に変換できるだろうと思って,とりあえずライブラリ使わず再生してみた.
とりあえず動いたやつ: https://github.com/binzume/mp4player-js
再生するだけで意外と面倒くさかったので,必要だった作業をメモ.
変換方法のメモ
https://www.w3.org/TR/mse-byte-stream-format-isobmff/ この辺の説明と実際のMP4のデータを見ながら適当に変換していけばよい.
MP4とかISOBMFFのドキュメントは大きくて真面目に把握するのはめんどいので,なるべく元のMP4のBox構造を触らずに書き換えが必要なBoxだけ触ることに.
最初,比較用にffmpegで生成したfragmentを見てたけど,どういうわけかffmpegで生成したやつはChromeのMediaSourceで読めなかった.
以下はMP4の構造は知っている前提の記述です.
initialization Segment
最初にinitialization Segmentを一度だけ渡す必要がある.内容は以下のような感じ.
- ftyp
- moov
- moovの下はMP4からコピーして少し修正
- mvex
- trex
元がMP4なら,ftyp + moovのうち各tracのstbl下のboxのうちstsd以外を空にして,mvexを追加すればよいだけ.
trexにはトラック番号だけ書いておけば良さそう.
media segment
普通のmp4ならmoovに入っていた情報を複数のmoofに分割して格納する必要がある.
moov → moof,trac → traf等対応しているBoxは名前でわかりやすいが,中身の格納方法は別物なので変換は地味に面倒.
- moof
- mfhd
- traf
- tfhd
- tfdt
- trun
- mdat
生成しなければいけない主なBox
trakとかmoofとか単に他のboxを持つだけの親は省く.
trex
基本的に各fragmentのtfhdに入れれば良いので少しでもバイト数削りたい場合以外は気にしなくて良さそう.
ただボックス自体は省略できないので,トラック番号だけ入れておく.
fthd,ftdt,trun
trunがサンプルの在り処を指す.stco,stsc,stsz,stts,stss,cttsから生成できる.
trunが持てる情報色々あって難しく感じるけど,生成するときは必要なやつ以外は書き込まなければよいので割とシンプル.
mdat
元のMP4のmdatから必要な範囲をコピーしてくる.どのサンプルがどの場所にあるかはstco,stsc,stszを見る.
MediaSource.addSourceBuffer() に渡す mimeType
video/mp4; codecs="avc1.42e01e,mp4a.40.2"
みたいな文字列を渡すのだけどど,Chromeとかだとcodecsの指定が必須.
(多くのサンプルコードで特に説明無くハードコードされてて,残念な雰囲気がある)
MP4のstsd boxからconfiguration recordを取り出して中身を見る必要がある.avc1
とかmp4a
のコーデック名はstsdの先頭あたりにそのまま入ってるが,後ろの数字はコーデックごとのパラメータを見ないといけない.
手抜き実装をするなら,h264のパラメータはavcCの後にある固定位置の3バイトを16進にして,aacは40.2固定にしてもほとんどの場合は良さそう.
雑感
- https://github.com/gpac/mp4box.js とかあるけど,単純に再生するのに使いやすそうなのが見当たらない
- ちゃんと動くの作るのは色々めんどう
- バイト列の扱いだるいのでwasmでやるのが良さそう
- MediaSourceとReadableStream,変なことしなくても普通につながってほしい