WebCodecs による動画処理

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

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

最新のウェブ技術では、動画を扱う方法が豊富に用意されています。Media Stream APIMedia Recording APIMedia Source APIWebRTC API を組み合わせることで、動画ストリームの録画、転送、再生のための豊富なツールセットを構築できます。これらの API は、特定のハイレベルなタスクを解決する際に、フレームやエンコードされた動画や音声の未結合チャンクなど、動画ストリームの個々のコンポーネントを操作できるようには設計されていません。これらの基本コンポーネントに低レベルからアクセスできるように、デベロッパーは WebAssembly を使用して動画コーデックとオーディオ コーデックをブラウザに取り込んできました。しかし、最新のブラウザにはすでにさまざまなコーデックが搭載されており(多くの場合、ハードウェアによって高速化されています)、それらを WebAssembly として再パッケージ化するのは、人力とコンピュータ リソースの浪費のように思えます。

WebCodecs API は、ブラウザにすでに存在するメディア コンポーネントを使用する方法をプログラマに提供することで、この非効率性を排除します。詳細は以下のとおりです。

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

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

動画処理のワークフロー

フレームは動画処理の中心的な要素です。したがって、WebCodecs では、ほとんどのクラスがフレームの消費または生成を行います。動画エンコーダは、フレームをエンコードされたチャンクに変換します。動画デコーダは逆の処理を行います。

また、VideoFrameCanvasImageSource であり、CanvasImageSource を受け入れるコンストラクタを備えているため、他の Web API と連携して使用できます。そのため、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 からネットワークまたはストレージへのパス
Canvas または ImageBitmap からネットワークまたはストレージへのパス

すべては 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 をスローします。構成で静的メソッド 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() コールバックが呼び出されます。ここでのもう一つの重要な点は、フレームが不要になったら 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() を呼び出してプロミスを待機します。

await encoder.flush();

デコード

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

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

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

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: チャンクを前のチャンクから独立してデコードできる場合
    • delta: チャンクをデコードできるのは、前の 1 つ以上のチャンクがデコードされた後の場合

また、エンコーダによって出力されたチャンクは、そのままデコーダで使用できます。エラー レポートとエンコーダのメソッドの非同期性について上記で説明した内容は、デコーダにも同様に当てはまります。

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);
}

開発のヒント

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

WebCodec をデバッグするためのメディアパネルのスクリーンショット
WebCodec をデバッグするための Chrome DevTools のメディアパネル。

デモ

次のデモは、キャンバスのアニメーション フレームを示しています。

  • MediaStreamTrackProcessor が 25 fps で ReadableStream にキャプチャした
  • ウェブワーカーに転送し
  • H.264 動画形式にエンコードされます。
  • 再度デコードされて一連の動画フレームに変換されます。
  • transferControlToOffscreen() を使用して 2 番目のキャンバスにレンダリングされます。

その他のデモ

他にも、以下のデモもご覧ください。

WebCodecs API の使用

特徴検出

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

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

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

フィードバック

Chrome チームは、WebCodecs API の使用感について、皆様のご意見をお聞かせいただきたいと考えています。

API 設計について

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

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

Chrome の実装にバグが見つかりましたか?それとも実装が仕様と異なるのでしょうか?new.crbug.com でバグを報告します。できる限り詳しい情報と、再現するための簡単な手順を記載し、[Components] ボックスに「Blink>Media>WebCodecs」と入力します。Glitch は、簡単な再現手順をすばやく共有するのに適しています。

API のサポートを表示する

WebCodecs API を使用する予定はありますか?一般公開でサポートすると、Chrome チームが機能の優先順位を決めるのに役立ち、他のブラウザ ベンダーにその機能のサポートがどれほど重要であるかを示します。

media-dev@chromium.org にメールを送信するか、ハッシュタグ #WebCodecs を使用して @ChromiumDev にツイートを送信し、どこでどのように使用しているかをお知らせください。

ヒーロー画像: UnsplashDenise Jans による