Commit 6f141ef1c56dc0114e8dbd0c460167fadb771293

Authored by yibo
1 parent f19672ff

Show a first-frame thumbnail on each preview player

preload="none" left every row's player a plain black box until
clicked, giving no way to eyeball which clip is which without playing
it. Switch to preload="metadata" (browsers paint the first frame once
metadata loads, without playing) so the row itself doubles as a
thumbnail. Load it lazily via IntersectionObserver rather than
src'ing all 361 rows on page load — /lib/video/:id has no local cache
and re-proxies to H3 Console on every hit, so firing that many
requests at once would hammer it; only rows actually scrolled into
view pull their frame.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
Showing 1 changed file with 18 additions and 2 deletions
admin.html
... ... @@ -182,6 +182,17 @@ function videoHref(c) {
182 182 return `/lib/video/${c.id}`;
183 183 }
184 184  
  185 +const previewObserver = new IntersectionObserver(
  186 + (entries) => {
  187 + for (const e of entries) {
  188 + if (!e.isIntersecting) continue;
  189 + e.target.src = e.target.dataset.src;
  190 + previewObserver.unobserve(e.target);
  191 + }
  192 + },
  193 + { rootMargin: "200px" }
  194 +);
  195 +
185 196 function render() {
186 197 const rows = filteredSorted();
187 198 const channelTotal = library.filter(inChannel).length;
... ... @@ -202,7 +213,7 @@ function render() {
202 213 <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td>
203 214 <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td>
204 215 <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td>
205   - <td class="preview"><video controls preload="none" src="${videoHref(c)}"></video></td>
  216 + <td class="preview"><video controls preload="metadata" data-src="${videoHref(c)}"></video></td>
206 217 <td>
207 218 <select data-field="channel">
208 219 <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option>
... ... @@ -227,7 +238,7 @@ function render() {
227 238 <td class="name">${escapeHtml(c.promptEn || "")}</td>
228 239 <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td>
229 240 <td><a class="video-link" href="${videoHref(c)}" target="_blank">${videoHref(c)}</a></td>
230   - <td class="preview"><video controls preload="none" src="${videoHref(c)}"></video></td>
  241 + <td class="preview"><video controls preload="metadata" data-src="${videoHref(c)}"></video></td>
231 242 <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td>
232 243 <td>${escapeHtml(traceability(c))}</td>
233 244 <td>${c.duration ?? "-"}s</td>
... ... @@ -243,6 +254,11 @@ function render() {
243 254 })
244 255 .join("");
245 256  
  257 + // Loading preload="metadata" for all 361 rows at once would hammer the
  258 + // /lib/video proxy (each clip is a fresh H3 Console fetch); only pull a
  259 + // row's first frame once it's actually scrolled into view.
  260 + tbody.querySelectorAll("td.preview video[data-src]").forEach((v) => previewObserver.observe(v));
  261 +
246 262 tbody.querySelectorAll("[data-edit]").forEach((b) => b.addEventListener("click", () => { editingId = b.dataset.edit; render(); }));
247 263 tbody.querySelectorAll("[data-cancel]").forEach((b) => b.addEventListener("click", () => { editingId = null; render(); }));
248 264 tbody.querySelectorAll("[data-save]").forEach((b) =>
... ...