Effectuer des opérations efficaces sur chaque image de la vidéo avec requestVideoFrameCallback()

Découvrez comment utiliser requestVideoFrameCallback() pour travailler plus efficacement avec les vidéos dans le navigateur.

La méthode HTMLVideoElement.requestVideoFrameCallback() permet aux auteurs Web d'enregistrer un rappel qui s'exécute lors des étapes de rendu lorsqu'une nouvelle image vidéo est envoyée au compositeur. Cela permet aux développeurs d'effectuer des opérations efficaces par frame vidéo, telles que le traitement et la peinture vidéo sur un canevas, l'analyse vidéo ou la synchronisation avec des sources audio externes.

Différence avec requestAnimationFrame()

Les opérations telles que le dessin d'une image vidéo sur un canevas à l'aide de drawImage() effectuées via cette API seront synchronisées au mieux avec la fréquence d'images de la vidéo lue à l'écran. Contrairement à window.requestAnimationFrame(), qui se déclenche généralement environ 60 fois par seconde, requestVideoFrameCallback() est lié à la fréquence d'images réelle de la vidéo, avec une exception importante :

La fréquence effective à laquelle les rappels sont exécutés est la plus faible entre la fréquence de la vidéo et celle du navigateur. Cela signifie qu'une vidéo à 25 FPS lue dans un navigateur qui effectue un rendu à 60 Hz déclenchera des rappels à 25 Hz. Une vidéo à 120 fps dans ce même navigateur à 60 Hz déclenchera des rappels à 60 Hz.

Qu'est-ce qui se cache derrière un nom ?

En raison de sa similitude avec window.requestAnimationFrame(), la méthode a d'abord été proposée sous le nom video.requestAnimationFrame(), puis renommée requestVideoFrameCallback(), ce qui a été décidé après une longue discussion.

Détection de caractéristiques

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

Prise en charge des navigateurs

Browser Support

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

Source

Remplissage

Un polyfill pour la méthode requestVideoFrameCallback() basé sur Window.requestAnimationFrame() et HTMLVideoElement.getVideoPlaybackQuality() est disponible. Avant d'utiliser cette fonctionnalité, tenez compte des limites mentionnées dans README.

Utiliser la méthode requestVideoFrameCallback()

Si vous avez déjà utilisé la méthode requestAnimationFrame(), vous vous sentirez immédiatement à l'aise avec la méthode requestVideoFrameCallback(). Vous enregistrez un rappel initial une seule fois, puis vous le réenregistrez chaque fois qu'il se déclenche.

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

Dans le rappel, now est un DOMHighResTimeStamp et metadata est un dictionnaire VideoFrameMetadata avec les propriétés suivantes :

  • presentationTime, de type DOMHighResTimeStamp : heure à laquelle l'agent utilisateur a soumis le frame pour composition.
  • expectedDisplayTime, de type DOMHighResTimeStamp : heure à laquelle l'user-agent s'attend à ce que le frame soit visible.
  • width, de type unsigned long : largeur du frame vidéo, en pixels média.
  • height, de type unsigned long : Hauteur du frame vidéo, en pixels média.
  • mediaTime, de type double : code temporel de présentation (PTS) en secondes de la frame présentée (par exemple, son code temporel sur la timeline video.currentTime).
  • presentedFrames, de type unsigned long : nombre de frames envoyées pour la composition. Permet aux clients de déterminer si des frames ont été manqués entre les instances de VideoFrameRequestCallback.
  • processingDuration, de type double : durée écoulée en secondes entre l'envoi du paquet encodé avec le même code temporel de présentation (PTS) que ce frame (par exemple, le même que mediaTime) au décodeur et le moment où le frame décodé était prêt à être présenté.

Pour les applications WebRTC, des propriétés supplémentaires peuvent s'afficher :

  • captureTime, de type DOMHighResTimeStamp : Pour les images vidéo provenant d'une source locale ou distante, il s'agit de l'heure à laquelle l'image a été capturée par la caméra. Pour une source distante, l'heure de capture est estimée à l'aide de la synchronisation de l'horloge et des rapports de l'expéditeur RTCP pour convertir les codes temporels RTP en heure de capture.
  • receiveTime, de type DOMHighResTimeStamp : Pour les images vidéo provenant d'une source distante, il s'agit de l'heure à laquelle la plate-forme a reçu l'image encodée, c'est-à-dire l'heure à laquelle le dernier paquet appartenant à cette image a été reçu sur le réseau.
  • rtpTimestamp, de type unsigned long : Code temporel RTP associé à cette image vidéo.

mediaTime est particulièrement intéressant dans cette liste. L'implémentation de Chromium utilise l'horloge audio comme source temporelle pour video.currentTime, tandis que mediaTime est directement renseigné par le presentationTimestamp du frame. mediaTime est ce que vous devez utiliser si vous souhaitez identifier précisément les frames de manière reproductible, y compris pour identifier exactement les frames que vous avez manqués.

Si les choses semblent décalées d'une image…

La synchronisation verticale (ou vsync) est une technologie graphique qui synchronise la fréquence d'images d'une vidéo et la fréquence d'actualisation d'un écran. Étant donné que requestVideoFrameCallback() s'exécute sur le thread principal, mais que, en coulisses, la composition vidéo se produit sur le thread du compositeur, tout ce qui provient de cette API est un effort maximal, et le navigateur n'offre aucune garantie stricte. Il est possible que l'API soit en retard d'une synchronisation verticale par rapport au moment où un frame vidéo est rendu. Il faut une synchronisation verticale pour que les modifications apportées à la page Web via l'API s'affichent à l'écran (comme pour window.requestAnimationFrame()). Par conséquent, si vous continuez à mettre à jour le mediaTime ou le numéro de frame sur votre page Web et que vous le comparez aux frames vidéo numérotés, la vidéo finira par sembler avoir une frame d'avance.

En réalité, le frame est prêt à la synchronisation verticale x, le rappel est déclenché et le frame est rendu à la synchronisation verticale x+1, et les modifications apportées au rappel sont rendues à la synchronisation verticale x+2. Vous pouvez vérifier si le rappel est en retard sur la synchronisation verticale (et si le frame est déjà affiché à l'écran) en vérifiant si metadata.expectedDisplayTime est à peu près égal à now ou à une synchronisation verticale dans le futur. Si elle se situe à environ cinq à dix microsecondes de now, le frame est déjà rendu. Si expectedDisplayTime se situe à environ seize millisecondes dans le futur (en supposant que votre navigateur/écran se rafraîchisse à 60 Hz), vous êtes synchronisé avec le frame.

Démo

J'ai créé une petite démonstration qui montre comment les frames sont dessinées sur un canevas exactement à la fréquence d'images de la vidéo et où les métadonnées des frames sont enregistrées à des fins de débogage.

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

Conclusions

Les utilisateurs traitent les images depuis longtemps, sans avoir accès aux images réelles, mais uniquement en se basant sur video.currentTime. La méthode requestVideoFrameCallback() améliore considérablement cette solution de contournement.

Remerciements

L'API requestVideoFrameCallback a été spécifiée et implémentée par Thomas Guilbert. Cet article a été examiné par Joe Medley et Kayce Basques. Image héros de Denise Jans sur Unsplash.