Player: buffering visuals — spinner ring, buffer-ahead gauge, CDN chip

- Loading/Buffering overlays now show a rotating ring, not just text
- thin gauge under the video: how much is buffered ahead (0-30s scale),
  fills while loading, drains as it plays — you can see a stall building
- corner chip: live CDN KB/s from /api/cdn, green dot OK / amber degraded,
  refreshed on every stall — explains WHY it's buffering

Verified in-browser (CDN-independent): chip reflects live state (0 KB/s
degraded), gauge fills from video.buffered, stall overlay + ring,
playing clears. 18/18 API tests. assets v=5.
This commit is contained in:
jozamudi committed 2026-08-31 15:37:06 +00:00
1 parent a61e7dd0e7
commit 733fa673c2
3 files changed
+66 -7

No files matched your search

+48 -5
View File
@@ -723,15 +723,17 @@ function videoEl() {
return v;
}
function stopHls() { if (hls) { hls.destroy(); hls = null; } }
function stopHls() { if (hls) { hls.destroy(); hls = null; } if (bufTimer) { clearInterval(bufTimer); bufTimer = null; } }
let currentEp = null, lastSavedPos = 0, playerBroken = false, nextCta = null;
function playerOverlay(text, onAction, actionLabel = "Retry") {
function playerOverlay(text, onAction, actionLabel = "Retry", spin = false) {
const p = document.getElementById("player");
let o = p.querySelector(".pl-overlay");
if (!o) { o = el("div", "pl-overlay"); p.append(o); }
o.replaceChildren(el("span", null, text));
o.replaceChildren();
if (spin) o.append(el("span", "pl-spin")); // rotating ring: buffering shown, not told
o.append(el("span", null, text));
if (onAction) {
const b = el("button", "btn", actionLabel);
b.onclick = onAction;
@@ -754,6 +756,42 @@ function playerFail(msg) {
playerOverlay("Playback failed: " + msg, () => play(ep_id, ep_no));
}
// ---------- buffering visuals: buffer-ahead gauge + CDN chip ----------
let bufTimer = null;
function bufferGauge() {
// thin bar under the video: how much is buffered AHEAD of playhead
// (0-30s scale). Fills while loading, drains as it plays — you can
// watch a stall build before it hits.
const p = document.getElementById("player");
let g = p.querySelector(".pl-buffer");
if (!g) { g = el("div", "pl-buffer"); g.append(el("i")); p.append(g); }
const bar = g.querySelector("i");
const tick = () => {
const m = p.querySelector("video");
if (!m || !m.duration) { bar.style.width = "0%"; return; }
let ahead = 0;
for (let i = 0; i < m.buffered.length; i++)
if (m.buffered.end(i) > m.currentTime) { ahead = m.buffered.end(i) - m.currentTime; break; }
bar.style.width = (Math.min(ahead / 30, 1) * 100).toFixed(1) + "%";
g.title = "buffered " + ahead.toFixed(0) + "s ahead (full = 30s)";
};
tick();
if (bufTimer) clearInterval(bufTimer);
bufTimer = setInterval(tick, 250);
}
function cdnChip() {
// corner badge: live CDN throughput from /api/cdn (dot: green=OK, amber=degraded)
const p = document.getElementById("player");
let c = p.querySelector(".pl-cdn");
if (!c) { c = el("div", "pl-cdn"); c.append(el("i")); p.append(c); }
api("/api/cdn").then(d => {
c.className = "pl-cdn " + (d.state === "OK" ? "ok" : d.state === "DEGRADED" ? "degraded" : "");
c.lastChild.textContent = d.kbps + " KB/s";
c.title = "CDN " + d.state + " — " + d.kbps + " KB/s";
}).catch(() => { c.className = "pl-cdn"; c.lastChild.textContent = "CDN ?"; });
}
// ---------- auto-next on episode end (5s countdown) ----------
let nextTimer = null;
function onEnded() {
@@ -821,7 +859,9 @@ async function play(ep_id, ep_no) {
const v = videoEl();
v.playbackRate = +(localStorage.getItem("aniweb_spd") || 1);
document.getElementById("player").replaceChildren(v);
playerOverlay("Loading\u2026");
playerOverlay("Loading\u2026", null, null, true);
bufferGauge();
cdnChip();
if (!window.Hls || !Hls.isSupported()) throw new Error("HLS not supported in this browser");
// the upstream CDN occasionally stalls on segment bytes (0 bytes for
// 10s+). hls.js's default 5s time-to-first-byte abort would drop the XHR
@@ -865,7 +905,10 @@ async function play(ep_id, ep_no) {
v.onerror = () => playerFail("media error");
v.addEventListener("playing", playerHideOverlay);
v.addEventListener("waiting", () => {
if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026");
if (currentEp && !playerBroken && !v.ended) {
playerOverlay("Buffering\u2026", null, null, true);
cdnChip(); // refresh CDN readout exactly when it matters
}
});
v.addEventListener("ended", onEnded);
// ponytail: play() before first data can reject with NotAllowedError in