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!
}

ブラウザ サポート

対応ブラウザ

  • 83
  • 83
  • x
  • 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);

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

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

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

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

このリストで特に重要なのは、mediaTime です。Chromium の実装では、video.currentTime をサポートするタイムソースとしてオーディオ クロックを使用しますが、mediaTime はフレームの presentationTimestamp によって直接入力されます。mediaTime は、再現可能な方法でフレームを正確に特定したい場合(見逃したフレームを正確に特定する場合など)に使用します。

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

垂直同期(または単に vsync)とは、動画のフレームレートとモニターのリフレッシュ レートを同期するグラフィックス技術です。requestVideoFrameCallback() はメインスレッド上で実行されますが、内部的には、動画の合成はコンポジタ スレッド上で行われるため、この API による処理はすべてベスト エフォート型であり、ブラウザは厳密な保証を行っていません。原因としては、動画フレームのレンダリングから API の vsync が 1 つ遅れることが考えられます。 API を使用してウェブページに加えた変更が画面に表示されるまでに vsync が 1 回発生します(window.requestAnimationFrame() と同様)。したがって、ウェブページの mediaTime またはフレーム番号を継続的に更新して、番号付きの動画フレームと比較すると、最終的に動画は 1 フレーム先にあるように見えます。

実際に起こっているのは、フレームが vsync x で準備され、コールバックが開始されてフレームが vsync x+1 でレンダリングされ、コールバックで行われた変更が vsync x+2 でレンダリングされることです。コールバックが vsync の遅延である(そしてフレームがすでに画面にレンダリングされている)かどうかを確認するには、metadata.expectedDisplayTime が約 now であるか、将来の vsync が 1 つであるかどうかを確認します。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)