From 829ee734b7093191705433b908b23e008c12b602 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 03:21:06 +0000 Subject: [PATCH] enableWorker is webpack-only: default it off, make it opt-in The previous commit turned the transmuxing worker on for everyone because media-streamer's live TV player sets it. media-streamer is Next.js. That is load-bearing, and it is the only reason it works there. mpegts.js builds its worker by STRINGIFYING `__webpack_modules__` -- webpack's internal module registry -- in `utils/webworkify-webpack.js`. A webpack host has that global. A host bundling with Bun, esbuild, Vite or Rollup does not, and the worker it assembles is broken. It then fails in the worst available way. `Transmuxer` does wrap the setup in a try/catch and fall back to inline transmuxing, but only a SYNCHRONOUS throw reaches that catch. A Worker that constructs from a blob whose body then fails is an asynchronous failure: nothing throws, nothing falls back, no init segment ever arrives, and the player sits there having reported no error at all. Every stream simply does not play. This is not hypothetical -- it took tipoffwatch's player out in production, which is bundled by Bun. Shipping it from here would have done the same to every consumer that is not Next.js. So the default is false and a host that knows it is webpack can pass `enableWorker: true` through MpegtsOptions. The capability is real where the bundler supports it; it just must never be the default. Nothing else changes. The restart budget still refills on `playing`, which is the fix that stops streams dying after a minute or two, and the buffering profile is still media-streamer's on every screen. 92 tests pass, typecheck and prettier clean. 0.4.0 was never published, so this lands as 0.4.1 before anything consumes it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S5KxNHPQPY9Do9eWfJnGP4 --- package.json | 2 +- src/engines/mpegts.ts | 42 +++++++++++++++++++++++++++++--------- test/mpegts-config.test.ts | 28 +++++++++++++++++++++---- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index e3e993c..359b85c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/player", - "version": "0.4.0", + "version": "0.4.1", "description": "One web player for every source a Profullstack site serves: MP4, HLS, MPEG-2 transport streams and audio, with one control bar, on desktop, mobile, PWA and television.", "keywords": [ "video", diff --git a/src/engines/mpegts.ts b/src/engines/mpegts.ts index 4737c24..b8f810d 100644 --- a/src/engines/mpegts.ts +++ b/src/engines/mpegts.ts @@ -46,6 +46,15 @@ export interface MpegtsOptions { withCredentials?: boolean; /** Appended to a codec failure, e.g. "VLC can — the button is beside Play." */ unplayableAdvice?: string; + /** + * Demux on a worker thread. Only set this on a **webpack** host. + * + * mpegts.js assembles its worker out of `__webpack_modules__`, so anywhere + * else the worker is broken — and broken asynchronously, which means no + * error, no fallback, and no playback at all. Defaults to false. See the + * note in `configFor`. + */ + enableWorker?: boolean; } /** @@ -73,19 +82,32 @@ export interface MpegtsOptions { * ever complained. */ /** Exported for the test that pins these values; not part of the public API. */ -export function configFor(_isTv: boolean): Record { +export function configFor(_isTv: boolean, options: MpegtsOptions = {}): Record { return { /* - * Demux on a worker thread. + * Off unless the host says otherwise, and the reason is not performance. + * + * Demuxing a broadcast-bitrate transport stream on the main thread competes + * with rendering the page it is playing on, so a worker looks like free + * performance. It is free only under webpack. + * + * mpegts.js builds its worker by STRINGIFYING `__webpack_modules__` -- + * webpack's internal module registry -- in `utils/webworkify-webpack.js`. + * A Next.js host has that global and the worker works. A host bundling with + * Bun, esbuild, Vite or Rollup does not, and the worker it assembles is + * broken. + * + * It then fails in the worst available way. `Transmuxer` wraps the setup in + * a try/catch and falls back to inline transmuxing, but only a SYNCHRONOUS + * throw reaches that catch. A Worker that constructs from a blob whose body + * then fails is asynchronous: nothing throws, nothing falls back, no init + * segment ever arrives, and the player sits there having reported no error. + * Every stream simply does not play. That is what turning this on by + * default did to tipoffwatch, whose bundle Bun builds. * - * A transport stream at broadcast bitrate is real work, and doing it on the - * main thread means it competes with rendering the page it is playing on -- - * which shows up as dropped frames rather than as an error. mpegts.js builds - * the worker from a blob URL, so a host serving a strict CSP needs - * `worker-src blob:` for this to take; without it the library falls back and - * the only thing lost is the contention it was avoiding. + * So it is opt-in, and only a host that knows it is webpack should opt in. */ - enableWorker: true, + enableWorker: options.enableWorker ?? false, /* * Read ahead, on every screen. @@ -159,7 +181,7 @@ export async function createMpegtsEngine( return { destroy: () => undefined, levels: () => [] }; } - const config = configFor(isTv); + const config = configFor(isTv, options); let player: MpegtsPlayer | null = null; let stopped = false; let restarts = 0; diff --git a/test/mpegts-config.test.ts b/test/mpegts-config.test.ts index afa70d2..eb05f10 100644 --- a/test/mpegts-config.test.ts +++ b/test/mpegts-config.test.ts @@ -43,10 +43,30 @@ describe('the mpegts buffering profile', () => { expect(desktop.stashInitialSize).toBe(384 * 1024); }); - it('demuxes off the main thread', () => { - // Broadcast-bitrate demuxing competes with rendering the page it plays on, - // and loses as dropped frames rather than as an error. - expect(desktop.enableWorker).toBe(true); + it('does NOT demux off the main thread unless the host opts in', () => { + /* + * A regression test with a real outage behind it. + * + * A worker reads like free performance, and it is -- under webpack only. + * mpegts.js assembles its worker by stringifying `__webpack_modules__`, + * webpack's internal module registry. A Next.js host has that global; a + * host bundling with Bun, esbuild, Vite or Rollup does not, and the worker + * it builds is broken. + * + * It fails silently, which is what makes it dangerous. `Transmuxer`'s + * try/catch only sees a SYNCHRONOUS throw, and a Worker that constructs + * from a bad blob fails asynchronously: nothing throws, nothing falls back + * to inline transmuxing, no init segment arrives, and the player reports no + * error. Every stream simply does not play. Turning this on by default did + * exactly that to tipoffwatch, whose bundle Bun builds. + */ + expect(desktop.enableWorker).toBe(false); + }); + + it('lets a webpack host ask for the worker anyway', () => { + // The capability is real where the bundler supports it, so it stays + // reachable -- just never by default. + expect(configFor(false, { enableWorker: true }).enableWorker).toBe(true); }); it('never pauses the download to idle', () => {