Skip to content

fix(producer): exports no longer fail on a video clip outside the composition's length - #5431

Merged
miguel-heygen merged 5 commits into
mainfrom
fix/vspeed-extract
Oct 11, 2026
Merged

miguel-heygen merged 5 commits into
mainfrom
fix/vspeed-extract

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

What

A render no longer aborts with Video "<id>" captured 0 of expected N frames when a video clip starts at or after the composition's end (the root data-duration). That clip is never on screen, so the export now completes without it. Distributed renders of the same composition no longer fail planning with meta/videos.json.extracted is missing declared video "<id>".

Why

Extraction already skips such a clip, and lint already warns that it is cut off. But the clip stayed in the composition's video list, so the frame-coverage gate expected its full length, found no frames and aborted a correct render; the distributed plan rejected it one step later for the same reason.

Re-running fails the same way within a second: the visible clips hit the extraction cache and the same clip is skipped again, so it looks like a stale cached result even though nothing stale is involved.

Repro on current main (3 s composition, one clip at 0 s and one at 4 s):

<div id="root" data-composition-id="past-end" data-width="320" data-height="240" data-start="0" data-duration="3">
  <video id="v-in" src="clip.mp4" data-start="0" data-duration="2" data-track-index="1" muted></video>
  <video id="v-past" src="clip.mp4" data-start="4" data-duration="2" data-track-index="2" muted></video>
</div>

hyperframes render on main fails: Video "v-past" captured 0 of expected 60 frames (coverage 0.0%, threshold 95.0%). With this change it completes: a 3.000 s mp4, 90 frames, with v-in visible.

Related work

None open. The extractor's early skip for these clips predates this change; this makes the rest of the render agree with it.

How

  • isVideoPastTimelineEnd(video, timelineEnd) (engine): start >= timelineEnd. The extractor's existing early skip now calls it.
  • The extract stage, which in-process and distributed renders both run, removes such clips from the composition before probing and extraction, so every later reader (coverage gate, frame lookup, HDR planning, distributed plan metadata) sees only clips that can be on screen.

Deliberately not covered: clips that lie entirely before 0. A clip with no authored length carries end 0 (script-created media, or a src set from a script), the same value as an authored end at 0, so "ends at or before 0" cannot be told apart from "natural length" here. Such clips keep main's behaviour (the coverage gate aborts loudly rather than shipping a frozen clip).

Review history: a first version set end = start on skipped clips (the window resolver reads that as natural length; distributed plan still broken). A second dropped clips with end <= 0, which froze natural-length clips; a third required end > start, which still froze a natural-length clip with a negative start. All three were caught by independent review and are replaced by the start-only rule.

Test plan

  • Unit tests added/updated: extractVideosStage.timelineBound.test.ts, run with and without materialized symlinks (the stage options the distributed plan uses; the plan builder itself is not exercised by a unit test, it was checked on the repro below): a clip starting exactly at the end never reaches extraction and leaves the composition; a clip starting just before the end, and natural-length clips (end 0, with start 0 and with start -1), stay. An over-eager drop (start + 0.5 >= end): 2 failed. Without the filter: 2 failed, 9 passed. With > instead of >=: 2 failed, 9 passed. 3 runs in a row: exit 0, 11 passed. videoFrameExtractor.test.ts unchanged: exit 0, 199 passed.
  • Manual testing performed (hyperframes render built from this branch):
    • The repro above: main fails; this branch completes (3.000 s, 90 frames, v-in visible).
    • A video whose src is set from a script, no authored length: extracted, frames at 1 s and 2 s differ (it plays).
    • The same with data-start="-1": fails with captured 0 of expected 30 frames, as on main.
    • Distributed plan() on the repro: main fails with meta/videos.json.extracted is missing declared video "v-past"; this branch plans one chunk.
  • Documentation updated (if applicable): not applicable.
  • Comments follow CONTRIBUTING.md "Comments".

Not exercised: distributed renderChunk and assemble end to end; a full HDR render with an HDR clip past the end. A dropped clip's <video> element still loads in the page, so with --no-best-effort a missing file for it still blocks the render (as on main).

…sition's length

A clip that starts at or after the composition's end (or ends before 0) is
never on screen, so extraction skips it. Its timeline window stayed as
authored, so the frame-coverage gate still expected its full length and
aborted the render with "captured 0 of expected N frames". Such a clip now
leaves extraction with an empty window, the same way a partly visible clip
is already trimmed to its visible part, so it owes no frames.
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2061 (base branch 2061), smooth 1541 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

…ud renders included

The previous commit marked such a clip with end = start, but the window
resolver reads a zero-length slot as "use the source's natural length", and
the distributed plan still required an extracted entry for every declared
clip. Instead, the extract stage (shared by in-process and distributed
renders) removes clips that lie entirely outside [0, duration) from the
composition before probing and extraction, using one engine predicate that
the extractor's own early skip also uses, so a clip ending before 0 is no
longer downloaded or probed either.
@miguel-heygen miguel-heygen changed the title fix(engine): exports no longer fail on a video clip outside the composition's length fix(producer): exports no longer fail on a video clip outside the composition's length Oct 10, 2026
…e timeline

A clip with no authored end carries end 0 (script-created media, or a src
set from a script), which the window resolver reads as "natural source
length". The outside-the-timeline check treated it as ending at 0 and dropped
it, so the clip froze on its first frame. Only an authored end (after the
start) at or before 0 now counts as before the timeline.
…'s end

A clip with no authored length carries end 0, the same value as an authored
end at 0, so "ends at or before 0" cannot be told apart from "natural
length" and a script-created clip with a negative start was dropped and
froze. Clips entirely before 0 keep main's behaviour; the filter now only
drops clips starting at or after the end, which never reads the end.

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

Reviewed at 1da8db78. Clean, no findings.

  • Both sides use the same rule. The stage filter and the extractor's existing skip both call isVideoPastTimelineEnd(video, composition.duration) (start >= end). The extractor already received timelineEnd: composition.duration (extractVideosStage.ts:508), so the composition's video list now matches exactly what extraction skipped.
  • A zero duration can't drop everything. The extract stage runs after the probe stage, which discovers a non-positive duration or aborts on it (probeStage.ts:481, :751). So start >= 0 can't empty the list.
  • Filtering before probe and extraction is right. The coverage gate, frame lookup, HDR planning and the distributed meta/videos.json all read the filtered list. A clip starting at or after the end has no on-screen time, so its audio and frames are absent either way.
  • Leaving clips that lie entirely before 0 alone is correct. An end of 0 can't be told apart from natural length.
  • Tests: extractVideosStage.timelineBound.test.ts passes 11/11. Mutants killed:
    • no stage filter: 2 fail, in-process and distributed
    • >= changed to >: 2 fail

Review by Somu

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

Approving at 1da8db78, unchanged since my COMMENT (5481247015). Required checks green, no CRs.

Review by Somu

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 81f60db Oct 11, 2026
161 checks passed
@miguel-heygen
miguel-heygen deleted the fix/vspeed-extract branch October 11, 2026 00:21
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.

2 participants