AOYAMA KOJI's PROGRAMMING BLOG

WebCodecsのVideoEncoderでエンコード【mp4ファイル生成解説】プログラマー向け実装解説

2025/05/03
WebCodecsのVideoEncoderでエンコード【mp4ファイル生成解説】

 mp4動画ファイル生成について、プログラマー向けに解説します。 大まかな流れとしては、各フレーム画像をエンコードし、mp4フォーマットに合わせてまとめます。 本記事ではその最初の工程、各フレーム画像をエンコードする処理について、詳述します。
 なお、実際にこれを用いて作られたWebアプリケーションがこの記事などにありますので、 プログラマー以外の方も、ぜひお試しください。
WebCodecsのVideoEncoderでエンコード【mp4ファイル生成解説】

[PR]

mp4動画ファイル生成の工程


 当ブログにおける、mp4動画ファイルを生成する工程は、以下の4ステップです。
 本記事では「1.WebCodecsのVideoEncoderで各フレーム画像をエンコード」を解説します。
  1. WebCodecsのVideoEncoderで各フレーム画像をエンコード ← 本記事で解説
  2. エンコード結果から NAL Unit を取得NAL Unit 取得
  3. NAL Unit 等からボックス形式のバイナリーデータを生成ボックス形式のバイナリーデータ生成
  4. バイナリーデータをファイルに保存バイナリーデータを保存

WebCodecsのVideoEncoderで各フレーム画像をエンコード


 最新Webブラウザーには、WebCodecs API という、動画を扱う方法が用意されています。 本記事では、その中の VideoEncoder を用いて、画像をエンコードする方法を解説します。

コードと補足


 以下に、JavaScriptのプログラムのソースコードを提示します。
 基本的には、コードを見てもらえればわかると思いますが、 補足が必要な箇所は後述します。

呼び出し方法


 プログラムの呼び出し方法は以下です。
  1. initVideoEncoder呼び出し
  2. 以下をフレーム数回ループ
    1. canvasに画像を生成
    2. addFrame呼び出し
  3. finishVideoEncoder呼び出し

 中身を順次解説します。

1.initVideoEncoder呼び出しでVideoEncoder初期化


 まず、initVideoEncoder で VideoEncoder を初期化します。
//*****************************************************************
// initVideoEncoder : ビデオエンコーダーを生成し初期化
//   引数
//     oCanvas .. 対象のcanvasエレメントオブジェクト
//   注意
//     async のため 呼び出し側で await する
//*****************************************************************
async function initVideoEncoder ( oCanvas ) {
  // エラー発生時に取得できるように try ~ catch する
  try {
    // ビデオエンコーダー生成
    oVideoEncoder = new VideoEncoder({
      // エンコード済データの処理
      output: ( oVideoFrame ) => {
        // バイナリーデータ長のバッファを用意
        let aData = new Uint8Array( oVideoFrame.byteLength );
        // データをコピー
        oVideoFrame.copyTo( aData );
        // エンコードデータ配列に追加
        aaFrame.push( aData );
      },
      // エンコードがエラーになったときの処理
      error: (e) => {
        // エラーをコンソールに表示
        console.error( 'Encoder error:' , e );
      }
    });
    // 初期設定
    await oVideoEncoder.configure({
      codec:       'avc1.42E01E',        // コーデック
      width:       oCanvas.width,        // 横幅
      height:      oCanvas.height,       // 縦幅
      bitrate:     5_000_000,            // ビットレート
      framerate:   30,                   // フレームレート(fps)
      avc:         { format:'annexb' },  // annexbフォーマット指定
    });
  }
  // エラー発生時の処理
  catch (e) {
    // エラーメッセージ表示
    alert( 'サポート対象外のWebブラウザーです' );
    // エラー
    throw e;
  }
}

initVideoEncoder


 initVideoEncoder は、VideoEncoder を初期化する、当ブログ自作関数です。
 WebCodecs の VideoEncoder のインスタンスを生成し、configure() で初期化します。 この configure が非同期のため、initVideoEncoder を async 関数として、await で処理完了を待てるようにしています。

output


 output には、エンコードされたデータオブジェクトを受け取る関数を記述します。 引数は VideoFrame オブジェクトです。
 VideoFrameはエンコードされたデータを保持していますが、使用者側でメモリーコピーして保存しておく必要があります。 そのための copyTo メソッドが用意されているので、必要なメモリーを確保して、そこにコピーします。
 そして、後の自作処理で使えるように、フレームごとのデータとして、当ブログ自作変数の aaFrame 配列に push で追加しておきます。

configure設定内容


 configureの設定内容を以下に補足します。

codec

 コーデックは 'avc1.42E01E' としました。
 'avc1' は h.264 コーデックの識別子です。 h.264 は mp4 のコーデックです。
 '42E01E' は、スマートフォンなど処理能力に制限のあるデバイスを想定した設定です。

width,height

 映像サイズはそれぞれ16の倍数にする必要があります。 拡張仕様があるようで、300x300 のサイズで動く環境があることも確認していますが、 基本的な仕様上16ピクセルの倍数と規定されているので、そうしておくのが良いと思います。
 また、環境により、最大サイズに制限があります。 ここでは、手元で確認した範囲で最小の、640 ピクセルを最大としています。

bitrate

 ビットレートは、生成AI(Gemini2)に、このコード生成をお願いしたときに記載されていた数値、そのままにしています。 5_000_000 は 5Mbps です。
 1.5Mbps程度でも良いとは思いますが、自作される場合は調整してください。

framerate

 フレームレートは 30fps をデフォルトとしています。
 なお、この記事などでは、mp4動画以外に、gifアニメーションも生成できます。 その場合、1フレームの長さの単位が 10ms のため、30fps は表現できません。 近い値としてフレーム長 30ms なら 33.33fps になります。

format:'annexb'

 avc の format で annexb を指定します。
 ここは試行錯誤しましたが、明示的に annexb を指定することで、必要な情報が取得できるようになりました。 この詳細は NAL Unit に関するこちらの記事で解説しています。

2.canvas画像をaddFrame呼び出しでエンコード


//*****************************************************************
// addFrame : フレームをエンコードして追加
//   引数
//     oCanvas      .. 対象のcanvasエレメントオブジェクト
//     iElapsedTime .. 動画開始からの経過時間(マイクロ秒)
//   注意
//     async関数
//*****************************************************************
async function addFrame( oCanvas , iElapsedTime ) {
  // ビデオフレームオブジェクトの生成
  let oFrame = new VideoFrame( oCanvas , { timestamp:iElapsedTime , duration:33333 } );
  // エンコード
  await oVideoEncoder.encode( oFrame );
  // ビデオフレーム終了
  oFrame.close();
}

addFrame


 addFrame は、今の canvas の画像をフレームに追加する、当ブログ自作関数です。
 initVideoEncoder で初期化した後に、「canvas に画像を生成して addFrame を呼ぶ」を、動画フレーム数回繰り返す想定で作られています。
 本関数内部では、VideoFrame を生成して、 VideoEncoder.encode を呼び出し、終了を待って close します。

duration


 durationには、このフレームを表示する長さをマイクロ秒で指定します。 30fpsの場合 33333.3333… ですが、整数値ですので、近い値を設定すれば問題ありません。

3.finishVideoEncoder呼び出しで終了


//*****************************************************************
// finishVideoEncoder : 終了処理
//   注意
//     async関数
//*****************************************************************
async function finishVideoEncoder() {
  // 途中の処理があれば強制的に完了させる
  await oVideoEncoder.flush();
  // ビデオエンコーダー終了
  oVideoEncoder.close();
}

finishVideoEncoder


 finishVideoEncoder は、動画エンコード処理を終了する、当ブログ自作関数です。 強制的に完了させ、終わり次第終了します。

aaFrame 配列を次の処理へ


 一連の処理で aaFrame 配列にエンコードデータが入ります。 これを、この記事の、「エンコード結果から NAL Unit を取得する処理」へ渡します。

実践コード


 本記事のコードは、読みやすく整形して開示しています。 実際のコードは、設定変更に対応するなど、より実践的で、視認性は劣ります。
 もし参考にされたい場合は、 この記事など、各mp4クリエイター記事のソースから、import 先を調べてみてください。 プログラムを読める方に限定するため、ここではあえて不親切に紹介させていただきますが、コードの再利用は、悪意の無い範囲でしたら、まったく問題ありません

既存ツールについて


 mp4動画を作成するツールとしては、ffmpegやmp4boxといった、既存のものがあります。 これらを利用しなかった理由は、エンコードにおいて、特許などの権利関係が不明だったからです。
 実際には問題ないかもしれませんが、本記事のように、WebCodecsを用いて、Webブラウザーの機能を利用すれば、 エンコードの権利に関する問題は回避できるので、利用して自作することにしました。 実際、本記事の内容は、ほぼ生成AI(Gemini2)が作ってくれましたので、簡単でした。
 ただし、これ以外の部分が当初想像していたよりかなり多く、大変でした。 それは次回以降の解説に続きます。
[PR]

まとめ


 本記事では、mp4動画ファイルを生成する工程のうち、WebCodecsのVideoEncoderで各フレーム画像をエンコードする部分について解説しました。 残り3ステップも順次解説します。

補足

  • WebCodecs API は比較的新しい技術のため、未対応のWebブラウザーもまだ多く使用されているかもしれません。
  • oVideoEncoder と aaFrame は、実践ではオブジェクトのメンバー変数として定義されていますが、本記事ではグローバル変数と読み替えてください。
  • 画像内のラスタライズ文字フォントにOpen Font LicenseNoto Sans Monoを使用しております。
  • (本記事公開後)本記事公開後に公開された記事へのリンクを追加しております。
  • (本記事公開後追記) マイクロ秒を指定する箇所について、当ブログの実装はミリ秒単位で扱っているため、例えば duration には33333ではなく33000を設定しています。
カテゴリー:プログラミング解説,mp4ファイル生成解説
[PR]