Repository navigation
feat(studio): a host can hold playback so nothing starts while it is held - #5060
Conversation
Edit accuracy: accurate 2040 (base branch 2040), smooth 1601 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
somanshreddy
left a comment
There was a problem hiding this comment.
Review at 621ca937 (full PR). No blockers.
Start paths vs the hold. I traced each call site that can start the film or its audio at this head:
play()atuseTimelinePlayer.ts:203is guarded. That covers the Play button, Space/togglePlay, forward shuttle andplaybackRequest(true)(FX audition, and theusePreviewInteraction.ts:108resume after a drag).- The keep-playing seek resume (
playbackSeek.ts,:326) is guarded. - The loop restart (
useTimelinePlayerLoop.ts:63) is guarded and falls through to the end-of-film stop. - Reverse shuttle (
playBackward) is only reachable throughshuttle, which is guarded atusePlaybackKeyboard.ts:63. - Scrub audio (
applyScrub) is only reached viascrubMusicAtSeekat:321, which is guarded. previewFrameStore.ts:19wraps the adapter, and itsplayonly runs from the guardedplay().
I found no unguarded start site. The new scrubMusicAtSeek condition (!keepPlaying && !playLocked) matches the old !shouldResumeAfterSeek && !keepPlaying plus the hold, because resume already implied keepPlaying. The hold isn't persisted (no persist in the player store), so it can't survive a reload.
Lows (non-blocking):
- Turning the hold on doesn't call
stopScrubPreviewAudio(). A scrub blip already sounding plays out its ≤140 ms tail (timelineIframeHelpers.tsstop timer). This is the "audio can blip first" case the PR describes, but bounded. A one-line stop in the new subscription at:354would close it. - A
playbackRequest(true)made while held is dropped silently. That's correct for a hold, butusePreviewInteraction's post-drag resume and an FX-audition hover don't come back when the hold is released. That matches "plays once released" only for an explicit Play. Worth one line in the host-facing doc forsetPlayLocked.
Mutations. 8 mutants against useTimelinePlayer.playLock.test.ts + src/player/lib; 8 caught, 0 survived:
- dropped
play()guard; - dropped
playLockedinshouldResumeForwardPlaybackAfterSeek; - scrub guard removed;
- loop guard removed;
- shuttle guard removed;
- lock-on pause subscription disabled;
- old early-return
pause()without an adapter; playLocked: falsepassed to the seek resume.
The tree was restored and verified clean after each one.
CI at 621ca937: 64 pass, 22 pending, 6 skipping, 0 failing at review time.
What I ran: a read of the full runtime diff and a grep of every play() / setIsPlaying(true) / scrub site in packages/studio/src. Then vitest run on the playLock test + src/player/lib: 22 files, 211 tests passing. Then the 8 mutants. I didn't view the Before/After videos.
This is a comment, not an approval.
— Somu
jrusso1020
left a comment
There was a problem hiding this comment.
Review at 621ca937. Approving.
Every start path checks the hold. The only places that start the film are the three adapter.play() calls:
play()atuseTimelinePlayer.ts:215- the keep-playing seek at
:326 - the loop restart at
useTimelinePlayerLoop.ts:67
All three are now behind the hold. Everything else funnels into them:
- The Play button and Space.
requestPlayback(true), which covers the FX audition and the resume after a preview drag.- The shadow-reload hand-over (
handOverPlayback,:439), which calls the guardedplay(). - The
previewFrameStorewrapper.
The reverse shuttle only runs through shuttle, and that now returns while the hold is on. Scrub audio is only reached through scrubMusicAtSeek, and that call is guarded at :321. The new scrub condition is the old one plus the hold, since resuming already required keepPlaying. CompositionsTab and AudioRow start their own players, which matches the PR's note that the hold covers the film only.
Reset and persistence. createTimelineResetState doesn't touch playLocked, so a timeline reset leaves the host's hold in place, as described. The player store has no persist, so a reload doesn't bring a hold back.
Tests. I ran src/player three times: 179 files and 2211 tests passed each time. I then undid each guard on its own and ran the new test file. All 7 changes were caught:
- the
play()guard - the resume guard
- the scrub guard
- the loop guard
- the shuttle guard
- the subscription that pauses when the hold turns on
- the
pause()change for when there's no player yet
CI has 69 checks passing and 0 failing. The edit-accuracy shards were still running when I posted.
Reuse / simplification. This copies the existing timelineReady gate and adds one flag next to it, so there's nothing to reuse and nothing to cut.
Non-blocking:
- I agree with the earlier note: a one-line
stopScrubPreviewAudio()in the new subscription would cut off a scrub sound that's already playing when the hold turns on. Without it, that sound runs out on its own within 140 ms. - Hosts reach the hold through the raw store, while
usePlayerHandleis the documented way for a host to read and control the player. AddingplayLocked/setPlayLockedto the handle later would keep that in one place.
— Rames
What
A host app can hold Studio's playback:
usePlayerStore.getState().setPlayLocked(true). While it is held nothing starts the film, and turning the hold on pauses a film that is playing. Seeking still moves the picture, but the scrub audio stays silent.setPlayLocked(false)gives playback back.Why
A host that edits the project underneath the preview (for example, an agent rewriting the composition) needs the film to stay still until it finishes. Pausing after a start is too late, because a frame can advance and audio can blip first. So the hold is checked where each start happens, before anything plays.
How
playLockedandsetPlayLockedlive in the readiness slice next totimelineReady, which already gates Play the same way. The host owns the hold, so a timeline reset leaves it alone.useTimelinePlayerchecks the hold inplay()(this covers the Play button, Space and playback requests) and in the seek that would keep playing, which also skips scrub audio while held. The loop restart inuseTimelinePlayerLoopstops at the loop's end instead. The J/L shuttle inusePlaybackKeyboardreturns before it changes the speed. A store subscription pauses when the hold turns on.pause()now stops the frame loop and clears the playing flag even before the preview has a player, so a hold set in that window cannot leave the bar reading Pause.Before
On main, with
playLockedset on the store, Space plays the film anyway (60f to 135f in 2.5 s). Studio's owndecision-treeexample, dev build, time shown in frames.playlock-before.mp4
After
On this branch, with
setPlayLocked(true), Space leaves the film at 60f; aftersetPlayLocked(false), Space plays it (60f to 135f).playlock-after.mp4
Tests
useTimelinePlayer.playLock.test.ts: while the hold is on, Play, a playback request, J and a keep-playing seek start nothing; a held seek moves the picture with no scrub audio; a looping film stops at its end; J and L keep the chosen speed; a hold set before the preview has a player still clears playing; turning it on pauses; turning it off lets Play start again.The hold covers the film only. Sidebar and asset previews keep their own players.