feat(recorders): stable elapsed timer + live HLS preview on the card; optimistic signal default
This commit is contained in:
parent
57c3871cc1
commit
57116dde42
2 changed files with 65 additions and 3 deletions
|
|
@ -77,7 +77,29 @@ router.get('/', async (req, res, next) => {
|
|||
const result = await pool.query(
|
||||
'SELECT * FROM recorders ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
// Annotate recording rows with the container's actual StartedAt + the
|
||||
// pre-created live asset id so the UI can keep a stable elapsed timer
|
||||
// and embed an HLS preview.
|
||||
const rows = await Promise.all(result.rows.map(async (r) => {
|
||||
if (r.status === 'recording' && r.container_id) {
|
||||
try {
|
||||
const insp = await dockerApi('GET', `/containers/${r.container_id}/json`);
|
||||
if (insp.status === 200 && insp.data && insp.data.State) {
|
||||
r.started_at = insp.data.State.StartedAt;
|
||||
}
|
||||
} catch (_) { /* leave started_at undefined */ }
|
||||
try {
|
||||
const live = await pool.query(
|
||||
`SELECT id FROM assets WHERE project_id = $1 AND display_name = $2 AND status = 'live' ORDER BY created_at DESC LIMIT 1`,
|
||||
[r.project_id, r.current_session_id]
|
||||
);
|
||||
if (live.rows.length > 0) r.live_asset_id = live.rows[0].id;
|
||||
} catch (_) { /* skip */ }
|
||||
}
|
||||
return r;
|
||||
}));
|
||||
res.json(rows);
|
||||
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
|
@ -422,13 +444,14 @@ router.get('/:id/status', async (req, res, next) => {
|
|||
const duration = Math.floor((now - startedAt) / 1000);
|
||||
|
||||
// Try to fetch live signal from the recorder's capture sidecar via network alias
|
||||
let signal = container.State.Running ? 'connecting' : 'stopped';
|
||||
let signal = container.State.Running ? 'receiving' : 'stopped';
|
||||
let signalKnown = false;
|
||||
let live = null;
|
||||
try {
|
||||
const captureRes = await fetch(`http://recorder-${id}:3001/capture/status`, { signal: AbortSignal.timeout(2000) });
|
||||
if (captureRes.ok) {
|
||||
live = await captureRes.json();
|
||||
if (live && live.signal) signal = live.signal;
|
||||
if (live && live.signal) { signal = live.signal; signalKnown = true; }
|
||||
}
|
||||
} catch (_) {
|
||||
// Container may not be ready yet, or alias hasn't propagated. Leave signal as default.
|
||||
|
|
@ -439,6 +462,7 @@ router.get('/:id/status', async (req, res, next) => {
|
|||
duration,
|
||||
containerId: recorder.container_id,
|
||||
signal,
|
||||
signalKnown,
|
||||
framesReceived: live ? live.framesReceived : null,
|
||||
currentFps: live ? live.currentFps : null,
|
||||
lastFrameAt: live ? live.lastFrameAt : null,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@
|
|||
transition: border-color var(--t-fast);
|
||||
}
|
||||
|
||||
.recorder-preview{position:relative;width:100%;aspect-ratio:16/9;background:#000;border-radius:var(--r-sm);overflow:hidden;margin:8px 0;border:1px solid var(--border)}
|
||||
.recorder-preview video{width:100%;height:100%;object-fit:contain;display:block}
|
||||
.recorder-preview-stamp{position:absolute;top:8px;left:8px;display:inline-flex;align-items:center;gap:6px;background:rgba(0,0,0,0.55);backdrop-filter:blur(4px);padding:3px 8px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:0.12em;color:#fff}
|
||||
.recorder-preview-dot{width:6px;height:6px;background:oklch(62% 0.22 25);border-radius:50%;animation:fsPulse 1.4s ease-in-out infinite}
|
||||
@keyframes fsPulse{0%,100%{opacity:0.7}50%{opacity:1}}
|
||||
.recorder-card.recording {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
|
|
@ -480,6 +485,7 @@
|
|||
${isRecording ? `<span class="recorder-timer" id="timer-${rec.id}">00:00:00</span>` : ''}
|
||||
</div>
|
||||
${isRecording ? `<div class="signal-strip" id="signalStrip-${rec.id}"><div class="signal-strip-fill"></div></div><div class="recorder-status-row" style="font-size:var(--text-xs);"><span id="signal-${rec.id}" style="color:var(--text-tertiary);font-family:var(--font-mono);letter-spacing:0.02em">Connecting…</span></div>` : ''}
|
||||
${isRecording && rec.live_asset_id ? `<div class="recorder-preview"><video id="livevideo-${rec.id}" data-live-id="${rec.live_asset_id}" muted playsinline autoplay></video><div class="recorder-preview-stamp"><span class="recorder-preview-dot"></span>LIVE</div></div>` : ''}
|
||||
|
||||
<div class="recorder-source">
|
||||
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" width="12" height="12"><path d="M2 4h10M2 8h7M2 12h5"/></svg>
|
||||
|
|
@ -520,6 +526,38 @@
|
|||
clearInterval(pState.timers[id]); delete pState.timers[id];
|
||||
}
|
||||
});
|
||||
// Attach an HLS source to each live-preview <video> on the page.
|
||||
pState.recorders.filter(r => r.status === 'recording' && r.live_asset_id).forEach(rec => {
|
||||
const v = document.getElementById('livevideo-' + rec.id);
|
||||
if (!v || v.dataset.attached === '1') return;
|
||||
const url = '/live/' + rec.live_asset_id + '/index.m3u8';
|
||||
const attach = () => {
|
||||
if (v.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
v.src = url; v.play().catch(() => {});
|
||||
} else if (window.Hls && window.Hls.isSupported()) {
|
||||
const hls = new Hls({ lowLatencyMode: true, liveSyncDuration: 2, liveMaxLatencyDuration: 6 });
|
||||
hls.loadSource(url); hls.attachMedia(v);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => v.play().catch(() => {}));
|
||||
v._hls = hls;
|
||||
}
|
||||
v.dataset.attached = '1';
|
||||
};
|
||||
if (window.Hls) attach();
|
||||
else {
|
||||
const sc = document.createElement('script');
|
||||
sc.src = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.0/dist/hls.min.js';
|
||||
sc.onload = attach;
|
||||
document.head.appendChild(sc);
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('video[data-live-id]').forEach(v => {
|
||||
const id = v.id.replace('livevideo-', '');
|
||||
const rec = pState.recorders.find(r => r.id === id);
|
||||
if (!rec || rec.status !== 'recording') {
|
||||
try { if (v._hls) { v._hls.destroy(); delete v._hls; } } catch (_) {}
|
||||
v.removeAttribute('src');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSignalBadge(rid, st) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue