Skip to content

Always show video frame at or before timestamp - #1531

Open
yannik131 wants to merge 7 commits into
masterfrom
1492-video-frame-mismatch
Open

Always show video frame at or before timestamp#1531
yannik131 wants to merge 7 commits into
masterfrom
1492-video-frame-mismatch

Conversation

@yannik131

@yannik131 yannik131 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Closes #1492

@yannik131 yannik131 changed the title Mediabunny for video frame mismatch Always show video frame at or before timestamp Aug 21, 2026
@yannik131
yannik131 marked this pull request as ready for review August 22, 2026 15:25
@yannik131

Copy link
Copy Markdown
Contributor Author

@mzur

  • mediabunny displays frame for paused video. Small visual glitch/delay when the video is paused and mediabunny displays a different frame than the last one the browser rendered, but that is as expected
  • mediabunny-like ffmpeg command for thumbnail generation
  • some unrelated tests had a division by zero error on my machine, quick fixes

Since my free plan can barely handle 2-3 reviews, from now on I'll just request a review from you and you can request an AI review if your budget allows it, I'll revisit the PR on my own if you do that.

@yannik131
yannik131 requested a review from mzur August 22, 2026 15:36

@mzur mzur left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very promising and decoding with Mediabunny seems as fast as the old "seek to currentTime on pause" trick. I hope that we can close all the issues related to frame-accuracy as part of this PR. The warning message for Firefox should be removed as part of this PR, too.

Did you test for correctness with the artificial videos again?

When I reload the page and the video is seeked to a previous time, I sometimes get the first video frame instead. Looks like an async race issue:

Screencast.From.2026-09-02.14-56-14.webm

More comments below.

Comment on lines +90 to +91
this.videoContext.drawImage(this.video, 0, 0, this.videoCanvas.width, this.videoCanvas.height);
this.videoSource.changed();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse drawBitmap() here?

if (this.video.paused) {
await this.renderPausedFrame();
} else {
this.renderVideo(true);

@mzur mzur Sep 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the else case still needed for anything? The old workaround was to seek to currentTime to force the browser to jump to the frame that will be displayed at that time (see setPausedAndSeek()). This was done to make sure the same frame will be shown next time based on the timestamp that was stored for an annotation.

This didn't fix the cross-browser and decoding inconsistencies that we want to fix with this PR. But maybe the currentTime = currentTime trick is no longer required with this PR, too? If we remove this we have to call renderPausedFrame directly on the pause event. Also we should make the process of decoding with Mediabunny visible in the UI somehow (currently the seeking state does this).

Ideally we can close #433 as part of this PR too.

Comment on lines +274 to +284
try {
this.mediabunnyInput = new Input({
source: new UrlSource(this.video.src),
formats: ALL_FORMATS
});
const track = await this.mediabunnyInput.getPrimaryVideoTrack();
this.mediabunnySink = new VideoSampleSink(track);
} catch (e) {
this.useMediabunnyFallback = false;
throw e;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this fail? I'd rather fail hard if Mediabunny is not available than to display wrong frames again as fallback (i.e. Mediabunny should be the default method, not the fallback). What do you think?

Comment on lines +291 to +295
const roundedTime = Math.round(time * 100) / 100;
if (this.cachedTime === roundedTime && this.cachedBitmap) {
this.drawBitmap(this.cachedBitmap);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rounds to 10s of ms, right? If decoding is fast, shouldn't we skip rounding/caching and use the raw timestamp for the sake of correctness instead?

protected function getVideoFrame(Video $video, float $time, int $trySeek = 60)
protected function getVideoFrame(string $sourcePath, float $time, int $trySeek = 60)
{
$fps = max($this->getVideoFps($sourcePath), 0.0001);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done outside the loop in processAnnotationChunk and maybe even before the chunking in handleFile. The fps can be cached once for the whole job.


$buffer = File::get($outputPath);
} catch (Throwable $e) {
$buffer = '';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this really be handled as empty buffer instead of failing immediately? If there is something wrong with the video, the job retries 60 times before giving up.

Comment on lines +334 to +337
this.video.addEventListener('loadedmetadata', () => {
this.initMediabunny();
});
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the same video object is reused and only src is updated, it should be enough to add the listener once in created().

// 1.5M annotations on 16k videos).
$time = max(0, $time - 0.033333333);
} while (empty($buffer) && $trySeek > 0);
$time = max(0, $time - (1 / 30.0));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now know the actual framerate so we can use exact steps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Largo and annotation tool can produce different frames in different video decoders

2 participants