);
}
// Live/recording assets: show a muted HLS live preview instead of a black
// box. The capture container writes HLS segments to /live//index.m3u8
// while recording is in progress; no thumbnail_s3_key exists yet.
if (asset.status === 'live' && asset.id) {
return ;
}
const altText = asset.name ? `Thumbnail for ${asset.name}` : 'Asset thumbnail';
return (
{thumbUrl
?
: }
);
}
// Muted inline HLS preview for a live/recording asset tile. Attaches hls.js
// (or native HLS on Safari) to show the live feed inside the library card.
// Shows a "connecting…" spinner while the manifest loads, falls back to a
// placeholder with a record icon if hls.js is unavailable or playback fails.
function LiveThumb({ assetId, aspect }) {
const videoRef = React.useRef(null);
const [ready, setReady] = React.useState(false);
const [failed, setFailed] = React.useState(false);
React.useEffect(() => {
const v = videoRef.current;
if (!v || !assetId) return;
const url = '/live/' + assetId + '/index.m3u8';
let destroyed = false;
let hls = null;
let retryTimer = 0;
let retryCount = 0;
const MAX_RETRIES = 6;
const clearRetry = () => { if (retryTimer) { clearTimeout(retryTimer); retryTimer = 0; } };
if (v.canPlayType('application/vnd.apple.mpegurl')) {
const tryLoad = () => {
if (destroyed) return;
v.removeAttribute('src');
v.load();
v.src = url;
v.play().catch(() => {});
};
v.addEventListener('playing', () => { if (!destroyed) { retryCount = 0; setReady(true); } });
v.addEventListener('error', () => {
if (destroyed || retryCount >= MAX_RETRIES) { setFailed(true); return; }
retryCount++;
clearRetry();
retryTimer = setTimeout(tryLoad, Math.min(1000 * retryCount, 8000));
});
tryLoad();
return () => { destroyed = true; clearRetry(); };
}
if (!window.Hls) { setFailed(true); return; }
const startHls = () => {
if (destroyed) return;
hls = new window.Hls({ liveSyncDurationCount: 2, lowLatencyMode: true, maxBufferLength: 10 });
hls.loadSource(url);
hls.attachMedia(v);
hls.on(window.Hls.Events.MANIFEST_PARSED, () => {
if (!destroyed) { retryCount = 0; setReady(true); v.play().catch(() => {}); }
});
hls.on(window.Hls.Events.ERROR, (_e, data) => {
if (!data.fatal) return;
try { hls.destroy(); } catch (_) {}
hls = null;
if (destroyed || retryCount >= MAX_RETRIES) { setFailed(true); return; }
retryCount++;
clearRetry();
retryTimer = setTimeout(startHls, Math.min(1000 * retryCount, 8000));
});
};
startHls();
return () => { destroyed = true; clearRetry(); if (hls) { try { hls.destroy(); } catch (_) {} } };
}, [assetId]);
return (
{/* Pulsing red border while connecting or playing, matches LIVE badge colour */}
{!ready && !failed && (