/** * What to do about a *fatal* hls.js network error. * * Jellyfin's transcoded HLS streams do not always terminate with an * `#EXT-X-ENDLIST`, so a stream that has simply run out looks identical to one * that broke: both arrive as a fatal network error. The only thing separating * them is how far playback had got, which is why this decision is worth * isolating from the player component — read the position wrong and a * recoverable stall turns into a skip to the next item. * * TRACES: UR-004, UR-074 | DR-177 | UT-174 */ /** Fraction of the runtime past which a fatal error reads as "the stream ended". */ const NEAR_END_FRACTION = 0.9; /** How many times to ask hls.js to resume before giving up on the stream. */ export const MAX_FATAL_NETWORK_RECOVERIES = 3; export type FatalNetworkErrorAction = "ended" | "retry" | "giveUp"; export interface FatalNetworkErrorInput { /** * Absolute position in the media, in seconds — the value the player displays. * * It is already absolute (`seekOffset + element.currentTime`): do NOT add the * transcode seek offset again. After a quality switch or a transcoded seek the * offset *is* the resume position, so double-counting it puts an apparent * position past the near-end threshold from roughly halfway through, and every * transient error then ends playback. */ positionSeconds: number; /** Known runtime in seconds; 0 or negative when the runtime isn't known yet. */ knownDurationSeconds: number; /** Recovery attempts already made against this hls.js instance. */ attempts: number; } /** Whether a failure at this position should be read as the stream ending. */ export function isNearEndOfStream(positionSeconds: number, knownDurationSeconds: number): boolean { if (knownDurationSeconds <= 0 || positionSeconds <= 0) return false; return positionSeconds / knownDurationSeconds > NEAR_END_FRACTION; } export function fatalNetworkErrorAction({ positionSeconds, knownDurationSeconds, attempts, }: FatalNetworkErrorInput): FatalNetworkErrorAction { if (isNearEndOfStream(positionSeconds, knownDurationSeconds)) return "ended"; return attempts <= MAX_FATAL_NETWORK_RECOVERIES ? "retry" : "giveUp"; }