feat(ingest): show capture port on recorder cards + growing indicator on schedule blocks
- Recorder cards now display a Port chip showing the bridge port (deltacast)
or SDI device index (blackmagic) so operators can see at a glance which
physical input each recorder is bound to.
- Schedule blocks render with a green accent + flame glyph when the backing
recorder has growing_enabled=true, so the EPG view distinguishes
edit-while-record slots from regular close-then-publish recordings.
🤖 Generated with Claude Code
This commit is contained in:
parent
8a675992c2
commit
6895fbc5af
1 changed files with 29 additions and 2 deletions
|
|
@ -492,6 +492,18 @@ function HlsPreview({ assetId, recorderId, muted = true, controls = false, class
|
||||||
/* ===== Recorders ===== */
|
/* ===== Recorders ===== */
|
||||||
function _normRecorder(r) {
|
function _normRecorder(r) {
|
||||||
const cfg = r.source_config || {};
|
const cfg = r.source_config || {};
|
||||||
|
// Surface the capture port for SDI / Deltacast recorders so the recorder card
|
||||||
|
// can show which physical input the recorder is bound to. For Deltacast,
|
||||||
|
// cfg.port is the bridge port index (0-7). For Blackmagic SDI, cfg.device
|
||||||
|
// is something like /dev/blackmagic/dv0 — we slice off the trailing index.
|
||||||
|
let capturePort = null;
|
||||||
|
if (r.source_type === 'deltacast') {
|
||||||
|
capturePort = cfg.port != null ? `Port ${cfg.port}` : null;
|
||||||
|
} else if (r.source_type === 'sdi') {
|
||||||
|
const dev = cfg.device || '';
|
||||||
|
const m = dev.match(/(\d+)$/);
|
||||||
|
if (m) capturePort = `SDI ${m[1]}`;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...r,
|
...r,
|
||||||
source: r.source_type || '·',
|
source: r.source_type || '·',
|
||||||
|
|
@ -500,6 +512,7 @@ function _normRecorder(r) {
|
||||||
res: r.recording_resolution || '·',
|
res: r.recording_resolution || '·',
|
||||||
framerate: r.recording_framerate || 'native',
|
framerate: r.recording_framerate || 'native',
|
||||||
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
||||||
|
capturePort,
|
||||||
elapsed: '·',
|
elapsed: '·',
|
||||||
bitrate: '·',
|
bitrate: '·',
|
||||||
health: 100,
|
health: 100,
|
||||||
|
|
@ -708,6 +721,11 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
<StatusDot status={recorder.status} /> {recorder.status.toUpperCase()}
|
<StatusDot status={recorder.status} /> {recorder.status.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
<span className="badge outline">{recorder.source}</span>
|
<span className="badge outline">{recorder.source}</span>
|
||||||
|
{recorder.capturePort && (
|
||||||
|
<span className="badge outline" title="Capture port" style={{ background: 'rgba(74,158,255,0.12)', borderColor: 'rgba(74,158,255,0.4)', color: 'var(--accent)' }}>
|
||||||
|
<Icon name="signal" size={10} style={{ marginRight: 4, verticalAlign: -1 }} />{recorder.capturePort}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="recorder-sub mono">{recorder.url}</div>
|
<div className="recorder-sub mono">{recorder.url}</div>
|
||||||
<div className="recorder-sub">
|
<div className="recorder-sub">
|
||||||
|
|
@ -1471,29 +1489,38 @@ function _EventBlock({ event, recorder, dayStart, dayEnd, pph, now, projects, on
|
||||||
const left = (startMin / 60) * pph;
|
const left = (startMin / 60) * pph;
|
||||||
const width = Math.max(40, ((endMin - startMin) / 60) * pph);
|
const width = Math.max(40, ((endMin - startMin) / 60) * pph);
|
||||||
|
|
||||||
|
// Schedules backed by a recorder configured to write a growing file get a
|
||||||
|
// green accent so operators can tell at a glance which slots will produce
|
||||||
|
// an edit-while-recording deliverable (vs. close-then-publish).
|
||||||
|
const isGrowing = !!(recorder && recorder.growing_enabled);
|
||||||
|
|
||||||
const classes = ['epg-block'];
|
const classes = ['epg-block'];
|
||||||
if (isLive) classes.push('live');
|
if (isLive) classes.push('live');
|
||||||
if (isFailed) classes.push('failed');
|
if (isFailed) classes.push('failed');
|
||||||
else if (isPast) classes.push('past');
|
else if (isPast) classes.push('past');
|
||||||
if (drag && drag.moved) classes.push('dragging');
|
if (drag && drag.moved) classes.push('dragging');
|
||||||
if (canDrag) classes.push('resizable');
|
if (canDrag) classes.push('resizable');
|
||||||
|
if (isGrowing) classes.push('growing');
|
||||||
|
|
||||||
|
const blockColor = isGrowing ? '#2ecc71' : (color || 'var(--text-3)');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={blockRef}
|
ref={blockRef}
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
style={{ left, width, '--epg-block-color': color || 'var(--text-3)' }}
|
style={{ left, width, '--epg-block-color': blockColor }}
|
||||||
onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); onContextMenu(event, ev); }}
|
onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); onContextMenu(event, ev); }}
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
onPointerUp={endDrag}
|
onPointerUp={endDrag}
|
||||||
onPointerCancel={endDrag}
|
onPointerCancel={endDrag}
|
||||||
title={event.name + ' · ' + _fmtTime(dispStart) + ' → ' + _fmtTime(dispEnd) + (event.error_message ? ' · ' + event.error_message : '')}>
|
title={event.name + (isGrowing ? ' · GROWING' : '') + ' · ' + _fmtTime(dispStart) + ' → ' + _fmtTime(dispEnd) + (event.error_message ? ' · ' + event.error_message : '')}>
|
||||||
<span className="epg-block-bar" />
|
<span className="epg-block-bar" />
|
||||||
{/* Body click → edit, body drag → move. We hang the click on pointerup
|
{/* Body click → edit, body drag → move. We hang the click on pointerup
|
||||||
so the threshold check above can demote a drag back to a click. */}
|
so the threshold check above can demote a drag back to a click. */}
|
||||||
<div className="epg-block-body" onPointerDown={(ev) => startDrag(ev, 'body')}>
|
<div className="epg-block-body" onPointerDown={(ev) => startDrag(ev, 'body')}>
|
||||||
<span className="epg-block-name">{event.name}</span>
|
<span className="epg-block-name">{event.name}</span>
|
||||||
<span className="epg-block-time mono">{_fmtTime(dispStart)}{drag && drag.moved ? ' → ' + _fmtTime(dispEnd) : ''}</span>
|
<span className="epg-block-time mono">{_fmtTime(dispStart)}{drag && drag.moved ? ' → ' + _fmtTime(dispEnd) : ''}</span>
|
||||||
|
{isGrowing && <span className="epg-block-glyph growing" title="growing file (edit-while-record)" style={{ color: '#2ecc71' }}>▶</span>}
|
||||||
{isLive && <span className="epg-block-glyph live" title="on air">●</span>}
|
{isLive && <span className="epg-block-glyph live" title="on air">●</span>}
|
||||||
{isFailed && <span className="epg-block-glyph failed" title={event.error_message || 'failed'}>!</span>}
|
{isFailed && <span className="epg-block-glyph failed" title={event.error_message || 'failed'}>!</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue