Commit graph

116 commits

Author SHA1 Message Date
OpenCode
105d04729a fix(capture): explicit slice threading on VC-3 growing encoder
dnxhd with ffmpeg default frame-threading starts at ~0.27x realtime and
buffers a long pipeline before output flows, so the fc_pipe ring laps ~344
startup frames (spotty audio+video for the first several seconds). Setting
-threads 32 -thread_type slice makes dnxhd encode >= realtime from frame 1
(measured 1.3x at start), cutting startup drops substantially. The finalized
master file is gap-free (even PTS, audio==video duration) — the remaining
skipped frames are pre-record spin-up frames dropped to the live edge, not
holes in the recording.
2026-06-05 03:12:42 +00:00
OpenCode
35fb84af4d feat(capture): VC-3/DNxHD growing MXF for Premiere edit-while-record
Replace the AVC-Intra growing path (which Premiere rejected as unsupported/
damaged) with VC-3/DNxHD written directly by ffmpeg native MXF muxer. The
frame-wrapped OP1a body grows readably mid-write and imports+grows live in
Adobe Premiere (matches vMix workflow). No raw2bmx, no FIFO orchestrator, no
footer-finalize ordering - one ffmpeg writes MXF straight to the SMB share.

Two operator-selectable bitrates: vc3_90 (~90 Mbps, default) and vc3_220
(~220 Mbps). Both 8-bit 4:2:2 @ 1080p59.94, essence VC3_1080p_1238. Stop uses
a plain SIGINT (ffmpeg flushes the MXF footer cleanly).

UI: growing codec select (90/220) replaces the AVC-Intra readonly field; the
freeform growing bitrate input is removed (bitrate is codec-fixed). mam-api
guards accept vc3_90/vc3_220, default vc3_90.

Verified on zampp3: both bitrates grow live + finalize clean (check-complete
passes, 0 decode errors), user-confirmed Premiere import + growth.
2026-06-05 03:04:45 +00:00
OpenCode
c8cddc19b2 feat: AVC-Intra 50/100/200 growing codec selector 2026-06-04 21:51:45 +00:00
ZGaetano
6af5d77d62 feat(growing): thread growing_codec through /capture/start session params 2026-06-04 20:58:20 +00:00
ZGaetano
a031ff1c9e feat(growing): add GPU-offload HEVC-NVENC frag-MOV growing codec
Second selectable growing path alongside AVC-Intra 100. GROWING_CODEC env
(per-recorder growing_codec field) picks: avci100 (CPU 4:2:2 MXF, default) or
hevc_nvenc (GPU all-intra HEVC 10-bit 4:2:0 fragmented MOV). The hevc path runs
a single ffmpeg (no raw2bmx/FIFO) writing frag-MOV directly; +empty_moov makes
it grow with readable duration mid-write. Proven live on zampp3: size+duration
advance monotonically, finalized file decodes RC=0 (hevc Main10 1080p59.94).
2026-06-04 20:53:13 +00:00
ZGaetano
967547ae97 fix(growing): drop FIFO FD-priming deadlock — ffmpeg-first ordering feeds raw2bmx frames
Root cause of frozen 22KB growing MXF: the parent held a stray read-write
priming writer (FD 7) on the video FIFO while raw2bmx opened it, so raw2bmx
read a malformed/empty stream start and exited after the header (Duration:0).
Proven live on zampp3: removing the FD-7/8 priming + fd-watch loop and simply
starting ffmpeg before raw2bmx lets the blocking FIFO opens pair up naturally
(ffmpeg sole writer). True-1080p59.94 AVC-Intra 100 then grows monotonically
on disk and a mid-write snapshot decodes to its last frame (481 frames @ 8s).
2026-06-04 19:31:48 +00:00
bf486b93dd feat(growing): TRUE 1080p59.94 via AVC-Intra 100 (libx264 4:2:2 10-bit)
User wants native 59.94p, not 30p. XDCAM HD422/MPEG-2 422 cannot do 1080p59.94 (raw2bmx rejects 60000/1001). AVC-Intra 100 CAN. Switched growing essence to libx264 high422 10-bit avcintra-class=100 (NVENC h264 cannot do 4:2:2, so CPU), -f h264, raw2bmx -t op1a --avci100_1080p -f 60000/1001. Verified via the live-equivalent fc_pipe|bash path WITH the FD-9 stdin fix: produces h264 High 4:2:2 Intra yuv422p10le 1920x1080 progressive 60000/1001 MXF (true 59.94p), openable in Premiere.
2026-06-04 19:02:11 +00:00
a00a280689 fix(growing): ffmpeg reads video from saved FD 9 — fixes empty-output growing
ROOT CAUSE FOUND + verified. When growing video comes from fc_pipe, node pipes it to the bash orchestrator's stdin. ffmpeg ran as a backgrounded subshell merely inheriting fd 0 (0<&0). With a PIPE source (not the working file/FIFO case), that subshell was starved of the raw video -> filtergraph 'No filtered frames' -> empty mpeg2video -> raw2bmx broken pipe -> sidecar crash (write EPIPE). Reproduced exactly with 'fc_pipe | bash -c orchestrator'. Fix: save original stdin to FD 9 BEFORE the FIFO-priming fd games (exec 9<&0), point ffmpeg's fd0 at fd9 (0<&9), close 9 in raw2bmx + parent. Verified the live-equivalent path now produces a valid mpeg2video 4:2:2 yuv422p progressive 30000/1001 MXF matching the working Delta7 file. Also added EPIPE handlers so a broken pipe never crashes the sidecar.
2026-06-04 18:59:53 +00:00
690f27218d fix(growing): parent shell drops fd0 so ffmpeg is sole stdin reader
Root cause of growing 'video empty / No filtered frames': when video comes from fc_pipe, node pipes it to the bash orchestrator's stdin (fd0). The ffmpeg subshell inherits fd0, BUT the parent bash kept fd0 open too (in its wait loop). Both held the read end of the same pipe, so the kernel split the raw video bytes between them and ffmpeg was starved -> zero decoded frames -> empty mpeg2video -> raw2bmx broken pipe. Manual 'fc_pipe | ffmpeg' worked because ffmpeg was the sole reader. Fix: parent execs 0</dev/null right after spawning ffmpeg, making ffmpeg the sole stdin reader. Verified the full pipeline (fc_pipe->ffmpeg mpeg2video 422->raw2bmx rdd9) produces a valid 1080p29.97 MXF matching the working Delta7 file.
2026-06-04 18:52:07 +00:00
c2e3ee7dd2 fix(growing): XDCAM HD422 1080p29.97 — exact match to working Delta7 file
ffprobe of the proven-working Delta7_20260603 file (Premiere opened it): mpeg2video 4:2:2 yuv422p 1920x1080 PROGRESSIVE 30000/1001 + 2x pcm_s16le. The 1080p59.94 source is frame-rate-halved to 1080p29.97 progressive (NOT interlaced, NOT 59.94p). Verified on-node: mpeg2video yuv422p -r 30000/1001 -> raw2bmx -t rdd9 --mpeg2lg_422p_hl_1080p -f 30000/1001 produces an MXF byte-identical in format to the working file (rc=0).
2026-06-04 18:39:29 +00:00
4609517756 fix(growing): AVC-Intra 100 1080p59.94 via libx264 (verified raw2bmx accepts)
Verified on-node: libx264 high422 10-bit + x264 avcintra-class=100 -> raw2bmx -t op1a --avci100_1080p -f 60000/1001 produces a valid MXF (ffprobe: h264 High 4:2:2 Intra yuv422p10le 60000/1001). True 1080p59.94, openable in Premiere. CPU encode for now per demo deadline; NVENC h264 cannot do 4:2:2.
2026-06-04 18:35:42 +00:00
e6b7856bb2 fix(growing): restore proven XDCAM HD422 (MPEG-2 422) rdd9 @1080i59.94
Today's codec churn (h264_nvenc/AVC-Intra/op1a) produced .mxf files Premiere could not open. Subagent diagnosis vs the old working files on the share proved the working format is XDCAM HD422 = mpeg2video 4:2:2 yuv422p, clip type rdd9, wrapped at 1080i59.94 (30000/1001). raw2bmx REJECTS MPEG-2 422 at 60000/1001, so a 1080p59.94 SDI feed must wrap as 1080i59.94. Reverted GROWING_VIDEO_ELEMENTARY_ARGS to mpeg2video, raw2bmx -t rdd9 --mpeg2lg_422p_hl_1080i -f 30000/1001, -f mpeg2video pipe, and rates() 59.94->30000/1001 with 1080 forced interlaced.
2026-06-04 18:31:03 +00:00
e6eb565e30 fix(growing): switch to NVENC H.264 High Intra
Using libx264 confirmed raw2bmx works with AVC-Intra parameters at 59.94. Switching back to hardware-accelerated h264_nvenc with matching parameters (High profile, all-intra GOP 1, yuv422p, aud) to keep CPU load low during 8-port burn tests.
2026-06-04 18:18:41 +00:00
4bbbc9f4dc fix(growing): add -aud 1 to h264_nvenc to fix raw2bmx parse failure 2026-06-04 18:12:50 +00:00
42806b5e10 fix(growing): use stable H.264 High All-Intra for 1080p59.94 MXF
XDCAM HD422 does not strictly support 1080p59.94, and ffmpeg/raw2bmx failed to negotiate the stream. Reverted to h264_nvenc (High profile, all-intra GOP 1, yuv420p) which raw2bmx can reliably wrap as OP1a (--avc_high) at 60000/1001. This restores NVENC hardware acceleration and Premiere edit-while-record compatibility.
2026-06-04 17:56:08 +00:00
f36de429e8 feat(growing): configure AVC-Intra 100 1080p59.94 via NVENC for growing files 2026-06-04 17:49:21 +00:00
1f71de494f fix(growing): correct growing frame rate selection for 59.94 fps 2026-06-04 17:31:43 +00:00
c2d15b4e3a debug: capture raw2bmx output to log 2026-06-04 17:11:47 +00:00
80f157968f fix(capture): pin NVENC to a GPU with ffmpeg -gpu N (privileged bypasses env)
NVIDIA_VISIBLE_DEVICES=1 was set but the sidecar still SAW /dev/nvidia0,1,2 and nvenc used GPU 0 — because capture sidecars run Privileged, which exposes every GPU device node regardless of NVIDIA_VISIBLE_DEVICES/DeviceRequests. Real fix: node-agent passes CAPTURE_GPU_INDEX to the sidecar and capture-manager adds ffmpeg '-gpu N' to the hevc_nvenc + h264_nvenc encoders, so each port's master+HLS encode is explicitly bound to its assigned L4. Spreads 8 ports across 3 cards.
2026-06-04 16:07:59 +00:00
4be12c6f9a fix(stability): spread capture encodes across all GPUs + GOP parse + filmstrip retry
VIDEO FREEZE UNDER BURN (transient stall, self-recovers): all 8 capture
sidecars ran NVIDIA_VISIBLE_DEVICES=all with no -gpu selector, so ffmpeg
nvenc put every session (8 HEVC masters + 8 HLS = 16) on physical GPU 0
while the other two L4s sat idle. GPU 0 NVENC hit 86%, encode fell below
realtime, the framecache ring lapped → video froze → caught up → recovered.
Bridge verified smooth at 60fps throughout. FIX: node-agent now round-robins
each sidecar to a GPU by capture port (port % detected-GPU-count) via
NVIDIA_VISIBLE_DEVICES, honoring an explicit gpuUuid when set. Auto-detects
GPU count from nvidia-smi (override CAPTURE_GPU_COUNT). ~3 encoders/GPU now.

GOP PARSE: Number.parseFloat('60000/1001') returns 60000, making GOP 120000
(near open-GOP) instead of ~120. Added parseFps() to handle rational rates;
fixed hevcNvencArgs + buildHlsVideoArgs.

FILMSTRIP: RustFS object store intermittently returns NoSuchKey on GET for
keys that List/Head confirm exist, blanking the strip. Generation/queue/DB
all verified healthy (13/15 assets HAVE filmstrips). FIX: API now serves the
filmstrip JSON through itself with retry-on-NoSuchKey (succeeds within a
couple attempts) instead of handing the browser a signed URL — also closes
the S3 CORS gap. Frontend updated to consume the direct JSON.
2026-06-04 15:56:44 +00:00
d3e520e3b1 fix(capture+gui): kill audio-drift regression + fix elapsed/signal status
A/V REGRESSION (no audio + start stutter): capture-manager.js dropped the
-use_wallclock_as_timestamps 1 flag on the audio FIFO input (re-added by
d6b0b3a). Wallclock stamped audio by arrival time while video is CFR
frame-count, so audio ran 3-18% longer and master aresample padded seconds
of LEADING SILENCE → silent head, late video start, apparent 'no audio'.
Removing it restores the sample-count PTS baseline (8e5405c/55a72af):
audio shares the SDI clock domain, no drift, no pad.

GUI BUG A (elapsed showed 1hr+ on standby/just-started): frontend seeded
elapsed from recorder.started_at = the standby CONTAINER boot time (hours
old). Now seeds ONLY from the sidecar session duration (liveStatus.duration
when live.recording), shows nothing when idle. Backend /status now returns
session-scoped duration + recording flag, not container uptime.

GUI BUG B (false 'stopped' signal on idle ports): backend inferred signal
from container Running state (running->receiving, down->stopped) — so idle
standby ports with down sidecars showed red 'stopped'. Now signal comes
from the sidecar session (live.recording); standby = neutral 'idle', never
a false 'stopped'/'receiving'.
2026-06-04 13:21:30 +00:00
0c405ae7d4 fix(growing): read GROWING_ENABLED from env at record time + drop dead const
Second half of the growing-never-engages bug. start() decided growing via the module-level const GROWING_ENABLED (captured false at standby boot) and referenced the now-removed GROWING_SMB_MOUNT const (ReferenceError, silently swallowed). Both made growingActive=false, so every growing record produced HEVC/S3 instead of XDCAM HD422 MXF. Now reads process.env.GROWING_ENABLED + growingSmbConfig().mount fresh at record start.
2026-06-04 13:02:23 +00:00
ac1d7e1e1f fix(growing): read SMB params from env at mount time, not module load
Root cause of growing producing .mov instead of XDCAM HD422 .mxf:

mountGrowingShare() used module-level consts (GROWING_SMB_MOUNT etc.)
captured from process.env at IMPORT time. Standby capture containers boot
with these unset and receive the SMB mount/credentials per-session over
/capture/start (capture.js sets process.env right before start()). Because
the consts were already frozen empty, mountGrowingShare() saw no mount
source, returned false, and growing silently fell back to S3 streaming —
producing an HEVC .mov while the asset key said .mxf.

Fix: growingSmbConfig() reads process.env fresh at mount time. Also drop
the stale const guard in unmountGrowingShare().
2026-06-04 12:51:32 +00:00
cb25711ec6 fix(growing): inline CIFS creds + capture caps + storage probe timeout
Three fixes to restore growing-files (XDCAM HD422 MXF) recording:

1. capture-manager mountGrowingShare: pass username=/password= inline
   instead of a credentials= file. TrueNAS SMB3 rejects the creds-file form
   with EACCES (-13, 'cannot mount read-only') while the identical inline
   creds mount fine. This was causing every growing record to silently fall
   back to the HEVC/S3 path (producing .mov, not .mxf).

2. docker-compose capture: add cap_add SYS_ADMIN + DAC_READ_SEARCH and
   apparmor:unconfined so mount.cifs can run inside the container.

3. storage /overview: wrap S3 HeadBucket/ListObjects probe in a 5s timeout
   so the admin 'Mount health' card stops hanging on 'Probing…' forever
   when S3 is slow.
2026-06-04 12:42:39 +00:00
d6b0b3a9a6 fix(capture): restore proven-clean wallclock audio (match de509c6 baseline)
Removing wallclock made A/V length drift far worse (audio 11.8% long). The
known-clean config used wallclock + master aresample=async=1; the leading
silence is a standby backlog artifact addressed by the bridge live-edge flush +
record-start audio FIFO drain, not by changing the timestamp source.
2026-06-04 05:06:40 +00:00
55a72af905 fix(capture): derive audio PTS from sample count (kill 2.5s leading silence)
The persistent ~2.5s of leading silence was the master aresample=async=1 PADDING
the audio to reconcile a PTS-origin mismatch: video PTS starts at frame 0
(-framerate), but -use_wallclock_as_timestamps stamped the first audio chunk at
its wall-clock arrival time (~2.5s after the ffmpeg graph opened). aresample
filled the gap with silence.

Drop wallclock: audio PTS now comes from the 48kHz sample count starting at 0 —
the same origin as video frame 0 — so the streams align with no pad. The bridge
already hands live audio (backlog flushed on attach), so no rate reference is
needed from wallclock.
2026-06-04 05:01:05 +00:00
b1a2249f36 fix(capture): align A/V at record start (kill leading silence + length drift)
Root cause of 'silent first ~1s then clean' + ~0.5% audio-too-long: in standby
the bridge keeps filling the audio FIFO while the idle-preview consumes only
video, so when recording starts ffmpeg reads a ~0.5s backlog of stale audio,
AND the video-only pre-roll discards video frames the audio never had.

Fix: (1) skip the video-only pre-roll in standby (warm slot = no unstable
frames), (2) drain the audio FIFO non-blocking immediately before ffmpeg opens
it, so audio starts at the live edge aligned with the first real video frame.
2026-06-04 04:49:53 +00:00
fffb6b63b5 fix(capture): revert 16ch audio to clean 2ch — fixes pitch/rate regression
The 16ch interleave in the deltacast bridge produced audio at HALF the correct
sample rate (measured 24224 vs 48000 samples/s/ch), which broke A/V sync and
pitch. Per the working baseline (audio was clean before the channel selector),
revert the bridge audio thread to the original single-group 2ch extraction and
the capture-manager audio input to -ac 2 + wallclock + aresample.

KEPT the good fixes: long-GOP HEVC for non-growing (NVENC realtime, no frame
drops) and GPU-only codec list. 16ch/channel-select is shelved for a separate,
properly-validated change.
2026-06-04 04:33:34 +00:00
b28393eb76 Revert "fix(capture): skip video-only pre-roll in standby to stop A/V pitch drift"
This reverts commit 51b66d882f.
2026-06-04 04:28:11 +00:00
51b66d882f fix(capture): skip video-only pre-roll in standby to stop A/V pitch drift
The pre-roll drained only the video pipe (fc_pipe) while the audio FIFO kept
buffering, so ffmpeg read ~PRE_ROLL_SECONDS of surplus pre-roll audio — making
audio longer than video, which when synced compresses audio ~0.5% (pitch-up,
measured: 2591573 audio samples vs 2579395 expected for the video duration).

In standby the framecache slot is already warm (no unstable startup frames), so
the drain is unnecessary; skipping it lets ffmpeg open video and audio together
from the same instant. Cold on-demand spawns keep the brief drain.
2026-06-04 04:24:08 +00:00
07eea02109 fix(capture): restore audio wallclock (throughput) + remove CPU codec options
- restore -use_wallclock_as_timestamps on audio input: without it ffmpeg's raw
  s16le reader stalled the graph (NVENC idle at 9%, ~half frames dropped). With
  it + long-GOP HEVC the encoder runs realtime and A/V length stays locked.
- remove all CPU codec options (prores*, dnxh*, libx264/265) from recorder UI;
  GPU NVENC only (hevc_nvenc / h264_nvenc). 3x L4 cluster, no reason for CPU.
- GPU codec defaults in env builders + proxy default h264_nvenc.
2026-06-04 04:14:59 +00:00
0ea22e1e53 fix(capture): gate all-intra HEVC on growing-files; normal record uses long-GOP
The hevc_nvenc codec was hardcoded to all-intra (-force_key_frames expr:1), which
is ~4x the NVENC load. Applied to every recording it exceeded the L4's realtime
budget at 1080p59.94 10-bit -> fc_pipe dropped ~half the frames -> video came out
shorter than the (correct) audio -> A/V drift + pitch-up on playback.

Now all-intra is used ONLY when growing-files is on (where it's required for the
editable head). Normal recordings use efficient long-GOP HEVC (2s GOP, 2 B-frames)
which NVENC sustains in realtime with zero drops.
2026-06-04 04:09:14 +00:00
8e5405c3f9 fix(capture): derive deltacast audio PTS from sample count, not wall-clock
Removing -use_wallclock_as_timestamps on the SDI audio input. The bridge writes
SDI-clock-paced samples, so PTS from the 48kHz sample count shares the video's
clock domain and the audio length tracks the video length exactly. Wall-clock
timestamps made audio length = real elapsed time, which drifted ~1% longer than
the frame-count video when the encoder dipped under realtime (pitch-up).
2026-06-04 04:01:54 +00:00
095306d9cf feat(recorders): 16ch SDI audio capture + per-recorder channel select + menu redesign
Audio:
- deltacast-bridge: always extract all 4 SDI audio groups (16ch), interleave to
  one 16ch s16le stream per port FIFO; format JSON reports audio_channels:16
- capture-manager: declare FIFO as 16ch input; keep first N discrete channels
  (2/8/16) via pan channelmap on the master (no downmix); HLS preview stays
  stereo. effAudioChannels drives -ac on the master container.
- config modal: Audio channels select (2/8/16)
- channel count already flows mam-api->node-agent->capture via RECORDING_AUDIO_CHANNELS

UI redesign (production craft):
- recorders grouped into per-node hardware 'rack' cards (online/offline state)
- lifecycle accent rail: grey DISABLED / green ENABLED / pulsing-red RECORDING
- promoted capture-port chip, monospaced metadata, Enable as primary CTA
- dedicated recorder CSS block; built on existing design tokens
2026-06-04 03:34:41 +00:00
5668c03615 chore(capture): remove stale legacy FIFO path + pin capture profile
- capture-manager: remove dead legacy deltacast FIFO video path (FC_SLOT_ID
  is now always set by node-agent, framecache mandatory on all SDI nodes)
- node-agent: correct stale comment about legacy FIFO fallback
- onboard-node.sh: harden detect_sdi (device-node checks, not just lspci) and
  persist COMPOSE_PROFILES so framecache survives every redeploy on SDI nodes
- remove committed capture.js.bak

Root cause of this session's outage: zampp3 came up without the capture
compose profile, so framecache never started; the bridge published to shm
with no consumer and recorders showed 'receiving' with no real capture.
2026-06-04 02:50:57 +00:00
Wild Dragon Dev
a096226072 fix(capture): remove -use_wallclock_as_timestamps from framecache video input
The framecache ring delivers frame-accurate frames at exactly the SDI clock
rate. -use_wallclock_as_timestamps was wrong for this source — it stamped
frames by ffmpeg arrival time rather than capture time, causing the recorded
file to report wrong framerates (e.g. 56.06 instead of 59.94) and a
glitchy first second at startup (NVENC cold-start backlog bunched timestamps).

Fix: remove -use_wallclock_as_timestamps from the rawvideo (pipe:0) input
and rely on -framerate for correct CFR timestamps from frame 0.
Audio keeps its FIFO wallclock; aresample=async=1 on the master output
resamples audio to align with the CFR video PTS.
2026-06-03 22:30:03 +00:00
Wild Dragon Dev
7631527f46 fix(capture): add auth header to finalize call in POST /capture/stop 2026-06-03 22:11:15 +00:00
Wild Dragon Dev
3d4880d944 fix(capture): reduce pre-roll to 1s in standby mode (slot already warm) 2026-06-03 22:05:11 +00:00
Wild Dragon Dev
ef57900583 feat(recorders): always-on standby sidecars for deltacast, sdi, blackmagic
Sidecars now spawn at recorder CREATE time instead of /start time.
The container boots in STANDBY=1 mode (idle preview only, no ffmpeg master).
On /start, mam-api sends per-session params (CLIP_NAME, ASSET_ID, PROJECT_ID)
to the running sidecar via HTTP POST /capture/start — ffmpeg starts in <1s.
On /stop, mam-api calls HTTP POST /capture/stop — container stays alive in
standby, ready for the next take immediately.
Container is only killed on recorder DELETE.

This eliminates: Docker create/start overhead (~1-2s), bridge startup (~2-5s),
and pre-roll wait (~5s). Latency from 'record' click to first encoded frame
drops from ~10s to ~1s.

Changes:
- capture/src/index.js: boot in standby when STANDBY=1 env is set; still
  start idle preview (live thumbnail visible before recording)
- capture/src/routes/capture.js: POST /start accepts full codec params and
  asset_id in body (skips mam-api asset creation when asset_id provided)
- node-agent/index.js: handleSidecarStandby() + POST /sidecar/standby route;
  warms bridge at recorder create time
- recorders.js POST /: spawn standby sidecar after DB insert (non-fatal)
- recorders.js POST /:id/start: HTTP fast-path to standby sidecar; falls
  back to on-demand spawn if standby not available
- recorders.js POST /:id/stop: HTTP /capture/stop, keep container in standby
- recorders.js GET /:id/status: use port-based URL for local capture status
2026-06-03 21:59:33 +00:00
Wild Dragon Dev
7172447644 fix(capture): remove leftover localMasterPath from session state 2026-06-03 21:42:35 +00:00
Wild Dragon Dev
37b325e1d8 fix(capture): restore direct-to-S3 streaming (pipe:1 + fragmented MOV)
Reverts the local-temp+faststart approach from 549ca6c. Masters now stream
ffmpeg stdout directly to S3 via multipart upload — no local disk consumed
on the worker. Uses +frag_keyframe+empty_moov+default_base_moof which
Premiere Pro 25.x handles natively (to be confirmed separately).

Zero /tmp/capture files. Worker disk stays flat during recording.
2026-06-03 21:40:58 +00:00
Wild Dragon Dev
b6545e61a9 fix(capture): add 5s pre-roll delay to stabilize SDI, remove framerate display
- services/capture/src/capture-manager.js:
  - Added 5s pre-roll delay for Deltacast, Blackmagic, and SDI capture paths.
  - Activates when  is present.
  - Spawns the  process immediately, drains/discards the unstable frames for 5 seconds, and then pipes the stdout of the  to the actual  process.
  - Keeps the master output perfectly clean from frame 0.

- services/web-ui/public/screens-ingest.jsx:
  - Removed currentFps framerate display from both recording and idle status states.
2026-06-03 17:08:09 +00:00
Wild Dragon Dev
9dc86aa3b6 Merge branch 'feat/unified-framecache' 2026-06-03 16:59:44 +00:00
Wild Dragon Dev
01211fef7a fix(framecache): address critical bugs from code review
C-Bug 1 (Torn read): fc_client.c zero-copy pointer replaced with consumer-owned
copy buffer + post-copy cursor revalidation to prevent reading torn frames when
the writer laps a slow consumer. New FC_LAPPED return code.
C-Bug 3 (Semaphore busy-spin): fc_client.c drains the semaphore (sem_trywait)
so the count never accumulates, relying entirely on write_cursor diff for
availability. Prevents 100% CPU loops + EOVERFLOW.
C-Bug 4 (GET /slots stack overflow): framecache.c uses heap allocation with
explicit bounds checking for JSON serialization instead of a 64KB stack buffer.
C-Bug 6 (DeckLink race): decklink-bridge uses pthread_mutex_t around fc_writer
calls and reopen_slot to prevent UAF/double-free from concurrent SDK callbacks.
C-Bug 2-net (Resolution resync): net_ingest explicitly scales to target W:H
so ffmpeg always outputs exactly frame_size bytes, ignoring source resolution
changes.
C-Bug 8 (strdup leak): net_ingest uses static caller-owned buffers for ffmpeg
args instead of strdup across listener reconnects.
C-Bug 9 (PROT_READ segfault): removed atomic write to hdr->dropped_frames from
the consumer read loop (which maps shm read-only).
2026-06-03 16:25:34 +00:00
97d725537b fix(capture): use ffmpeg rolling fps value for currentFps display — fixes wrong framerate shown on recorder tiles 2026-06-03 16:14:22 +00:00
Wild Dragon Dev
99723da00f feat(framecache): phase 5 — network ingest (RTMP/SRT) via framecache
- services/framecache/src/net_ingest.c: new network ingest process
  - Spawns ffmpeg to decode SRT/RTMP → raw UYVY422 stdout
  - Reads decoded frames and writes into framecache slot via shm
  - Registers slot with framecache HTTP API on startup
  - Deregisters slot on clean exit (SIGTERM)
  - Reconnect loop for listener mode (stays alive between sessions)
  - --url, --slot-id, --fc-url, --width, --height, --fps-num/den,
    --source-type, --listen, --listen-port, --stream-key args
  - Emits format JSON to stderr on first frame

- services/framecache/CMakeLists.txt: add net_ingest target
- services/framecache/Dockerfile: copy net_ingest to runtime image

- services/node-agent/index.js:
  - startNetIngest() / stopNetIngest(): lifecycle management per recorder
  - Spawns net_ingest before sidecar start for srt/rtmp sourceTypes
  - Injects FC_SLOT_ID=net-<containerId> into sidecar env
  - Sets IpcMode=host for network sidecars using framecache
  - Maps temp id → real containerId after container create
  - stopNetIngest() called on sidecar stop
  - NET_INGEST_BIN env var (default: docker exec framecache net_ingest)

- services/capture/src/capture-manager.js:
  - _buildInputArgs srt/rtmp: framecache path when FC_SLOT_ID set
    (spawns fc_pipe, uses pipe:0 rawvideo input — same as SDI path)
  - Falls back to direct URL when FC_SLOT_ID not set (legacy path)
  - audioMap: network via framecache uses '0🅰️0?' (video-only fc_pipe,
    no audio FIFO — audio-in-shm is roadmap)
  - HLS tee: sdiHlsDir covers network-via-framecache; legacy tee gated
    on !FC_SLOT_ID to avoid duplicate HLS outputs
  - fc_pipe piped to ffmpeg stdin for network framecache path

- docker-compose.worker.yml: FC_URL + NET_INGEST_BIN in node-agent env
2026-06-03 15:37:17 +00:00
Wild Dragon Dev
b700902200 feat(framecache): phase 4 — capture-manager reads from framecache
- services/framecache/client/fc_pipe.c: new slot→stdout pipe adapter
  - Opens framecache slot as consumer (independent cursor per instance)
  - Streams raw UYVY422 frames to stdout continuously
  - SIGPIPE detection via write() return — exits cleanly on ffmpeg exit
  - SIGTERM/SIGINT clean stop from capture-manager
  - Periodic stats to stderr (every 300 frames)
  - Exit codes: 0=clean, 1=slot not found, 2=EPIPE

- services/framecache/CMakeLists.txt: add fc_pipe target + install
- services/framecache/Dockerfile: copy fc_pipe to runtime image

- services/capture/Dockerfile:
  - New fc-pipe-builder stage (builds fc_pipe from framecache sources)
  - Copies fc_pipe binary to /usr/local/bin/fc_pipe in runtime image

- services/capture/src/capture-manager.js:
  - _buildInputArgs: new framecache path for deltacast + sdi/blackmagic
    when FC_SLOT_ID env is set (injected by node-agent from bridge fmt JSON)
    - Spawns fc_pipe <slot_id> as child process
    - Uses pipe:0 as ffmpeg rawvideo input 0
    - Audio FIFO (unchanged) as ffmpeg input 1
    - Falls back to legacy FIFO path when FC_SLOT_ID unset
  - audioMap: covers blackmagic via framecache (input 1 for audio FIFO)
  - isInterlacedSource: covers blackmagic interlaced signals
  - hiresStdio: pipe stdin when bridgeProcess set (fc_pipe stdout→ffmpeg)
  - Non-growing spawn: pipes fc_pipe.stdout → ffmpeg.stdin
  - Growing orchestrator spawn: pipes fc_pipe.stdout → bash.stdin
  - sdiHlsDir: covers blackmagic source type
  - Session state stores _fcPipeProcess for clean stop
  - stop(): sends SIGTERM to fc_pipe after ffmpeg SIGINT
2026-06-03 15:32:40 +00:00
a04ef2de3a feat(promotion): implement manual growing files promotion via BullMQ queue + pending_migration status + right click Move to S3 2026-06-03 00:38:50 +00:00
600af4564e fix: restore screens-library.jsx and visuals.jsx to clean state + add deriveGrowingRaster scanHint fix 2026-06-03 00:19:55 +00:00
228f68ab6d fix(audio): use_wallclock_as_timestamps on both raw FIFOs to align A/V by arrival time (no deadlock, replaces barrier) 2026-06-02 22:02:32 +00:00