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!
}
ブラウザ サポート
ポリフィル
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);
コールバックでは、now
は DOMHighResTimeStamp
で、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 タイムスタンプからキャプチャ時間に変換されます。receiveTime
(DOMHighResTimeStamp
タイプ): リモートソースからの動画フレームの場合、これはエンコードされたフレームをプラットフォームが受信した時刻、つまり、このフレームに属する最後のパケットがネットワーク経由で受信した時刻です。rtpTimestamp
(unsigned 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 Medley と Kayce Basques によってレビューされました。ヒーロー画像(作成者: Denise Jans、Unsplash)