A destroyed provider leaves nothing playing - #46
Open
karngyan wants to merge 1 commit into
Open
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
kino | 8a16c28 | Commit Preview URL Branch Preview URL |
Aug 11 2026, 09:23 PM |
📝 WalkthroughWalkthroughMux and native provider destruction now pauses media and clears its source before element removal. New tests cover synchronous, asynchronous, and remount teardown cases. ChangesProvider teardown
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.changeset/provider-teardown-stops-playback.md:
- Line 5: Update the source-swap statement in the changeset description to
remove “a source swap” or explicitly qualify it as a full provider recreation,
since swapSource() keeps the existing element mounted.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 81347a4c-ba3f-43f8-9b29-72cedab4e77a
📒 Files selected for processing (5)
.changeset/provider-teardown-stops-playback.mdsrc/mux/provider.test.tssrc/mux/provider.tssrc/native/provider.test.tssrc/native/provider.ts
destroy() removed its listeners and detached the element, but detaching a media element does not stop it. A removed <mux-video> keeps playing and keeps pulling HLS segments, and nothing on the page can reach it any more, because every control talks to the provider that owned it. React invokes a mount effect twice in development, so a player created with autoPlay would mount, tear down and mount again, and the discarded element played on underneath the live one until the tab closed. Pause the element and drop its source before removing it. For mux that means clearing the src attribute, which is what makes mux-video tear its playback engine down; the element's own disconnectedCallback cannot cover this, because mux-video finishes its setup a microtask after mount and a mount and teardown that land in the same tick get there first. The native provider already released its source, and now pauses as well, so its stop no longer rides entirely on load(). YouTube, Vimeo and scenes were already clean: the first two hand teardown to the SDK's own destroy(), which takes the player iframe with it, and scenes removes the host iframe, which discards the audio element inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
karngyan
force-pushed
the
fix/provider-teardown-stops-playback
branch
from
August 11, 2026 21:22
3077a63 to
8a16c28
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
destroy()removed its listeners and then calledel.remove(). Detaching an element does not pause it and does not release its source, so a destroyed provider left a live<mux-video>behind: still playing, still pulling HLS segments, and unreachable, because every control on the page talks to the provider that owned it.React invokes a mount effect twice in development (mount, destroy, mount), so a player created with
autoPlayended up with two audio tracks a beat apart and only one of them steerable. Measured in a real browser by countingmux-videocreations:The consuming site worked around it by dropping
autoPlay, which only means nothing starts the orphan. The leak was still there for anyone who passesautoPlay, or whose element is already playing when a teardown happens: a route change mid-playback, a remount on a changedkey, a fast refresh. A source change is not one of these, since it flows throughswapSource, which keeps the element and never callsdestroy().What changed
src/mux/provider.tsandsrc/native/provider.ts, indestroy(), after the listeners come off and beforeel.remove():el.removeAttribute("src"). A detached element that keeps pulling segments is a bandwidth leak even when it is silent.Why clearing
srcis the release, and whydisconnectedCallbackis not enoughRead out of
node_modules, not guessed:MuxVideoBaseElement.attributeChangedCallbackforsrccallsthis.unload()when the old value was truthy and the new one is not.unload()isteardown(nativeEl, coreRef, this)in@mux/playback-core, which doesengine.detachMedia(),engine.destroy(), thennativeEl.removeAttribute("src")andnativeEl.load(). That is the library's own release path.disconnectedCallback()calls the sameunload(), so in theoryel.remove()alone would do it. In practice it loses a race:mux-videodefers its setup by a microtask (await Promise.resolve()beforethis.load()), so an element mounted and destroyed in the same tick, which is exactly the React double effect, is disconnected before there is anything to tear down, and then finishes coming up, withautoplay, on a detached element. Clearingsrcfirst wins that race: a lateinitialize()with no src builds no engine and callsremoveAttribute("src")on the inner<video>instead of loading it.el.playbackId = undefineddoes not release anything: theplaybackIdgetter falls back to parsing the currentsrc, so the derivedsrcis recomputed to the same URL and the attribute survives.el.unload()on its own releases the engine but leavessrcin place, so the pending setup re-initializes it a microtask later.The native provider already did
removeAttribute("src")plusload(), and the load algorithm pauses, so it was not leaking. Its stop rode entirely onload(), which the provider itself swallows where that is unavailable, so it now pauses explicitly too rather than being fixed by side effect.The other providers
Not touched, and not the same shape underneath:
destroy(), which takes the player iframe with it. Both already have tests for it (destroy tears down the player,tears down the player on destroy), and both guard the case where the SDK resolves after teardown.Test
src/mux/provider.test.tsis new;src/native/provider.test.tsgains one case. jsdom decodes no media, so they assert the observable contract instead:pause()is called on the element, the element reports itself paused, it is detached, and its source is gone. One case waits a tick to prove the release survivesmux-videofinishing its own setup, and one runs the mount, destroy, mount sequence and checks the first element is left stopped while the second is live.Against the unfixed providers:
destroy detaches the elementpasses on both sides, as it should: detaching was never the broken half.With the fix:
Checks
Everything CI runs, locally, all green:
Not verified, and one thing found on the way
srcclear silence a real orphan is reasoned from the mux and playback-core source, not measured in a browser.destroy()removing their iframes is from their published API contracts and their existing fakes, not from a live SDK.mount()in the mux provider registerstextTrackslisteners without thetypeof tt.addEventListener === "function"guard its native sibling carries, so it throws in jsdom, wheretextTracksis a plain array. The new test shims that in the test file rather than changing the provider, to keep this PR to the teardown bug. Worth a separate look, since it means consumers cannot mount a Mux player in their own jsdom tests.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests