Reputation: 361
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.
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
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:
vid
is the HTML video elementlast_media_time
and last_frame_run
will be used later to determine differences between frames and get the FPSfps
is obviousfps_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 2frame_not_seeked
is to stop users from moving around the video and messing up FPS, more about this in step 3var vid = document.querySelector("video");
var last_media_time, last_frame_num, fps;
var fps_rounder = [];
var frame_not_seeked = true;
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);
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;
});
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;
}
// 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