diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index 165f3fa..0146b36 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -25,38 +25,109 @@ function AssetDetail({ asset, onClose }) { const [comments, setComments] = React.useState(SEED_COMMENTS || []); const [newComment, setNewComment] = React.useState(""); - const totalMs = parseDuration(asset.duration); + // Stream / video state + const [streamUrl, setStreamUrl] = React.useState(null); + const [streamType, setStreamType] = React.useState(null); + const [streamLoading, setStreamLoading] = React.useState(false); + const [videoDuration, setVideoDuration] = React.useState(0); + const [retrying, setRetrying] = React.useState(false); + const videoRef = React.useRef(null); + const assetId = asset && asset.id; + const totalMs = videoDuration > 0 ? videoDuration : parseDuration(asset.duration); + + // Fetch stream URL when asset changes React.useEffect(() => { - if (!playing || totalMs <= 0) return; - const i = setInterval(() => { - setCurrentMs(t => { + if (!assetId) return; + setStreamUrl(null); + setStreamType(null); + setVideoDuration(0); + setCurrentMs(0); + setPlaying(false); + setStreamLoading(true); + window.ZAMPP_API.fetch('/assets/' + assetId + '/stream') + .then(function(r) { + if (r && r.url) { + setStreamUrl(r.url); + setStreamType(r.type || 'mp4'); + } + }) + .catch(function() {}) + .finally(function() { setStreamLoading(false); }); + }, [assetId]); + + // Wire hls.js for live HLS streams + React.useEffect(() => { + if (!streamUrl || streamType !== 'hls' || !videoRef.current) return; + if (!window.Hls) return; + const hls = new window.Hls(); + hls.loadSource(streamUrl); + hls.attachMedia(videoRef.current); + return function() { hls.destroy(); }; + }, [streamUrl, streamType]); + + // Fake playback timer — only used when no real video stream + React.useEffect(() => { + if (!playing || totalMs <= 0 || streamUrl) return; + const i = setInterval(function() { + setCurrentMs(function(t) { const next = t + 100; if (next >= totalMs) { setPlaying(false); return totalMs; } return next; }); }, 100); - return () => clearInterval(i); - }, [playing, totalMs]); + return function() { clearInterval(i); }; + }, [playing, totalMs, streamUrl]); - const seek = (ms) => setCurrentMs(Math.max(0, Math.min(totalMs || 0, ms))); - const addComment = () => { + const togglePlay = function() { + if (videoRef.current) { + if (videoRef.current.paused) { videoRef.current.play(); } + else { videoRef.current.pause(); } + } else { + setPlaying(function(p) { return !p; }); + } + }; + + const seek = function(ms) { + const clamped = Math.max(0, Math.min(totalMs || 0, ms)); + setCurrentMs(clamped); + if (videoRef.current) videoRef.current.currentTime = clamped / 1000; + }; + + const retryProcessing = function() { + if (retrying) return; + setRetrying(true); + window.ZAMPP_API.fetch('/assets/' + assetId + '/retry', { method: 'POST' }) + .then(function() { window.alert('Re-queued for processing. Refresh in a moment.'); }) + .catch(function(e) { window.alert('Retry failed: ' + (e.message || 'unknown error')); }) + .finally(function() { setRetrying(false); }); + }; + + const addComment = function() { if (!newComment.trim()) return; const t = msToTimecode(currentMs); - setComments(c => [...c, { - id: `n${Date.now()}`, - who: "Zach Gaetano", - avatar: "ZG", - time: t, - real: "just now", - text: newComment, - resolved: false, - frame: Math.floor(currentMs / 1000 * 30), - }]); + setComments(function(c) { + return [...c, { + id: 'n' + Date.now(), + who: "Zach Gaetano", + avatar: "ZG", + time: t, + real: "just now", + text: newComment, + resolved: false, + frame: Math.floor(currentMs / 1000 * 30), + }]; + }); setNewComment(""); }; - const visibleComments = comments.filter(c => showResolved || !c.resolved); + const visibleComments = comments.filter(function(c) { return showResolved || !c.resolved; }); + + const statusMessage = + asset.status === 'processing' ? 'Processing…' : + asset.status === 'live' ? 'Live recording in progress' : + asset.status === 'error' ? 'Processing failed' : + 'Preview not yet available'; return (
@@ -81,32 +152,63 @@ function AssetDetail({ asset, onClose }) {
- -
- {!playing && totalMs > 0 && ( - - )} - {totalMs <= 0 && ( -
-
-
- {asset.status === 'processing' ? 'Processing…' : asset.status === 'live' ? 'Live recording in progress' : 'Preview not yet available'} + {streamUrl ? ( +