去年書いた以下の記事はmp4/m4aといいつつ、実はファイルの中に動画のストリームが入っているケースを考慮できていない。
izumisy.work
その記事を書いた時点では「どうやらストリームが複数ある場合にmdatは数サンプルごとのチャンクの詰め合わせで構成されているっぽい」ということしか分かっていなかった。軽く調べても簡単にわかりそうな感じでもなかったので、とりあえず音声のストリームだけがmdatに含まれる前提で話を進めることにしていたが、実際的なユースケースではmp4には動画と音声がセットで含まれているケースのほうが多い。
というわけで、そのようなケースでどのようにmdatを読み取ればいいのか改めて調べてみたので備忘録的に残しておく。
サンプルとチャンク
日本語の参考資料としては以下のWikiが最も参考になる。
github.com
これ以外にもstsc, stco, stszあたりの関係性は以下のstackoverflowが参考になった。
stackoverflow.com
これらの記事によると、複数ストリームから構成される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
SamplesPerChunk uint32
}
この二つの情報が手に入れば話は早い。
自分は以下のような感じで Samples
と Chunks
からmdat中のサンプルのオフセットとサイズを計算するロジックを書いた。やっていることはシンプルで、チャンクのカウンタとチャンク内のサンプルのカウンタを加算しながら「いまどのチャンクのどのサンプルを読んでいるか?」をイテレーションして返していくだけの実装。
type FrameIterator struct {
samples mp4.Samples
sampleIndex int
chunks mp4.Chunks
chunkIndex int
chunkSampleIndex int
chunkSampleOffset uint32
}
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,
}
}
github.com
ここまで出来たらば FrameIterator
へ音声のストリームに該当するTrackの値を与えてやるだけでmdat中の音声サンプルのオフセットとサイズを取得できるので、取得された情報をもとにSectionReader
あたりでファイルをシークしつつバイナリデータを取り出してfdkaacでデコードするなりすればいい。