Skip to content

@remotion/web-renderer: Preserve final video sample duration - #12177

Merged
JonnyBurger merged 2 commits into
remotion-dev:mainfrom
alec-watts:codex/fix-final-browser-export-frame-duration
Oct 12, 2026
Merged

JonnyBurger merged 2 commits into
remotion-dev:mainfrom
alec-watts:codex/fix-final-browser-export-frame-duration

Conversation

@alec-watts

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

Copy link
Copy Markdown
Contributor

Browser MP4 exports contain the requested pictures but give the final video sample zero duration. A 61-frame export at 30 fps therefore has a 2-second video track instead of 61/30 seconds; a one-frame export has zero video duration.

The renderer constructs a VideoFrame with a timestamp but no duration. VideoSample defaults the missing duration to zero. The MP4 muxer can infer earlier sample durations from subsequent timestamps, but the final sample has no subsequent timestamp. Pass 1 / resolved.fps through the existing sample helper so the muxer receives the intended final-frame duration, including when onFrame returns a replacement frame without a duration.

Validation against upstream source and public Mediabunny 1.61.3 in headless Chrome:

  • All 19 browser tests in frame-range.test.tsx, on-frame.test.tsx, and render-media.test.tsx pass.
  • New MP4 regressions inspect video-track duration, packet count, and the final packet duration for 1, 2, and 61 frames at 30 fps, plus 7 frames at 24 and 60 fps. The two-frame case returns a replacement frame through onFrame.
  • Removing both production changes makes all five new cases and the tightened existing nonzero-start frame-range duration test fail. Restoring the changes passes. The existing test's one-decimal tolerance previously allowed a missing 30-fps frame.
  • Focused TypeScript check of the renderer and modified test, formatting, and git diff --check pass. Local browser validation used a temporary config to resolve upstream core/licensing bundles and select installed Chrome; no test setup or dependency changes are committed. Firefox, WebKit, other codecs, and the full monorepo build were not run locally.
File Why it changes
packages/web-renderer/src/add-sample.ts Give the existing video sample helper an explicit duration in seconds.
packages/web-renderer/src/render-media-on-web.tsx Supply one composition frame's duration to that helper.
packages/web-renderer/src/test/frame-range.test.tsx Pin the exported MP4 sample timing and tighten the existing duration assertion.

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:12pm UTC
remotion Ready Ready Preview Oct 9, 2026 9:12pm UTC

Request Review

@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

  • add-sample.ts — explicit video sample duration: addVideoSampleAndCloseFrame() now accepts durationInSeconds and forwards it to new VideoSample(frame, {duration}), so the muxer no longer defaults the sample duration to zero. The duration is applied to every frame, which is what lets the muxer derive the final sample's end time.
  • render-media-on-web.tsx — per-frame duration: the encode loop passes 1 / resolved.fps, matching the microsecond timestamp step already used at render-media-on-web.tsx:628. This also covers onFrame replacement frames, since frameToEncode is whatever validateVideoFrame() returns.
  • frame-range.test.tsx — regressions: adds a test.for matrix asserting track duration, packet count, and final-packet duration for 1/2/61 frames at 30 fps plus 7 frames at 24/60 fps (one case exercising an onFrame replacement), and tightens the existing frame-range duration tolerance from 1 to 5 decimals.

Notes verified during review:

  • 1 / resolved.fps truncates below the rounded microsecond timestamp step (e.g. 24 fps → 41666 vs 41667), so explicit durations cannot overlap the following sample.
  • Mediabunny's timestamp/duration reconstruction (preciseTimings in media-source.js) marks a duration invalid only when two samples share a timestamp with differing durations; constant per-frame durations keep every entry valid.
  • No transform frameRate is set on the VideoSampleSource, so the retiming/padding path stays inactive (the frameRate at render-media-on-web.tsx:542 only feeds the track metadata).
  • addVideoSampleAndCloseFrame is internal (single call site), so the new required positional argument is not a public API break.

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

@JonnyBurger
JonnyBurger merged commit a11b31c into remotion-dev:main Oct 12, 2026
18 of 19 checks passed

This branch was successfully deployed

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