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 apps/web/public/vendor-mpegts.js

Large diffs are not rendered by default.

30 changes: 24 additions & 6 deletions apps/web/src/client/tv.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,32 @@ export function isTvBrowser(userAgent) {
export function playerConfig(_isTv) {
return {
/*
* Demux on a worker thread.
* NOT on a worker thread, and this one is a trap worth the paragraph.
*
* A transport stream at broadcast bitrate is real work, and on the main
* thread 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; we serve no CSP, so there is nothing to allow.
* Demuxing a broadcast-bitrate transport stream on the main thread competes
* with rendering the page it is playing on, so `enableWorker: true` looks
* like free performance, and media-streamer's live TV player does set it.
* It cannot be copied here.
*
* mpegts.js builds its worker by STRINGIFYING `__webpack_modules__` --
* webpack's internal module registry -- in `utils/webworkify-webpack.js`.
* That global exists in media-streamer, which is Next.js and therefore
* webpack. This bundle is built by Bun, where there is no such global, so
* the worker it assembles is broken.
*
* And it fails in the worst available way. `Transmuxer` wraps the worker
* 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 an asynchronous failure: nothing throws,
* nothing falls back, the init segment never arrives, and the player sits
* there having reported no error at all. That is not a dropped frame, it is
* "the stream does not play", which is what shipping this caused.
*
* Everything else in this profile came over from the television's, which
* had been running in production for weeks. This was the one line that had
* never run anywhere but under webpack.
*/
enableWorker: true,
enableWorker: false,

/*
* Read ahead, on every screen.
Expand Down
24 changes: 19 additions & 5 deletions test/player-codecs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,25 @@ describe('the buffering profile follows the screen', () => {
expect(config.stashInitialSize).toBe(384 * 1024);
});

test('the demuxer runs 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. We serve no CSP, so
// mpegts.js can build its blob worker unimpeded.
expect(playerConfig(false).enableWorker).toBe(true);
test('the demuxer does NOT run off the main thread', () => {
/*
* A regression test with a real outage behind it.
*
* `enableWorker: true` reads like free performance and media-streamer's
* live TV player does set it, so it was copied over -- and every stream
* stopped playing. mpegts.js builds its worker by stringifying
* `__webpack_modules__`, webpack's internal module registry. media-streamer
* is Next.js, so that global is there. This bundle is built by Bun, so it
* is not, and the worker is broken.
*
* The failure is silent: 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
* ever arrives, and the player reports no error at all.
*
* Do not turn this on again without changing how this file is bundled.
*/
expect(playerConfig(false).enableWorker).toBe(false);
});

test('both profiles still drop what has been watched, and never idle', () => {
Expand Down