fix(plex): the slow line says both rates in one unit

Each rate took its own unit, so a link under a megabit read "needs 1.4
Mbps, 800 kbps arrives". `fmtBitrates` picks the unit the largest takes
for all of them. The away-window header now says what ends an absence
outside hls.js (the answer) and after the error (the viewer's retry).
This commit is contained in:
2026-11-08 00:49:22 +01:00
parent e8b9bdd7c1
commit 470caacfb0
5 changed files with 66 additions and 10 deletions
+6 -4
View File
@@ -31,10 +31,12 @@ export const RECLAIM_WINDOW_MS = 120_000;
// less often, and says it cannot play only once the server has been away this long. It used to say
// so at the first 502, over a picture still playing from the buffer, and never asked again.
//
// The absence ends when MEDIA arrives again (a fragment loads), not when a session request is
// answered: a server that answers every session and delivers none of them is still not serving, and
// ending the absence at each answer restarted the stream forever. The same window bounds a link too
// slow for the stream (see `slowLink`).
// Under hls.js the absence ends when MEDIA arrives again (a fragment loads), not when a session
// request is answered: a server that answers every session and delivers none of them is still not
// serving, and ending the absence at each answer restarted the stream forever. What hls.js does not
// play (a direct file, HLS the browser plays natively) reports no fragments, so there the answer
// ends it (`answered`); and the viewer trying again after the error starts a window of its own
// (`start`). The same window bounds a link too slow for the stream (see `slowLink`).
export const AWAY_WINDOW_MS = 180_000;
const AWAY_RETRY_MS = [2_000, 4_000, 8_000, 15_000];
const SERVER_AWAY = new Set([0, 502, 503, 504]);
+8
View File
@@ -6,6 +6,7 @@ import {
buildStatRows,
createFragMeter,
fmtBitrate,
fmtBitrates,
fmtBufferSecs,
fmtClock,
fmtCodec,
@@ -28,6 +29,13 @@ describe("plexStats formatters", () => {
expect(fmtBitrate(4_200_000, "hu")).toBe("4,2 Mbps"); // the UI language's decimal mark
});
it("fmtBitrates says rates side by side in the unit the largest takes", () => {
expect(fmtBitrates([1_400_000, 800_000], "en")).toEqual(["1.4 Mbps", "0.8 Mbps"]);
expect(fmtBitrates([1_400_000, 800_000], "hu")).toEqual(["1,4 Mbps", "0,8 Mbps"]);
expect(fmtBitrates([900_000, 400_000], "en")).toEqual(["900 kbps", "400 kbps"]);
expect(fmtBitrates([2_000_000, 0], "en")).toEqual(["2.0 Mbps", DASH]);
});
it("fmtBufferSecs shows one decimal, dashes negatives/non-finite", () => {
expect(fmtBufferSecs(12.34, "en")).toBe("12.3 sec");
expect(fmtBufferSecs(0, "en")).toBe("0.0 sec");
+10
View File
@@ -64,6 +64,16 @@ export function fmtBitrate(bps: number | undefined, lang: string): string {
return `${formatNumber(Math.round(bps / 1000), lang)} kbps`;
}
// Rates read side by side ("needs 1.4 Mbps, 0.8 Mbps arrives"): one unit for all of them, the one
// the largest would take alone, so they compare at a glance — never "1.4 Mbps" beside "800 kbps".
export function fmtBitrates(rates: number[], lang: string): string[] {
const largest = Math.max(0, ...rates);
if (largest < 1_000_000) return rates.map((bps) => fmtBitrate(bps, lang));
return rates.map((bps) =>
bps > 0 ? `${formatDecimal(bps / 1_000_000, lang)} Mbps` : fmtBitrate(bps, lang),
);
}
// Seconds of buffer ahead of the playhead → "12.3 sec" / "12,3 mp".
export function fmtBufferSecs(s: number, lang: string): string {
if (!Number.isFinite(s) || s < 0) return DASH;