diff --git a/static/app.js b/static/app.js index 5560943..94de47a 100644 --- a/static/app.js +++ b/static/app.js @@ -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 diff --git a/static/index.html b/static/index.html index e21b23f..bd7ece8 100644 --- a/static/index.html +++ b/static/index.html @@ -6,7 +6,7 @@ ani-web - +
@@ -30,6 +30,6 @@ max-age=300 in browser AND possibly in your HTTPS proxy, and a stale copy is exactly how users keep seeing an old UI after a deploy --> - + diff --git a/static/style.css b/static/style.css index 849118c..8225cb0 100644 --- a/static/style.css +++ b/static/style.css @@ -145,6 +145,22 @@ button.tiny.on { border-color: var(--acc); color: var(--acc); } .pl-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; gap: .6rem; align-items: center; justify-content: center; background: rgba(0, 0, 0, .72); border-radius: 10px; color: var(--fg); font-size: .95rem; z-index: 1; } +/* buffering visuals: spinner ring + buffer-ahead gauge + CDN chip */ +.pl-spin { width: 48px; height: 48px; flex: none; border-radius: 50%; + border: 4px solid rgba(255, 255, 255, .18); border-top-color: var(--acc); + animation: plspin .8s linear infinite; } +@keyframes plspin { to { transform: rotate(1turn); } } +.pl-buffer { height: 4px; margin-top: 6px; border-radius: 2px; overflow: hidden; + background: rgba(255, 255, 255, .1); } +.pl-buffer i { display: block; height: 100%; width: 0; + background: linear-gradient(90deg, var(--acc), #fbbf24); transition: width .25s ease; } +.pl-cdn { position: absolute; top: 10px; right: 10px; z-index: 2; + display: flex; align-items: center; gap: .4rem; + font: 600 .72rem/1 system-ui, sans-serif; padding: .3rem .55rem; border-radius: 999px; + background: rgba(0, 0, 0, .55); color: #fff; pointer-events: none; } +.pl-cdn i { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #6b7280; } +.pl-cdn.ok i { background: #22c55e; } +.pl-cdn.degraded i { background: #f59e0b; } #toasts { position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: .4rem; z-index: 9; } @keyframes toast-in { from { opacity: 0; transform: translateY(.4rem); } }