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', () => {