From fff0b7a984a211009f4e3f4c3469575cb84af7ef Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 14 Sep 2026 13:11:06 +0200 Subject: [PATCH] Fix subtitle selection timing out: hls.js's default timeouts too tight Reported: selecting a subtitle track (only subtitles, not audio/quality) made the request "time out". Confirmed from the browser's own error: hls.js gave up with a fatal fragLoadTimeOut after 4 retries, each hitting its default 10s fragment-load timeout - not a hang, and not a repeat of the earlier "override doesn't reach Emby" bug (see docs/architecture.md's Player-track-selection section) - the same error's frag.baseurl already shows SubtitleMethod=Encode correctly present in the request Emby received, so the override chain works end to end. It's just slow: burning a subtitle in forces a real re-encode (not a rendition, not a remux), which measurably takes Emby longer than 10s to produce a first segment for. Fixed by giving both players' Hls instances explicit longer timeouts (manifestLoadingTimeOut/levelLoadingTimeOut: 30s, fragLoadingTimeOut: 60s) instead of hls.js's untouched defaults - applied to musicBar/MusicBar.ts too, not just the video player, since music tracks go through the same PlaybackInfo/transcode pipeline and could hit the same latency for a quality change even without subtitles being involved there today. global.d.ts's HlsStatic constructor gained an optional HlsConfig parameter (only the four properties actually used) so this doesn't need an `as any`/unsafe cast to pass config through. Verified the property names are real and actually honored by hls.js 1.5.17 (the exact pinned CDN build), not silently ignored: constructed a real Hls instance with this config and read back hls.config.* - all four values came back as set, not as hls.js's own (tighter) defaults. Co-Authored-By: Claude Sonnet 5 --- client/src/global.d.ts | 12 +++++++++- client/src/musicBar/MusicBar.ts | 15 ++++++++++++- client/src/views/player/Player.ts | 22 +++++++++++++++++- docs/architecture.md | 37 +++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/client/src/global.d.ts b/client/src/global.d.ts index 2ab3b49..4b4fe92 100644 --- a/client/src/global.d.ts +++ b/client/src/global.d.ts @@ -46,8 +46,18 @@ interface HlsInstance { destroy(): void; } +/** Only the loading-timeout knobs Player.ts/musicBar actually override - + * see their own HLS_CONFIG comment for why the defaults are too tight + * for this app's real transcode latency. */ +interface HlsConfig { + manifestLoadingTimeOut?: number; + manifestLoadingMaxRetry?: number; + levelLoadingTimeOut?: number; + fragLoadingTimeOut?: number; +} + interface HlsStatic { - new (): HlsInstance; + new (config?: HlsConfig): HlsInstance; isSupported(): boolean; Events: { MANIFEST_PARSED: string; ERROR: string }; } diff --git a/client/src/musicBar/MusicBar.ts b/client/src/musicBar/MusicBar.ts index 5f1a5f3..4e3d06b 100644 --- a/client/src/musicBar/MusicBar.ts +++ b/client/src/musicBar/MusicBar.ts @@ -32,6 +32,19 @@ import type { MusicCurrentPublic, MusicPublicState, PartyPublicState, SocketMemb * for music parties. */ +// See views/player/Player.ts's own HLS_CONFIG for why these are raised +// well above hls.js's defaults (10s manifest/level/frag load timeouts) - +// every track here also goes through CatalogService.proxyStream into a +// fresh Emby transcode, same latency risk as the video player. Duplicated +// rather than shared - the two players' hls.js setup is already +// independently duplicated for the same reason (see loadTrack below). +const HLS_CONFIG: HlsConfig = { + manifestLoadingTimeOut: 30000, + manifestLoadingMaxRetry: 2, + levelLoadingTimeOut: 30000, + fragLoadingTimeOut: 60000 +}; + let barEl: HTMLElement | null = null; let activePartyId: string | null = null; let conn: PartyConnection | null = null; @@ -168,7 +181,7 @@ function loadTrack(track: MusicCurrentPublic, elapsedSeconds: number, isPaused: // skip vote is the actual recovery path, not a crashed bar. try { if (window.Hls && window.Hls.isSupported()) { - hls = new window.Hls(); + hls = new window.Hls(HLS_CONFIG); hls.on(window.Hls.Events.MANIFEST_PARSED, resume); hls.on(window.Hls.Events.ERROR, (_event, data) => { if (data.fatal) console.error('[watchparty musicBar] HLS error', data); diff --git a/client/src/views/player/Player.ts b/client/src/views/player/Player.ts index 740e8d6..c12ed82 100644 --- a/client/src/views/player/Player.ts +++ b/client/src/views/player/Player.ts @@ -15,6 +15,26 @@ const DRIFT_TOLERANCE_SECONDS = 1.5; const PAUSE_REQUEST_COOLDOWN_MS = 5000; // matches the server's own throttle in SocketGateway const PAUSE_REQUEST_HIGHLIGHT_MS = 4000; // how long the banner + the orange badge stay up +// hls.js's own defaults (10s manifest/level/frag load timeouts) assume a +// file server or a source already being direct-played/remuxed - too tight +// for this app, where every request chains through our own proxy into a +// FRESH Emby transcode (see CatalogService.proxyStream/applyPlaybackOverrides). +// Confirmed for real: picking a subtitle track forces SubtitleMethod=Encode +// (a real re-encode, to actually burn the subtitle in - see +// applyPlaybackOverrides's own comment), which measurably took Emby longer +// than 10s to produce a first segment for, and hls.js gave up with a fatal +// fragLoadTimeOut after its default 4 retries - not a hang, a real but slow +// transcode start being mistaken for a dead server. Same config duplicated +// in musicBar/MusicBar.ts (its own HLS_CONFIG) rather than shared - the two +// players' hls.js setup is already independently duplicated for the same +// reason (see that file's own loadTrack). +const HLS_CONFIG: HlsConfig = { + manifestLoadingTimeOut: 30000, + manifestLoadingMaxRetry: 2, + levelLoadingTimeOut: 30000, + fragLoadingTimeOut: 60000 +}; + // "Quality" is really "max transcoding bitrate", the same simplification // Emby's/Jellyfin's own web clients use - not true adaptive-bitrate // switching. See docs/architecture.md. @@ -241,7 +261,7 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli }; if (window.Hls && window.Hls.isSupported()) { - hls = new window.Hls(); + hls = new window.Hls(HLS_CONFIG); hls.on(window.Hls.Events.MANIFEST_PARSED, resume); hls.on(window.Hls.Events.ERROR, (_event, data) => { if (!data.fatal) return; diff --git a/docs/architecture.md b/docs/architecture.md index 91499ce..1c60767 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -446,6 +446,43 @@ once this log line confirms `subtitleMethod=Encode` really was sent, the fault has been narrowed to Emby's own handling of that parameter rather than anything before it in this app. +**Round 3, after a real retest: the override reaches Emby correctly - it's +just slow.** Not a repeat of Round 2's bug. Confirmed directly from the +browser's own error report this time (`SubtitleMethod%3DEncode` present, +URL-decoded, right there in the failing fragment's own request URL - even +better evidence than the server-side stream log, since it's the literal +resolved URL rather than what this app computed before sending it), so the +override chain genuinely works end to end: Emby *is* being asked to burn +the subtitle in, and does. The failure is downstream of that, in a place +Round 1/2 had no reason to look at: `hls.js` is constructed with no config +at all (`new Hls()`, defaults only), and its default fragment-load timeout +is 10 seconds - fine for a source Emby can remux almost instantly, too +tight for a source Emby has to fully re-encode from scratch, which +`SubtitleMethod=Encode` always requires (see the "not a rendition" reasoning +above - burning a subtitle in is fundamentally a heavier operation than +picking an audio track or even changing quality, neither of which +necessarily forces a re-encode the same way). hls.js retried 4 times, each +timing out the same way, then gave up with a fatal `fragLoadTimeOut` - +correctly reported by the existing error handling, just for a cause +("Emby hasn't finished transcoding yet") this player had never actually +hit before subtitles specifically forced a re-encode on what was probably, +until now, mostly direct-play/remux traffic. + +Fixed by giving both players' `Hls` instances explicit, longer timeouts +(`manifestLoadingTimeOut`/`levelLoadingTimeOut`: 30s, `fragLoadingTimeOut`: +60s - `views/player/Player.ts`'s `HLS_CONFIG`, duplicated in +`musicBar/MusicBar.ts` for the same reason the rest of their hls.js setup +already is) rather than only patching the video player: music tracks go +through the exact same `PlaybackInfo` → transcode pipeline (see "Music: +participative playlist" above), so a slow-to-start transcode is a latent +risk there too, quality changes included, even though only video actually +has subtitles to trigger it today. Verified the config values are actually +honored, not silently ignored by a wrong property name, by constructing a +real `Hls` instance from the exact pinned CDN build +(`hls.js/1.5.17/hls.min.js`) with this config and reading back +`hls.config.fragLoadingTimeOut` etc. - all four came back as set, not as +hls.js's own defaults. + ## Assumptions to verify **Confirmed against the real server** (not a guess — actually exercised,