WebCodecs + MP4Box.js を用いたブラウザ単体での MP4 生成
1. Codecs と Containerの役割
| 観点 | Codec | Container |
|---|---|---|
| 役割 | 画や音を圧縮・復元する | 圧縮済みデータを時刻情報つきで格納する |
| 具体例 | H.264 / AVC, AAC, AV1 | MP4, WebM, Matroska |
| このデモでの担当 | VideoEncoder が H.264 の EncodedVideoChunk を出力 | MP4Box.js が MP4 のトラックとサンプル表を構築 |
| 入れ替え可能性 | 同じ MP4 に別 codec を入れることがある | 同じ H.264 を MP4 以外の入れ物に入れることもある |
「MP4 を生成する」=「圧縮済みの映像データを作る」+「再生できる MP4 に組み立てる」の二段階になる。
2. MP4 コンテナの骨格
MP4 は ISO Base Media File Format 系のファイルで、実体は box の並びでできている。
先頭から size と type を読んでいく構造で、よく出てくる
ftyp, moov, mdat もその box の名前である。
2-1. MP4コンテナの構造
ツリー構造にすると以下のようになる(今回は映像トラック 1 本だけ)。
mp4
├─ ftyp
├─ moov
│ └─ trak
│ └─ mdia
│ └─ minf
│ └─ stbl
│ ├─ stsd
│ ├─ stts
│ ├─ stss
│ ├─ stsc
│ ├─ stsz
│ └─ stco / co64
└─ mdat
2-2. boxに含まれる要素
| box | 役割 | 今回の見方 |
|---|---|---|
ftyp | ファイル種別と互換ブランドを書く | MP4 系ファイルであることを示す |
moov | 全体のメタデータを持つ | トラック情報や sample table の親になる |
trak | 各トラックの入れ物 | 今回は映像トラック 1 本だけ |
mdia | トラック内のメディア情報 | vide ハンドラの映像トラックになる |
minf | メディア種別ごとの情報 | 映像なら vmhd などが入る |
stbl | サンプル表の集合 | 時間、サイズ、同期点を持つ |
mdat | 圧縮済みメディアデータ本体 | EncodedVideoChunk のバイト列がここへ入る |
2-3. 映像トラックで重要になる sample table
再生側に必要なのは映像データ本体だけではない。
どのサンプルが何バイトで、どれだけの時間続き、どこがキーフレームかという表も要る。
そのため stbl の下にいくつかの table がぶら下がる。
| box | 役割 | 実装上の見方 |
|---|---|---|
stsd | サンプル記述。どの codec でどう解釈するかを書く | avc1 がここにぶら下がる |
stts | decoding time to sample | 各サンプルの継続時間を表す |
ctts | composition time to sample | B-frame を使うと表示順との差分が入る |
stss | sync sample table | キーフレーム一覧になる |
stsc | sample to chunk | サンプルと chunk の対応を持つ |
stsz | sample size table | 各サンプルのバイト数を記録する |
stco / co64 | chunk offset | mdat 内の位置を指す |
2-4. H.264 を MP4 に入れるときの avc1
MP4RA の codec 登録でいう avc1 は H.264/AVC の sample entry を表す。
つまり「このトラックのサンプルは H.264 として解釈する」という宣言にあたる。
このページでは details よりも、H.264 のサンプル列を MP4 の sample table と結びつける流れに集中する。
3. WebCodecs が返すものと MP4 に必要なもの
WebCodecs の output callback は EncodedVideoChunk と、
必要に応じて EncodedVideoChunkMetadata を返す。
デモで重要なのは、各 chunk のサイズ・時刻・キーフレームかどうかを MP4 の sample table に写すことだ。
| WebCodecs 側の値 | 意味 | MP4 側での使い道 |
|---|---|---|
chunk.byteLength | 圧縮済みサンプルのサイズ | stsz 相当の情報になる |
chunk.timestamp | 提示時刻 | dts / cts の計算材料になる |
chunk.type | "key" / "delta" | stss に反映される |
3-1. AVC の annexb と avc
AVC では出力形式に annexb と avc がある。
MP4 に詰めるなら、最初から avc を選んでおくほうが扱いやすい。
| 形式 | パラメータセットの置き場 | 向いている用途 | このデモでの扱い |
|---|---|---|---|
annexb | ビットストリーム内に SPS / PPS を周期的に含める | ライブ配信やストリーム途中参加 | 使わない |
avc | サンプル列とは別に初期化情報を扱いやすい | MP4 のように初期化情報を別に保持する形式 | 採用する |
3-2. キーフレームが重要な理由
キーフレームは「そのフレーム単体を起点に復号を再開できる場所」なので、シークやサムネイル生成の起点になる。
逆に delta フレームは前のフレーム群を参照するため、そこだけ切り出しても正しく復号できないことがある。
このデモでは 1 秒ごとにキーフレームを打ち、chunk.type === "key" のサンプルを MP4 の同期サンプルとして登録している。
4. 実装方針
- 各フレームを
VideoFrameとして作る。 VideoEncoderを H.264 で構成し、avc: { format: "avc" }を指定する。- 最初の output callback でトラックを初期化する。
- 各
EncodedVideoChunkをaddSample()で MP4 に積む。キーフレームならis_syncを立てる。 - 最後に
save()でBlob化し、そのままブラウザで再生・保存する。
// エンコーダーの構成
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
// 最初の chunk が来たタイミングで MP4 の映像トラックを作る
if (trackId === null) {
trackId = mp4File.addTrack({
timescale: 1000,
width,
height,
hdlr: "vide",
type: "avc1",
avcDecoderConfigRecord: getAvcDecoderConfigRecord(metadata),
});
}
// chunk.type が key なら MP4 の sync sample として扱う
mp4File.addSample(trackId, chunkBytes, {
duration,
dts,
cts: dts,
is_sync: chunk.type === "key",
});
},
});
encoder.configure({
codec: "avc1.42001f",
width,
height,
bitrate,
framerate: fps,
avc: { format: "avc" },
});
5. 動作デモ
下のデモでは、Canvas に描いたフレームをそのまま H.264 にして MP4 に詰める。
生成後は <video> で再生でき、サイズ、サンプル数、キーフレーム数を確認できる。
6. 補足
6-1. timescale と duration
MP4 の時刻は整数で持つので、実装側で timescale を決めて
dts, cts, duration をその単位で積むことになる。
ここでは計算を見やすくするため timescale = 1000 としている。
1 フレームぶんの長さは整数 timescale に丸めながら積まれるので、
30 fps のような値では端数を含む継続時間が混ざることがある。
6-2. 実装時の注意
VideoEncoderが見えていても、その codec string や profile で encode できるとは限らない。先にVideoEncoder.isConfigSupported()を通しておく。