diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index 48e92f0..08354fb 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -491,13 +491,6 @@ function HlsPreview({ assetId, recorderId, muted = true, controls = false, class /* ===== Recorders ===== */ function _normRecorder(r) { - let elapsed = '·'; - if (r.status === 'recording' && r.started_at) { - const s = Math.floor((Date.now() - new Date(r.started_at)) / 1000); - elapsed = String(Math.floor(s / 3600)).padStart(2, '0') + ':' + - String(Math.floor((s % 3600) / 60)).padStart(2, '00') + ':' + - String(s % 60).padStart(2, '0'); - } const cfg = r.source_config || {}; return { ...r, @@ -505,8 +498,9 @@ function _normRecorder(r) { url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '·', codec: r.recording_codec || '·', res: r.recording_resolution || '·', + framerate: r.recording_framerate || 'native', node: r.node_id ? r.node_id.slice(0, 8) : 'primary', - elapsed, + elapsed: '·', bitrate: '·', health: 100, audio: false, @@ -610,15 +604,43 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { return () => clearInterval(id); }, [isRec, recorder.id]); + // Tick elapsed every second while recording. Seed from liveStatus.duration + // (authoritative from the capture container) when available; fall back to + // wall-clock diff from recorder.started_at so the counter never freezes. + const [elapsedSecs, setElapsedSecs] = React.useState(0); + React.useEffect(() => { + if (!isRec) { setElapsedSecs(0); return; } + const base = () => { + if (liveStatus && liveStatus.duration != null) return liveStatus.duration; + if (recorder.started_at) return Math.floor((Date.now() - new Date(recorder.started_at).getTime()) / 1000); + return 0; + }; + // Snap to latest authoritative value immediately, then tick from there. + const anchor = { at: Date.now(), secs: base() }; + setElapsedSecs(anchor.secs); + const id = setInterval(() => { + setElapsedSecs(anchor.secs + Math.floor((Date.now() - anchor.at) / 1000)); + }, 1000); + return () => clearInterval(id); + // Re-anchor whenever liveStatus.duration arrives from the poll. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isRec, liveStatus && liveStatus.duration, recorder.started_at]); + const displayElapsed = React.useMemo(() => { - if (liveStatus && liveStatus.duration != null) { - const d = Math.max(0, liveStatus.duration); - return String(Math.floor(d / 3600)).padStart(2, '0') + ':' + - String(Math.floor((d % 3600) / 60)).padStart(2, '0') + ':' + - String(d % 60).padStart(2, '0'); + if (!isRec) return '·'; + const d = Math.max(0, elapsedSecs); + return String(Math.floor(d / 3600)).padStart(2, '0') + ':' + + String(Math.floor((d % 3600) / 60)).padStart(2, '0') + ':' + + String(d % 60).padStart(2, '0'); + }, [isRec, elapsedSecs]); + + // Show live fps when recording and signal is healthy; fall back to configured value. + const displayFramerate = React.useMemo(() => { + if (isRec && liveStatus && liveStatus.currentFps != null && liveStatus.currentFps > 0) { + return Number(liveStatus.currentFps).toFixed(2) + ' fps'; } - return recorder.elapsed; - }, [liveStatus, recorder.elapsed]); + return recorder.framerate || 'native'; + }, [isRec, liveStatus, recorder.framerate]); const displaySignal = liveStatus ? (liveStatus.signal || '·') @@ -709,12 +731,10 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { {displaySignal} - {liveStatus?.currentFps != null && ( -