Skip to content

Regression (1.13.0): provider-setup never fires when provider-change precedes media-player-connect — eager media never loads #1836

@mfauveau

Description

@mfauveau

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:

  1. React 18, client-side only (no SSR, not in StrictMode).
  2. 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>
  3. Load the page.
    • On 1.13.0: fails every time.
    • On 1.15.3: hard-reload several times (~1 in 3–5 fails).
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions