Commit graph

92 commits

Author SHA1 Message Date
3eacb35c1e fix(capture): replace continuous idle preview with 1fps JPEG snapshot to stop FIFO contention halving capture fps 2026-06-02 21:40:52 +00:00
de3e09f39e fix: restore correct capture files after merge 2026-06-02 21:31:46 +00:00
52f039554c merge: keep correct local capture files, reject placeholder uploads 2026-06-02 21:31:29 +00:00
ec1699907d fix(deltacast): non-blocking write + 64MB FIFO + GPU runtime for capture container 2026-06-02 21:31:06 +00:00
1cef8c3bb3 fix(deltacast-bridge): non-blocking write_all + larger FIFO (64MB) to prevent frame drops 2026-06-02 17:06:40 -04:00
82697d92de fix(deltacast-bridge): restore BUFFER_PACKING YUV422_8 fix that was lost during git checkout 2026-06-02 16:59:23 -04:00
87fd3164c6 Fix: restore all deltacast-bridge fixes (buffer packing YUV422_8, SDI interface, F_SETPIPE_SZ, deinterlacing bypass) 2026-06-02 16:43:45 -04:00
0aa010dacc fix(deltacast-bridge): restore BUFFER_PACKING YUV422_8 + SDI interface + F_SETPIPE_SZ fixes 2026-06-02 16:42:45 -04:00
9ac27ff194 fix(capture): deinterlacing bypass for progressive signals + fps calculation fix 2026-06-02 16:24:45 -04:00
13feb0a6a2 fix(uxp): promisify fs calls for UXP compatibility (v2.2.3)
fs.writeFile/fs.readFile/fs.stat are callback-based and don't return
Promises in the UXP sandbox. await on them resolves immediately, causing
race conditions where files aren't written before import.

Added _writeFile/_readFile/_stat wrappers that use fs.promises when
available and fall back to manual Promise wrapping otherwise.

Also bumped version to 2.2.3 to match web-ui data.jsx.
2026-06-02 20:17:00 +00:00
1ff0f2d865 fix(capture-manager): apply deinterlacing bypass + fps calculated from frames/elapsed 2026-06-02 15:14:40 -04:00
bdebc3adac fix(capture-manager): calculate currentFps from frames/elapsed instead of ffmpeg running average 2026-06-02 14:46:09 -04:00
a200cabad4 fix(deltacast-bridge): pin buffer packing + sdi interface + f_setpipe_sz 2026-06-02 14:21:44 -04:00
3ac685ed3b fix(deltacast-bridge): pin BUFFER_PACKING to YUV422_8; guard frame size
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>
2026-06-02 17:00:07 +00:00
Claude
22853da023 fix(capture): authenticate sidecar->mam-api calls with bearer token
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
2026-06-02 16:00:13 +00:00
Claude
b40f640fa1 build(capture): add .dockerignore to keep stale bridge build/ out of image context
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
2026-06-02 15:23:03 +00:00
Claude
a2790601c9 feat(library): first-frame poster thumbnail for live recordings
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
2026-06-02 15:21:05 +00:00
Claude
08be8fea77 fix(deltacast-bridge): video EPIPE reopens FIFO instead of stopping port permanently
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
2026-06-02 11:33:17 +00:00
Claude
858c9f7b97 fix(deltacast-bridge): call VHD_SetBiDirCfg before board open + set channel SDI mode
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
2026-06-02 11:23:39 +00:00
Zac
8a675992c2 fix(deltacast-bridge): configure both stereo channels + INTERFACE for audio extraction (SDK Sample_RXAudio) 2026-06-02 02:01:33 +00:00
Claude
c083d1006a fix: deltacast audio missing after ffmpeg restart (EPIPE cascade + stale FIFO guard)
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>
2026-06-02 01:35:07 +00:00
Zac
1c068b470e fix(capture-manager): default deltacast framerate to 60000/1001 (1080p59.94) 2026-06-02 01:15:42 +00:00
Claude
a61e385693 feat(deltacast): replace per-port bridges with shared multi-port daemon
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.
2026-06-02 00:21:52 +00:00
cad1e52c38 fix(deltacast-bridge): reset signal-wait deadline AFTER acquiring flock
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.
2026-06-01 19:32:01 -04:00
Zac Gaetano
6979f07307 fix(capture-manager): raise readFirstStderrLine timeout 35s->300s for flock queue
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.
2026-06-01 23:23:07 +00:00
Zac Gaetano
eba8e94887 fix(capture-manager): readFirstStderrLine skips non-JSON bridge log lines
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.
2026-06-01 23:09:21 +00:00
a06b5ed304 fix(capture-manager): readFirstStderrLine skips non-JSON bridge log lines
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.
2026-06-01 19:07:15 -04:00
7d704d3af3 fix(deltacast-bridge): serialize VHD_OpenBoardHandle via flock to prevent BufMngr wedge
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.
2026-06-01 18:54:00 -04:00
Zac Gaetano
9809cdd13e fix(capture): lower GROWING_DEFAULT_BITRATE 50M→25M
25 Mbps is sufficient for XDCAM HD422 1080i/1080p at broadcast quality
and halves storage use. Operators can still override via recording_video_bitrate.
2026-06-01 22:42:07 +00:00
9adcae0329 fix(capture): connect deltacast growing-master filtergraph (split output 1 unconnected)
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>
2026-06-01 16:10:21 -04:00
0eac34529b feat(deltacast): wire source_config.port/board into capture start
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>
2026-06-01 14:58:58 -04:00
d6e515e1a8 fix(capture): map deltacast audio from input 1; per-recorder channel/port; fix bridge stdin pipe
- 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>
2026-06-01 14:57:53 -04:00
3d3c8c48de fix(deltacast): always open audio FIFO writer (silence fallback) to stop ffmpeg input deadlock
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>
2026-06-01 14:52:24 -04:00
12d76edc42 feat(deltacast): support 8 RX channels (ports 0-7) on DELTA-12G-elp-h 2026-06-01 13:31:06 -04:00
b875376887 Merge remote-tracking branch 'wilddragon/main' 2026-06-01 09:22:02 -04:00
252aa713d4 fix(capture): drop dur-patch; rdd9 self-maintains growing Duration
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>
2026-06-01 09:07:28 -04:00
e3be8745d3 fix(capture): restore frame counting + growing MXF for Premiere
The previous sed/python in-place edits on the node broke capture: the
hires stderr parser was written with literal 0x08 BACKSPACE bytes instead
of regex word boundaries, so it never matched ffmpeg output.
framesReceived stayed 0, the shutdown handler saw "no frames" and marked
every asset as an error even though video was captured. The ffmpeg base
args had also been changed to -progress pipe:2, whose key=value output
puts frame= and fps= on separate lines and does not match a combined
regex.

Fixes:
- Parser: single robust regex matching ffmpeg's classic -stats line
  (frame= and fps= together). No backspace bytes, no word boundaries.
- ffmpeg base args back to -stats (drop -progress pipe:2).

Growing-file (Premiere edit-while-record), per bmx thread 87ac5750 and
Drastic/Softron edit-while-ingest docs:
- raw2bmx clip type op1a -> rdd9 (Sony XDCAM / RDD-9, the flavour Premiere
  reads while growing) with --index-follows so the IndexTableSegment is
  written in the same partition as the essence it indexes (lets a reader
  re-scanning body partitions seek toward the record head). NOT --avid-gf
  (Avid OP-Atom, Media-Composer-only, needs a companion AAF).
- dur-patch.py: overwrite header Duration=-1 to 0 immediately at
  clip-open (Premiere rejects -1 on import), then track the live frame
  count every 3s from the last body partition IndexTableSegment. Shipped
  as services/capture/dur-patch.py (/app/dur-patch.py in the image).

Deployed to wild-dragon-capture:latest on zampp2 via overlay build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:00:23 -04:00
7d408035ac fix(capture): fix VHD_CORE_SP_BUFFERQUEUE_DEPTH constant name for SDK 6.34 2026-06-01 08:59:06 -04:00
884c8829a0 merge: integrate remote changes + deltacast bridge capture
Merges remote main (raw2bmx growing-file, playout, auth) with local
Deltacast SDI capture implementation (bridge binary, _buildInputArgs,
start/stop lifecycle).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 08:41:05 -04:00
962c7c8f20 fix(capture): log SDK errors in video loop, warn on unsupported port index 2026-06-01 08:00:55 -04:00
b46abc9b1a fix(capture): shell injection, stale FIFO, stderr listener, bridge exit handler 2026-06-01 08:00:06 -04:00
adfbeac217 feat(capture): accept deltacast as valid source_type in /start handler 2026-06-01 07:56:43 -04:00
d4924b044f feat(capture): wire bridge process lifecycle into start/stop for deltacast 2026-06-01 07:55:34 -04:00
4da7bf8b41 feat(capture): replace deltacast _buildInputArgs stub with real bridge spawn 2026-06-01 07:54:17 -04:00
6fec41aaf9 feat(capture): add readFirstStderrLine helper for deltacast bridge handshake 2026-06-01 07:53:12 -04:00
de6e44b991 build(capture): add Deltacast SDK extraction and bridge build stages to Dockerfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 07:52:03 -04:00
c7e07df515 fix(capture): handle partial writes in audio_thread FIFO write loop 2026-06-01 07:51:10 -04:00
67c071a0ee feat(capture): add deltacast-capture bridge binary source 2026-06-01 07:50:07 -04:00
3529590160 build(capture): add CMakeLists for deltacast-capture bridge binary 2026-06-01 07:47:32 -04:00
1750298bb8 revert(capture): remove --growing-file flag (not valid in bmx v1.6)
raw2bmx v1.6 does not have a --growing-file option; using it causes
'Unknown Input Option' and immediately crashes the pipeline. The
--part interval alone is sufficient — body partitions with updated
IndexDuration are written every 30 frames, and the file has no footer
(open state) while recording, which is what Premiere's growing-file
reader polls for.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 07:37:27 -04:00