Files
website/app/static/video-test.html
Jimmy 3f5235daaf Improve gallery video UX and add upload-to-publish media workflow.
Stage raw files in upload/, publish with make sync-media/publish, and polish the lightbox: autoplay, remembered volume, Escape to close, and image/video icons without poster or caption clutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 23:55:43 +12:00

276 lines
7.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Video playback test — Technical Kiwi</title>
<style>
:root {
--bg: #14100c;
--surface: #2a2219;
--text: #f5efe6;
--muted: #b5a090;
--accent: #c47b3a;
--border: rgba(245, 239, 230, 0.12);
--mono: ui-monospace, "Cascadia Code", monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
}
main {
max-width: 960px;
margin: 0 auto;
padding: 1.5rem;
}
h1 { font-size: 1.35rem; margin: 0 0 0.5rem; }
p.lead { color: var(--muted); margin: 0 0 1.5rem; }
label { display: block; font-size: 0.85rem; color: var(--muted); margin-bottom: 0.35rem; }
.row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
input[type="text"] {
flex: 1 1 16rem;
min-width: 0;
padding: 0.55rem 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text);
font: inherit;
}
button, .btn {
padding: 0.55rem 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text);
font: inherit;
cursor: pointer;
}
button:hover, .btn:hover { border-color: var(--accent); }
.panel {
border: 1px solid var(--border);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
background: rgba(42, 34, 25, 0.5);
}
.panel h2 { font-size: 1rem; margin: 0 0 0.75rem; }
video {
display: block;
width: 100%;
max-height: 60vh;
background: #000;
border-radius: 8px;
}
.meta {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
gap: 0.5rem 1rem;
font-family: var(--mono);
font-size: 0.8rem;
margin-top: 0.75rem;
}
.meta dt { color: var(--muted); }
.meta dd { margin: 0; word-break: break-all; }
#log {
font-family: var(--mono);
font-size: 0.75rem;
max-height: 14rem;
overflow: auto;
margin: 0;
padding: 0.75rem;
background: #0a0806;
border-radius: 8px;
list-style: none;
}
#log li { margin: 0.15rem 0; }
#log .err { color: #f87171; }
#log .ok { color: #86efac; }
table.codec {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
table.codec th, table.codec td {
text-align: left;
padding: 0.35rem 0.5rem;
border-bottom: 1px solid var(--border);
}
table.codec th { color: var(--muted); font-weight: 500; }
a { color: var(--accent); }
</style>
</head>
<body>
<main>
<h1>Video playback test</h1>
<p class="lead">
Served from <code>/static/video-test.html</code>.
Use with <code>make dev</code> at
<a href="http://localhost:7331/static/video-test.html">localhost:7331</a>
or <a href="http://localhost:8080/static/video-test.html">8080</a>.
Path is under <code>/images/…</code> (copy from gallery Network tab).
</p>
<div class="panel">
<label for="path">Video path (site-relative)</label>
<div class="row">
<input id="path" type="text" placeholder="/images/album/clip.mp4" spellcheck="false">
<button type="button" id="load">Load</button>
<a class="btn" id="open-tab" href="#" target="_blank" rel="noopener">Open URL</a>
</div>
<p style="margin:0;font-size:0.85rem;color:var(--muted)">
Query link: <code>?path=/images/…</code> (encode slashes as <code>%2F</code> if needed)
</p>
</div>
<div class="panel">
<h2>Player</h2>
<video id="player" controls playsinline preload="auto"></video>
<dl class="meta" id="stats">
<div><dt>src</dt><dd id="stat-src"></dd></div>
<div><dt>readyState</dt><dd id="stat-ready"></dd></div>
<div><dt>dimensions</dt><dd id="stat-dim"></dd></div>
<div><dt>duration</dt><dd id="stat-dur"></dd></div>
<div><dt>networkState</dt><dd id="stat-net"></dd></div>
<div><dt>error</dt><dd id="stat-err"></dd></div>
</dl>
</div>
<div class="panel">
<h2>Browser codec hints (canPlayType)</h2>
<table class="codec" id="codec-table">
<thead><tr><th>MIME</th><th>Result</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="panel">
<h2>Events</h2>
<ul id="log"></ul>
</div>
</main>
<script>
(function () {
var pathInput = document.getElementById("path");
var player = document.getElementById("player");
var logEl = document.getElementById("log");
var openTab = document.getElementById("open-tab");
var codecs = [
'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
'video/mp4; codecs="hvc1.1.6.L93.B0, mp4a.40.2"',
'video/mp4; codecs="hev1.1.6.L93.B0, mp4a.40.2"',
"video/mp4",
"video/quicktime",
"video/webm; codecs=\"vp9, opus\"",
"video/webm",
];
function log(msg, cls) {
var li = document.createElement("li");
if (cls) li.className = cls;
li.textContent = new Date().toISOString().slice(11, 23) + " " + msg;
logEl.prepend(li);
}
function updateStats() {
document.getElementById("stat-src").textContent = player.currentSrc || player.src || "—";
document.getElementById("stat-ready").textContent = String(player.readyState);
document.getElementById("stat-dim").textContent =
player.videoWidth + "×" + player.videoHeight;
document.getElementById("stat-dur").textContent = isFinite(player.duration)
? player.duration.toFixed(2) + "s"
: "—";
document.getElementById("stat-net").textContent = String(player.networkState);
var err = player.error;
document.getElementById("stat-err").textContent = err
? err.code + " " + (err.message || mediaErrorLabel(err.code))
: "—";
}
function mediaErrorLabel(code) {
return (
{ 1: "MEDIA_ERR_ABORTED", 2: "MEDIA_ERR_NETWORK", 3: "MEDIA_ERR_DECODE", 4: "MEDIA_ERR_SRC_NOT_SUPPORTED" }[code] || "?"
);
}
function normalizePath(raw) {
raw = (raw || "").trim();
if (!raw) return "";
if (raw.startsWith("http://") || raw.startsWith("https://")) {
try {
var u = new URL(raw);
return u.pathname;
} catch (e) {
return raw;
}
}
return raw.startsWith("/") ? raw : "/" + raw;
}
function loadFromInput() {
var path = normalizePath(pathInput.value);
if (!path) {
log("No path entered", "err");
return;
}
pathInput.value = path;
logEl.innerHTML = "";
log("Loading " + path);
player.removeAttribute("poster");
player.src = path;
openTab.href = path;
player.load();
history.replaceState(null, "", "?path=" + encodeURIComponent(path));
updateStats();
}
[
"loadstart", "loadedmetadata", "loadeddata", "canplay", "canplaythrough",
"playing", "pause", "ended", "error", "stalled", "waiting", "suspend",
].forEach(function (ev) {
player.addEventListener(ev, function () {
var cls = ev === "error" ? "err" : ev === "canplay" || ev === "playing" ? "ok" : "";
log(ev + (ev === "error" && player.error ? " " + mediaErrorLabel(player.error.code) : ""));
if (cls && ev !== "error") logEl.querySelector("li").className = cls;
updateStats();
});
});
document.getElementById("load").addEventListener("click", loadFromInput);
pathInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") loadFromInput();
});
var tbody = document.querySelector("#codec-table tbody");
var probe = document.createElement("video");
codecs.forEach(function (mime) {
var tr = document.createElement("tr");
var tdM = document.createElement("td");
var tdR = document.createElement("td");
tdM.textContent = mime;
tdR.textContent = probe.canPlayType(mime) || '""';
tr.appendChild(tdM);
tr.appendChild(tdR);
tbody.appendChild(tr);
});
var q = new URLSearchParams(location.search).get("path");
if (q) {
pathInput.value = q;
loadFromInput();
}
})();
</script>
</body>
</html>