AskyMcAnswerFace
AskyMcAnswerFace

Reputation: 362

Event for every frame of HTML Video?

I'd like to build an event handler to deal with each new frame of an HTML 5 Video element. Unfortunately, there's no built in event that fires for each new video frame (the timeupdate event is the closest but fires for each time change rather than each video frame).

Has anyone else run into this same issue? Is there a good way around it?

Upvotes: 3

Views: 4423

Answers (1)

Kaiido
Kaiido

Reputation: 136627

There is an HTMLVideoElement.requestVideoFrameCallback() method that is still being drafted, and thus neither stable, nor widely implemented (it is only in Chromium based browsers), but which does what you want, along with giving many other details about that frame.

For your Firefox users, this browser has a non standard seekToNextFrame() method, which, depending on what you want to do you could use. This won't exactly work as an event though, it more of a way to, well... seek to the next frame. So this will greatly affect the playing of the video, since it won't respect the duration of each frames.

And for Safari users, the closest is indeed the timeupdate event, but as you know, this doesn't really match the displayed frame.

(async() => {
const log = document.querySelector("pre");
const vid = document.querySelector("video");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
if( vid.requestVideoFrameCallback ) {
  await vid.play();
  canvas.width = vid.videoWidth;
  canvas.height = vid.videoHeight;
  ctx.filter = "invert(1)";
  const drawingLoop = (timestamp, frame) => {
    log.textContent = `timestamp: ${ timestamp }
frame: ${ JSON.stringify( frame, null, 4 ) }`;
    ctx.drawImage( vid, 0, 0 );
    vid.requestVideoFrameCallback( drawingLoop );  
  };
  vid.requestVideoFrameCallback( drawingLoop );
}
else if( vid.seekToNextFrame ) {
  const requestNextFrame = (callback) => {
    vid.addEventListener( "seeked", () => callback( vid.currentTime ), { once: true } );
    vid.seekToNextFrame();
  };
  await vid.play();
  await vid.pause();
  canvas.width = vid.videoWidth;
  canvas.height = vid.videoHeight;
  ctx.filter = "invert(1)";
  const drawingLoop = (timestamp) => {
    log.textContent = "timestamp: " + timestamp;
    ctx.drawImage( vid, 0, 0 );
    requestNextFrame( drawingLoop );
  };
  requestNextFrame( drawingLoop );
}
else {
  console.error("Your browser doesn't support any of these methods, we should fallback to timeupdate");
}
})();
video, canvas {
  width: 260px;
}
<pre></pre>
<video src="https://upload.wikimedia.org/wikipedia/commons/2/22/Volcano_Lava_Sample.webm" muted controls></video>
<canvas></canvas>

Note that the encoded frames and the displayed ones are not necessarily the same thing anyway and that browser may not respect the encoded frame rate at all. So based on what you are willing to do, maybe a simple requestAnimationFrame loop, which would fire at every update of the monitor might be better.

Upvotes: 8

Related Questions