Skip to content

@remotion/media: Avoid rounding seeks below fractional keyframes - #12132

Open
alec-watts wants to merge 4 commits into
remotion-dev:mainfrom
alec-watts:codex/exact-frame-bank-start
Open

alec-watts wants to merge 4 commits into
remotion-dev:mainfrom
alec-watts:codex/exact-frame-bank-start

Conversation

@alec-watts

@alec-watts alec-watts commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Rounding the opening timestamp downward can move a seek just before a fractional MP4 keyframe. Passing the raw timestamp avoids that extra GOP, but regresses WebM when its millisecond-quantized keyframe timestamp lies slightly above the composition timestamp.

Use Math.max(initialTimestampRequest, roundTo4Digits(initialTimestampRequest)): preserve the existing upward tolerance, while never rounding the opening seek below the exact request. Later bank comparisons and frame-matching tolerance remain unchanged. This addresses the WebM counterexample in the maintainer review.

Native decoder verification

Fresh comparison against this branch's source, public Mediabunny 1.61.3, and native WebCodecs in hidden Electron. Each variant ran three trials for each of eight requests: the MP4 and WebM boundaries, requests 0.1 ms before their composition boundaries, requests 2 ms before the stored keyframes, and zero-time controls. 72 delivered frames matched full RGBA hashes from independent VideoSampleSink.getSample() references. All measured decoders were closed after bank/input disposal.

Native VideoDecoder.decode() calls at the boundary Original rounded seek Previous PR raw seek Revised one-sided seek
H.264 MP4: frame 61 at 61061 / 30000 104 43 43
VP9 WebM: frame 62 requested at 62 / 30, stored at 2.067 42 104 42

Counts were identical across all three trials per case. The MP4 improvement remains 61 avoided decode calls; the WebM regression is removed. The revised WebM seek also preserves 42 calls just before the boundary. MP4 requests just before its exact keyframe still require the earlier GOP, and requests 2 ms before either stored keyframe still deliver the preceding frame. Zero-time controls are unchanged. This measures decoder work for these fixtures, not editor latency or a general playback speedup.

Fixture generation:

ffmpeg -f lavfi -i testsrc2=size=128x72:rate=30000/1001 -frames:v 150 \
  -c:v libx264 -g 61 -keyint_min 61 -sc_threshold 0 -bf 0 \
  -video_track_timescale 30000 fractional-keyframe.mp4
ffmpeg -f lavfi -i testsrc2=size=128x72:rate=30 -frames:v 130 \
  -c:v libvpx-vp9 -g 62 -keyint_min 62 quantized.webm

Validation

  • 26 targeted browser tests pass across seven files: keyframe bank, iterator manager, preview iterator, nonzero video start, missing beginning frames/WebM, variable FPS, and after-end behavior.
  • Extend the existing bank regression to cover fractional MP4 and millisecond WebM boundaries plus requests just before each. It runs the real bank with VideoSample objects and a boundary-aware sink, checks consumed frames, and checks samples close exactly once.
  • Replacing the revised seek with the previous PR's raw seek makes both WebM cases fail. Replacing it with unconditional rounding makes the fractional MP4 boundary case fail. Restoring the revision passes the full targeted suite.
  • Focused TypeScript check of the bank and modified test, formatting, and git diff --check pass.
  • Browser tests use a temporary configuration resolving bundles from this checkout and installed headless Chrome. Native measurement uses a temporary hidden Electron harness. No test configuration, dependency changes, fixtures, or instrumentation ship. Full monorepo CI remains a separate check.

Why each file changes

File Reason
packages/media/src/video-extraction/keyframe-bank.ts Keep upward seek tolerance while preventing downward rounding before a fractional keyframe.
packages/media/src/test/keyframe-bank.test.ts Guard both timestamp boundaries, nearby requests, consumption, and sample cleanup in the existing suite.

Diff cleanup found no unrelated formatting, dependencies, parallel decoder mechanism, or unused exports.

Shared CI test repair

Includes the one-line caption-inspector track selector fix from #12179. Current main fails this test before it can exercise caption persistence. Both caption-inspector E2E cases pass in the actual Studio with this repair; restoring the old selector reproduces the failure.

File Reason
packages/example/e2e/captions-inspector.test.mts Repair the existing main-branch selector so shared CI can exercise caption persistence.

@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bugs Ready Ready Preview Oct 9, 2026 9:10pm UTC
remotion Ready Ready Preview Oct 9, 2026 9:10pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T03:27:44.519688Z 8a39498 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Exact seek timestamp for the frame bank — makeKeyframeBank now passes initialTimestampRequest straight to VideoSampleSink.samples() instead of roundTo4Digits(...), so a fractional keyframe boundary like 61061 / 30000 is no longer rounded down below itself. This is sound: Mediabunny's getKeyPacket returns the last key packet with start timestamp <= the request, so an unrounded timestamp can only select the same or a later keyframe — never the previous GOP — and the sample sink's predecessor handling still yields the requested frame first.
  • Fractional-boundary regression test — the new starts at a fractional keyframe without reading the preceding frame case drives the real bank with a boundary-aware mock sink and asserts only the requested frame is consumed and each emitted sample is closed exactly once. It correctly fails against the pre-fix bank (which also consumes the 2.002 predecessor).

I traced the changed path through extract-frame.ts → keyframe-manager.ts → keyframe-bank.ts and confirmed the timestamp handed to samples() is the requested frame's presentation time, and that the existing rounded comparisons used for in-bank frame matching are untouched.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash | 𝕏

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a394985c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const sampleIterator = videoSampleSink.samples(
roundTo4Digits(initialTimestampRequest),
);
const sampleIterator = videoSampleSink.samples(initialTimestampRequest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain the quantized-track seek tolerance

When a keyframe's stored timestamp is rounded upward by a millisecond-timescale container, the raw composition timestamp falls just before that keyframe. For example, a 30 fps WebM keyframe stored at 0.067 is requested as 2 / 30 (0.06666…); passing that value to samples() starts decoding from the previous GOP, while the previous roundTo4Digits() call landed on 0.067. The bank still returns the right frame, but each new bank/seek can now decode an entire unnecessary GOP for these tracks, so this needs a one-sided or timescale-aware tolerance rather than removing the rounding unconditionally.

Useful? React with 👍 / 👎.

@JonnyBurger JonnyBurger 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.

The fractional MP4 improvement is valid, but removing seek rounding unconditionally introduces the opposite performance regression for millisecond-quantized WebM timestamps.

I independently reproduced the counterexample with a real 30 fps VP9 WebM, 130 frames, and GOP length 62:

ffmpeg -f lavfi -i testsrc2=size=128x72:rate=30 -frames:v 130 \
  -c:v libvpx-vp9 -g 62 -keyint_min 62 quantized.webm

Frame 62's composition timestamp is 62 / 30 = 2.066666666666667, but its stored keyframe timestamp is 2.067. Querying Mediabunny's EncodedPacketSink.getKeyPacket() with verifyKeyPackets: true gives:

Opening seek Selected keyframe
Current rounded seek: 2.067 2.067
PR's raw seek: 2.066666666666667 0

The raw seek therefore starts in the preceding GOP and requires decoding through 62 earlier frames. This verifies keyframe selection on an actual file; it is not a native decoder-call or latency measurement. The bank's existing rounded comparisons should still select the intended output frame, but they cannot recover the decoder work already spent reaching it.

Please preserve upward seek tolerance while preventing downward rounding, and add a regression covering this WebM case alongside the fractional MP4 case. A candidate is Math.max(initialTimestampRequest, roundTo4Digits(initialTimestampRequest)); validate it for both boundaries and requests just before a keyframe before choosing the final implementation.

@alec-watts alec-watts changed the title perf(media): preserve exact frame-bank seek timestamps @remotion/media: Avoid rounding seeks below fractional keyframes Oct 9, 2026

This branch was successfully deployed

2 active deployments
Preview – remotion — a80f014b Deployed Oct 9, 2026 by vercel[bot]
Preview – bugs — a80f014b Deployed Oct 9, 2026 by vercel[bot]
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.

3 participants