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

requestVideoFrameCallback() を使用してブラウザで動画を効率的に操作する方法をご覧ください。

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

requestAnimationFrame() との違い

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

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

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

window.requestAnimationFrame() との類似性から、このメソッドは当初 video.requestAnimationFrame() として提案され、長い議論の末に requestVideoFrameCallback() に改名されました。

特徴検出

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

ブラウザ サポート

Browser Support

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

Source

ポリフィル

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

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

  • presentationTimeDOMHighResTimeStamp 型): ユーザー エージェントがコンポジションのためにフレームを送信した時刻。
  • expectedDisplayTime(型: DOMHighResTimeStamp): ユーザー エージェントがフレームの表示を想定している時刻。
  • width(型: unsigned long): 動画フレームの幅(メディア ピクセル単位)。
  • height(型: unsigned long): 動画フレームの高さ(メディア ピクセル単位)。
  • mediaTime(型: double): 提示されたフレームのメディア プレゼンテーション タイムスタンプ(PTS)(秒単位)(例: video.currentTime タイムラインのタイムスタンプ)。
  • presentedFramesunsigned long 型): 構成用に送信されたフレームの数。クライアントは、VideoFrameRequestCallback のインスタンス間でフレームが欠落したかどうかを判断できます。
  • processingDuration(型: double): このフレームと同じプレゼンテーション タイムスタンプ(PTS)(mediaTime と同じなど)を持つエンコードされたパケットの送信から、デコードされたフレームがプレゼンテーションの準備が整うまでの経過時間(秒単位)。

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

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

このリストで特に重要なのは mediaTime です。Chromium の実装では、video.currentTime をバックアップするタイムソースとしてオーディオ クロックが使用されますが、mediaTime はフレームの presentationTimestamp によって直接入力されます。mediaTime は、再現可能な方法でフレームを正確に識別する場合(どのフレームが欠落したかを正確に識別する場合など)に使用する必要があります。

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

垂直同期(VSync)は、動画のフレームレートとモニターのリフレッシュ レートを同期するグラフィック技術です。requestVideoFrameCallback() はメインスレッドで実行されますが、内部的にはコンポジタ スレッドで動画の合成が行われるため、この API からのすべての処理はベスト エフォートであり、ブラウザは厳密な保証を提供しません。API が動画フレームのレンダリングに対して 1 回の垂直同期遅延している可能性があります。API を介してウェブページに加えられた変更が画面に表示されるまでに 1 回の垂直同期が必要です(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 で更新されている場合)であれば、フレームと同期しています。

デモ

動画のフレームレートとまったく同じレートでキャンバスにフレームを描画する方法と、デバッグ用にフレーム メタデータをログに記録する場所を示す小さなデモを作成しました。

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 によって審査されました。ヒーロー画像: Unsplash の Denise Jans