// screens-asset.jsx — asset detail (Frame.io-style player, filmstrip, comments) // Simple gradient palette — replaces the missing thumbGrad function const _FRAME_GRADIENTS = [ 'linear-gradient(135deg,#1a1f2e 0%,#2a3045 100%)', 'linear-gradient(135deg,#1e2030 0%,#2d2040 100%)', 'linear-gradient(135deg,#0d1520 0%,#1a2535 100%)', 'linear-gradient(135deg,#1f1a2e 0%,#2a2040 100%)', 'linear-gradient(135deg,#0f1e18 0%,#1a3028 100%)', 'linear-gradient(135deg,#1e1510 0%,#302018 100%)', 'linear-gradient(135deg,#1a1020 0%,#281830 100%)', 'linear-gradient(135deg,#101828 0%,#182438 100%)', 'linear-gradient(135deg,#1e2820 0%,#283830 100%)', 'linear-gradient(135deg,#201820 0%,#302030 100%)', 'linear-gradient(135deg,#181e28 0%,#202838 100%)', ]; function AssetDetail({ asset, onClose }) { const [playing, setPlaying] = React.useState(false); const [currentMs, setCurrentMs] = React.useState(0); const [tab, setTab] = React.useState("comments"); const [showResolved, setShowResolved] = React.useState(false); const [comments, setComments] = React.useState([]); const [newComment, setNewComment] = React.useState(""); const [commentsLoading, setCommentsLoading] = React.useState(false); // Stream / video state const [streamUrl, setStreamUrl] = React.useState(null); const [streamType, setStreamType] = React.useState(null); // Why the stream is unavailable: 'no_proxy' (has hi-res source, browser // can't play it directly) or null (still loading / live / playable). const [streamReason, setStreamReason] = React.useState(null); const [streamHasSource, setStreamHasSource] = React.useState(false); const [streamLoading, setStreamLoading] = React.useState(false); const [videoDuration, setVideoDuration] = React.useState(0); const [retrying, setRetrying] = React.useState(false); const [filmFrames, setFilmFrames] = React.useState([]); const [filmstripLoading, setFilmstripLoading] = 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 (!assetId) return; setStreamUrl(null); setStreamType(null); setStreamReason(null); setStreamHasSource(false); setVideoDuration(0); setCurrentMs(0); setPlaying(false); setFilmFrames([]); setStreamLoading(true); window.ZAMPP_API.fetch('/assets/' + assetId + '/stream') .then(function(r) { if (r && r.url) { setStreamUrl(r.url); setStreamType(r.type || 'mp4'); } else if (r) { // {url: null, reason: 'no_proxy', has_source: true|false} setStreamReason(r.reason || null); setStreamHasSource(!!r.has_source); } }) .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]); // Build filmstrip from real video frames. HLS streams use hls.js probe. // For MP4, we fetch the first 10 MB as a Blob to feed the probe video — // this works reliably even in headless browsers where