Go言語のMP4ライブラリ abema/go-mp4

# はじめに

本稿では、 ABEMA で開発した Go 言語の MP4 ライブラリを紹介します。

# GitHub

https://github.com/abema/go-mp4

# 経緯

以前の記事 [AbemaTV の MPEG-DASH 対応](https://qiita.com/sunfish-shogi/items/008b3da9309b08465604) で ABEMA （当時の AbemaTV）の自作システムによる MPEG-DASH の配信について書きました。
その中で MP4 の Box を編集して配信に使用している事にも触れています。

もともとは当初必要だった最小限の実装のみでしたが、 MP4 の解析を必要とする機会も段々と増えました。
そこで、 MP4 の読み書き部分をライブラリとして実装を切り出して汎用的に使える形に改修しました。

# MP4 とは

映像を扱う規格（H.264など）や音声を扱う規格（aacなど）はそれぞれにいくつもの種類があります。
しかし、それぞれの規格だけでは映像と音声を同期させて記録・再生することができません。
そこで、 MP4 や MPEG2-TS などのコンテナフォーマットを使って映像と音声を中に詰め込み、タイムコードの情報などを管理します。

ISO/IEC 14496-12 ISO Base Media File において Box (Atom とも言われる) というデータ単位と、それを使ってツリー構造を表現することや各役割ごとの Box の種類などが規定されています。
それを拡張したものが ISO/IEC 14496-14 MP4 file format です。

# MP4 (ISO Base Media File Format) の構造

下図は MP4 の中身を hexdump で出力した例です。
前述の通り Box と呼ばれるデータの集まりで構成されており、各 Box の先頭 4 bytes で Box Size、次の 4 bytes で Box Type が書かれています。
（Box Size が 4 bytes に収まらない場合は最初の 4 bytes に 0x00000001 を書き、 Box Type の次の 8bytes に書き込まれます。また、 Box Size に 0x00000000 が書かれている場合はその Box が EOF まで続くことを意味します。）

<img width="819" alt="Screen Shot 2020-04-28 at 23.15.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78005/b7c5ca7f-4b81-c6c1-2f3f-b8b0625c0f66.png">

moov Box を見ると Size は `0x000004b3 = 1203 bytes` です。
その区間には mvhd Box や trak Box を含んでいるのがわかります。
更に trak Box の中には tkhd Box や mdia Box が含まれています。
つまり、このデータは次の図のようなツリー構造になっています。

<img width="461" alt="Screen Shot 2020-04-28 at 23.15.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78005/656bc234-ad9b-5dbf-0f28-9e6445ff6377.png">

それぞれの Box Type の役割やデータの持ち方、子供に持つことができる Box Type などは規格の中で具体的に書かれています。

# abema/go-mp4 について

## 開発のモチベーション

特定の Box の内容を書き換えたり Box を加えたりする際、ライブラリが無くてもケースによっては比較的簡単に実装ができます。
しかし、深い場所にある Box を読み出したり書き込むには、何度も Seek しながら目的の場所にたどり着き、データサイズが変わったら親の Box Size の場所に Seek で戻り修正しなければなりません。
Go 言語で汎用的に使える充実したライブラリを探していましたが、そういったものは見つけることができませんでした。

また、ツリーを展開して人が見やすい形で表示させたいことがしばしばあります。
そのためのツールはいくつも出回っているのですが、対応していない Box Type があったり、 Box 内の一部のデータしか表示されないものが多い印象です。

そこで独自に作ったプログラムをより汎用的なものに発展させ、これらの要件を満たせる様に開発したものが [abema/go-mp4](https://github.com/abema/go-mp4) です。

## CLI Tool

go-mp4 には CLI Tool が用意してあります。
`mp4tool dump` コマンドを使うと Box のツリーを見ることができます。

```
$ mp4tool dump sample.mp4
[ftyp] Size=32 MajorBrand="isom" MinorVersion=512 CompatibleBrands=[{CompatibleBrand="isom"}, {CompatibleBrand="iso2"}, {CompatibleBrand="avc1"}, {CompatibleBrand="mp41"}]
[free] (unsupported box type) Size=8 Data=[...] (use -a option to show all)
[mdat] Size=6402 Data=[...] (use -mdat option to expand)
[moov] Size=1836 
  [mvhd] Size=108 ... (use -a option to show all)
  [trak] Size=743 
    [tkhd] Size=92 ... (use -a option to show all)
    [edts] Size=36 
      [elst] Size=28 Version=0 Flags=0x000000 EntryCount=1 Entries=[{SegmentDurationV0=1000 MediaTimeV0=2048 MediaRateInteger=1}]
    [mdia] Size=607 
      [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=0 ModificationTimeV0=0 TimescaleV0=10240 DurationV0=10240 Pad=false Language="eng" PreDefined=0
      [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=0 HandlerType="vide" Name="VideoHandle"
              :
              :
```

デフォルトでは長い行が省略表示になりますが `-a` オプションで mdat Box 以外の全ての内容を出力できます。
`-mdat` オプションを付ければ mdat Box の内容も出力されます。

## Library としての活用

次の例の様に ReadBoxStructure 関数で Box を辿っていくことができます。

```go
_, err := mp4.ReadBoxStructure(file, func(h *mp4.ReadHandle) (interface{}, error) {
    fmt.Println("depth", len(h.Path))    // Path にルートからの経路が入ります。
    fmt.Println(h.BoxInfo.Type.String()) // Box Type
    fmt.Println(h.BoxInfo.Size)          // Box Size

    if h.BoxInfo.Type.IsSupported() {
        box, _, _ := h.ReadPayload()     // 各 Box 固有のデータを取得します。
        fmt.Println(mp4.Stringify(box))

        return h.Expand()                // 子供の Box を展開します。
    }
    return nil, nil
})
```

また、次の様に ExtractBox 関数を使って特定の Box を一撃で抽出することができます。

```go
// tkhd Box を列挙します。
path := mp4.BoxPath{mp4.BoxTypeMoov(), mp4.BoxTypeTrak(), mp4.BoxTypeTkhd()}
boxes, err := mp4.ExtractBoxWithPayload(file, nil, path)
```

## リフレクションを使った実装

まだ規格に出てくる全ての Box Type に対応しているわけではありません。
しかし、 Box Type の追加が簡単でなおかつライブラリの外部からも差し込めるのが go-mp4 の特徴です。
例えば tfdt Box の実装は次の様になっています。

```tfdt.go
package mp4

func BoxTypeTfdt() BoxType { return StrToBoxType("tfdt") }

func init() {
  AddBoxDef(&Tfdt{}, 0, 1)
}

// Tfdt is ISOBMFF tfdt box type
type Tfdt struct {
  FullBox                      `mp4:"extend"`
  BaseMediaDecodeTimeV0 uint32 `mp4:"size=32,ver=0"`
  BaseMediaDecodeTimeV1 uint64 `mp4:"size=64,ver=1"`
}

// GetType returns the BoxType
func (*Tfdt) GetType() BoxType {
  return BoxTypeTfdt()
}
```

構造体の定義とわずかな実装があるだけで、ほとんどロジックを持っていません。
tfdt Box はバージョン番号によって BaseMediaDecodeTime のサイズが 32bit か 64bit で変わります。
タグに `mp4:"size=32,ver=0"` などと書くことでそれが表されています。
ほとんどのケースでは個別のロジックを書くことなく、新しい Box の定義を追加できます。

欠点はリフレクションを使って実行時に評価が行われることです。
ただ、多くのアプリケーションにおいてその処理コストは相対的に問題にならないでしょう。
（コードジェネレータを書けばこの点も完全に解消し、他の言語への対応もしやすくなりますがそこまでは手が回っていません。）

# おわりに

独自に開発した Go 言語による MP4 のライブラリを紹介させていただきました。

他にも動画配信関連で Open Source 化したいものがいくつかあります。
限りある時間の中でどれも初めは自社サービス専用に作っている実情があり、すぐにとはいきませんが今後も成果物の共有や技術情報の共有をしていけたらと思っています。

# 参考文献

- [ISO/IEC 14496-12:2015 Coding of audio-visual objects - Part 12: ISO base media file format](https://www.iso.org/standard/68960.html)
- [ISO/IEC 14496-12:2015/AMD 1:2017 Coding of audio-visual objects - Part 12: ISO base media file format - Amendment 1](https://www.iso.org/standard/70430.html)
- [ISO/IEC 14496-12:2015/AMD 2:2018 Coding of audio-visual objects - Part 12: ISO base media file format - Amendment 2](https://www.iso.org/standard/71851.html)