Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
42 changes: 32 additions & 10 deletions src/engines/mpegts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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<string, unknown> {
export function configFor(_isTv: boolean, options: MpegtsOptions = {}): Record<string, unknown> {
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.
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 24 additions & 4 deletions test/mpegts-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading