Repository navigation
fix(core): the preview transport keeps ticking in a hidden tab - #5872
Conversation
miguel-heygen
left a comment
There was a problem hiding this comment.
Reviewed at f3920d67.
Strengths
init.ts: every transport scheduler now goes throughrequestTransportTick/cancelTransportTick. I greppedtransportRafIdat this head: the only remaining uses are inside those two helpers andscheduleNextTransportFrame's reset, so no site still asks only "is a frame pending" while a hidden-tab timer is pending.- The
visibilitychangehandoff covers the edge that is easy to miss: a frame requested just before the tab hides would never fire. - Teardown cancels both kinds of tick and removes the listener.
Verified locally
transportPark.test.ts: 44/44 pass at this head. Withinit.tsrestored from the merge base, the two new tests fail ("keeps the transport ticking on a timer while the document is hidden", "moves a pending tick between animation frames and a timer as visibility changes"), so they are not vacuous.transportTicktakes no arguments, so calling it fromsetTimeoutinstead ofrequestAnimationFrameloses nothing.
Nits (not blocking)
- Chromium only skips timer throttling for an audible page. A hidden tab playing muted or silent media is clamped to about 1 timer per second (and intensive throttling after 5 minutes), so there a clip ends up to ~1 s late instead of on time. That is still far better than never ending, but worth a line in the comment above
HIDDEN_TRANSPORT_TICK_MS. - As the PR notes, #5754 touches the same two files; whichever lands second rebases.
Verdict: APPROVE
Reasoning: The change routes every transport scheduler through one place, the tests fail without it, and nothing outside the transport loop changes.
|
Real-browser check, since the unit tests run in jsdom and cannot show Chromium's hidden-tab behaviour. Setup. Google Chrome 155.0.8059.82 (headed), macOS on Apple silicon, driven by puppeteer. One composition with the report's two trimmed clips of one 8 s WAV: Without the change (
With the change.
Limits: a single run on one machine and one Chrome version, with an 8 s synthetic tone, not the reporter's talking-head take. It covers cause 1 only; I did not exercise the Web Audio path of cause 2. |
Head branch was pushed to by a user without write access
|
Thanks for the review. I took the nit: |
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at bf3d5a27. Thanks, Krish, this is a clean fix.
On your three questions:
- The claim: confirmed. #5710 has no other linked PR and nobody on our side is working on it, so cause 1 is yours.
- The approach: no change wanted. It's the one the issue proposed, and it fits how the transport already schedules itself (details below).
- Cause 2 (the unmute on a natural end in
webAudioTransport.ts): leave it with the reporter for now. They already run a patch with unit tests and a browser test for that race (5 of 6 runs fail without it), and they offered to open the PR. If they haven't opened one in a few days, ask on the issue and take it then.
What I checked at this head:
- Every scheduler path goes through the two new helpers.
transportRafIdis now only read or written insiderequestTransportTick/cancelTransportTick, plus the reset at the top ofscheduleNextTransportFrame. So no site still asks only "is a frame pending" while a hidden timer is pending.wakeTransportusestransportTickPending()for both kinds of pending tick. The initial tick and teardown go through the helpers too, and teardown removes the listener. - The timer path keeps the tick contract. It clears its id before calling
transportTick(). That function takes no arguments, so calling it from a timer is the same as from a frame. The tail (scheduleNextTransportFrame) still decides between ticking and parking, so a paused, hidden transport parks as before. A parked transport has nothing pending, sovisibilitychangeleaves it alone, and the next wake uses whichever scheduler fits the current state. - Nothing else is affected. Only how the tick is scheduled changes. What a tick does, the park timer and
WebAudioTransportare untouched. A page that starts hidden simply begins on the timer. - Your A/B on Chrome 155 is credible. The tab is genuinely hidden by focusing a second tab, not emulated. The same composition runs on both builds, and the before/after tables show exactly the failure and the fix (
a1pauses at source 1.52 s anda2plays 5.00 to 6.50 s while hidden). It's one run with a synthetic tone, which you note. That's enough for a scheduling change whose behaviour the unit tests already pin. - The tests cover both halves: the stall (the playhead passes 1 s and the element is paused with no animation frames), and the hand-off both ways.
- The two later commits since Miguel's approval at
f3920d67change comments only.
Notes:
- CI workflows haven't run on this fork PR yet. Only Graphite and WIP report, so a maintainer needs to approve the workflow runs. The merge queue runs the required checks before anything lands.
- The commits aren't signed. That's fine here: the queue squash-merges, and GitHub signs the squash.
No live run on our side. I didn't run your tests locally; the review is from reading the code and your A/B.
— Rames
|
@krishs0404 one thing before this can merge: all three commits ( Each commit should then show Verified on GitHub. Note that a new push needs a fresh approval, so ping here once it's done. Also: I've approved the CI workflow runs for this head, so the checks are running now. — Rames |
A hidden document runs no animation frames, but its media keeps playing. The transport ticked on requestAnimationFrame only, so syncRuntimeMedia stopped running: a clip was never paused at the end of its window and the next clip never started until the tab was shown again. While document.visibilityState is "hidden", the transport now schedules its tick on a 16 ms timer instead. A visibilitychange listener moves a pending tick to whichever scheduler the new state calls for, and teardown removes it. A paused, hidden transport still parks as before. Refs heygen-com#5710 (cause 1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ment share Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
bf3d5a2 to
1717b0b
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
Re-approving at 1717b0bf, now that the commits are signed. Thanks @krishs0404.
- All three commits (
4dab867f,d2d6583c,1717b0bf) show as verified. git range-diffagainst the head I approved before (bf3d5a27) shows all three patches identical, on the same base (84349457). Only the signatures changed, so my earlier review still stands.- I've approved the CI workflow runs for this head. They need to pass before it can merge.
Need: yes · Size: ok · Reuse: the existing transport tick · Simpler: no.
No live run.
— Rames
Fixes cause 1 of #5710. Cause 2 (the unmute on a natural buffer end in
webAudioTransport.ts) is not part of this PR.Problem
A hidden document runs no animation frames, but its media keeps playing. The transport ticked on
requestAnimationFrameonly, sosyncRuntimeMediastopped running while the tab was hidden. A clip was never paused at the end of its window and the next clip never started until the tab was shown again. Someone listening to a draft in another tab hears the raw source past each trim.Change
In
packages/core/src/runtime/init.ts:requestTransportTickschedules the next tick on a 16 ms timer whiledocument.visibilityState === "hidden", and onrequestAnimationFrameotherwise.cancelTransportTickcancels either.scheduleNextTransportFrame,wakeTransport, the initial tick) and the teardown cancel now go through these two helpers.wakeTransportchecks for a pending tick of either kind.visibilitychangelistener moves a pending tick to the scheduler the new state calls for: a frame requested just before the tab hid would never fire. Teardown removes the listener.Chromium does not throttle the timers of a page that is playing audio, so the hidden tick keeps time (per the report in #5710).
Tests
Two tests in
packages/core/src/runtime/transportPark.test.ts:Both fail on
mainand pass here.Ran locally:
bunx vitest runinpackages/core: 4510 pass, 1 fail. 7 suites fail with and without this change, because they cannot resolve unbuilt sibling packages (@hyperframes/lint,@hyperframes/studio-server).bun run --filter @hyperframes/core test:hyperframe-runtime-ci: passes.tsc --noEmit, oxlint, oxfmt, and the comment checks: clean.Not covered by an automated test: real Chromium behaviour in a hidden tab. jsdom has no throttling, so the tests hold animation frames and drive the timer directly.
Notes
init.tsandtransportPark.test.ts; none of its changed lines touch the transport scheduling.🤖 Generated with Claude Code