fix(runtime): make audio/media sync boundary inclusive to match visibility fix#1173
fix(runtime): make audio/media sync boundary inclusive to match visibility fix#1173miguel-heygen wants to merge 1 commit into
Conversation
…ility fix `init.ts` (#1166) changed visibility to `<= computedEnd` so elements stay visible at exactly t=duration. Audio clock (`init.ts:1908`) and `syncRuntimeMedia` (`media.ts:163`) still used `< end`, leaving a 1-frame desync where the host was visible but audio was silent at the boundary. Change both to `<=` for symmetry: - At clip end (seeking to t=duration): audio plays through the final frame - At adjacent boundaries: the `break` in syncRuntimeMedia ensures only the outgoing clip's audio is attached — no simultaneous dual activation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vanceingalls
left a comment
There was a problem hiding this comment.
Exactly the right fix — init.ts audio clock and media.ts syncRuntimeMedia both updated from < end to <= end, symmetric with the visibility change from #1166. At t = duration, audio holds the final state alongside visible elements. The syncRuntimeMedia dual-activation concern doesn't apply here because it breaks on the first active clip. CI in progress, no failures. LGTM.
— Vai
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at d701c08c. The 2-line diff is right; the PR description's reasoning is partially wrong — works out fine in practice but the audible-safety argument needs correction for the next reader.
The fix shape is right (verified at HEAD)
Both sites change < → <= symmetrically with #1166's visibility fix:
init.ts:1908(audio clock): ✓ — and this site DOES have abreak(line 1918) inside the active branch. The audio clock attaches the first matching element and exits. Att = boundary, the outgoing clip matches<= end→ attached → break → incoming never reached. Magi's "break on first active" claim holds for this site.media.ts:163(syncRuntimeMedia): ✓ for the boundary semantics, but the "no dual-activation because the loop breaks" claim is false for this site. Grep confirmssyncRuntimeMedia's for-loop has zerobreakstatements (only twocontinues: one for disconnected elements, one at the end of the active branch). The loop iterates EVERY clip every tick.
Why the fix still works, restated correctly
At t = boundary where outgoing.end === incoming.start:
| Clip | isActive after fix |
el.paused? |
play() fires? |
|---|---|---|---|
| outgoing (A) | true (now <= end) |
false (mid-play) | no — gated on el.paused at line ~9700 (if (params.playing && el.paused && !playRequested.has(el))) |
| incoming (B) | true (>= start) |
true (not yet started) | yes |
So both clips run through the active-branch of the loop, but only B actually calls play(). A's volume/sync code re-runs (cheap, idempotent). At t = boundary + 1 frame, A's isActive flips to false → cleanup branch → if (!el.paused) el.pause() → A pauses.
Net: 1-frame audio overlap at every clip boundary (~33ms at 30fps, ~17ms at 60fps). For typical content this is below the audibility threshold for a brief crossfade. For sample-perfect transitions (sharp transients exactly at the boundary), it could land as a barely-perceptible blend. Not destructive, but worth knowing the actual mechanism.
Recommendation
Two equally-valid paths:
- Fix the PR description: replace "syncRuntimeMedia iterates clips and
breaks on the first active one" with the actual mechanism: "theplay()gate isel.paused, so the outgoing clip — already playing — doesn't re-trigger; only the incoming clip starts. 1-frame audio overlap at boundaries is imperceptible." That's an accurate description of why the fix is safe. (Cheap; preserves the current diff.) - Add a
breakto media.ts to match init.ts: after the active branch insyncRuntimeMedia, addbreak(or a sentinel + early-exit). Genuinely prevents the dual-activation Magi claimed already didn't happen. Symmetric with init.ts's pattern. (More defensive; touches 1 more line.)
I'd lean toward (1) — the existing fix works; the description just over-claims a defense that isn't there. (2) would be a separate "tighten the pattern" PR, not gating this one.
Severity walk
Without this fix: at t = duration, the host is visible (per #1166's <= end) but audio is silent. A user seeking to the exact end frame hears nothing while seeing the final state. Asymmetric and surprising.
With this fix: symmetry restored. The 1-frame audio overlap at boundaries is the cost; the visible+audible final-frame is the benefit. Worth it.
Verdict
Materially LGTM on the 2-line change. The PR description's reasoning for syncRuntimeMedia is incorrect (no break exists) but the fix itself is sound — the el.paused gate is what actually prevents dual play() calls, not a loop break. Updating the PR body would close the documentation gap; adding an actual break would close the pattern asymmetry.
Stamp held — same protocol. James gates.
— Rames Jusso (hyperframes)
71deda2 to
d701c08
Compare
Producer regression test results (post-merge)Ran the full suite inside
The Stale baseline PR: #1178 |
Summary
Follows up on #1166 which changed element visibility to `<= computedEnd`. The audio clock and `syncRuntimeMedia` still used `< end`, creating a 1-frame asymmetry: the host was visible at exactly `t = duration` but audio was silent.
Changed:
Why `<=` is safe for audio at clip boundaries:
For `init.ts` (audio clock): the loop does `break` on the first active clip (line 1918), so at `t = boundary` only the outgoing clip is attached — incoming is never reached.
For `media.ts` (`syncRuntimeMedia`): the loop has no `break` — it iterates all clips. At `t = boundary` both clips satisfy `<= end / >= start`, so both are `isActive = true`. However, the outgoing clip's `el.paused` is `false` (mid-play), so the `el.paused` gate skips its `play()` call. Only the incoming clip's `play()` fires. The result is a ~1-frame audio overlap (~33ms at 30fps) — imperceptible for typical content. The `el.paused` gate, not a loop break, is what prevents dual-activation here.
Effect:
Test plan