WebCodecs による動画処理

動画ストリーム コンポーネントの操作。

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

最新のウェブ技術では、動画を操作する方法が豊富に用意されています。 Media Stream APIMedia Recording APIMedia Source API、 およびWebRTC APIを組み合わせることで、 動画ストリームの録画、転送、再生のための豊富なツールセットが実現します。 これらの API は、特定の高度なタスクを解決する一方で、ウェブ プログラマーが動画ストリームの個々のコンポーネント(フレームや、エンコードされた動画や音声の非多重化されたチャンクなど)を操作することはできません。 これらの基本コンポーネントに低レベルでアクセスするために、デベロッパーは WebAssembly を使用して動画コーデックと音声コーデックをブラウザに組み込んでいました。しかし、最新のブラウザにはすでにさまざまなコーデックが搭載されており(多くの場合、ハードウェア アクセラレーションが適用されています)、それらを WebAssembly として再パッケージ化するのは、人的リソースとコンピューター リソースの無駄のように思えます。

WebCodecs API は、ブラウザにすでに存在するメディア コンポーネントをプログラマーが使用できるようにすることで、この非効率性を解消します。具体的には次のとおりです。

  • 動画デコーダーと音声デコーダー
  • 動画エンコーダと音声エンコーダ
  • 未加工の動画フレーム
  • 画像デコーダー

WebCodecs API は、動画エディタ、ビデオ会議、動画ストリーミングなど、メディア コンテンツの処理方法を完全に制御する必要があるウェブ アプリケーションに役立ちます。

動画処理のワークフロー

フレームは動画処理の中心です。そのため、WebCodecs のほとんどのクラスはフレームを使用するか、フレームを生成します。動画エンコーダはフレームをエンコードされたチャンクに変換します。動画デコーダはその逆を行います。

また、VideoFrame は他のウェブ API とうまく連携します。これは、CanvasImageSource であり、CanvasImageSource を受け取る コンストラクタ を備えているためです。 そのため、drawImage()texImage2D() などの関数で使用できます。また、キャンバス、ビットマップ、動画要素、その他の動画フレームから構築することもできます。

WebCodecs API は、WebCodecs を メディア ストリーム トラックに接続する Insertable Streams API のクラスと連携して動作します。

  • MediaStreamTrackProcessor は、メディア トラックを個々のフレームに分割します。
  • MediaStreamTrackGenerator は、フレームのストリームからメディア トラックを作成します。

WebCodecs とウェブワーカー

WebCodecs API は、設計上、メインスレッドから離れて非同期で処理を行います。 ただし、フレームとチャンクのコールバックは 1 秒間に複数回呼び出されることが多いため、メインスレッドが煩雑になり、ウェブサイトの応答性が低下する可能性があります。 そのため、個々のフレームとエンコードされたチャンクの処理をウェブワーカーに移動することをおすすめします。

ReadableStreamたとえば、MediaStreamTrackProcessor を使用して、ウェブカメラから送信されるメディア ストリーム トラックの ReadableStream を取得できます。その後、ストリームはウェブワーカーに転送され、フレームが 1 つずつ読み取られて VideoEncoder にキューイングされます。

HTMLCanvasElement.transferControlToOffscreen を使用すると、レンダリングをメインスレッドから離れて行うこともできます。ただし、高レベルのツールがすべて不便な場合は、VideoFrame自体を転送して、ワーカー間で移動できます。

WebCodecs の活用例

エンコード

Canvas または ImageBitmap からネットワークまたはストレージへのパス
からネットワークまたはストレージへのパス
CanvasImageBitmap

すべては VideoFrame から始まります。 動画フレームを構築する方法は 3 つあります。

  • キャンバス、画像ビットマップ、動画要素などの画像ソースから。

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • MediaStreamTrackProcessor を使用して MediaStreamTrack からフレームをプルする

    const stream = await navigator.mediaDevices.getUserMedia({});
    const track = stream.getTracks()[0];
    
    const trackProcessor = new MediaStreamTrackProcessor(track);
    
    const reader = trackProcessor.readable.getReader();
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      const frameFromCamera = result.value;
    }
    
  • BufferSource のバイナリ ピクセル表現からフレームを作成する

    const pixelSize = 4;
    const init = {
      timestamp: 0,
      codedWidth: 320,
      codedHeight: 200,
      format: "RGBA",
    };
    const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
    for (let x = 0; x < init.codedWidth; x++) {
      for (let y = 0; y < init.codedHeight; y++) {
        const offset = (y * init.codedWidth + x) * pixelSize;
        data[offset] = 0x7f;      // Red
        data[offset + 1] = 0xff;  // Green
        data[offset + 2] = 0xd4;  // Blue
        data[offset + 3] = 0x0ff; // Alpha
      }
    }
    const frame = new VideoFrame(data, init);
    

フレームは、どこから取得されたものであっても、VideoEncoder を使用して EncodedVideoChunk オブジェクトにエンコードできます。

エンコードする前に、VideoEncoder に次の 2 つの JavaScript オブジェクトを指定する必要があります。

  • エンコードされたチャンクとエラーを処理する 2 つの関数を含む初期化ディクショナリ。これらの関数はデベロッパーが定義するものであり、VideoEncoder コンストラクタに渡された後は変更できません。
  • 出力動画ストリームのパラメータを含むエンコーダ構成オブジェクト。これらのパラメータは、後で configure() を呼び出して変更できます。

ブラウザで構成がサポートされていない場合、configure() メソッドは NotSupportedError をスローします。構成がサポートされているかどうかを事前に確認し、その Promise を待つには、構成を使用して静的メソッド VideoEncoder.isConfigSupported() を呼び出すことをおすすめします。

const init = {
  output: handleChunk,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};

const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
  const encoder = new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

エンコーダの設定が完了すると、encode() メソッドを使用してフレームを受け入れる準備が整います。 configure()encode() はどちらも、実際の処理が完了するのを待たずにすぐに返されます。これにより、複数のフレームを同時にエンコード用にキューに入れることができます。encodeQueueSize は、前のエンコードが完了するまでキューで待機しているリクエストの数を示します。 エラーは、引数またはメソッド呼び出しの順序が API コントラクトに違反している場合は例外を直ちにスローするか、コーデック実装で発生した問題に対して error() コールバックを呼び出すことで報告されます。エンコードが正常に完了すると、新しいエンコードされたチャンクを引数として output() コールバックが呼び出されます。 もう 1 つ重要な点は、フレームが不要になったら close() を呼び出して通知する必要があることです。

let frameCounter = 0;

const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);

const reader = trackProcessor.readable.getReader();
while (true) {
  const result = await reader.read();
  if (result.done) break;

  const frame = result.value;
  if (encoder.encodeQueueSize > 2) {
    // Too many frames in flight, encoder is overwhelmed
    // let's drop this frame.
    frame.close();
  } else {
    frameCounter++;
    const keyFrame = frameCounter % 150 == 0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

最後に、エンコーダから出力されたエンコード済み動画のチャンクを処理する関数を作成して、エンコード コードを終了します。 通常、この関数はデータチャンクをネットワーク経由で送信するか、多重化してメディア コンテナに保存します。

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }

  // actual bytes of encoded data
  const chunkData = new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);

  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

保留中のエンコード リクエストがすべて完了したことを確認する必要がある場合は、flush() を呼び出してその Promise を待つことができます。

await encoder.flush();

デコード

ネットワークまたはストレージから Canvas または ImageBitmap へのパス。
ネットワークまたはストレージから Canvas または ImageBitmap へのパス。

VideoDecoder の設定は、VideoEncoder の場合と同様です。デコーダの作成時に 2 つの関数が渡され、コーデック パラメータが configure() に渡されます。

コーデック パラメータのセットはコーデックによって異なります。たとえば、H.264 コーデック バイナリ blob が、Annex B 形式(encoderConfig.avc = { format: "annexb" })でエンコードされていない限り、AVCC のバイナリ blob が必要になる場合があります。

const init = {
  output: handleFrame,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};

const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder = new VideoDecoder(init);
  decoder.configure(config);
} else {
  // Try another config.
}

デコーダが初期化されたら、EncodedVideoChunk オブジェクトをフィードできます。 チャンクを作成するには、次のものが必要です。

  • エンコードされた動画データの BufferSource
  • チャンクの開始タイムスタンプ(マイクロ秒単位)(チャンク内の最初のエンコードされたフレームのメディア時間)
  • チャンクのタイプ。次のいずれかです。
    • チャンクを前のチャンクから独立してデコードできる場合は key
    • 1 つ以上の前のチャンクがデコードされた後にのみチャンクをデコードできる場合は delta

また、エンコーダによって出力されたチャンクは、そのままデコーダで使用できます。上記のエラー報告とエンコーダのメソッドの非同期性に関する説明は、デコーダにも当てはまります。

const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
  const chunk = new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

次に、デコードされたばかりのフレームをページに表示する方法を示します。デコーダの出力コールバック(handleFrame())がすぐに返されるようにすることをおすすめします。次の例では、レンダリングの準備ができたフレームのキューにフレームを追加するだけです。 レンダリングは別個に行われ、次の 2 つのステップで構成されます。

  1. フレームを表示する適切なタイミングを待つ。
  2. キャンバスにフレームを描画する。

フレームが不要になったら、ガベージ コレクタが処理する前に close() を呼び出して基盤となるメモリを解放します。これにより、ウェブ アプリケーションで使用されるメモリの平均量が削減されます。

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime == 0) baseTime = performance.now();
  let mediaTime = performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow = pendingFrames.length == 0;
  if (underflow) return;

  const frame = pendingFrames.shift();

  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r) => {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();

  // Immediately schedule rendering of the next frame
  setTimeout(renderFrame, 0);
}

デベロッパー向けのヒント

[Media] パネルを Chrome DevTools で使用して、メディアログを表示し、WebCodecs をデバッグします。

WebCodecs のデバッグ用のメディア パネルのスクリーンショット
Chrome DevTools の [Media] パネルで WebCodecs をデバッグします。

デモ

このデモでは、キャンバスのアニメーション フレームが次のようになります。

  • MediaStreamTrackProcessor によって 25 fps で ReadableStream にキャプチャされる
  • ウェブワーカーに転送される
  • H.264 動画形式にエンコードされる
  • 動画フレームのシーケンスに再度デコードされる
  • transferControlToOffscreen() を使用して 2 番目のキャンバスにレンダリングされる

その他のデモ

その他のデモもご覧ください。

WebCodecs API の使用

機能検出

WebCodecs のサポートを確認するには:

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

WebCodecs API は 安全なコンテキストでのみ使用できます。 そのため、self.isSecureContext が false の場合は検出に失敗します。

詳細

WebCodecs を初めて使用する場合は、WebCodecs の基本で詳細な記事と多くの例を確認して、詳細をご確認ください。

フィードバック

Chrome チームは、WebCodecs API の使用感について皆様のご意見をお待ちしています。

API 設計について

API が想定どおりに動作しない点はありますか?アイデアを実装するために必要なメソッドやプロパティが不足していますか?セキュリティ モデルについてご質問やご意見はありますか?対応する GitHub リポジトリで仕様に関する問題を提出するか、既存の問題に 意見を追加してください。

実装に関する問題を報告する

Chrome の実装にバグが見つかりましたか?実装が仕様と異なっていますか?new.crbug.com でバグを報告してください。できる限り詳細な情報、再現手順を記載し、Blink>Media>WebCodecs を [Components] ボックスに入力してください。

API のサポートを示す

WebCodecs API を使用する予定はありますか?皆様からの公開サポートは、Chrome チームが機能の優先順位を決定するのに役立ち、他のブラウザ ベンダーにサポートの重要性を示すことができます。

media-dev@chromium.org にメールを送信するか、ハッシュタグ #WebCodecsを使用して @ChromiumDevにツイートして、使用している場所と方法をお知らせください。