Runner in the High

技術のことをかくこころみ

abema/go-mp4で複数ストリームが含まれるmdatから音声のサンプルのみを取り出す

去年書いた以下の記事は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 // mdatにおけるchunkのオフセット
    SamplesPerChunk uint32 // chunkに含まれるサンプル数
}

この二つの情報が手に入れば話は早い。

自分は以下のような感じで SamplesChunks から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,
    }
}

github.com

ここまで出来たらば FrameIterator へ音声のストリームに該当するTrackの値を与えてやるだけでmdat中の音声サンプルのオフセットとサイズを取得できるので、取得された情報をもとにSectionReaderあたりでファイルをシークしつつバイナリデータを取り出してfdkaacでデコードするなりすればいい。