Skip to content

Video import + free-run preview: Source Video plays on the Screen (source-media issue 04) - #15

Merged
lenxism merged 2 commits into
mainfrom
feat/video-import-freerun-preview
Jul 14, 2026
Merged

Video import + free-run preview: Source Video plays on the Screen (source-media issue 04)#15
lenxism merged 2 commits into
mainfrom
feat/video-import-freerun-preview

Conversation

@lenxism

@lenxism lenxism commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Drop, file picker, and paste accept video files through the same flow as images. Import gates on file capability, not a format list: the file is opened with mediabunny, must carry a readable video track, and the track.canDecode() verdict is recorded on the Source Video record (a playable-but-undecodable file still imports). Non-media files reject with a toast and leave the editor untouched.
  • Free-run preview per ADR-0009: an off-DOM muted looping <video> feeds a VideoTexture, with requestVideoFrameCallback driving one invalidate() per presented frame — never frameloop="always". Pause (new ephemeral videoPreviewPlaying flag + a chip beside the replace control) freezes the frame and renders zero GL frames; a fresh bind always autoplays.
  • Oversized sources downscale per presented frame onto a canvas clamped to min(GPU limit, 4096) instead of being rejected; mipmaps stay off on the video texture. Fit/crop and screen stylization apply through the existing Screen material path via the new useSourceMediaTexture wrapper (which now owns the placeholder fallback and the displayedSourceAspect publish for both kinds).
  • The deterministic decode path stays export-only (ticket 07); still export captures the currently displayed frame with playback left alone.

Verification

  • Vitest: 437 passing (new store-seam tests for the autoplay/pause preview state, red→green); typecheck, lint, and build green.
  • Browser-verified with GL draw-call instrumentation, evidence in .scratch/source-media/evidence/04/ (untracked): autoplay on the Device Screen; paused ⇒ 0 draw calls over 2 s; Cover + crop zoom and Pixels/CRT stylization visibly applied to the moving video; rejection toast for a text file; a generated 4320×2160 video previewing through the clamped-canvas path; still PNG export completing with playback still running.

Test plan

  • Drop an .mp4 → autoplays looping on the Screen; pause chip freezes the frame
  • Drop a non-media file → toast, editor untouched
  • Export a still while the video plays → file captures the displayed frame, playback continues

Note

Medium Risk
New async import and GPU texture lifecycle for video (object URLs, rVFC, oversized canvas path) touch core preview rendering; document/undo paths are unchanged.

Overview
Adds Source Video alongside images: drag, paste, and file picker accept video through the same flow as images, with capability-based import (mediabunny + readable video track, canDecode recorded on the record) and a toast when a file isn’t usable media.

Scene: useSourceMediaTexture routes image vs video, keeps the placeholder and displayedSourceAspect publish; new useSourceVideoTexture drives preview with a muted looping <video>, VideoTexture (or per-frame canvas when oversized), and requestVideoFrameCallbackinvalidate() under demand rendering (ADR-0009). Pause uses ephemeral videoPreviewPlaying; binding media resets autoplay.

UI: SourceMediaDropzone replaces the image-only dropzone; a play/pause chip appears next to replace when a video is bound.

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

lenxism added 2 commits July 13, 2026 22:26
…urce-media issue 04)

Drop/pick/paste a video like an image: import gates on file capability
(mediabunny getPrimaryVideoTrack), records the canDecode() verdict, and
rejects non-media with a toast. Preview is an off-DOM muted looping
<video> feeding a VideoTexture, with requestVideoFrameCallback driving
invalidate() per presented frame (ADR-0009) — pause freezes the frame
and renders zero GL frames. Oversized sources downscale per-frame onto
a canvas clamped to the texture cap; mipmaps stay off. Fit/crop and
screen stylization apply through the existing Screen material path.
…ardening

Address code-review findings on issue 04: use mediabunny's InputVideoTrack
type instead of an inferred Awaited<ReturnType<...>>, say "image or video"
in upload chrome (CONTEXT.md avoids bare "media"), seed the oversized
canvas on loadeddata so a rejected autoplay still shows the first frame,
and time out a wedged metadata probe so the import fails with the toast
instead of hanging and leaking the object URL.
@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 1:37am

@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 1 potential issue.

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 65ff8ca. Configure here.

export function useSourceMediaTexture(media: SourceMedia | null): ResolvedSourceMedia {
const image = useSourceImageTexture(media)
const video = useSourceVideoTexture(media)
const loaded = image ?? video

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale media shown crossing kind switch

Medium Severity

useSourceMediaTexture picks the displayed pair with image ?? video, so when media.kind changes the inactive hook can still return its previous loaded state until a useEffect clears it. The screen then samples the wrong kind (e.g. video after an image replace) and displayedSourceAspect can follow that stale pair.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 65ff8ca. Configure here.

@lenxism
lenxism merged commit 87ca255 into main Jul 14, 2026
4 checks passed
lenxism added a commit that referenced this pull request Jul 14, 2026
The wrapper merged the two kind hooks with `image ?? video`, so on the
render right after a cross-kind replace (or a clear) the previous kind's
hook still returned its stale pair and the Screen sampled it — with
displayedSourceAspect following. Gate the pick on the bound media's kind
(exhaustive switch): same-kind replacements still hold the previous pair
while the new file decodes, but a kind switch falls to the placeholder
until the new kind resolves. (Bugbot finding on PR #15.)
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