Commit graph

4 commits

Author SHA1 Message Date
Zac Gaetano
97267aa857 fix(framecache): single-input streaming AVI muxer in fc_pipe (kills 2-pipe ffmpeg deadlock)
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).
2026-06-05 14:06:35 +00:00
2f37119379 fix(framecache): frame-coupled audio — video+audio in ONE ring entry
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).
2026-06-05 12:46:22 +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
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