requestVideoFrameCallback() を使用して、動画に対して動画フレームごとの効率的な操作を行う

requestVideoFrameCallback() を使用してブラウザで動画をより効率的に操作する方法について学びます。

HTMLVideoElement.requestVideoFrameCallback() メソッドを使用すると、ウェブ作成者は、新しい動画フレームがコンポーザに送信されたときにレンダリング ステップで実行されるコールバックを登録できます。これにより、デベロッパーは、動画の処理、キャンバスへのペイント、動画分析、外部オーディオ ソースとの同期など、動画フレームごとに効率的なオペレーションを実行できます。

requestAnimationFrame() との違い

この API を介して実行される drawImage() を使用して動画フレームをキャンバスに描画するなどのオペレーションは、画面上で再生される動画のフレームレートとベスト エフォートとして同期されます。通常は 1 秒あたり約 60 回呼び出される window.requestAnimationFrame() とは異なり、requestVideoFrameCallback() は実際の動画フレームレートにバインドされます。ただし、重要な例外があります。

コールバックが実行される有効なレートは、動画のレートとブラウザのレートのうち、低い方のレートです。つまり、ブラウザで再生される 25 fps の動画を 60 Hz で描画すると、25 Hz でコールバックが発生します。 同じ 60 Hz ブラウザで 120 fps の動画をアップロードすると、60 Hz でコールバックが発生します。

サイトマップの名前について

window.requestAnimationFrame() と類似しているため、このメソッドは当初 video.requestAnimationFrame() として提案され、requestVideoFrameCallback() に名前が変更されました。これは、長時間の議論の末に合意されたものです。

特徴検出

if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
  // The API is supported!
}

ブラウザ サポート

対応ブラウザ

  • Chrome: 83。
  • Edge: 83。
  • Firefox: 132.
  • Safari: 15.4。

ソース

ポリフィル

Window.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality() に基づく requestVideoFrameCallback() メソッドのポリフィルを利用できます。これを使用する前に、README に記載されている制限事項に注意してください。

requestVideoFrameCallback() メソッドの使用

requestAnimationFrame() メソッドを使ったことがあれば、requestVideoFrameCallback() メソッドにはすぐに慣れるでしょう。最初のコールバックを 1 回登録し、コールバックが発生するたびに再登録します。

const doSomethingWithTheFrame = (now, metadata) => {
  // Do something with the frame.
  console.log(now, metadata);
  // Re-register the callback to be notified about the next frame.
  video.requestVideoFrameCallback(doSomethingWithTheFrame);
};
// Initially register the callback to be notified about the first frame.
video.requestVideoFrameCallback(doSomethingWithTheFrame);

コールバックでは、nowDOMHighResTimeStampmetadata は次のプロパティを持つ VideoFrameMetadata 辞書です。

  • presentationTimeDOMHighResTimeStamp 型): ユーザー エージェントが合成用にフレームを送信した時刻。
  • expectedDisplayTimeDOMHighResTimeStamp 型): ユーザー エージェントがフレームが表示されると予想する時刻。
  • widthunsigned long 型): 動画フレームの幅(メディア ピクセル単位)。
  • heightunsigned long 型): 動画フレームの高さ(メディア ピクセル単位)。
  • mediaTimedouble 型): 表示されたフレームのメディア プレゼンテーション タイムスタンプ(PTS)(video.currentTime タイムラインのタイムスタンプなど)。
  • presentedFramesunsigned long 型): 合成のために送信されたフレーム数。クライアントが VideoFrameRequestCallback のインスタンス間でフレームが欠落したかどうかを判断できるようにします。
  • processingDurationdouble 型): このフレームと同じプレゼンテーション タイムスタンプ(PTS)(mediaTime と同じなど)を持つエンコードされたパケットをデコーダに送信してから、デコードされたフレームが表示可能になるまでの経過時間(秒単位)。

WebRTC アプリケーションの場合、追加のプロパティが表示されることがあります。

  • captureTimeDOMHighResTimeStamp タイプ): ローカルまたはリモートのソースからの動画フレームの場合、これはフレームがカメラでキャプチャされた時刻です。リモート ソースの場合、キャプチャ時間はクロック同期と RTCP 送信側レポートを使用して推定され、RTP タイムスタンプがキャプチャ時間に変換されます。
  • receiveTimeDOMHighResTimeStamp タイプ): リモートソースからの動画フレームの場合、これはエンコードされたフレームがプラットフォームが受信した時刻、つまりこのフレームに属する最後のパケットがネットワーク経由で受信した時刻です。
  • rtpTimestampunsigned long 型): この動画フレームに関連付けられた RTP タイムスタンプ。

このリストで特に注目すべきは mediaTime です。Chromium の実装では、video.currentTime をサポートする時間ソースとして音声クロックが使用されますが、mediaTime はフレームの presentationTimestamp によって直接入力されます。mediaTime は、どのフレームを見逃したのかを正確に特定するなど、再現可能な方法でフレームを正確に識別するために使用します。

1 フレームずれているように見える場合

垂直同期(または単に vsync)は、動画のフレームレートとモニターのリフレッシュ レートを同期させるグラフィック技術です。 requestVideoFrameCallback() はメインスレッドで実行されますが、内部的にはコンポジタ スレッドでビデオ コンポジットが実行されるため、この API のすべてはベスト エフォートであり、ブラウザは厳密な保証を提供しません。動画フレームがレンダリングされたときと比較して、API が 1 vsync 遅れている可能性があります。API を介してウェブページに加えた変更が画面に表示されるまでには、1 つの vsync が必要です(window.requestAnimationFrame() の場合と同じです)。そのため、ウェブページの mediaTime またはフレーム番号を更新し続け、番号付きの動画フレームと比較すると、最終的に動画が 1 フレーム進んでいるように見えます。

実際に起こっているのは、フレームが vsync x で準備完了し、コールバックがトリガーされてフレームが vsync x+1 でレンダリングされ、コールバックに加えた変更が vsync x+2 でレンダリングされることです。コールバックが vsync 遅延しているかどうか(フレームがすでに画面にレンダリングされているかどうか)を確認するには、metadata.expectedDisplayTime がおよそ now か、1 vsync 先かどうかを確認します。now から 5 ~ 10 マイクロ秒以内であれば、フレームはすでにレンダリングされています。expectedDisplayTime が約 16 ミリ秒先であれば(ブラウザまたは画面が 60 Hz で更新されていると仮定)、フレームと同期しています。

デモ

Glitch で簡単なデモを作成しました。このデモでは、動画のフレームレートとまったく同じフレームレートでフレームがキャンバスに描画される方法と、デバッグ目的でフレームメタデータがロギングされる場所を示しています。

let paintCount = 0;
let startTime = 0.0;

const updateCanvas = (now, metadata) => {
  if (startTime === 0.0) {
    startTime = now;
  }

  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

  const elapsed = (now - startTime) / 1000.0;
  const fps = (++paintCount / elapsed).toFixed(3);
  fpsInfo.innerText = `video fps: ${fps}`;
  metadataInfo.innerText = JSON.stringify(metadata, null, 2);

  video.requestVideoFrameCallback(updateCanvas);
};

video.requestVideoFrameCallback(updateCanvas);

まとめ

長い間、実際のフレームにアクセスせずに、video.currentTime に基づいてフレームレベルの処理が行われてきました。requestVideoFrameCallback() メソッドは、この回避策を大幅に改善します。

謝辞

requestVideoFrameCallback API は、Thomas Guilbert によって仕様が定義され、実装されました。この投稿は、Joe MedleyKayce Basques が確認しました。ヒーロー画像: Denise Jans(Unsplash)