- Revert accent from Flame Blue #1025A1 to electric amber #E8821C
- Restore all rgba accent values to orange spectrum
- Revert avatar to bg-3 background + accent text (was blue bg + white)
- Revert primary button text to dark #0a0c10 (was white on blue)
- Restore centered hero with logo pulse + 'Let's Create' kicker
- Restore single-grid tile layout (all tiles in one launcher-grid)
- Restore settings as separate centered row below main grid
- Restore centered footer + large wordmark with amber glow
Co-Authored-By: OWL <noreply@anthropic.com>
Relying on the SDK default for VHD_CORE_SP_BUFFER_PACKING caused the board
to deliver raw slot buffers whose byte size did not match the width*height*2
contract declared to ffmpeg (pix_fmt=uyvy422). ffmpeg consumed frames at the
wrong stride, causing every frame to shift progressively, rolling and
shearing the picture ("bounces and bends").
Fix: explicitly set VHD_BUFPACK_VIDEO_YUV422_8 on the video stream before
StartStream so the board always delivers tightly-packed 8-bit UYVY.
Safety net: video thread now asserts sz==width*height*2 and skips+warns on
any mismatch so a future packing divergence is immediately visible in logs
rather than silently corrupting video.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The live-thumbnail and manual /start,/stop sidecar->mam-api calls hit the CSRF
guard (403 missing X-Requested-With). Match the working pattern in index.js:
send Authorization: Bearer $MAM_API_TOKEN (= CAPTURE_TOKEN, injected by
recorders.js), which is CSRF-exempt. Falls back to the UI header only when no
token is set (dev). Fixes [livethumb] failed ... 403 — posters now persist.
🤖 Generated with Claude Code
A native deltacast-bridge build leaves services/capture/deltacast-bridge/build/
with a CMakeCache.txt pinned to the host path. When copied into the Docker
build context it conflicts with the in-image cmake step and fails the capture
image build. Exclude build artifacts from the context.
🤖 Generated with Claude Code
Replace the HLS 'connecting…' player in the library with a real frame grabbed
from the start of the recording, while the recording is still live.
Flow:
- recorders.js already pre-creates the asset as status='live' + ASSET_ID env
- capture-manager.start() fires _publishLiveThumbnail() (non-blocking): polls
/live/<id> for the first seg-*.ts, extracts frame 0 via ffmpeg (scaled JPEG,
yuvj420p), uploads to S3 thumbnails/<id>.jpg, then POSTs the key to mam-api
- new mam-api POST /assets/:id/live-thumbnail sets thumbnail_s3_key on the still
-live row (status untouched); idempotent no-op once finalized
- visuals.jsx AssetThumb: for live assets, show the static poster once the key /
signed URL is available, else fall back to the live HLS preview. Pulsing LIVE
border kept either way
- POST /assets gains an optional status param (default 'processing'); 'live'
skips the proxy/thumbnail queue
- capture /stop route now finalizes the pre-created asset by id (guarded) instead
of POSTing a duplicate
🤖 Generated with Claude Code
Replace the electric amber placeholder with the official brand accent from
the Wild Dragon Forge Noir Edition identity guide (Pantone 286 C).
- --accent: #1025A1 (Flame Blue)
- --accent-hover: #1830B8 (Ember-adjacent)
- --accent-text: #C7CFEA (Halo — readable on dark surfaces, 13:1 contrast)
- Primary button text reverts to white (11.7:1 contrast on Flame Blue)
- Avatar uses Flame Blue background + white initials
- All rgba hardcodes updated to match new hue across CSS and JSX
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Lever 1 (color): Replace #5B7CFA AI-blue with electric amber #E8821C across
all accent tokens, tile tones, logo glows, and hardcoded rgba values. Dark
text on amber primary buttons for WCAG AA contrast.
Lever 2 (home): Collapse centered logo hero into compact left-aligned header.
Split tile grid into primary ops row (Library, Recorders, Playout) + secondary
4-col row (Downloads, Jobs, Dashboard, Settings) with reduced visual weight.
Lever 3 (typography): Remove v1.2.0 from sidebar. Fix em-dashes to hyphens or
periods across all visible UI strings (option labels, body copy, error messages).
Topbar height 56px -> 48px.
Lever 4 (motion): Staggered entry animation for launcher tiles
(prefers-reduced-motion gated). Tactile scale(0.97) on primary/record buttons.
Smooth 150ms nav active-item transitions.
Lever 5 (blocks): Jobs stats row semantic card variants (amber glow when
active, red border when failed, quiet muted style for Total).
Lever 6 (spacing): Topbar 48px, launcher inner gap tightened, status left-aligned.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
When a capture sidecar crashes before finalize() runs (e.g. wrong node,
filter error, hardware fault), the asset stays 'live' indefinitely — library
shows 'Recording' badge for up to 120 minutes until the stale-timeout fires.
Add an orphan check that runs every scheduler tick: if an asset is 'live'
and its recorder is 'stopped', mark it 'error' immediately. This runs before
the 120-minute staleness guard so the library clears within 15 seconds.
🤖 Generated with Claude Code
When a capture sidecar stopped/restarted, the bridge video thread got EPIPE
on the FIFO write, set g_port_stop[port]=1, and the port went dead — requiring
a full bridge restart to recover. Subsequent record attempts on that port would
hang in 'connecting' forever.
Fix: mirror the audio thread pattern — on EPIPE, close the FIFO and loop back
to open() blocking for the next reader. Hardware lock errors (SDK failures)
still stop the port via g_port_stop as before. Only reader-disconnect (EPIPE)
now recovers gracefully.
This was the cause of port 6 (Ghost) failure in the burn test.
🤖 Generated with Claude Code
ROOT CAUSE of 'connecting' hangs and intermittent port failures:
The DELTA-12G-e-h 8c is a bidirectional card. Without calling
VHD_SetBiDirCfg(board_index, VHD_BIDIR_80) before streaming, the
board remains in its default bi-dir config (likely 4RX/4TX) — so
RX stream opens fail with VHDERR_RESOURCEUNAVAILABLE on channels
configured as TX, causing random 'connecting' hangs per the SDK docs.
Per SDK Tools.cpp SetNbChannels() pattern:
1. Open temporary board handle
2. Check IS_BIDIR + channel counts
3. Call VHD_SetBiDirCfg(board_index, VHD_BIDIR_80) for 8ch bidir
4. Close temp handle, then open real board handle for streaming
Also add VHD_SetChannelProperty(VHD_CHANNEL_MODE_SDI) for ASI-type
channels per Sample_RX.cpp — required for 12G-ASI/3G-ASI channel
types to correctly detect incoming video standard.
🤖 Generated with Claude Code
- MonitorTile now persists lastAssetId in local state so tiles continue
showing content after a recording stops (frozen HLS, then thumbnail)
- When recording stops: HlsPreview stays alive briefly while segments are
hot, then fetches /api/v1/assets/{id}/thumbnail for a frozen still
- Idle tiles that never recorded show FauxFrame with IDLE badge as before
- Per-tile elapsed timer ticks every second using feed.started_at
- capturePort badge (Port N / SDI N) visible on each tile
- Monitors poll interval tightened from 5s -> 3s for faster live_asset_id pickup
🤖 Generated with Claude Code
Two bugs causing deltacast recorder 500s:
1. startDeltacastBridge() had no proc.on('error') handler — ENOENT when
deltacast-bridge binary not in container PATH crashed the entire node-agent
process (unhandled error event). Added error handler that logs and clears
_dcBridge so the sidecar start continues.
2. node-agent container lacked pid:host — _dcBridgeProcessAlive() scans /proc
but without host PID namespace it only sees container PIDs, so it always
returned false and tried to re-spawn the bridge (hitting bug 1).
pid:host lets the /proc scan see the host's deltacast-bridge systemd process.
🤖 Generated with Claude Code
- 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
Live/in-progress assets had no thumbnail_s3_key, so AssetThumb fell
through to FauxFrame (black box) and then an absolute red border div
was drawn on top, producing the 'black box with red outline' symptom.
Fix: when asset.status === 'live', render a new LiveThumb component
instead of FauxFrame + border overlay. LiveThumb attaches hls.js (or
native HLS on Safari) to /live/<assetId>/index.m3u8, shows a muted
live video feed, and displays a 'Connecting…' placeholder with a
record icon + live-colour border while the manifest loads. Falls back
to a 'Recording…' placeholder if hls.js is unavailable or playback
fails after retries.
The red border overlay is removed from the non-live path; the LIVE
badge rendered by AssetCard's thumb-status div still appears on top
of the live video.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause A (main.c): audio_thread set the global g_stop flag on EPIPE
(ffmpeg reader died). This killed ALL port threads across the entire bridge
process. Bridge process then exited with all 8 ports gone.
Root cause B (node-agent/index.js): startDeltacastBridge() skipped respawn
when FIFOs existed in /dev/shm/deltacast, even if the bridge process was dead.
Next ffmpeg opened the audio FIFO read-end and blocked forever (no writer) →
no audio (or video) for any new recording.
Fix A (main.c):
- Add per-port atomic g_port_stop[MAX_PORTS] flags.
- Audio thread: on EPIPE, close the FIFO fd and loop back to reopen it.
The VHD ANC stream stays open across reconnects. Other ports unaffected.
- Video thread: on EPIPE or stream error, set only g_port_stop[port], not
the global g_stop. Other ports keep running.
- MAX_PORTS #define moved before globals so g_port_stop[MAX_PORTS] compiles.
Fix B (node-agent/index.js):
- Add _dcBridgeProcessAlive() — scans /proc/<pid>/cmdline for deltacast-bridge.
- startDeltacastBridge(): if FIFOs exist but no live bridge process is found,
spawn a fresh bridge instead of silently returning. Detects bridges started
externally (e.g. sudo on the host before node-agent started).
Requires: bridge rebuild + restart on zampp3. No capture image rebuild needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _normRecorder: add framerate field (recording_framerate || 'native');
remove stale static elapsed calc (was computing at poll time, never ticked)
- RecorderRow: replace frozen useMemo elapsed with a live 1s setInterval
that anchors to liveStatus.duration (authoritative from capture container)
and falls back to wall-clock diff from recorder.started_at so the counter
starts immediately on record and never freezes between 3s status polls
- displayFramerate: shows currentFps (2dp + 'fps') when recorder is live and
currentFps > 0; falls back to configured recording_framerate or 'native'
- Framerate stat block: always visible (was conditional on currentFps != null);
replaced the separate FPS-only block with a unified Framerate stat
- Also fixes latent padStart(2, '00') typo on minutes field in old elapsed calc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Live/in-progress assets had no thumbnail_s3_key, so AssetThumb fell
through to FauxFrame (black box) and then an absolute red border div
was drawn on top, producing the 'black box with red outline' symptom.
Fix: when asset.status === 'live', render a new LiveThumb component
instead of FauxFrame + border overlay. LiveThumb attaches hls.js (or
native HLS on Safari) to /live/<assetId>/index.m3u8, shows a muted
live video feed, and displays a 'Connecting…' placeholder with a
record icon + pulsing live-colour border while the manifest loads.
Falls back to a 'Recording…' placeholder if hls.js is unavailable
or playback fails after retries.
The red border overlay is removed from the non-live path; the LIVE
badge rendered by AssetCard's thumb-status div still appears on top
of the live video.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capture bridge emits per-port format JSON on signal lock. Node-agent
now caches these by port and injects DELTACAST_VIDEO_SIZE, DELTACAST_FRAMERATE,
DELTACAST_INTERLACED into the sidecar env so capture-manager uses
actual signal dimensions instead of hardcoded 1920x1080/25fps defaults.
The old architecture spawned one deltacast-capture per recorder port; each
called VHD_OpenBoardHandle, triggering a BufMngr.c:781 OOB fault in the
delta_x300 kernel driver whenever two opens raced.
Fix: a single deltacast-bridge daemon opens the board once, opens RX
streams for all requested ports concurrently, and writes each port's
video/audio to named FIFOs (/dev/shm/deltacast/video-<N>.fifo,
/dev/shm/deltacast/audio-<N>.fifo). Capture sidecars read from those
FIFOs directly — no board handle, no race, no flock.
Changes:
services/capture/deltacast-bridge/main.c
- Complete rewrite: --ports csv arg, board opened once, one
video+audio thread pair per port, FIFO paths per port, format
JSON emitted per port on signal lock, SIGTERM clean shutdown.
- flock/serialize logic removed (no longer needed).
- --port single-port compat alias retained.
services/capture/deltacast-bridge/CMakeLists.txt
- Rename target deltacast-capture -> deltacast-bridge.
- POST_BUILD symlink deltacast-capture -> deltacast-bridge for compat.
services/capture/src/capture-manager.js
- deltacast _buildInputArgs: remove bridge spawn; wait up to 30s
for FIFOs to exist (bridge may be starting); return rawvideo +
s16le FIFO inputArgs. bridgeProcess=null.
- audioMap: keyed on sourceType instead of bridgeProcess (both
inputs are always present for deltacast).
- Remove readFirstStderrLine helper (no longer needed).
- Remove bridgeProcess.stdout.pipe / processes.bridge stop signal.
services/node-agent/index.js
- Add import spawn for bridge daemon management.
- Add startDeltacastBridge / stopDeltacastBridge: host-process
lifecycle for the shared bridge, ref-counted by sidecar count.
- handleSidecarStart: on deltacast, increment counter + start bridge;
decrement on container create/start failure.
- handleSidecarStop: decrement counter; stop bridge when last sidecar.
- _containerSourceType map tracks containerId->sourceType for stop.
- Old acquireDcLock mutex retained but no longer called.
Remove 'Capture' from the Operations nav section in shell.jsx — users
configure recorders via the Recorders page; the Capture route/component
is left intact for any remaining references.
Also remove 'capture' from the ingest open-group list (it was listed
as an ingest child despite living in Operations, now moot).
Add a prominent amber warning banner at the top of the Playout page
body (screens-playout.jsx) to make clear the feature is in testing and
not ready for production use.
No cherry-pick from fix/library-and-signal-indicator — all commits on
that branch are already present on main.
The signal timeout deadline was set at process start before waiting for
the flock. Bridges queued behind earlier ports waited minutes for the
lock, then found their 30s signal deadline had already expired before
they even opened the board, causing false "no signal" failures on ports
that have live signal.
Fix: move clock_gettime deadline initialisation to AFTER flock acquired
and board opened, so the full sig_timeout is always available for signal
detection regardless of queue wait time.
With 8 deltacast bridges serializing via flock (each holding the lock
for ~35s during signal wait + settle), the last bridge in queue waits
~280s before getting the lock. The 35s readFirstStderrLine timeout was
firing before those bridges could even open the board, causing them to
fail silently while the bridge was still queued. 300s (5min) covers
8 bridges * 35s each with margin.
The flock-based board serialization in deltacast-bridge emits [board] log
lines to stderr before the JSON format line. readFirstStderrLine was
failing on the first non-JSON line. Now loops over complete lines,
skips any not starting with {, and waits for the actual JSON.
The deltacast bridge now emits [board] log lines before the format JSON
(while waiting for flock). readFirstStderrLine was parsing the first line
only and failing with 'invalid JSON'. Now it accumulates all lines and
skips any that do not start with '{', continuing to wait for the JSON
format line. Error lines ({\"error\":...}) still reject immediately.
Concurrent VHD_OpenBoardHandle calls from multiple capture sidecars
trigger delta_x300 BufMngr.c:781 array-index-out-of-bounds, wedging all
RX channels until the module is reloaded. The node-agent stagger only
delays container start — the bridge binary starts ~2s later and can still
race. This fix acquires an exclusive flock on /dev/shm/deltacast/bridge.lock
before VHD_OpenBoardHandle and holds it until signal lock succeeds (then
adds a 4s settle before releasing so the board's buffer queues stabilize).
Lock is released on signal failure too so the next bridge is never
permanently blocked. All 8 channels can now start safely by serializing
through the same lock file mounted into every sidecar.
Simultaneous VHD_OpenBoardHandle calls from 8 sidecars trigger a kernel
array-index-out-of-bounds in delta_x300 BufMngr.c:781 that wedges all
RX channels. Serialize deltacast-only sidecar starts through a
promise-chain mutex with a configurable settle delay
(DELTACAST_START_STAGGER_MS, default 3500ms). All other source types
(SDI, SRT, RTMP) are unaffected — they bypass the mutex entirely.
25 Mbps is sufficient for XDCAM HD422 1080i/1080p at broadcast quality
and halves storage use. Operators can still override via recording_video_bitrate.
Simultaneous VHD_OpenBoardHandle calls from 8 sidecars trips a kernel
array-index-out-of-bounds in BufMngr.c:781 (delta_x300 v6.34.1). Fix:
a process-wide promise-chain mutex gates deltacast sidecar starts so only
one board open is in flight at a time, with a configurable settle delay
(DELTACAST_START_STAGGER_MS, default 3500ms) before releasing the lock.
SDI, SRT, RTMP and all other source types are unaffected.
The growing-master ffmpeg orchestrator declared split=2[vhi][vlo] but only
consumed [vlo] inside the `if (hlsDir)` block. For deltacast sources the
caller passed hlsDir=null (the ternary only matched sourceType==='sdi'), so
[vlo] was left unconnected → ffmpeg aborted with "Filter 'split' has output 1
(vlo) unconnected" / "Error binding filtergraph inputs/outputs" → 0 frames →
no HLS → "playback failed" on all deltacast previews.
Fix:
- Pass sdiHlsDir for deltacast as well as sdi (deltacast also produces the
2nd-output HLS preview from the single SDI read).
- Make the orchestrator filter_complex conditional: split=2[vhi][vlo] when an
HLS dir is present, split=1[vhi] (master only) otherwise, so no split output
is ever orphaned regardless of source type.
Restores deltacast growing-master capture (master MXF + HLS preview). No poster
tap (the incomplete recorder-thumbnails poster on the deploy node added an
mjpeg output that destabilised the shared ffmpeg; tracked separately on the
feature/recorder-thumbnails branch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Deltacast picker's selected index is the capture channel on the single
board. Write it into source_config.port (in addition to device_index) so the
capture sidecar maps "pick channel N" to the bridge's --port N. device_index is
retained for backward-compatible display/fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse the recorder's SOURCE_CONFIG JSON in the bootstrap and pass the deltacast
capture channel (`port`) and optional `board` into captureManager.start(), so a
recorder can select which of the board's 8 channels to capture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Audio map: the deltacast bridge delivers audio on a separate FIFO wired as
ffmpeg input 1, so the finalized master + HLS preview (and the growing
orchestrator) now map audio via `audioMap` (1🅰️0? for deltacast, 0🅰️0? for
DeckLink SDI / network) instead of an unconditional 0🅰️0?. Without this the
deltacast master/preview carried no audio.
- Channel/port: spawn the bridge with --device = board index (default 0) and
--port = source_config.port (falling back to the device index), so a recorder
can capture from any of the board's 8 channels. Adds `port`/`board` params to
start() and _buildInputArgs().
- Bridge stdin: the finalized-master ffmpeg reads the bridge's raw video from
pipe:0, so its stdin must be 'pipe' when a bridge is present (was 'ignore',
which made hiresProcess.stdin null and threw "Cannot read properties of null
(reading 'on')" at bridgeProcess.stdout.pipe(...)).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ffmpeg opens all inputs before processing; input 1 is the audio FIFO. The
bridge previously opened the FIFO writer only after VHD_OpenStreamHandle +
VHD_StartStream succeeded, returning early on failure / no embedded audio and
never opening the FIFO -> ffmpeg blocked forever on input 1 -> 0 fps and an
empty HLS preview. Now the FIFO writer is opened unconditionally and first,
and the audio thread feeds a continuous, wall-clock-paced s16le stereo stream
(real samples when available, otherwise silence). SIGPIPE is ignored so a
dying ffmpeg returns EPIPE instead of killing the bridge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-node empirical testing of this bmx v1.6 build showed that raw2bmx's
rdd9 writer with --part already maintains a live, correct header Duration
as the file grows: ffprobe reads a growing duration mid-write (e.g. 2.04s
of a 10s clip while still recording), and the structural-metadata
Duration fields (tags 02020008 / 30020008) hold the real frame count
(0x33 = 51), not -1.
The dur-patch.py added in the previous commit searched the header for
Duration=-1 (0xFF*8) and found 0 fields on rdd9 ("[dur-patch] 0 Duration
fields"), so it was a no-op. Worse, opening the MXF r+b to patch it while
raw2bmx appends over CIFS is a concurrency hazard. Remove it entirely and
rely on raw2bmx's native growing Duration. rdd9 + --index-follows remains
the Premiere-recommended growing flavour (Sony XDCAM essence, index in the
essence partition).
Verified on-node (ffprobe/byte-probe). Live edit-while-record in Premiere
itself still requires user confirmation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>