去年書いた以下の記事はmp4/m4aといいつつ、実はファイルの中に動画のストリームが入っているケースを考慮できていない。
その記事を書いた時点では「どうやらストリームが複数ある場合にmdatは数サンプルごとのチャンクの詰め合わせで構成されているっぽい」ということしか分かっていなかった。軽く調べても簡単にわかりそうな感じでもなかったので、とりあえず音声のストリームだけがmdatに含まれる前提で話を進めることにしていたが、実際的なユースケースではmp4には動画と音声がセットで含まれているケースのほうが多い。
というわけで、そのようなケースでどのようにmdatを読み取ればいいのか改めて調べてみたので備忘録的に残しておく。
サンプルとチャンク
日本語の参考資料としては以下のWikiが最も参考になる。
これ以外にもstsc, stco, stszあたりの関係性は以下のstackoverflowが参考になった。
これらの記事によると、複数ストリームから構成されるmp4ファイルのmdatは、複数のサンプルを持つチャンクと呼ばれる単位をひとつの塊としているらしい。
それぞれのストリームはmdat中での自分のストリームに該当するチャンクのオフセットの値を保持しているため、mdatを前から順番にサンプルサイズで舐めていったとしてもオフセット値が正しくなければ、中途半端なサンプルだったり別のストリームのサンプルを取り出してしまうことになる。従って、複数のストリームが存在するmp4ファイルにおいてはストリームのサンプルサイズだけではなくチャンクのオフセットを知ることが重要になる。オフセットが分からなければサンプルサイズが分かっても意味がない。
abema/go-mp4を使う例
abema/go-mp4は実は複数ストリームが存在しているケースを扱うのに非常に便利で、パッケージから提供されるTrack
という構造体を見るだけで大体のことは完結させられる。
type Track struct { TrackID uint32 Timescale uint32 Duration uint64 Codec Codec Encrypted bool EditList EditList Samples Samples Chunks Chunks AVC *AVCDecConfigInfo MP4A *MP4AInfo }
ここにあるChunksというのが、まさにmdatにおけるストリームのチャンク情報そのものである。
type Chunks []*Chunk type Chunk struct { DataOffset uint32 // mdatにおけるchunkのオフセット SamplesPerChunk uint32 // chunkに含まれるサンプル数 }
この二つの情報が手に入れば話は早い。
自分は以下のような感じで Samples
と Chunks
からmdat中のサンプルのオフセットとサイズを計算するロジックを書いた。やっていることはシンプルで、チャンクのカウンタとチャンク内のサンプルのカウンタを加算しながら「いまどのチャンクのどのサンプルを読んでいるか?」をイテレーションして返していくだけの実装。
type FrameIterator struct { samples mp4.Samples // track.Samples sampleIndex int // 0 chunks mp4.Chunks // track.Chunks chunkIndex int // 0 chunkSampleIndex int // 0 chunkSampleOffset uint32 // track.Chunks[0].DataOffset } type Frame struct { Offset uint32 Size uint32 } // イテレーションしてフレームの情報を返すメソッド func (v *FrameIterator) Next() *Frame { if len(v.samples) <= v.sampleIndex || len(v.chunks) <= v.chunkIndex { return nil } offset := v.chunkSampleOffset size := v.samples[v.sampleIndex].Size currentChunk := v.chunks[v.chunkIndex] if currentChunk.SamplesPerChunk <= uint32(v.chunkSampleIndex+1) { if len(v.chunks) <= v.chunkIndex+1 { return nil } v.chunkIndex++ v.chunkSampleIndex = 0 v.chunkSampleOffset = v.chunks[v.chunkIndex].DataOffset } else { v.chunkSampleIndex++ v.chunkSampleOffset += v.samples[v.sampleIndex].Size } v.sampleIndex++ return &Frame{ Offset: offset, Size: size, } }
ここまで出来たらば FrameIterator
へ音声のストリームに該当するTrackの値を与えてやるだけでmdat中の音声サンプルのオフセットとサイズを取得できるので、取得された情報をもとにSectionReader
あたりでファイルをシークしつつバイナリデータを取り出してfdkaacでデコードするなりすればいい。