From 12115a053a0dd063bcfc839cba48aed8db4a1ff6 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 31 May 2026 11:45:25 -0400 Subject: [PATCH] feat(playout): fix 409 drag bug, add HLS preview, advanced playlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix event bubbling: e.stopPropagation() in onItemDrop prevents duplicate POST when dropping on an existing playlist item - Wrap all drop handlers in try/catch with inline error display - ProgramMonitor: replace text placeholder with hls.js video player loading /media/live//index.m3u8; falls back to native HLS on Safari; destroys Hls instance on channel stop/unmount - Playlist: per-item duration (MM:SS), staging progress bar with animated stripe while staging, now-playing highlight + ▶ indicator driven by engine.currentIndex from 4s status poll - Playlist footer: clip count + total duration sum - Transport: Play button disabled + shows '⏳ N staging' until all items are media_status=ready, eliminating the staging-not-ready 409 Co-Authored-By: Claude Sonnet 4.6 --- services/web-ui/public/screens-playout.jsx | 200 +++++++++++++++------ services/web-ui/public/styles-playout.css | 76 +++++++- 2 files changed, 217 insertions(+), 59 deletions(-) diff --git a/services/web-ui/public/screens-playout.jsx b/services/web-ui/public/screens-playout.jsx index 49f9d23..965d32c 100644 --- a/services/web-ui/public/screens-playout.jsx +++ b/services/web-ui/public/screens-playout.jsx @@ -24,6 +24,26 @@ async function poFetch(path, opts) { return window.ZAMPP_API.fetch('/playout' + path, opts); } +// ── Helpers ────────────────────────────────────────────────────────────────── + +function fmtDuration(secs) { + if (!secs || secs < 0) return '—'; + const s = Math.floor(secs); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const ss = s % 60; + const mm = String(m).padStart(2, '0'); + const ssStr = String(ss).padStart(2, '0'); + return h > 0 ? `${h}:${mm}:${ssStr}` : `${m}:${ssStr}`; +} + +function itemEffectiveDuration(it) { + const total = (it.asset_duration_ms || 0) / 1000; + const inPt = it.in_point != null ? Number(it.in_point) : 0; + const outPt = it.out_point != null ? Number(it.out_point) : total; + return Math.max(0, outPt - inPt); +} + // ── Output-config sub-form (varies by output type) ─────────────────────────── function OutputConfigFields({ type, config, onChange }) { const set = (k, v) => onChange({ ...config, [k]: v }); @@ -175,29 +195,37 @@ function MediaBin({ projectId }) { ); } -const MEDIA_STATUS_BADGE = { - ready: 'success', staging: 'warn', pending: 'neutral', error: 'error', -}; +// ── Staging progress bar ────────────────────────────────────────────────────── +function StagingBar({ status }) { + return ( +