Wykonywanie wydajnych operacji na poszczególnych klatkach filmu

Dowiedz się, jak używać requestVideoFrameCallback(), aby wydajniej pracować z filmami w przeglądarce.

Data publikacji: 8 stycznia 2023 r.

Browser Support

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

Source

Metoda HTMLVideoElement.requestVideoFrameCallback() umożliwia autorom stron internetowych zarejestrowanie wywołania zwrotnego, które jest wykonywane w krokach renderowania, gdy do kompozytora wysyłana jest nowa klatka wideo. Umożliwia to deweloperom wykonywanie wydajnych operacji na poszczególnych klatkach filmu, takich jak przetwarzanie filmu i rysowanie na płótnie, analiza filmu czy synchronizacja z zewnętrznymi źródłami dźwięku.

Różnica w stosunku do metody requestAnimationFrame()

Operacje wykonywane za pomocą tego interfejsu API, takie jak rysowanie klatki wideo na płótnie za pomocą funkcji drawImage(), są synchronizowane w miarę możliwości z liczbą klatek na sekundę odtwarzanego na ekranie filmu. Różni się to od window.requestAnimationFrame(), które zwykle jest wywoływane około 60 razy na sekundę.

requestVideoFrameCallback() jest powiązana z rzeczywistą liczbą klatek na sekundę filmu, z ważnym wyjątkiem:

Efektywna częstotliwość wywoływania funkcji zwrotnych to mniejsza z dwóch wartości: częstotliwości filmu i częstotliwości przeglądarki. Oznacza to, że film o częstotliwości 25 klatek na sekundę odtwarzany w przeglądarce, która odświeża obraz z częstotliwością 60 Hz, będzie wywoływać wywołania zwrotne z częstotliwością 25 Hz. Film o częstotliwości 120 klatek na sekundę w przeglądarce o częstotliwości 60 Hz będzie wywoływać wywołania zwrotne z częstotliwością 60 Hz.

Wykrywanie cech

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

Watolina poliestrowa

Dostępny jest polyfill dla metody requestVideoFrameCallback() oparty na Window.requestAnimationFrame() i HTMLVideoElement.getVideoPlaybackQuality(). Zanim zaczniesz korzystać z tej funkcji, zapoznaj się z ograniczeniami wymienionymi w README.

Użyj metody

Jeśli używasz metody requestAnimationFrame(), rozpoznasz metodę requestVideoFrameCallback(). Zarejestruj początkowe wywołanie zwrotne raz, a następnie rejestruj je ponownie za każdym razem, gdy zostanie wywołane.

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

W wywołaniu zwrotnym now to DOMHighResTimeStamp, a metadata to słownik VideoFrameMetadata o tych właściwościach:

  • presentationTime, typu DOMHighResTimeStamp: Czas, w którym klient użytkownika przesłał ramkę do kompozycji.
  • expectedDisplayTime, typu DOMHighResTimeStamp: Czas, w którym klient użytkownika oczekuje, że ramka będzie widoczna.
  • width, typu unsigned long: Szerokość klatki filmu w pikselach multimedialnych.
  • height, typu unsigned long: Wysokość klatki filmu w pikselach multimedialnych.
  • mediaTime, typ double: Sygnatura czasowa prezentacji multimedialnej (PTS) w sekundach klatki prezentowanej (np. sygnatura czasowa na osi czasu video.currentTime).
  • presentedFrames, typu unsigned long: Liczba klatek przesłanych do kompozycji. Umożliwia klientom określenie, czy między wystąpieniami VideoFrameRequestCallback pominięto klatki.
  • processingDuration, typu double: Czas w sekundach, który upłynął od przesłania zakodowanego pakietu z tym samym znacznikiem czasu prezentacji (PTS) co ta klatka (np. takim samym jak mediaTime) do dekodera, aż zdekodowana klatka była gotowa do prezentacji.

W przypadku aplikacji WebRTC mogą się pojawić dodatkowe właściwości:

  • captureTime, typu DOMHighResTimeStamp: W przypadku klatek wideo pochodzących ze źródła lokalnego lub zdalnego jest to czas, w którym klatka została zarejestrowana przez kamerę. W przypadku zdalnego źródła czas przechwytywania jest szacowany na podstawie synchronizacji zegara i raportów nadawcy RTCP, aby przekonwertować sygnatury czasowe RTP na czas przechwytywania.
  • receiveTime, typu DOMHighResTimeStamp: W przypadku klatek wideo pochodzących ze zdalnego źródła jest to czas, w którym zakodowana klatka została odebrana przez platformę, czyli czas, w którym ostatni pakiet należący do tej klatki został odebrany przez sieć.
  • rtpTimestamp, typu unsigned long: Sygnatura czasowa RTP powiązana z tą klatką wideo.

Na tej liście szczególnie interesująca jest mediaTime. Implementacja Chromium używa zegara audio jako źródła czasu, które obsługuje video.currentTime, natomiast mediaTime jest bezpośrednio wypełniana przez presentationTimestamp klatki. mediaTime to element, którego należy użyć, jeśli chcesz dokładnie identyfikować klatki w powtarzalny sposób, w tym określać, których klatek brakuje.

Jeśli wydaje Ci się, że obraz jest przesunięty o 1 klatkę

Synchronizacja pionowa (lub po prostu vsync) to technologia graficzna, która synchronizuje liczbę klatek filmu z częstotliwością odświeżania monitora. Ponieważ funkcja requestVideoFrameCallback() działa w wątku głównym, ale w tle kompozycja wideo jest tworzona w wątku kompozytora, wszystko z tego interfejsu API jest wykonywane w miarę możliwości, a przeglądarka nie oferuje żadnych ścisłych gwarancji.

Możliwe, że interfejs API jest o 1 synchronizację pionową opóźniony w stosunku do momentu renderowania klatki wideo. Wyświetlenie na ekranie zmian wprowadzonych na stronie internetowej za pomocą interfejsu API zajmuje 1 synchronizację pionową (tak samo jak w przypadku window.requestAnimationFrame()). Jeśli więc będziesz aktualizować mediaTime lub numer klatki na stronie internetowej i porównywać to z numerowanymi klatkami filmu, w końcu film będzie wyglądał tak, jakby był o 1 klatkę do przodu.

W rzeczywistości klatka jest gotowa przy synchronizacji pionowej x, wywoływane jest wywołanie zwrotne, a klatka jest renderowana przy synchronizacji pionowej x+1, a zmiany wprowadzone w wywołaniu zwrotnym są renderowane przy synchronizacji pionowej x+2. Możesz sprawdzić, czy wywołanie zwrotne jest opóźnione w stosunku do synchronizacji pionowej (a klatka jest już renderowana na ekranie), sprawdzając, czy wartość metadata.expectedDisplayTime jest w przybliżeniu równa now lub o 1 synchronizację pionową w przyszłości. Jeśli jest ona w zakresie od około 5 do 10 mikrosekund od now, klatka jest już renderowana. Jeśli expectedDisplayTime przypada za około 16 milisekund (zakładając, że przeglądarka odświeża się z częstotliwością 60 Hz), oznacza to, że synchronizacja z klatką jest prawidłowa.

Prezentacja

Utworzyłem małą wersję demonstracyjną, która pokazuje, jak klatki są rysowane na płótnie dokładnie z częstotliwością klatek filmu, oraz gdzie metadane klatek są rejestrowane na potrzeby debugowania.

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

Podsumowanie

Przetwarzanie na poziomie klatek jest stosowane od dawna, ale bez dostępu do rzeczywistych klatek, tylko na podstawie video.currentTime. Metoda requestVideoFrameCallback() znacznie ulepsza to obejście.

Podziękowania

Interfejs requestVideoFrameCallback API został określony i wdrożony przez Thomasa Guilberta. Ten post został sprawdzony przez Joego Medleya i Kayce Basques.