Effiziente Vorgänge pro Videobild ausführen

requestVideoFrameCallback() verwenden, um effizienter mit Videos im Browser zu arbeiten

Veröffentlicht am 8. Januar 2023

Browser Support

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

Source

Mit der Methode HTMLVideoElement.requestVideoFrameCallback() können Webautoren einen Callback registrieren, der in den Rendering-Schritten ausgeführt wird, wenn ein neuer Videoframes an den Compositor gesendet wird. So können Entwickler effiziente Vorgänge pro Videobild ausführen, z. B. Videoverarbeitung und Rendern auf einem Canvas, Videoanalyse oder Synchronisierung mit externen Audioquellen.

Unterschied zu requestAnimationFrame()

Vorgänge, die mit dieser API ausgeführt werden, z. B. das Zeichnen eines Videoframes auf einem Canvas mit drawImage(), werden nach Möglichkeit mit der Framerate des auf dem Bildschirm wiedergegebenen Videos synchronisiert. Das unterscheidet sich von window.requestAnimationFrame()>, das in der Regel etwa 60-mal pro Sekunde ausgelöst wird.

requestVideoFrameCallback() ist an die tatsächliche Videoframerate gebunden. Es gibt jedoch eine wichtige Ausnahme:

Die effektive Rate, mit der Callbacks ausgeführt werden, ist die niedrigere Rate zwischen der Rate des Videos und der des Browsers. Das bedeutet, dass bei einem Video mit 25 fps, das in einem Browser mit 60 Hz wiedergegeben wird, Rückrufe mit 25 Hz ausgelöst werden. Bei einem 120‑fps-Video in diesem 60‑Hz-Browser werden Rückrufe mit 60 Hz ausgelöst.

Funktionserkennung

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

Polyfill

Ein Polyfill für die Methode requestVideoFrameCallback() basierend auf Window.requestAnimationFrame() und HTMLVideoElement.getVideoPlaybackQuality() ist verfügbar. Beachten Sie vor der Verwendung die im README genannten Einschränkungen.

Methode verwenden

Wenn Sie die Methode requestAnimationFrame() verwenden, werden Sie die Methode requestVideoFrameCallback() erkennen. Registrieren Sie einen ersten Callback einmal und dann immer wieder, wenn der Callback ausgelöst wird.

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

Im Callback ist now ein DOMHighResTimeStamp und metadata ein VideoFrameMetadata-Wörterbuch mit den folgenden Eigenschaften:

  • presentationTime vom Typ DOMHighResTimeStamp: Die Uhrzeit, zu der der User-Agent den Frame für die Komposition gesendet hat.
  • expectedDisplayTime vom Typ DOMHighResTimeStamp: Die Zeit, zu der der User-Agent erwartet, dass der Frame sichtbar ist.
  • width, vom Typ unsigned long: Die Breite des Videobilds in Media-Pixeln.
  • height, vom Typ unsigned long: Die Höhe des Videobilds in Media-Pixeln.
  • mediaTime, vom Typ double: Der PTS (Presentation Timestamp, Präsentationszeitstempel) des präsentierten Frames in Sekunden (z. B. der Zeitstempel auf der video.currentTime-Zeitachse).
  • presentedFrames vom Typ unsigned long: Die Anzahl der Frames, die für die Komposition eingereicht wurden. Clients können so feststellen, ob zwischen den Instanzen von VideoFrameRequestCallback Frames fehlen.
  • processingDuration, vom Typ double: Die verstrichene Dauer in Sekunden von der Übermittlung des codierten Pakets mit demselben Präsentationszeitstempel (Presentation Timestamp, PTS) wie dieser Frame (z.B. derselbe wie mediaTime) bis zum Decoder, bis der decodierte Frame für die Präsentation bereit war.

Für WebRTC-Anwendungen können zusätzliche Attribute angezeigt werden:

  • captureTime vom Typ DOMHighResTimeStamp: Bei Videobildern aus einer lokalen oder Remote-Quelle ist dies der Zeitpunkt, zu dem das Bild von der Kamera aufgenommen wurde. Bei einer Remote-Quelle wird die Aufnahmezeit mithilfe der Uhrzeitsynchronisierung und der RTCP-Senderberichte geschätzt, um RTP-Zeitstempel in Aufnahmezeit umzuwandeln.
  • receiveTime, vom Typ DOMHighResTimeStamp: Bei Videoframes, die von einer Remotequelle stammen, ist dies der Zeitpunkt, zu dem der codierte Frame von der Plattform empfangen wurde. Das heißt, der Zeitpunkt, zu dem das letzte Paket, das zu diesem Frame gehört, über das Netzwerk empfangen wurde.
  • rtpTimestamp, vom Typ unsigned long: Der RTP-Zeitstempel, der diesem Videobild zugeordnet ist.

Von besonderem Interesse in dieser Liste ist mediaTime. In der Chromium-Implementierung wird die Audio-Clock als Zeitquelle für video.currentTime verwendet, während mediaTime direkt mit dem presentationTimestamp des Frames gefüllt wird. mediaTime ist die richtige Option, wenn Sie Frames reproduzierbar identifizieren möchten, auch um genau festzustellen, welche Frames Sie verpasst haben.

Wenn die Synchronisierung um einen Frame verschoben zu sein scheint

Die vertikale Synchronisierung (oder kurz VSync) ist eine Grafiktechnologie, die die Framerate eines Videos und die Aktualisierungsrate eines Monitors synchronisiert. Da requestVideoFrameCallback() im Hauptthread ausgeführt wird, die Videokomposition aber im Compositor-Thread erfolgt, ist alles aus dieser API ein Best-Effort-Ansatz und der Browser bietet keine strengen Garantien.

Möglicherweise ist die API im Vergleich zum Rendern eines Videoframes um einen Vsync verzögert. Es dauert einen Vsync, bis Änderungen, die über die API an der Webseite vorgenommen wurden, auf dem Bildschirm angezeigt werden (genau wie bei window.requestAnimationFrame()). Wenn Sie also die mediaTime oder die Framenummer auf Ihrer Webseite ständig aktualisieren und mit den nummerierten Videoframes vergleichen, sieht das Video irgendwann so aus, als wäre es einen Frame voraus.

Tatsächlich ist es so, dass der Frame bei VSync x fertig ist, der Callback ausgelöst wird und der Frame bei VSync x+1 gerendert wird. Änderungen, die im Callback vorgenommen werden, werden bei VSync x+2 gerendert. Sie können prüfen, ob der Callback zu spät erfolgt ist (und der Frame bereits auf dem Bildschirm gerendert wurde), indem Sie prüfen, ob metadata.expectedDisplayTime ungefähr now oder ein VSync in der Zukunft ist. Wenn es innerhalb von etwa 5 bis 10 Mikrosekunden nach now liegt, wird der Frame bereits gerendert. Wenn expectedDisplayTime etwa 16 Millisekunden in der Zukunft liegt (vorausgesetzt, Ihr Browser wird mit 60 Hz aktualisiert), sind Sie mit dem Frame synchron.

Demo

Ich habe eine kleine Demo erstellt, die zeigt, wie Frames mit genau der Framerate des Videos auf einem Canvas gezeichnet werden und wo die Frame-Metadaten zu Debugging-Zwecken protokolliert werden.

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

Zusammenfassung

Die Verarbeitung auf Frame-Ebene ist schon lange möglich – ohne Zugriff auf die tatsächlichen Frames, nur auf Grundlage von video.currentTime. Die requestVideoFrameCallback()-Methode ist eine deutliche Verbesserung gegenüber dieser Problemumgehung.

Danksagungen

Die requestVideoFrameCallback API wurde von Thomas Guilbert spezifiziert und implementiert. Dieser Beitrag wurde von Joe Medley und Kayce Basques geprüft.