Skip to content

Deterministic video export: frame-accurate Source Video decode (source-media issue 07) - #18

Merged
lenxism merged 3 commits into
mainfrom
feat/source-media-07-deterministic-video-export
Jul 14, 2026
Merged

Deterministic video export: frame-accurate Source Video decode (source-media issue 07)#18
lenxism merged 3 commits into
mainfrom
feat/source-media-07-deterministic-video-export

Conversation

@lenxism

@lenxism lenxism commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • A bound decodable Source Video now survives into the exported MP4 frame-accurately: the export loop decodes it with mediabunny's read half (InputVideoSampleSink.samplesAtTimestamps over t = frame / fps) instead of sampling the wall-clock preview <video>. No wall-clock playback during export; audio is stripped.
  • New core/media-time seam videoSampleTimeAtPlayhead (preview-matching wrap + first-frame hold) drives the sample timestamps; the Exporter now enforces core's video-export availability/duration policy, so the Animation duration truncates longer clips.
  • Texture path: VideoFrameTexture (zero-blit) for in-clamp unrotated sources, CanvasSink at the clamped size for oversized/rotated ones. The export texture is published through an ephemeral override seam (useSyncExternalStore) so the Screen swaps to it only for the capture, then returns to the preview.
  • Mid-export decode corruption aborts with a dedicated SourceVideoDecodeError toast ("the video could not be decoded") and no partial file. Image-only export paths are unchanged.

Test plan

  • videoSampleTimeAtPlayhead covered TDD-style in src/core/media-time.test.ts (hold, wrap, negative first timestamp, non-finite inputs) — 473 tests green
  • typecheck + lint + build green locally
  • Browser evidence (Playwright, headed Chrome): hue-coded frame-counter clip exported at 5s (truncation) and 12s (wrap); decoded output frames spot-checked (0, 1, 37, 74, 148, 149 and wrap points) match expected source frames; exported MP4 has no audio track
  • Corrupted-blob export aborts with the decode-error toast and no download

Note

Medium Risk
Changes the video export capture path and GL texture sourcing; failures are gated and named, but regressions could affect export determinism, preview during export, or duration/availability behavior.

Overview
Video export no longer samples the preview <video> during encode. When a decodable Source Video is bound, the capture loop opens an export-only mediabunny decode stream (samplesAtTimestamps at frame / fps), stages each frame on a dedicated texture before render, and tears the stream down after restore.

videoSampleTimeAtPlayhead in core drives those sample times (preview-matching wrap plus first-frame hold when the track’s first packet is after 0). videoTimeAtPlayhead now passes in-range playheads through without extra modulo so export timestamps stay exact.

The Screen reads the decode texture via a short-lived setExportSourceMedia override (useSyncExternalStore); timeline-locked preview skips playhead seeks while export runs. The Exporter enforces resolveVideoExportAvailability / resolveVideoExportDuration (with sourceMediaFacts from the store) and throws named errors the UI maps to specific notices. Export panel video availability and duration follow the same core rules (decodable video without Animation, codec notice for preview-only).

Reviewed by Cursor Bugbot for commit becd893. Bugbot is set up for automated code reviews on this repo. Configure here.

…e-media issue 07)

A bound decodable Source Video now survives into the exported MP4
frame-accurately: the export loop decodes it with mediabunny's read half
(Input -> VideoSampleSink.samplesAtTimestamps over t = frame / fps) instead
of sampling the wall-clock preview <video>. VideoFrameTexture is the
zero-blit path for in-clamp unrotated sources; oversized/rotated sources go
through CanvasSink at the clamped size. First/last-frame hold, wrap matching
the timeline-locked preview, verifyKeyPackets, and eager sample/frame close()
are all in place; the decode streams (never buffers the file). The Exporter
enforces core's availability/duration policy, and mid-export corruption
aborts with its own error toast and no partial file.
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
mockstudio Ready Ready Preview, Comment Jul 14, 2026 4:32am

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2ae05ed. Configure here.

Comment thread src/scene/Exporter.tsx Outdated
Comment thread src/scene/Exporter.tsx
An availability refusal (a bound preview-only video) now throws with its own
shared Error.name (VIDEO_EXPORT_UNAVAILABLE_ERROR_NAME) so the UI explains
the codec/availability cause instead of blaming the browser's encoder. The
first progress write moves ahead of the decode prelude (blob fetch + stream
open) so the overlay's Cancel control exists from the job's first visible
moment; the fetch rides the capture's AbortSignal, and a cancel landing while
the stream opens is honored before any capture work.
…#19)

## Summary

- The export panel now obeys `core`'s availability verdict
(`resolveVideoExportAvailability`) instead of its UI-local "requires an
Animation" gate: video export is offered when the document has an
Animation OR the bound Source Media is a decodable Source Video, and
disabled otherwise.
- With no Animation, the panel's Duration row (and the export itself,
enforced by the Exporter since #18) uses `resolveVideoExportDuration`:
the video's own duration capped at 30 s, playing from its start.
- A bound playable-but-undecodable Source Video pins a persistent "codec
is not supported in this browser" notice on the export panel (not a
transient toast — the condition holds while the file is bound); import
and preview stay unaffected.
- The `sourceMediaFacts` store→core projection the Exporter kept private
moves to `state/store.ts` so the panel and the Exporter share one seam
(red→green in `state/source-media.test.ts`).
- CONTEXT.md's Exporter entry already reflects the availability rule
(landed with the issue 06 policy module); verified, no remaining
"requires an Animation" text.

Stacked on #18 (deterministic video export) — merge that first.

## Test plan

- [x] Full Vitest suite (477), typecheck, lint, build green
- [x] Browser evidence (`.scratch/source-media/evidence/08/`, system
Chrome): bare 8 s frame-counter clip with no Animation exports an 8.000
s MP4, no audio, frames 0/1/60/150/238/239 exactly matching source
frames (plays from start)
- [x] 45 s record clamps the panel to "30.0 s · 900 frames"
- [x] No media + no Animation renders disabled with the hint;
`canDecode: false` renders the persistent codec notice with export
blocked and the preview still advancing

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes user-visible export eligibility and messaging for video; logic
is delegated to tested core policy but mis-synced facts would block or
allow exports incorrectly.
> 
> **Overview**
> The export panel stops gating video on a local “has Animation” check
and instead uses **`core`** via `resolveVideoExportAvailability` and
`resolveVideoExportDuration`, with bound media projected through shared
**`sourceMediaFacts`** in `state/store.ts` (moved out of `Exporter.tsx`
so the panel and exporter stay aligned).
> 
> Video export is enabled when the document has an Animation **or** a
decodable bound Source Video; duration and frame counts follow
`resolveVideoExportDuration` (including source-only clips capped at
30s). When export is blocked, **`VideoUnavailableNotice`** shows either
a hint to import video or add motion (`nothing-to-export`) or a
**persistent** amber codec warning for preview-only undecodable video
(`undecodable-video`), while import/preview stay available.
> 
> Unit tests cover the `sourceMediaFacts` projection (null, image,
video, `canDecode: false`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
113d25c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@lenxism
lenxism merged commit 6e1b839 into main Jul 14, 2026
4 checks passed
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.

1 participant