Frame-coupled VC-3/DNxHD MXF (_buildGrowingVc3Mxf) is now the only growing
master path. Delete provably-unreferenced legacy builders and their constants,
and update stale comments to match the VC-3 reality. No live-path behavior
change: the removed stop() raw2bmx branch was gated on a hard-coded false, so
the kept SIGINT path is byte-identical to the previously-executed else branch.
Removed (all confirmed unreferenced repo-wide outside this file):
- _buildGrowingOrchestrator() + JSDoc (raw2bmx bash orchestrator)
- _buildGrowingHevcMov() + JSDoc (HEVC fragmented-MOV growing)
- deriveGrowingRaster() (raw2bmx raster-flag mapper)
- growingVideoElementaryArgs() + GROWING_AVCI_CLASS
- GROWING_DEFAULT_BITRATE, GROWING_PART_INTERVAL_FRAMES,
GROWING_HEVC_DEFAULT_BITRATE, GROWING_VC3_CODECS (unused Set)
- isRaw2bmxGrowing dead branch in stop()
- stale raw2bmx/XDCAM HD422/AVC-Intra/frozen-picture comment blocks
Kept (live): _buildGrowingVc3Mxf, growingCodec, growingVc3Bitrate,
growingExtFor, GROWING_EXT, audioOffsetArgs (+AUDIO_OFFSET_MS), all
non-growing + network/SRT/RTMP/fc_pipe paths.
node --check passes.
VHD_SlotExtractAudio on the JOINED slot returned -91dB silence when only
pAudioChannels[0] was declared. On this card/SDK declaring BOTH channel[0] and
channel[1] of the stereo pair makes the SDK land real PCM. Verified -13.3dB
audible against the live SDI source (was -91dB).
fc_pipe now muxes video + that frame's embedded audio into ONE streaming AVI
container on stdout, so ffmpeg reads a SINGLE input (-f avi -i pipe:0) instead
of a raw video pipe + a separate live audio FIFO. The two-live-pipe design
deadlocked ffmpeg (it stalled forever probing input 0); a single interleaved
stream removes that failure mode entirely.
fc_pipe.c:
- New AVI mode (argv[3] == "--avi"/"avi"). Writes RIFF('AVI ') + LIST(hdrl)
{ avih + strl(vids: strh+BITMAPINFOHEADER UYVY 16bpp) + strl(auds:
strh+WAVEFORMATEX PCM s16le 48k 2ch) } + LIST(movi) once, then per ring
entry a '00dc' video chunk followed by a '01wb' audio chunk (LE sizes,
even-pad). RIFF/movi sizes use the 0x7FFFFFFF streaming sentinel (pipe is
unseekable); dwFlags has NO index bits. Frame-coupled by construction: both
chunks come from the SAME ring entry in one read-loop iteration.
- dwScale/dwRate = fps_den/fps_num (video) and nBlockAlign/nAvgBytesPerSec
(audio). If a frame has audio_size 0, emits one frame-interval of silence
(round(48000*fps_den/fps_num) samples) so the audio timeline tracks video
and ffmpeg never starves on the audio demuxer.
- Legacy raw video-only mode retained when no avi flag is given. The old
split-stdout/audio-FIFO threaded path is removed (it was the deadlock).
fc_client.{h,c}:
- Add fc_consumer_info() / fc_stream_info_t to expose the slot header's
width/height/fps/audio params to fc_pipe for the AVI header.
capture-manager.js (_buildInputArgs deltacast/sdi framecache branch):
- Spawn fc_pipe with "--avi" (no audio FIFO). Remove the mkfifo + audio-FIFO
creation for this path.
- inputArgs: ONE input -thread_queue_size 512 -f avi [AUDIO_OFFSET_MS] -i pipe:0
(was: -f rawvideo -i pipe:0 AND -f s16le -ar 48000 -ac 2 -i <fifo>).
- audioInputIndex 0, audioFifo null. Growing VC-3/HEVC builders already map
[0:v] and audioMap 0🅰️0?; with one AVI input that resolves to 0:v / 0:a.
Validated on zampp3 against the LIVE deltacast-0-0 slot: fc_pipe --avi | ffmpeg
-f avi -i pipe:0 -> dnxhd/pcm_s24le MXF gives 360 video / 360 audio packets in
6.006s (no stall at 2 frames). A synthetic 1 kHz sine slot through the same
path yields mean_volume -9 dB / max -6 dB, proving the muxer carries real audio
end-to-end (the live SDI input currently carries no embedded audio, so the
bridge's silence fallback reads -91 dB — upstream of the muxer).
Re-engineer the framecache so each video frame carries its own SDI-embedded
audio through ONE transport, eliminating the "audio ahead of video" offset at
the root: there is no longer a second independent audio buffer/FIFO that can
race ahead of video.
slot.h (FC_VERSION 1 -> 2):
- Per ring entry data region is now [video frame_size][audio FC_MAX_AUDIO_BYTES].
- fc_frame_t: the former _pad u32 is REUSED as audio_size (header still 24B).
- Header gains audio_max_bytes / audio_rate / audio_channels (self-describing).
- New fc_entry_stride()/fc_frame_audio() helpers; shm size includes audio.
- Readers/writer check version == FC_VERSION and FAIL SAFE on mismatch so an
old reader against a new writer (or vice-versa) refuses rather than misparses.
slot.c: populate audio header fields; add fc_slot_write_av(); version gate in open.
fc_client.[ch]: fc_frame_ref_t gains audio/audio_size; copy buffer holds
video+audio; both copied from the SAME entry in one read -> frame-locked.
fc_pipe.c: now <slot_id> <wait_ms> <audio_fifo_path>; per ring entry writes
video -> stdout AND that frame's audio -> the audio FIFO IN LOCKSTEP from one
cursor read (no second buffer to drift). Auto-reattaches FIFO on EPIPE.
deltacast-bridge:
- SILENT-AUDIO FIX: audio_extract_init now configures ONLY pAudioChannels[0]
of group 0 as one stereo pair (Mode=STEREO, BufferFormat=AF_16, pData=buf),
leaving pAudioChannels[1] ZEROED, exactly like Deltacast's own FFmpeg fork
(libavdevice/videomaster_common.c init_audio_info). The prior JOINED code
ALSO set channel[1].Mode/BufferFormat=STEREO/AF_16, declaring a second pair
the signal does not carry -> VHD_SlotExtractAudio returned zero samples ->
-91 dB silent audio. DataSize is (re)set to capacity before each extract.
- VHD_SDI_SP_INTERFACE now set from the channel-detected interface
(VHD_SDI_CP_INTERFACE) before StartStream, per the same fork — required for
embedded-audio extraction on JOINED SDI streams.
- fc_writer.[ch]: add fc_writer_write_av(); struct/stride bumped to v2.
- video_thread (framecache path) extracts each frame's audio from the SAME
locked JOINED slot and writes BOTH via fc_writer_write_av. Silence fallback
at the source: a frame with no embedded audio gets one frame-interval of
silence so the audio timeline length always equals the video timeline length.
- The separate audio FIFO + audio_thread + apcm ring are retained ONLY for the
legacy (-DLEGACY_FIFO / framecache-unreachable) fallback; on the primary
framecache path the bridge no longer owns the audio FIFO.
capture-manager.js: deltacast/sdi framecache branch now CREATES the audio FIFO
and passes its path to fc_pipe (argv[3]); ffmpeg keeps two raw inputs
(rawvideo pipe:0 + s16le 48k input 1) but both are now fed frame-locked from
the same ring entry. Stale-audio pre-flush retained as harmless safety.
All changes versioned; mismatched binaries refuse to attach (fail safe).
Re-architect the Deltacast bridge from two independently-buffered VHD
streams (DISJOINED_VIDEO + a separate DISJOINED_ANC audio stream) to a
single VHD_SDI_STPROC_JOINED RX stream per port. Each locked slot now
carries both the video frame and that frame's SDI-embedded audio, so
audio is extracted (VHD_SlotExtractAudio) from the SAME slot the video
came from — eliminating the constant "audio ahead of video" offset at
its root instead of papering over it with --audio-delay-ms.
This mirrors Deltacast's own FFmpeg fork (libavdevice/videomaster_common.c):
open JOINED, then per frame LockSlot -> GetSlotBuffer(VIDEO) ->
SlotExtractAudio(same slot) -> Unlock.
Changes (main.c):
- main(): RX stream opened with VHD_SDI_STPROC_JOINED (was
VHD_SDI_STPROC_DISJOINED_VIDEO). SDI interface still propagated so
audio extraction yields real samples.
- video_thread(): becomes the single per-frame consumer. After locking
each JOINED slot it extracts the embedded audio (audio_extract_slot)
and pushes de-interleaved s16le stereo PCM into a new lock-free SPSC
ring (ApcmRing), then writes the video frame to the framecache ring
(or legacy video FIFO). Audio is extracted on BOTH the framecache and
legacy-FIFO paths, before the video packing check so it is never
dropped on a rare packing mismatch.
- audio_thread(): no longer opens any VHD stream. It is now purely the
audio-FIFO sink: drains the ApcmRing into the named audio FIFO
(ffmpeg input 1), flushes the ring to the live edge on reader attach,
survives ffmpeg restarts (EPIPE -> reopen), and emits wall-clock-paced
silence when the ring is empty (preserves the silence-fallback when the
signal carries no embedded audio).
- Audio stays 48 kHz stereo s16le to match ffmpeg's expectations.
- --audio-delay-ms / FC_AUDIO_DELAY_MS retained but now unnecessary
(should be left at 0); kept for compatibility / emergency tuning.
Compiles clean (only pre-existing %lu/ULONG format warnings in main()).
Not installed, not deployed — branch only.
Adds a per-recorder audio offset (ms) control, applied as an ffmpeg -itsoffset
on the audio input: positive delays audio (fixes audio-ahead), negative
advances, 0 = none. Flows DB (migration 037) -> mam-api (RECORDER_FIELDS +
env AUDIO_OFFSET_MS + start body) -> capture.js (sets process.env per session)
-> capture-manager audioOffsetArgs() -> ffmpeg. UI: number field in the
recorder config modal. Verified end-to-end (setting 120 -> -itsoffset 0.1200 on
the live ffmpeg). Default 0, clamped +/-1000ms, non-destructive.
Note: this is an interim trim control; the root-cause A/V fix (Deltacast JOINED
single-slot embedded-audio extraction) is tracked separately.
The deltacast bridge captures audio (DISJOINED_ANC VHD stream -> FIFO) and
video (VHD stream -> framecache ring -> fc_pipe -> ffmpeg) on separate paths
with independent buffering. The video path is buffered deeper, so audio reaches
the muxer AHEAD of its matching video frame (user-confirmed: audio ahead of
video on lip-sync). --audio-delay-ms / FC_AUDIO_DELAY_MS prepends N ms of real
PCM silence to the audio stream once per reader-attach, shifting the audio
timeline N ms later to re-align. One value, all 8 ports, drift-free (ffmpeg
derives audio PTS from sample count). Default 0 = unchanged (non-destructive).
Operator tunes once against a lip-sync reference. Also fixes promotion worker
to reset orphaned processing assets on failure (was stuck forever).
The deltacast bridge captures audio and video on separate VHD streams; any
constant capture-path latency difference shows as a fixed A/V offset (e.g.
audio slightly ahead of video) even though stream lengths stay locked (no
drift, verified ~1 frame over 461s). AUDIO_OFFSET_MS applies an -itsoffset on
the SDI/Deltacast audio input only: positive DELAYS audio (audio-ahead case),
negative advances it. Default 0 = no change, fully non-destructive, clamped
to +/-1000ms. Lets an operator dial out residual offset with a lipsync loop
without a code change.
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.
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.
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).
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.
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.
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).
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.
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.
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.
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.
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.
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.
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'.
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.
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().
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.
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.
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.
The ~2.5s of leading silence at record start was the VHD audio slot QUEUE: while
the recorder is idle (no FIFO reader), the bridge blocks on open(O_WRONLY) but the
board keeps buffering audio slots. When the record ffmpeg attaches, the bridge
streamed that stale backlog first — heard as leading silence and pushing audio
out of alignment with the live video.
On each reader attach, drain slots that lock FAST (already-queued backlog) and
stop at the first lock that takes ~a frame period (= waiting on a live slot), so
the reader is handed the live edge, A/V aligned.
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.
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.
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.
- 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.
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.
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).
Taking the MAX sample count across the 4 audio groups could emit more audio
frames per slot than group 0 (the SDI-clock reference), drifting the audio
stream slightly longer than video — heard as a ~1% pitch-up. Group 0 paces the
timeline exactly as the original 2ch path did; shorter groups are silence-padded
to its length, never extending it.
- 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.