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; 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 { interface HlsStatic {
new (): HlsInstance; new (config?: HlsConfig): HlsInstance;
isSupported(): boolean; isSupported(): boolean;
Events: { MANIFEST_PARSED: string; ERROR: string }; Events: { MANIFEST_PARSED: string; ERROR: string };
} }

View file

@ -32,6 +32,19 @@ import type { MusicCurrentPublic, MusicPublicState, PartyPublicState, SocketMemb
* for music parties. * 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 barEl: HTMLElement | null = null;
let activePartyId: string | null = null; let activePartyId: string | null = null;
let conn: PartyConnection | 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. // skip vote is the actual recovery path, not a crashed bar.
try { try {
if (window.Hls && window.Hls.isSupported()) { 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.MANIFEST_PARSED, resume);
hls.on(window.Hls.Events.ERROR, (_event, data) => { hls.on(window.Hls.Events.ERROR, (_event, data) => {
if (data.fatal) console.error('[watchparty musicBar] HLS error', 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_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 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 // "Quality" is really "max transcoding bitrate", the same simplification
// Emby's/Jellyfin's own web clients use - not true adaptive-bitrate // Emby's/Jellyfin's own web clients use - not true adaptive-bitrate
// switching. See docs/architecture.md. // switching. See docs/architecture.md.
@ -241,7 +261,7 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
}; };
if (window.Hls && window.Hls.isSupported()) { 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.MANIFEST_PARSED, resume);
hls.on(window.Hls.Events.ERROR, (_event, data) => { hls.on(window.Hls.Events.ERROR, (_event, data) => {
if (!data.fatal) return; 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 fault has been narrowed to Emby's own handling of that parameter rather
than anything before it in this app. 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 ## Assumptions to verify
**Confirmed against the real server** (not a guess — actually exercised, **Confirmed against the real server** (not a guess — actually exercised,