fix(core): replay ended audio after backward seeks - #3383
Conversation
miguel-heygen
left a comment
There was a problem hiding this comment.
Reviewed exact head b28de8cc43614d92ccda3967fba1e3e4d90f90cf.
The recovery gate is narrow enough to fix backward transport without turning ordinary EOF into a loop:
media.ts:243-256admits ended audio only when it is non-looping, has a known playable source window, is inside that source, and either has no prior desired-time baseline (true activation) or moves backward by more than the strict 40 ms threshold.media.ts:491-499preserves only desired source time while native EOF is still inside the authored window, so repeated EOF/force-sync ticks cannot manufacture a fresh activation; a real outside-window transition andevictMediaSyncStatestill clear every baseline for true re-entry/source swap.media.ts:383-393rewinds stale short audio only when the native element is ahead on its first active tick, while cold/buffering audio at zero is not forced forward. The non-looping video held-tail/backward-seek behavior remains structurally unchanged.
The regressions distinguish ended re-entry, within-window backward seek, repeated EOF force-sync ticks, past-source time, stale-forward re-entry, and cold audio. All required checks are green at this exact head, including Linux + Windows tests, runtime contract, build, typecheck, Windows render, and regression.
Verdict: APPROVE
Reasoning: The new desired-time baseline separates actual backward transport from EOF and transition ticks, closes the silent short-audio replay failure, and preserves existing video/tail semantics with direct edge-case coverage.
— Magi
Twelve of the stack's feature commits landed on main as squashes (#3274 through #3292, plus #3401's canary removal); the 96 review-and-fix commits that followed them here did not, and main moved 64 commits on in the meantime. This reconciles the two. 58 files conflicted. 44 were audio-only — main's side there is the squashed form of commits this branch already carries and has since superseded, so the branch side stands. The rest needed real work, in both directions: **Taken from main, absent here.** - `ensureAudioGroupInertStyle` (#3278's review). An `<hf-audio-group>` is an unknown custom element, so it still takes a flex/grid slot and can open a line box — adding a group shifted authored layout. The helper and its `init.ts` call never came back to the branch, and this branch is what emits the element. - `#3383`'s ended-audio replay: `canSeekEndedMediaBackward` and its five siblings in `media.ts`, with all six tests. Not present here in any form. - `#3380`'s `asetpts=N/SR/TB` between `apad` and `atrim`. Also applied to `mixGroupMembers`, the group submix, which is new on this branch and so had the same bug in a path main's fix could not reach: delayed members padded then amix'd, where a group of four or more silently loses one. - `#3401`'s `displayNumber` thread. The header derives its row from the group-aware order and the undo label from ascending element keys, so once a group exists the same click said "Hide track 2" and recorded "Hide track 1". - `#3413`/`#3421`'s viewport handling — the popover's height cap and `inset()`, and `resolveFloatingPanelPosition` for the grouping dialog, which lives in a track header at the bottom of the window. - Two extractions this branch had inline and at exactly the 600-line cap: `useTimelineDeleteOps` and `editingModeSlice`. Bodies were identical. **Kept from the branch, against main.** Mute and solo are gone by deliberate breaking change (`remove mute and solo from tracks and groups`, `remove the group volume slider and level meter`), so eight files main still carries are deleted again, `PlayerControls` keeps no `previewIframeRef` (it existed only to feed `SoloBanner`), the group-levels branch comes out of main's new `previewMessageRouter`, and `STRIP_H` goes with the bus strip it sized. Main's `TimelineTrackPlainHeader.test.tsx` is rewritten against the control that actually exists — the visibility eye, withheld from an audible audio row and offered back once hidden, which is the only way out of `data-hidden`. **Unioned.** `TimelineFxPopover` — main's positioning, this branch's audition telemetry (`auditionPresetChain`, `storedChain`, `onAuditionTracked`); `SKILL.md` — main's #3416 "keep the carve group a voice group" beside this branch's bus section, with the canary paragraph dropped since the canaries no longer exist. Every port is mutation-checked. core 2508, studio 4460, lint 528, engine 1630, cli 2813, sdk 549, producer green; tsc, oxlint, oxfmt, fallow and the 600-line cap clean.
What
Recover ended non-looping audio after a real backward timeline seek or true clip re-entry, including short clips whose native media element is still near EOF.
This PR covers the Studio/player replay half of the Framey audio investigation. The independent offline-render timestamp fix is already in #3380.
Why
After a short audio element reached native EOF, the runtime rejected it in the active gate before it could rewind
currentTimeand callplay(). Seeking the Studio timeline back to the beginning therefore made short lines such as “who?” and “really?” deterministically silent once their elements entered the browser's ended state.A broad ended-audio recovery would introduce a second bug: native EOF can lead the runtime clock by a few milliseconds, and ordinary play/pause or rate-change force-sync ticks must not replay that tail.
How
Test plan
Added regressions for:
forceSyncVerified locally:
Companion renderer fix: #3380.