Skip to content

Unmounting <MediaPlayer> during HLS chunk load throws null.addEventListener #1771

@rbao

Description

@rbao

Current Behavior:

TypeError: Cannot read properties of null (reading 'addEventListener') thrown from EventsController.add when an HLS-sourced <MediaPlayer> is unmounted while the HLS provider chunk is still being dynamically imported.

The crash has two compounding causes:

1. Stale-target read across await in HLSProviderLoader.load (packages/vidstack/src/providers/video/hls/loader.ts):

async load(context) {
  if (__SERVER__) throw Error(...);
  return new (await import('./provider')).HLSProvider(this.target, context);
  //                                                  ^^^^^^^^^^^ read AFTER await
}

this.target is mutable. Between dispatching the dynamic import and its resolution, MediaProvider#runLoader(null) (triggered by the <video> ref callback firing with null on unmount) reassigns loader.target = null. The post-await read sees the null, and new HLSProvider(null, ctx) is constructed.

2. canUsePictureInPicture returns true for null input (packages/vidstack/src/utils/support.ts):

export function canUsePictureInPicture(video: HTMLVideoElement | null): boolean {
  if (__SERVER__) return false;
  return !!document.pictureInPictureEnabled && !video?.disablePictureInPicture;
  //                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  //                            !undefined === true when video is null
}

This gates the unguarded branch in VideoProvider's constructor:

} else if (canUsePictureInPicture(video)) {
  this.pictureInPicture = new VideoPictureInPicture(video, ctx); // video is null
}

new EventsController(null).add('enterpictureinpicture', ...) then dereferences null and crashes.

This is the same predicate-bug class as #989, which fixed canUseVideoPresentation but did not extend the fix to canUsePictureInPicture. The last comment on #989 from @dinonondi reports the same null PiP crash. The HLSProviderLoader race underneath it appears not to have been reported separately.

Expected Behavior:

HLSProviderLoader.load() should either return a provider for the target it was configured with, or return null to signal the target was invalidated mid-load — so the caller can skip broadcasting a doomed provider. canUsePictureInPicture should reject null inputs, matching the contract its name implies and the fix already applied to canUseVideoPresentation in aff2d294.

Steps To Reproduce:

  1. Render an HLS source in <MediaPlayer> — e.g. src="https://.../manifest/video.m3u8" with <MediaProvider /> inside.
  2. Open DevTools → Network → enable Disable cache.
  3. Set throttling to Slow 4G (or any preset where the HLS provider chunk takes >500 ms to download).
  4. Hard-reload the page so the HLS chunk is uncached in the ESM module registry.
  5. Mount the player, then unmount it before the HLS chunk finishes downloading. With throttling on, this is a reliable several-second window.
  6. When the chunk finishes loading, the addEventListener crash fires.

Minimal reproducer:

function Repro() {
  const [show, setShow] = useState(false)
  return (
    <>
      <button onClick={() => setShow(s => !s)}>Toggle</button>
      {show && (
        <MediaPlayer src="https://.../manifest/video.m3u8">
          <MediaProvider />
        </MediaPlayer>
      )}
    </>
  )
}

Click "Toggle" once → wait until the HLS chunk is still pending in the Network panel → click "Toggle" again → observe the crash when the chunk arrives.

Environment:

  • @vidstack/react: 1.12.13 (next tag)
  • hls.js: 1.6.15
  • React: 19.1.0
  • Node: 24.11.1
  • Browser: Chromium 136 (Electron 40.1)
  • OS: macOS 15

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions