derder56
derder56

Reputation: 361

How do I get the frame rate of an HTML video with JavaScript?

Edit: I was able to figure this out, scroll down to the top answer to see it. AFAIK no other post on Stack (or anywhere online) with this question had a working and consistent answer.

Anyway...

I’m building a retiming tool for videos on YouTube and other websites, and finding the frame rate of the video element is necessary for frame-by-frame seeking and rounding reasons.

The way I’m doing it so far is by using requestVideoFrameCallback() and finding the smallest difference between two frame times: (sorry for bad formatting, I’m on mobile)

var v = document.querySelector(“video”)
var c = 0; // time of last frame 
var framelength = 1; // length of one frame 
var fps;
function check(t, m) {
  var diff = Math.abs(m.mediaTime - c); // difference between this frame and the last
  if (diff && diff < framelength) {
    framelength = diff;
    fps = Math.round( 1 / framelength)
  }
  c = m.mediaTime;
  v.requestVideoFrameCallback(check);
}
v.requestVideoFrameCallback(check);

However, there are sometimes rounding issues due to the mediaTime having only 3 decimal places, so is there a better way to find the FPS with JavaScript?

Upvotes: 8

Views: 5723

Answers (1)

derder56
derder56

Reputation: 361

Using a mix of some hacky requestVideoFrameCallback stuff, as well as making an array and rounding it, I was able to get a consistent and accurate FPS for any video. I wish I had been able to found this online when I needed it, but here it is for anybody else who had the same question as me:

Step 1: Set variables

  • vid is the HTML video element
  • last_media_time and last_frame_run will be used later to determine differences between frames and get the FPS
  • fps is obvious
  • fps_rounder is for rounding and stopping stuttering video frames from messing up the FPS, it actually contains the differences between frames, we make it have like 50 items to be sure, more about this in step 2
  • frame_not_seeked is to stop users from moving around the video and messing up FPS, more about this in step 3
var vid = document.querySelector("video");
var last_media_time, last_frame_num, fps;
var fps_rounder = [];
var frame_not_seeked = true;

Step 2: Set a ticker function with rVFC

We use .mediaTime and .presentedFrames to get a good diff, which would be either 0.016 or 0.017, assuming the video is 60fps. Then we shove it all in fps_rounder so we can actually have it even out to 0.016666ish so we can be sure it's 60fps and not like 59 or 61. I would recommend not trying to reference the FPS in the code unless fps_rounder has at least 50 items in it, just so you can be sure that stuttering won't mess it up.

function ticker(useless, metadata) {
  var media_time_diff = Math.abs(metadata.mediaTime - last_media_time);
  var frame_num_diff = Math.abs(metadata.presentedFrames - last_frame_num);
  var diff = media_time_diff / frame_num_diff;
  if (diff && diff < 1 && frame_not_seeked && fps_rounder.length < 50 && vid.playbackRate === 1 && document.hasFocus()) {
    fps_rounder.push(diff);
    fps = Math.round(1 / get_fps_average());
  }
  frame_not_seeked = true;
  last_media_time = metadata.mediaTime;
  last_frame_num = metadata.presentedFrames;
  vid.requestVideoFrameCallback(ticker);
}
vid.requestVideoFrameCallback(ticker);

Step 3: Stop seeking from messing it up

As you might have seen in the above code, it references frame_not_seeked. This is because when a user clicks and changes the timestamp of a video, it can sometimes mess things up since .mediaTime is changing a lot but .presentedFrames is only changing by 1. So when the video is seeked, we remove the last item from fps_rounder, and stop the next diff from being added to fps_rounder by setting frame_not_seeked to false, just to be safe.

vid.addEventListener("seeked", function () {
  fps_rounder.pop();
  frame_not_seeked = false;
});

Step 4: Make an averaging function

This is mentioned in step 2, so obviously we need it to exist. It's just a simple function that averages an array using .reduce(), in this case getting the average of fps_rounder and therefore an accurate FPS.

function get_fps_average() {
  return fps_rounder.reduce((a, b) => a + b) / fps_rounder.length;
}

Putting it all together

    // Part 1:
    var vid = document.querySelector("video");
    var last_media_time, last_frame_num, fps;
    var fps_rounder = [];
    var frame_not_seeked = true;
    // Part 2 (with some modifications):
    function ticker(useless, metadata) {
      var media_time_diff = Math.abs(metadata.mediaTime - last_media_time);
      var frame_num_diff = Math.abs(metadata.presentedFrames - last_frame_num);
      var diff = media_time_diff / frame_num_diff;
      if (
        diff &&
        diff < 1 &&
        frame_not_seeked &&
        fps_rounder.length < 50 &&
        vid.playbackRate === 1 &&
        document.hasFocus()
      ) {
        fps_rounder.push(diff);
        fps = Math.round(1 / get_fps_average());
        document.querySelector("p").textContent = "FPS: " + fps + ", certainty: " + fps_rounder.length * 2 + "%";
      }
      frame_not_seeked = true;
      last_media_time = metadata.mediaTime;
      last_frame_num = metadata.presentedFrames;
      vid.requestVideoFrameCallback(ticker);
    }
    vid.requestVideoFrameCallback(ticker);
    // Part 3:
    vid.addEventListener("seeked", function () {
      fps_rounder.pop();
      frame_not_seeked = false;
    });
    // Part 4:
    function get_fps_average() {
      return fps_rounder.reduce((a, b) => a + b) / fps_rounder.length;
    }
<p>The FPS will appear here!</p>
<video id="myVideo" width="320" height="176" controls>
  <source src="https://www.w3schools.com/tags/mov_bbb.mp4" type="video/mp4">
</video>

Have fun!

Upvotes: 7

Related Questions