Melakukan operasi per frame video yang efisien pada video dengan requestVideoFrameCallback()

Pelajari cara menggunakan requestVideoFrameCallback() untuk bekerja lebih efisien dengan video di browser.

Metode HTMLVideoElement.requestVideoFrameCallback() memungkinkan penulis web mendaftarkan callback yang berjalan dalam langkah-langkah rendering saat frame video baru dikirim ke kompositor. Hal ini memungkinkan developer melakukan operasi per frame video yang efisien pada video, seperti pemrosesan dan gambar video ke kanvas, analisis video, atau sinkronisasi dengan sumber audio eksternal.

Perbedaan dengan requestAnimationFrame()

Operasi seperti menggambar frame video ke kanvas menggunakan drawImage() yang dibuat melalui API ini akan disinkronkan sebagai upaya terbaik dengan kecepatan frame video yang diputar di layar. Berbeda dengan window.requestAnimationFrame(), yang biasanya diaktifkan sekitar 60 kali per detik, requestVideoFrameCallback() terikat dengan kecepatan frame video yang sebenarnya—dengan pengecualian penting:

Kecepatan efektif saat callback dijalankan adalah rasio yang lebih rendah antara kecepatan video dan kecepatan browser. Artinya, video 25 fps yang diputar di browser yang menggambar pada 60 Hz akan memicu callback pada 25 Hz. Video 120 fps di browser 60 Hz yang sama akan memicu callback pada 60 Hz.

Apalah arti sebuah nama?

Karena kesamaannya dengan window.requestAnimationFrame(), metode ini awalnya diusulkan sebagai video.requestAnimationFrame() dan diganti namanya menjadi requestVideoFrameCallback(), yang disepakati setelah diskusi panjang.

Deteksi fitur

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

Dukungan browser

Dukungan Browser

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

Sumber

Polyfill

Polyfill untuk metode requestVideoFrameCallback() yang didasarkan pada Window.requestAnimationFrame() dan HTMLVideoElement.getVideoPlaybackQuality() tersedia. Sebelum menggunakannya, perhatikan batasan yang disebutkan dalam README.

Menggunakan metode requestVideoFrameCallback()

Jika pernah menggunakan metode requestAnimationFrame(), Anda akan langsung merasa akrab dengan metode requestVideoFrameCallback(). Anda mendaftarkan callback awal satu kali, lalu mendaftar ulang setiap kali callback diaktifkan.

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

Dalam callback, now adalah DOMHighResTimeStamp dan metadata adalah kamus VideoFrameMetadata dengan properti berikut:

  • presentationTime, dari jenis DOMHighResTimeStamp: Waktu saat agen pengguna mengirimkan frame untuk komposisi.
  • expectedDisplayTime, dari jenis DOMHighResTimeStamp: Waktu saat agen pengguna mengharapkan frame terlihat.
  • width, dari jenis unsigned long: Lebar frame video, dalam piksel media.
  • height, dari jenis unsigned long: Tinggi frame video, dalam piksel media.
  • mediaTime, dari jenis double: Stempel waktu presentasi media (PTS) dalam detik dari frame yang ditampilkan (misalnya, stempel waktunya di linimasa video.currentTime).
  • presentedFrames, dari jenis unsigned long: Jumlah frame yang dikirim untuk komposisi. Memungkinkan klien menentukan apakah frame terlewat di antara instance VideoFrameRequestCallback.
  • processingDuration, dari jenis double: Durasi yang berlalu dalam detik sejak pengiriman paket yang dienkode dengan stempel waktu presentasi (PTS) yang sama dengan frame ini (misalnya, sama dengan mediaTime) ke dekoder hingga frame yang didekode siap untuk presentasi.

Untuk aplikasi WebRTC, properti tambahan dapat muncul:

  • captureTime, dari jenis DOMHighResTimeStamp: Untuk frame video yang berasal dari sumber lokal atau jarak jauh, ini adalah waktu saat frame diambil oleh kamera. Untuk sumber jarak jauh, waktu pengambilan diperkirakan menggunakan sinkronisasi jam dan laporan pengirim RTCP untuk mengonversi stempel waktu RTP menjadi waktu pengambilan.
  • receiveTime, dari jenis DOMHighResTimeStamp: Untuk frame video yang berasal dari sumber jarak jauh, ini adalah waktu frame yang dienkode diterima oleh platform, yaitu waktu saat paket terakhir yang termasuk dalam frame ini diterima melalui jaringan.
  • rtpTimestamp, dari jenis unsigned long: Stempel waktu RTP yang terkait dengan frame video ini.

Yang menarik secara khusus dalam daftar ini adalah mediaTime. Implementasi Chromium menggunakan jam audio sebagai sumber waktu yang mendukung video.currentTime, sedangkan mediaTime diisi langsung oleh presentationTimestamp frame. mediaTime adalah yang harus Anda gunakan jika ingin mengidentifikasi frame secara tepat dengan cara yang dapat direproduksi, termasuk untuk mengidentifikasi frame mana yang Anda lewatkan.

Jika ada yang tampak hanya satu frame...

Sinkronisasi vertikal (atau hanya vsync), adalah teknologi grafis yang menyinkronkan kecepatan frame video dan kecepatan refresh monitor. Karena requestVideoFrameCallback() berjalan di thread utama, tetapi, di balik layar, komposisi video terjadi di thread kompositor, semuanya dari API ini adalah upaya terbaik, dan browser tidak menawarkan jaminan ketat apa pun. Yang mungkin terjadi adalah API dapat berupa satu vsync yang terlambat dibandingkan dengan saat frame video dirender. Diperlukan satu vsync agar perubahan yang dibuat pada halaman web melalui API muncul di layar (sama seperti window.requestAnimationFrame()). Jadi, jika Anda terus memperbarui mediaTime atau nomor frame di halaman web dan membandingkannya dengan frame video bernomor, video akan terlihat seperti satu frame di depan.

Yang sebenarnya terjadi adalah frame siap di vsync x, callback diaktifkan, dan frame dirender di vsync x+1, dan perubahan yang dibuat dalam callback dirender di vsync x+2. Anda dapat memeriksa apakah callback adalah vsync terlambat (dan frame sudah dirender di layar) dengan memeriksa apakah metadata.expectedDisplayTime kira-kira now atau satu vsync di masa mendatang. Jika dalam waktu sekitar lima hingga sepuluh mikrodetik dari now, frame sudah dirender; jika expectedDisplayTime adalah sekitar enam belas milidetik di masa mendatang (dengan asumsi browser/layar Anda dimuat ulang pada 60 Hz), Anda akan disinkronkan dengan frame.

Demo

Saya telah membuat demo kecil di Glitch yang menunjukkan cara frame digambar di kanvas dengan tepat kecepatan frame video dan tempat metadata frame dicatat ke dalam log untuk tujuan proses debug.

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

Kesimpulan

Orang telah melakukan pemrosesan tingkat frame sejak lama—tanpa memiliki akses ke frame sebenarnya, hanya berdasarkan video.currentTime. Metode requestVideoFrameCallback() sangat meningkatkan solusi ini.

Ucapan terima kasih

requestVideoFrameCallback API ditentukan dan diterapkan oleh Thomas Guilbert. Postingan ini ditinjau oleh Joe Medley dan Kayce Basques. Banner besar oleh Denise Jans di Unsplash.