Efektywne wykonywanie operacji na poszczególnych klatkach wideo za pomocą metody requestVideoFrameCallback()

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

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 takie jak rysowanie klatki filmu na obszarze roboczym za pomocą interfejsu drawImage() będą synchronizowane w miarę możliwości z liczbą klatek na sekundę filmu odtwarzanego na ekranie. W przeciwieństwie do zdarzenia window.requestAnimationFrame(), które zwykle uruchamia się około 60 razy na sekundę, zdarzenie requestVideoFrameCallback() jest powiązane z rzeczywistą liczbą klatek na sekundę w filmie. Istnieje jednak ważny wyjątek:

Efektywna częstotliwość wywoływania funkcji zwrotnych jest mniejszą z dwóch wartości: częstotliwości wideo i częstotliwości przeglądarki. Oznacza to, że film o częstotliwości 25 kl./s 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.

Co kryje się pod nazwą?

Ze względu na podobieństwo do window.requestAnimationFrame() metoda ta została początkowo zaproponowana jako video.requestAnimationFrame() i zmieniona na requestVideoFrameCallback(), co zostało uzgodnione po długiej dyskusji.

Wykrywanie cech

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

Obsługa przeglądarek

Browser Support

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

Source

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.

Korzystanie z metody requestVideoFrameCallback()

Jeśli kiedykolwiek korzystałeś(-aś) z metody requestAnimationFrame(), od razu poczujesz się swobodnie z metodą requestVideoFrameCallback(). Początkową funkcję zwrotną rejestrujesz raz, a potem ponownie za każdym razem, gdy jest ona wywoływana.

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 słownik DOMHighResTimeStamp, a metadata to słownik VideoFrameMetadata z tymi właściwościami:

  • 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, typu double: Sygnatura czasowa prezentacji multimediów (PTS) w sekundach wyświetlanej klatki (np. sygnatura czasowa na osi czasu video.currentTime).
  • presentedFrames, typ unsigned long: Liczba klatek przesłanych do złożenia. Umożliwia klientom określenie, czy między wystąpieniami VideoFrameRequestCallback pominięto klatki.
  • processingDuration, typu double: Czas, który upłynął od przesłania zakodowanego pakietu z tą samą sygnaturą czasową prezentacji (PTS) co ta klatka (np. taką samą jak mediaTime) do dekodera, aż zdekodowana klatka była gotowa do wyświetlenia.

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 źródła zdalnego czas przechwytywania jest szacowany na podstawie synchronizacji zegara i raportów nadawcy RTCP, aby przekształcić 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: Znacznik czasu RTP powiązany z tą klatką wideo.

Na tej liście szczególnie interesująca jest pozycja 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 coś jest nie tak…

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 głównym wątku, ale w tle kompozycja wideo jest tworzona w wątku kompozytora, wszystko w tym interfejsie API jest wykonywane w miarę możliwości, a przeglądarka nie daje żadnych ścisłych gwarancji. Może się zdarzyć, ż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 wideo, w końcu wideo będzie wyglądać tak, jakby było 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. 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 to około 5–10 mikrosekund od now, klatka jest już wyrenderowana. Jeśli expectedDisplayTime jest około 16 milisekund w przyszłości (zakładając, że przeglądarka lub ekran odświeża się z częstotliwością 60 Hz), to synchronizacja z klatką jest prawidłowa.

Prezentacja

Utworzyłem małe demo, które 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 API requestVideoFrameCallback został określony i wdrożony przez Thomasa Guilberta. Ten post został sprawdzony przez Joego Medleya i Kayce Basques. Baner powitalny: Denise Jans, Unsplash.