Spots

Build a React video player hook that actually releases the player (and a…

TL;DR Single-page apps leak video players on every route change unless three things are torn down in the right order: the hls.js instance, the <video> element's decoder, and your own references. We'll build a useHlsPlayer hook that does it correctly, then a Playwright test that mounts the player 100 times and fails CI if Chrome's "too many WebMediaPlayers" error ever appears.

Single-page apps leak video players on every route

Single-page apps leak video players on every route change unless three things are torn down in the right order: the hls.js instance, the <video> element's decoder, and your own references. We'll build a useHlsPlayer hook that does it correctly, then a Playwright test that mounts the player 100 times and fails CI if Chrome's "too many WebMediaPlayers" error ever appears.

A React hook, useHlsPlayer, that owns the whole

A React hook, useHlsPlayer, that owns the whole player lifecycle, and an end-to-end test that catches the regression when someone deletes the cleanup. Stack: React 19, hls.js 1.7, Vite, Playwright. Node 22. Why this matters: Chrome counts your players

Since around Chrome 92, Chromium enforces a cap

Since around Chrome 92, Chromium enforces a cap on media players per frame: 75 on desktop, 40 on mobile. Cross it and the next <video> you try to play logs this and refuses:

The Chromium team picked those numbers as the

The Chromium team picked those numbers as the 99.9th percentile of what real pages create, specifically to stop infinite-scroll video sites from holding thousands of players in memory. If your app can reach the cap, it is leaking players. Let's make sure it can't.

Add a test stream. Any public HLS URL

Add a test stream. Any public HLS URL works; the hls.js demo streams are fine for a leak test since we only care about player creation, not content. 2. The naive hook (this is what leaks) Here is the version that shows up in most codebases. It works on first mount and leaks on every remount.

Every time src changes or the component remounts

Every time src changes or the component remounts, a new Hls instance is created and the old one is still alive, still attached, still holding a MediaSource. Remove the <video> from the DOM and the browser-side player is still there too, because nothing told it to release. 3. The hook with correct teardown Three layers, torn down in this order.

⚠️ Note: there is a maintainer-confirmed footgun here

⚠️ Note: there is a maintainer-confirmed footgun here. If you call hls.removeAllListeners() before hls.destroy(), destroy cannot complete, because hls.js's internal components listen for the DESTROYING event to clean themselves up. You end up with a half-dead instance that still ticks on media events and throws Cannot read properties of null (reading 'getAppendedFrag') on the next route. Let destroy() do the listener removal itself. 💡 Tip: if you ever set video.src to a blob or object URL, add URL.revokeObjectURL(url) to the cleanup. Clearing src does not free the Blob.

⚠️ Note: there is a maintainer-confirmed footgun here

⚠️ Note: there is a maintainer-confirmed footgun here. If you call hls.removeAllListeners() before hls.destroy(), destroy cannot complete, because hls.js's internal components listen for the DESTROYING event to clean themselves up. You end up with a half-dead instance that still ticks on media events and throws Cannot read properties of null (reading 'getAppendedFrag') on the next route. Let destroy() do the listener removal itself.

💡 Tip: if you ever set video.src to

💡 Tip: if you ever set video.src to a blob or object URL, add URL.revokeObjectURL(url) to the cleanup. Clearing src does not free the Blob.

News

Build a React video player hook that actually releases the player (and a Playwright test that proves it)

TL;DR Single-page apps leak video players on every route change unless three things are torn down in the right order: the hls.js instance, the <video> element's decoder, and your own references.

@spots #dev
Source: Dev.to
See more like this