Current Behavior:
Since 1.13.0, a <MediaPlayer load="eager"> (audio) can end up in a dead state where the media never loads. When the player's children (<MediaProvider> + a custom layout) commit in the same React render as the host element, provider-change sometimes fires before media-player-connect. When that ordering happens, provider-setup never fires — the provider's media element is never created, load-start/loaded-metadata/can-play never fire, canLoad/duration stay 0, and playback can never start (seek bar stuck at 0:00, play button / .play() are no-ops).
Bisected:
| Version |
Behavior |
| 1.12.13 |
✅ Reliable (hundreds of loads, zero failures) |
| 1.13.0 |
❌ 100% broken — media never loads |
| 1.15.2 / 1.15.3 |
⚠️ Intermittent — fails ~1 in 3–5 loads |
The first bad version is 1.13.0, which contains:
React: "use useLayoutEffect for hasMounted to prevent source element race condition"
That change appears to have introduced a different source/provider ordering race for setups that mount children alongside the player. On 1.14.0+ the maverick@0.44.1 bump seems to jitter the timing, turning the deterministic 1.13.0 break into the intermittent 1.15.x one.
Expected Behavior:
With load="eager", the provider should set up and the source should load to can-play reliably — i.e. provider-setup should fire (and loading proceed) even when the element's media-player-connect happens after a provider-change. An element that connects after the provider has already changed should not be left permanently un-set-up.
Steps To Reproduce:
- React 18, client-side only (no SSR, not in StrictMode).
- Render an audio player with eager load and a custom layout (not
<DefaultAudioLayout>) whose child consumes media state:
<MediaPlayer src={mp3Url} viewType="audio" load="eager" playsInline>
<MediaProvider />
<CustomLayout /> {/* uses useMediaState('paused'/'duration') + useMediaRemote() */}
</MediaPlayer>
- Load the page.
- On 1.13.0: fails every time.
- On 1.15.3: hard-reload several times (~1 in 3–5 fails).
- Observe the lifecycle event order on the
<media-player> element. On failure, provider-change fires before media-player-connect, and provider-setup / can-play never fire; duration stays 0.
Reproduction Link: I wasn't able to isolate a standalone repro, the failure only manifests in our embed bundle under real timing (player mounts after an async config fetch, on a busy host page). Stripped-down sandboxes mounting the player on first render didn't reproduce. The evidence below (clean version bisect + side-by-side pass/fail event timelines) should be enough to localize it; happy to gather any additional traces you need.
Environment:
- Framework: React 18.2
- Meta Framework: none (Vite SPA / embeddable widget)
@vidstack/react + vidstack: 1.15.3 (regressed at 1.13.0; 1.12.13 is the last good version)
- Node: 23.11.10
- Device: Desktop
- OS: macOS
- Browser: Chrome 148
Anything Else?
Event timelines (ms from player creation; listeners attached to the <media-player> element). The only difference between pass and fail is the order of media-player-connect vs the first provider-change:
✅ Working (1.12.13):
player-ref-attached
→ media-player-connect ← element connects FIRST
→ source-change → provider-change → sources-change ×2 → provider-change ×2
→ provider-setup → load-start → duration-change → loaded-metadata → can-play (~135 ms)
❌ Broken (1.13.0, deterministic):
player-ref-attached (isConnected: false at ref time)
→ provider-change → provider-change ← provider changes BEFORE connect
→ media-player-connect ← connect happens AFTER
→ source-change → provider-change → sources-change ×2
→ (nothing further — no provider-setup, no load-start, no can-play, ever)
Workarounds tried in app code (all failed on 1.15.x):
- Gating
<MediaProvider> to mount only after media-player-connect — restored connect-first ordering, but provider-setup still never fired.
- Rendering
<MediaPlayer> directly with a plain ref (no custom mount logic).
load="play" + explicit player.startLoading() on the user gesture.
- Delaying mount.
Only fix: pin exact @vidstack/react@1.12.13 + vidstack@1.12.13 (the ^ range silently resolves to 1.15.x).
Current Behavior:
Since 1.13.0, a
<MediaPlayer load="eager">(audio) can end up in a dead state where the media never loads. When the player's children (<MediaProvider>+ a custom layout) commit in the same React render as the host element,provider-changesometimes fires beforemedia-player-connect. When that ordering happens,provider-setupnever fires — the provider's media element is never created,load-start/loaded-metadata/can-playnever fire,canLoad/durationstay0, and playback can never start (seek bar stuck at0:00, play button /.play()are no-ops).Bisected:
The first bad version is 1.13.0, which contains:
That change appears to have introduced a different source/provider ordering race for setups that mount children alongside the player. On 1.14.0+ the
maverick@0.44.1bump seems to jitter the timing, turning the deterministic 1.13.0 break into the intermittent 1.15.x one.Expected Behavior:
With
load="eager", the provider should set up and the source should load tocan-playreliably — i.e.provider-setupshould fire (and loading proceed) even when the element'smedia-player-connecthappens after aprovider-change. An element that connects after the provider has already changed should not be left permanently un-set-up.Steps To Reproduce:
<DefaultAudioLayout>) whose child consumes media state:<media-player>element. On failure,provider-changefires beforemedia-player-connect, andprovider-setup/can-playnever fire;durationstays0.Reproduction Link: I wasn't able to isolate a standalone repro, the failure only manifests in our embed bundle under real timing (player mounts after an async config fetch, on a busy host page). Stripped-down sandboxes mounting the player on first render didn't reproduce. The evidence below (clean version bisect + side-by-side pass/fail event timelines) should be enough to localize it; happy to gather any additional traces you need.
Environment:
@vidstack/react+vidstack: 1.15.3 (regressed at 1.13.0; 1.12.13 is the last good version)Anything Else?
Event timelines (ms from player creation; listeners attached to the
<media-player>element). The only difference between pass and fail is the order ofmedia-player-connectvs the firstprovider-change:✅ Working (1.12.13):
❌ Broken (1.13.0, deterministic):
Workarounds tried in app code (all failed on 1.15.x):
<MediaProvider>to mount only aftermedia-player-connect— restored connect-first ordering, butprovider-setupstill never fired.<MediaPlayer>directly with a plain ref (no custom mount logic).load="play"+ explicitplayer.startLoading()on the user gesture.Only fix: pin exact
@vidstack/react@1.12.13+vidstack@1.12.13(the^range silently resolves to 1.15.x).