Files
kgva/assets/js/player.js
T
kgva CMS 46aff56577 perf: smoother video player — rAF-driven scrub bar, less cold-start stutter
scrub fill/time were driven off timeupdate, which browsers fire irregularly (a handful of times a second) — visible as the bar stepping instead of gliding. now driven by requestAnimationFrame while playing (display refresh rate), with a direct sync on pause/seeked so it still tracks immediately when not running.\n\npreload=none meant zero data was fetched until the play button was pressed, causing a visible stall right at playback start. preload=metadata lets the browser prime the connection and read the moov atom ahead of time without fetching the full video for visitors who never press play.
2026-08-15 00:24:17 +00:00

67 lines
3.0 KiB
JavaScript

/* minimal terminal-style video player — custom controls in our own font.
re-inits after pjax navigation. */
(function () {
"use strict";
function fmt(t) {
t = Math.max(0, t | 0);
var m = (t / 60) | 0, s = t % 60;
return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
}
function init(root) {
var v = root.querySelector("video");
var play = root.querySelector(".player__play");
var big = root.querySelector(".player__big");
var scrub = root.querySelector(".player__scrub");
var fill = root.querySelector(".player__fill");
var time = root.querySelector(".player__time");
var mute = root.querySelector(".player__mute");
var full = root.querySelector(".player__full");
if (!v) return;
function toggle() { if (v.paused) v.play(); else v.pause(); }
if (play) play.addEventListener("click", toggle);
if (big) big.addEventListener("click", toggle);
v.addEventListener("click", toggle);
// drive the scrub bar off rAF instead of `timeupdate` — timeupdate only
// fires a handful of times a second (browser-dependent), which made the
// fill visibly step instead of glide; rAF ties it to the display's own
// refresh rate, only running while actually playing.
var raf = null;
function sync() {
var p = v.duration ? v.currentTime / v.duration : 0;
if (fill) fill.style.width = (p * 100) + "%";
if (time) time.textContent = fmt(v.currentTime) + " / " + fmt(v.duration || 0);
}
function tick() { sync(); raf = requestAnimationFrame(tick); }
v.addEventListener("play", function () {
root.classList.add("is-playing"); if (play) play.textContent = "❚❚";
if (!raf) raf = requestAnimationFrame(tick);
});
v.addEventListener("pause", function () {
root.classList.remove("is-playing"); if (play) play.textContent = "▶";
if (raf) { cancelAnimationFrame(raf); raf = null; }
sync(); // one last sync so the bar lands exactly on the paused frame
});
v.addEventListener("ended", function () { if (raf) { cancelAnimationFrame(raf); raf = null; } });
v.addEventListener("loadedmetadata", function () { if (time) time.textContent = "00:00 / " + fmt(v.duration); });
v.addEventListener("seeked", sync); // scrubbing while paused still needs to reflect immediately
if (scrub) scrub.addEventListener("click", function (e) {
var r = scrub.getBoundingClientRect();
if (v.duration) v.currentTime = ((e.clientX - r.left) / r.width) * v.duration;
});
if (mute) mute.addEventListener("click", function () { v.muted = !v.muted; mute.textContent = v.muted ? "muted" : "vol"; });
if (full) full.addEventListener("click", function () {
if (document.fullscreenElement) document.exitFullscreen();
else if (root.requestFullscreen) root.requestFullscreen();
});
}
function initAll() {
var ps = document.querySelectorAll(".player");
for (var i = 0; i < ps.length; i++) if (!ps[i].__init) { ps[i].__init = 1; init(ps[i]); }
}
initAll();
document.addEventListener("pjax:load", initAll);
})();