Commit 6f141ef1c56dc0114e8dbd0c460167fadb771293
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) => | ... | ... |