WebCodecs による動画処理

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

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

最新のウェブ テクノロジーにより、動画を扱うための十分な手段が提供されています。 Media Stream API Media Recording APIMedia Source APIWebRTC API など 動画ストリームの録画、転送、再生のための豊富なツールセットを提供します。 これらの API は特定の高レベルのタスクを解決する一方で、 プログラマーは動画ストリームの個々のコンポーネント(フレーム、 分割および多重化されていないチャンクに分割されます。 これらの基本コンポーネントへの低レベルのアクセスを得るために、デベロッパーは WebAssembly を使用して、動画コーデックとオーディオ コーデックをブラウザに組み込みます。しかし、 最新のブラウザにはすでにさまざまなコーデック( ハードウェアによって高速化されるなど)、それらを WebAssembly として再パッケージ化するのは無駄のように思えるかもしれません。 人とコンピュータのリソースが 削減されます

WebCodecs API を使用すると、このような非効率な作業を排除できます。 既存のメディア コンポーネントを使用する方法をプログラマーが提供し、 アクセスできます。詳細は以下のとおりです。

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

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

動画処理のワークフロー

フレームは動画処理の中心です。そのため、WebCodec では、ほとんどのクラス 使用するか生成するかを決める必要があります動画エンコーダはフレームをエンコードされた文字列に変換 分割されます。動画デコーダの処理はその逆です。

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

WebCodecs API は Insertable Streams API のクラスと連携して適切に機能する これは WebCodec をメディア ストリーム トラックに接続するものです。

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

WebCodecs とウェブ ワーカー

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

その方法については、ReadableStream をご覧ください。 メディアからのすべてのフレームを自動転送するための トラックをワーカーに送りますたとえば、MediaStreamTrackProcessor を使用すると、 ウェブカメラからのメディア ストリーム トラックの場合は ReadableStream。その後 ストリームはウェブワーカーに転送され、ウェブワーカーでフレームが 1 つずつ読み取られ、キューに格納されます。 VideoEncoder に変換します。

HTMLCanvasElement.transferControlToOffscreen を使用すると、レンダリングもメインスレッドの外部で実行できます。すべての概要ツールが VideoFrame 自体は譲渡可能であり、 ワーカー間の移動などです

WebCodec の活用例

エンコード

<ph type="x-smartling-placeholder">
</ph> 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() は、事前に確認する構成に置き換えます。 構成がサポートされていることを確認し、その Promise を待ちます。

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() を呼び出して Promise を待ちます。

await encoder.flush();

デコード

<ph type="x-smartling-placeholder">
</ph> ネットワークまたはストレージから Canvas または ImageBitmap へのパス。
ネットワークまたはストレージから Canvas または ImageBitmap へのパス。

VideoDecoder の設定は、前のステップで行った設定とほぼ同じです。 VideoEncoder: デコーダの作成時に、2 つの関数とコーデックが渡されます。 パラメータは configure() に指定されます。

コーデック パラメータのセットは、コーデックによって異なります。例: H.264 コーデック バイナリ blob が必要な可能性がある AVCC の形式を使用します(ただし、いわゆる Annex B 形式(encoderConfig.avc = { format: "annexb" })でエンコードされている場合を除く)。

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
  • マイクロ秒単位のチャンクの開始タイムスタンプ(チャンク内で最初にエンコードされたフレームのメディア時間)
  • チャンクの型。次のいずれかです。 <ph type="x-smartling-placeholder">
      </ph>
    • チャンクを以前のチャンクから独立してデコードできる場合は key
    • delta: 1 つ以上の前のチャンクがデコードされた後にのみチャンクをデコードできる場合は、

また、エンコーダから出力されたチャンクも、そのままデコーダで利用できます。 Error Reporting と非同期の特性について上記のすべて すべてのメソッドは、デコーダにも当てはまります。

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

開発のヒント

メディアパネルを使用する メディアログを表示し、WebCodecs をデバッグできます。

<ph type="x-smartling-placeholder">
</ph> WebCodec をデバッグするためのメディアパネルのスクリーンショット
WebCodec をデバッグするための Chrome DevTools のメディアパネル。

デモ

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

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

その他のデモ

他のデモもご覧ください:

WebCodecs API の使用

機能検出

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

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 にメールを送信するか、ツイートしてください。 ハッシュタグを使用して @ChromiumDev に送信します #WebCodecs どこで、どのように使用されているかをお知らせください。

ヒーロー画像: Denise Jans 氏 Unsplash より