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

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

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

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

ブラウザ サポート

対応ブラウザ

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

コールバックで、nowDOMHighResTimeStamp で、metadata は次のプロパティを持つ 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)