Fix subtitle selection timing out: hls.js's default timeouts too tight
All checks were successful
Publier l'image Docker / test (push) Successful in 24s
Publier l'image Docker / publish (push) Successful in 9s

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 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-14 13:11:06 +02:00
parent 4eab0323d6
commit fff0b7a984
4 changed files with 83 additions and 3 deletions

View file

@ -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 };
}

View file

@ -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);

View file

@ -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;

View file

@ -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,