DEV Community

Simon Cleriot
Simon Cleriot

Posted on

4 2

Video.js: frame-accurate subtitles

In the post-production and broadcast industry videos are processed frame by frame.
Subtitles follow the same rule : in and out timecodes are frame accurate (ex: 00:00:23:22 -> 23 seconds and 22 images).

Browsers process videos with millisecond timestamps, that's why subtitles have to be converted based on video framerate: for video at 25 images per second the previous timecode would look like 00:00:23.880 in milliseconds format (1000/25 * 22 = 880).

Frame accuracy is really important : subtitles need to disappear right before the next cut and appear right after the previous one.
Problem is that default HTML5 video's refresh rate is too low: subtitles appear and disappear too late creating a poor viewer's experience (and a non-broadcast compliant one).

Example below higlights the issue. Subtitles in the fixed video disappear right before the change of plan:

Demo gif

It might seem marginal, but the post production industry needs frame accuracy.

Due to browser limitations, timeupdate (event fired when the playing position of a video has changed) is fired every 150-250 milliseconds. It is not enough for frame-accuracy: 25fps means an update every 40ms.
We need to compute which subtitle has to be displayed every frame (instead of doing it every 4-5 frames by default). Video.js subtitles engine does the computation each time the text track's attribute activeCues getter is called.

text-track.js extract from Video.js source code:

Object.defineProperty(tt, 'activeCues', {
    get() {
        if (!this.loaded_) {
            return null;
        }
        // nothing to do
        if (this.cues.length === 0) {
            return activeCues;
        }
        const ct = this.tech_.currentTime();
        const active = [];
        for (let i = 0, l = this.cues.length; i < l; i++) {
            const cue = this.cues[i];

            if (cue.startTime <= ct && cue.endTime >= ct) {
                active.push(cue);
            } else if (cue.startTime === cue.endTime &&
                cue.startTime <= ct &&
                cue.startTime + 0.5 >= ct) {
                active.push(cue);
            }
        }
        changed = false;
        if (active.length !== this.activeCues_.length) {
            changed = true;
        } else {
            for (let i = 0; i < active.length; i++) {
                if (this.activeCues_.indexOf(active[i]) === -1) {
                    changed = true;
                }
            }
        }
        this.activeCues_ = active;
        activeCues.setCues_(this.activeCues_);
        return activeCues;
    },
    set() {}
});
Enter fullscreen mode Exit fullscreen mode

Then you need to call trigger('cuechange') on the text track to make sure the video display is up to date:

player.textTracks()[0].activeCues; // computes the current subtitle based on current time
player.textTracks()[0].trigger('cuechange'); // updates the display
Enter fullscreen mode Exit fullscreen mode

requestAnimationFrame is optimized for animations and has a lot less delay than setInterval or setTimeout, so we are going to use it for our time sensitive loop (Frame rate control source here).

Here is the complete source code:

var fps = 25;
var now;
var then = Date.now();
var interval = 1000/fps;
var delta;

function reloadCues() {
    requestAnimationFrame(reloadCues);

    now = Date.now();
    delta = now - then;

    if (delta > interval) {
        then = now - (delta % interval);

        if(videojs.players.player.textTracks().length == 1) {
            videojs.players.player.textTracks()[0].activeCues;
            videojs.players.player.textTracks()[0].trigger('cuechange')
        }
    }
}
reloadCues();
Enter fullscreen mode Exit fullscreen mode

Demo project is available here.

Originally published on my personal blog.

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs