Skip to content

fix(studio): audio keeps playing after a timeline move or trim in a static composition - #6428

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/static-comp-audio-6302
Oct 28, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/static-comp-audio-6302

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

What

Moving or trimming an audio clip in Studio no longer silences preview audio in a composition that has no GSAP timeline, and an edit made during playback no longer leaves the transport stopped.

Fixes #6302. Thanks to the reporter, whose diagnosis named both causes exactly.

Why

Two separate causes, both from the issue:

  1. After a timing edit the runtime rebinds its timeline. With no timeline registered, that branch set the clock to zero length instead of the declared composition length, so Studio fell back to seek-driven playback, which never starts media.
  2. The soft reload seeks and rebinds, which stops the transport. Nothing started it again, so an edit made while playing left the Play/Pause button on Pause with nothing playing.

How

  • reconcileTimeline (core runtime): the no-timeline branch now uses getSafeTimelineDurationSeconds(null, 0), the same helper that already resolves the declared length. A genuinely empty composition still gets 0. transport.play had its own copy of the declared-length fallback; it is removed so the length has one owner.
  • finalizeSoftReload (Studio): records whether the transport was playing before the seek and calls play() after the rebind and manual edits are applied. A paused edit stays paused.

Design call: resume at finalizeSoftReload, the one function every soft-reload caller (live retime, undo restore, track visibility, timing sync) goes through. The alternatives were a runtime-level seek that preserves play state, or restoring play state in each caller; both leave a path that can still stop the transport.

Tests

  • init.compositionLength.test.ts: the declared 6 s length survives __hfForceTimelineRebind and play works; an empty composition gives 0. Fails on main with expected +0 to be 6.
  • gsapSoftReload.test.ts: a playing transport resumes after seek, rebind and manual edits (in that order), a paused one stays paused, and applySoftReload resumes too. Fail on main.

fails without the fix: packages/core/src/runtime/init.compositionLength.test.ts > "keeps a timeline-less composition's declared length when a rebind finds no timeline", and packages/studio/src/utils/gsapSoftReload.test.ts > "resumes playing once the timeline is rebuilt when an edit lands during playback". Rechecked after the rebase onto main: with main's two source files, all three new tests fail; on this branch they pass 3 runs in a row.

Run on base 575f68cf5b with only the two source files reverted, then on the branch three times:

init.compositionLength.test.ts gsapSoftReload.test.ts
main fails: keeps a timeline-less composition's declared length when a rebind finds no timeline fails: resumes playing once the timeline is rebuilt when an edit lands during playback, resumes after a script soft reload too, which shares the finalization
branch, 3 runs 13/13, 13/13, 13/13 62/62, 62/62, 62/62

Studio check

The reduced fixture from the issue, driven in Studio with real timeline drags: seek to 70 s from the ruler, move the audio clip by about 5 s, trim its right edge, each once paused and once while playing, reading the audio meters.

Case Main This PR
Paused move, then play silent, adapter warning logged audio, no warning
Paused trim, then play silent, adapter warning logged audio, no warning
Move while playing transport stopped, button says Pause, silent keeps playing, audio
Trim while playing transport stopped, button says Pause, silent keeps playing, audio

One note on the fixture: as written, with no script at all, Studio reloads the preview after an edit and the failure does not show. Adding the gsap script tag with no timeline registered takes the soft-reload path and reproduces it on main.

Captures of the playing-move case follow.

Before

Before: edit while playing, playhead stuck at 1:09, button shows Pause

After

After: edit while playing, playback continues

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 145 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)

Unstable (2)

  • seqplainkeys-none-center-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

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

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head 78193530

The code looks right. The one blocker is the PR body: the required Studio and player captures check fails at this head (runs 40342610006, 40343059105) because the body has no ## Before / ## After headings. The two screenshots are there, but they sit under "Studio check". Put them under ## Before and ## After. Edit the body text in place (gh pr edit --body-file drops attachments), and the check will rerun.

Scope
  • reconcileTimeline: getSafeTimelineDurationSeconds(null, 0) returns the declared length first (resolveCompositionLengthSeconds), so removing the transport.play fallback changes nothing. A composition with no timeline now gets the same media/derived length as at first bind, and an empty one still gets 0.
  • finalizeSoftReload: isPlaying is read after any async commit, so a pause during the save is respected. play() is idempotent. A parent-owned audio proxy follows the paused→playing postState. All five callers (live retime, undo restore, track visibility, timing sync, script soft reload) pass through it.
  • Run locally: the new tests fail with the source files reverted to the merge base and pass at the head. core/src/runtime passes (2549 tests), and so do studio utils+hooks, including the gsapUndoRestore and gsapValueAtPlayhead browser tests on Chromium.
  • CI on this head: CI, regression, Windows render, CodeQL, preview-regression and both player perf runs are green (the first run set; a later duplicate set is still running).

…tatic composition

A composition with no GSAP timeline was treated as zero length after an edit, which dropped preview audio, and an edit made during playback left the transport stopped. The runtime now keeps the declared length when it rebinds, and the soft reload resumes a transport that was playing.
@miguel-heygen
miguel-heygen force-pushed the fix/static-comp-audio-6302 branch from 7819353 to fe51748 Compare October 28, 2026 17:54

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

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · 1 resolved, 0 open, 1 new · 78193530 → fe51748c

The captures blocker is fixed: ## Before / ## After are in place and PR captures passes at this head.

New blocker, in the PR body only: this fixes #6302, so the body needs a literal fails without the fix: line naming the test, e.g. fails without the fix: init.compositionLength.test.ts > keeps a timeline-less composition's declared length when a rebind finds no timeline. The Tests table says this in prose, but there is no such line. Edit the body in place so the attachments stay.

Scope
  • reconcileTimeline: the no-timeline branch now resolves the declared length (plus media floors). An empty root still gives 0.
  • transport.play: the removed else branch was already dead. getSafeTimelineDurationSeconds(null, 0) returns the declared length first.
  • finalizeSoftReload: isPlaying is read when finalize runs, so on the deferred MotionPath path a pause made in the meantime is respected. All six callers go through it.
  • CI, regression, Windows and Safari perf were still running at this head when I reviewed.

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

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · 1 resolved, 0 open, 0 new · fe51748c (body-only edit)

The fails without the fix: line is now in the body. I checked the core test locally: it fails with the base init.ts and passes here (13/13). CI, regression, Windows, PR captures and Comments all pass at this head.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 9aa6346 Oct 28, 2026
130 checks passed
@miguel-heygen
miguel-heygen deleted the fix/static-comp-audio-6302 branch October 28, 2026 19:27
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.

Studio: moving/resizing audio silences static compositions (duration reset + lost playback state, v0.8.196)

1 participant