dragonflight/services/framecache/client/fc_pipe.c

406 lines
17 KiB
C
Raw Normal View History

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 11:32:40 -04:00
/**
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:a: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 10:06:35 -04:00
* fc_pipe.c Framecache slot stdout pipe adapter.
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 11:32:40 -04:00
*
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 08:46:22 -04:00
* FRAME-COUPLED AUDIO (FC_VERSION 2):
* Each framecache ring entry carries the VIDEO frame AND that frame's
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:a: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 10:06:35 -04:00
* SDI-embedded AUDIO together (written by the JOINED bridge from one slot).
* fc_pipe reads ONE entry per loop iteration.
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 08:46:22 -04:00
*
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:a: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 10:06:35 -04:00
* TWO OUTPUT MODES:
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 11:32:40 -04:00
*
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:a: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 10:06:35 -04:00
* 1) AVI MODE (default when audio is wanted; selected with --avi or by giving
* an arg of "avi"): fc_pipe writes a SINGLE streaming AVI container to
* stdout video and audio INTERLEAVED in one byte stream. ffmpeg reads it
* as ONE input:
* ffmpeg -f avi -i pipe:0 -map 0:v ... -map 0:a ...
* This eliminates the two-live-pipe deadlock: when ffmpeg was given a raw
* video pipe AND a separate audio FIFO it stalled forever probing input 0.
* The AVI muxer writes its header once, then for each ring entry emits a
* '00dc' video chunk followed by a '01wb' audio chunk frame-coupled by
* construction (both come from the same ring entry in the same iteration).
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 11:32:40 -04:00
*
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:a: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 10:06:35 -04:00
* 2) RAW MODE (legacy, video-only): if no audio FIFO / avi flag is given,
* fc_pipe writes raw UYVY422 video bytes to stdout as before.
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 08:46:22 -04:00
*
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:a: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 10:06:35 -04:00
* The old split video-stdout / audio-FIFO design is REMOVED it was the
* source of the ffmpeg deadlock.
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 08:46:22 -04:00
*
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:a: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 10:06:35 -04:00
* Usage: fc_pipe <slot_id> [wait_ms] [mode]
* mode: "--avi" | "avi" single streaming AVI (video+audio) on stdout.
* omitted | "-" raw UYVY422 video-only on stdout.
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 11:32:40 -04:00
*
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:a: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 10:06:35 -04:00
* Terminates on: SIGTERM/SIGINT, stdout EPIPE (ffmpeg exited), slot gone.
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 11:32:40 -04:00
*/
#include "../src/slot.h"
#include "fc_client.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
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:a: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 10:06:35 -04:00
#include <math.h>
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 11:32:40 -04:00
static volatile int g_stop = 0;
static void on_signal(int s) { (void)s; g_stop = 1; }
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 08:46:22 -04:00
/* Write all bytes to fd (blocking). Returns 0 on success, -1 on EPIPE/error. */
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 11:32:40 -04:00
static int write_all_fd(int fd, const void *buf, size_t len) {
const uint8_t *p = (const uint8_t *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = write(fd, p + off, len - off);
if (n > 0) { off += (size_t)n; continue; }
if (n < 0 && errno == EINTR) continue;
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:a: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 10:06:35 -04:00
return -1;
}
return 0;
}
/* ── Little-endian byte emitters into a caller buffer ────────────────────────── */
static inline void put_u16(uint8_t **pp, uint16_t v) {
uint8_t *p = *pp; p[0] = (uint8_t)(v & 0xff); p[1] = (uint8_t)((v >> 8) & 0xff); *pp = p + 2;
}
static inline void put_u32(uint8_t **pp, uint32_t v) {
uint8_t *p = *pp;
p[0] = (uint8_t)(v & 0xff); p[1] = (uint8_t)((v >> 8) & 0xff);
p[2] = (uint8_t)((v >> 16) & 0xff); p[3] = (uint8_t)((v >> 24) & 0xff);
*pp = p + 4;
}
static inline void put_fourcc(uint8_t **pp, const char *cc) {
uint8_t *p = *pp; p[0] = (uint8_t)cc[0]; p[1] = (uint8_t)cc[1];
p[2] = (uint8_t)cc[2]; p[3] = (uint8_t)cc[3]; *pp = p + 4;
}
/* ── Streaming AVI header ─────────────────────────────────────────────────────
* Builds RIFF('AVI ') + LIST('hdrl'){ avih + strl(vids) + strl(auds) } +
* LIST('movi'). For a streaming AVI over a pipe we cannot seek back to patch
* the RIFF and movi sizes, so we set them to 0x7FFFFFFF; ffmpeg's AVI demuxer
* reads the strf headers and the 00dc/01wb chunk stream regardless. The hdrl
* LIST size IS fixed/known, so it is written correctly. dwFlags is 0 we do
* NOT set AVIF_HASINDEX / AVIF_MUSTUSEINDEX (there is no index in a stream).
*
* Writes the header to *out and returns its length. Buffer must be >= 512. */
static size_t build_avi_header(uint8_t *out,
uint32_t width, uint32_t height,
uint32_t fps_num, uint32_t fps_den,
uint32_t video_bytes,
uint32_t audio_rate, uint32_t audio_channels,
uint32_t audio_sample_bytes) {
const uint32_t STREAMING = 0x7FFFFFFFu;
const uint16_t bits_per_sample = (uint16_t)(audio_sample_bytes * 8u);
const uint16_t block_align = (uint16_t)(audio_channels * audio_sample_bytes);
const uint32_t avg_bytes_sec = audio_rate * block_align;
/* dwMicroSecPerFrame = 1e6 * fps_den / fps_num */
const uint32_t usec_per_frame =
(uint32_t)((1000000.0 * (double)fps_den / (double)fps_num) + 0.5);
/* Fixed sub-sizes (data bytes only, excluding the 8-byte ckID+ckSize). */
const uint32_t AVIH_DATA = 56; /* MainAVIHeader */
const uint32_t STRH_DATA = 56; /* AVISTREAMHEADER */
const uint32_t BIH_DATA = 40; /* BITMAPINFOHEADER */
const uint32_t WFX_DATA = 18; /* WAVEFORMATEX (cbSize=0) */
/* LIST('strl') sizes = 4 (the 'strl' fourcc) + contained chunks. */
const uint32_t vstrl_size = 4 + (8 + STRH_DATA) + (8 + BIH_DATA); /* 4+64+48 = 116 */
const uint32_t astrl_size = 4 + (8 + STRH_DATA) + (8 + WFX_DATA); /* 4+64+26 = 94 */
/* LIST('hdrl') size = 4 (the 'hdrl' fourcc) + avih chunk + both strl LISTs. */
const uint32_t hdrl_size = 4 + (8 + AVIH_DATA) + (8 + vstrl_size) + (8 + astrl_size);
uint8_t *p = out;
/* RIFF 'AVI ' (size unseekable → streaming sentinel) */
put_fourcc(&p, "RIFF");
put_u32(&p, STREAMING);
put_fourcc(&p, "AVI ");
/* LIST 'hdrl' */
put_fourcc(&p, "LIST");
put_u32(&p, hdrl_size);
put_fourcc(&p, "hdrl");
/* avih — MainAVIHeader (56 bytes) */
put_fourcc(&p, "avih");
put_u32(&p, AVIH_DATA);
put_u32(&p, usec_per_frame); /* dwMicroSecPerFrame */
put_u32(&p, 0); /* dwMaxBytesPerSec */
put_u32(&p, 0); /* dwPaddingGranularity */
put_u32(&p, 0); /* dwFlags — NO index flags */
put_u32(&p, 0); /* dwTotalFrames (unknown in stream) */
put_u32(&p, 0); /* dwInitialFrames */
put_u32(&p, 2); /* dwStreams (video + audio) */
put_u32(&p, 0); /* dwSuggestedBufferSize */
put_u32(&p, width); /* dwWidth */
put_u32(&p, height); /* dwHeight */
put_u32(&p, 0); put_u32(&p, 0); put_u32(&p, 0); put_u32(&p, 0); /* dwReserved[4] */
/* LIST 'strl' — VIDEO */
put_fourcc(&p, "LIST");
put_u32(&p, vstrl_size);
put_fourcc(&p, "strl");
/* strh — AVISTREAMHEADER 'vids' (56 bytes) */
put_fourcc(&p, "strh");
put_u32(&p, STRH_DATA);
put_fourcc(&p, "vids"); /* fccType */
put_fourcc(&p, "UYVY"); /* fccHandler */
put_u32(&p, 0); /* dwFlags */
put_u16(&p, 0); /* wPriority */
put_u16(&p, 0); /* wLanguage */
put_u32(&p, 0); /* dwInitialFrames */
put_u32(&p, fps_den); /* dwScale = 1001 */
put_u32(&p, fps_num); /* dwRate = 60000 */
put_u32(&p, 0); /* dwStart */
put_u32(&p, 0); /* dwLength (unknown) */
put_u32(&p, video_bytes); /* dwSuggestedBufferSize */
put_u32(&p, 0xFFFFFFFFu); /* dwQuality (-1 default) */
put_u32(&p, video_bytes); /* dwSampleSize (fixed for uncompressed) */
put_u16(&p, 0); put_u16(&p, 0); /* rcFrame.left, top */
put_u16(&p, (uint16_t)width); /* rcFrame.right */
put_u16(&p, (uint16_t)height); /* rcFrame.bottom */
/* strf — BITMAPINFOHEADER (40 bytes) */
put_fourcc(&p, "strf");
put_u32(&p, BIH_DATA);
put_u32(&p, 40); /* biSize */
put_u32(&p, width); /* biWidth */
put_u32(&p, height); /* biHeight */
put_u16(&p, 1); /* biPlanes */
put_u16(&p, 16); /* biBitCount (UYVY422 = 16bpp) */
put_fourcc(&p, "UYVY"); /* biCompression fourcc */
put_u32(&p, video_bytes); /* biSizeImage = W*H*2 */
put_u32(&p, 0); /* biXPelsPerMeter */
put_u32(&p, 0); /* biYPelsPerMeter */
put_u32(&p, 0); /* biClrUsed */
put_u32(&p, 0); /* biClrImportant */
/* LIST 'strl' — AUDIO */
put_fourcc(&p, "LIST");
put_u32(&p, astrl_size);
put_fourcc(&p, "strl");
/* strh — AVISTREAMHEADER 'auds' (56 bytes) */
put_fourcc(&p, "strh");
put_u32(&p, STRH_DATA);
put_fourcc(&p, "auds"); /* fccType */
put_u32(&p, 0); /* fccHandler (none for PCM) */
put_u32(&p, 0); /* dwFlags */
put_u16(&p, 0); /* wPriority */
put_u16(&p, 0); /* wLanguage */
put_u32(&p, 0); /* dwInitialFrames */
put_u32(&p, block_align); /* dwScale = nBlockAlign */
put_u32(&p, avg_bytes_sec); /* dwRate = nAvgBytesPerSec */
put_u32(&p, 0); /* dwStart */
put_u32(&p, 0); /* dwLength (unknown) */
put_u32(&p, avg_bytes_sec); /* dwSuggestedBufferSize (~1s) */
put_u32(&p, 0xFFFFFFFFu); /* dwQuality */
put_u32(&p, block_align); /* dwSampleSize = nBlockAlign */
put_u16(&p, 0); put_u16(&p, 0); put_u16(&p, 0); put_u16(&p, 0); /* rcFrame */
/* strf — WAVEFORMATEX (18 bytes) */
put_fourcc(&p, "strf");
put_u32(&p, WFX_DATA);
put_u16(&p, 1); /* wFormatTag = WAVE_FORMAT_PCM */
put_u16(&p, (uint16_t)audio_channels); /* nChannels */
put_u32(&p, audio_rate); /* nSamplesPerSec */
put_u32(&p, avg_bytes_sec); /* nAvgBytesPerSec */
put_u16(&p, block_align); /* nBlockAlign */
put_u16(&p, bits_per_sample); /* wBitsPerSample */
put_u16(&p, 0); /* cbSize */
/* LIST 'movi' — frames follow. Size unseekable → streaming sentinel. */
put_fourcc(&p, "LIST");
put_u32(&p, STREAMING);
put_fourcc(&p, "movi");
return (size_t)(p - out);
}
/* Write a single AVI chunk: 4-byte fourcc + u32 LE size + data (+ pad byte if
* the size is odd, per the RIFF even-alignment rule). Returns 0 / -1. */
static int write_avi_chunk(int fd, const char *cc,
const uint8_t *data, uint32_t size) {
uint8_t hdr[8];
uint8_t *p = hdr;
put_fourcc(&p, cc);
put_u32(&p, size);
if (write_all_fd(fd, hdr, 8) < 0) return -1;
if (size && write_all_fd(fd, data, size) < 0) return -1;
if (size & 1u) {
uint8_t pad = 0;
if (write_all_fd(fd, &pad, 1) < 0) return -1;
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 11:32:40 -04:00
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
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:a: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 10:06:35 -04:00
fprintf(stderr, "Usage: %s <slot_id> [wait_ms] [--avi|-]\n", argv[0]);
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 11:32:40 -04:00
return 1;
}
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:a: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 10:06:35 -04:00
const char *slot_id = argv[1];
uint64_t wait_ms = argc >= 3 ? (uint64_t)atoll(argv[2]) : 30000;
/* AVI mode is selected by an explicit flag in argv[3]. Anything that is not
* "--avi"/"avi" (including "-" or omitted) legacy raw video-only mode. */
int avi_mode = 0;
if (argc >= 4) {
const char *m = argv[3];
if (strcmp(m, "--avi") == 0 || strcmp(m, "avi") == 0) avi_mode = 1;
}
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 11:32:40 -04:00
signal(SIGTERM, on_signal);
signal(SIGINT, on_signal);
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:a: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 10:06:35 -04:00
signal(SIGPIPE, SIG_IGN);
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 11:32:40 -04:00
fcntl(STDOUT_FILENO, F_SETFL,
fcntl(STDOUT_FILENO, F_GETFL, 0) & ~O_NONBLOCK);
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:a: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 10:06:35 -04:00
fprintf(stderr, "[fc_pipe] opening slot '%s' (wait %llums) mode=%s\n",
slot_id, (unsigned long long)wait_ms, avi_mode ? "avi" : "rawvideo");
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 11:32:40 -04:00
fc_consumer_t *c = fc_consumer_open(slot_id, wait_ms);
if (!c) {
fprintf(stderr, "[fc_pipe] slot '%s' not found within %llums\n",
slot_id, (unsigned long long)wait_ms);
return 1;
}
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:a: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 10:06:35 -04:00
/* Pull stream format from the slot header for the AVI header. */
fc_stream_info_t si;
if (fc_consumer_info(c, &si) != 0 || si.width == 0 || si.height == 0) {
fprintf(stderr, "[fc_pipe] failed to read slot stream info\n");
fc_consumer_close(c);
return 1;
}
if (si.fps_num == 0) { si.fps_num = 60000; si.fps_den = 1001; }
if (si.fps_den == 0) si.fps_den = 1;
if (si.audio_rate == 0) si.audio_rate = 48000;
if (si.audio_channels == 0) si.audio_channels = 2;
if (si.audio_sample_bytes == 0) si.audio_sample_bytes = 2;
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 11:32:40 -04:00
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:a: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 10:06:35 -04:00
const uint32_t video_bytes = si.frame_size ? si.frame_size
: si.width * si.height * 2u;
const uint32_t a_blockalign = si.audio_channels * si.audio_sample_bytes;
/* Samples per video frame for synthesized silence when a frame has no audio:
* round(audio_rate * fps_den / fps_num). Bytes = samples * blockalign. */
uint32_t silence_bytes = 0;
{
double spf = (double)si.audio_rate * (double)si.fps_den / (double)si.fps_num;
uint32_t samples = (uint32_t)(spf + 0.5);
silence_bytes = samples * a_blockalign;
}
uint8_t *silence = NULL;
if (avi_mode && silence_bytes) {
silence = (uint8_t *)calloc(1, silence_bytes);
if (!silence) silence_bytes = 0;
}
if (avi_mode) {
uint8_t hdr[512];
size_t hlen = build_avi_header(hdr, si.width, si.height,
si.fps_num, si.fps_den, video_bytes,
si.audio_rate, si.audio_channels,
si.audio_sample_bytes);
if (write_all_fd(STDOUT_FILENO, hdr, hlen) < 0) {
fprintf(stderr, "[fc_pipe] stdout EPIPE writing AVI header\n");
fc_consumer_close(c); free(silence);
return 1;
}
fprintf(stderr,
"[fc_pipe] slot open, streaming AVI(video+audio) → stdout "
"(%ux%u %u/%u, %ub/frame, audio %uHz %uch s%ule, silence=%uB/frame)\n",
si.width, si.height, si.fps_num, si.fps_den, video_bytes,
si.audio_rate, si.audio_channels, si.audio_sample_bytes * 8u,
silence_bytes);
} else {
fprintf(stderr, "[fc_pipe] slot open, streaming raw video → stdout (%ux%u)\n",
si.width, si.height);
}
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 08:46:22 -04:00
uint64_t frames_out = 0;
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 11:32:40 -04:00
uint64_t total_dropped = 0;
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 08:46:22 -04:00
uint64_t audio_bytes = 0;
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:a: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 10:06:35 -04:00
uint64_t audio_gaps = 0;
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 11:32:40 -04:00
while (!g_stop) {
fc_frame_ref_t ref;
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:a: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 10:06:35 -04:00
int rc = fc_consumer_read(c, &ref, 2000);
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 11:32:40 -04:00
if (rc == FC_TIMEOUT) continue;
if (rc == FC_ERROR) break;
if (rc == FC_LAPPED) {
total_dropped = fc_consumer_dropped(c);
fprintf(stderr, "[fc_pipe] WARNING: frame lapped mid-read — total dropped: %llu\n",
(unsigned long long)total_dropped);
continue;
}
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 11:32:40 -04:00
if (rc == FC_DROPPED) {
total_dropped = fc_consumer_dropped(c);
fprintf(stderr, "[fc_pipe] WARNING: consumer fell behind — total dropped: %llu\n",
(unsigned long long)total_dropped);
}
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:a: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 10:06:35 -04:00
if (avi_mode) {
/* Interleave THIS frame's video + audio in one stream. Both are
* sourced from the SAME ring entry frame-coupled by construction.
* Video first (00dc), then audio (01wb). */
if (write_avi_chunk(STDOUT_FILENO, "00dc", ref.data, ref.size) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE (video) — ffmpeg exited\n");
break;
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 08:46:22 -04:00
}
if (ref.audio_size > 0 && ref.audio) {
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:a: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 10:06:35 -04:00
if (write_avi_chunk(STDOUT_FILENO, "01wb", ref.audio, ref.audio_size) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE (audio) — ffmpeg exited\n");
break;
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 08:46:22 -04:00
}
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:a: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 10:06:35 -04:00
audio_bytes += ref.audio_size;
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 08:46:22 -04:00
} else {
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:a: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 10:06:35 -04:00
/* No embedded audio this frame: emit one frame-interval of
* silence so the audio stream length tracks the video and
* ffmpeg never starves on the audio demuxer. */
if (silence_bytes &&
write_avi_chunk(STDOUT_FILENO, "01wb", silence, silence_bytes) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE (silence) — ffmpeg exited\n");
break;
}
audio_bytes += silence_bytes;
audio_gaps++;
}
} else {
/* Legacy raw video-only: write the UYVY422 bytes straight to stdout. */
if (write_all_fd(STDOUT_FILENO, ref.data, ref.size) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE — ffmpeg exited\n");
break;
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 08:46:22 -04:00
}
}
frames_out++;
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 11:32:40 -04:00
if (frames_out % 300 == 0) {
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 08:46:22 -04:00
fprintf(stderr, "[fc_pipe] frames=%llu dropped=%llu audio_bytes=%llu gaps=%llu\n",
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 11:32:40 -04:00
(unsigned long long)frames_out,
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 08:46:22 -04:00
(unsigned long long)total_dropped,
(unsigned long long)audio_bytes,
(unsigned long long)audio_gaps);
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 11:32:40 -04:00
}
}
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:a: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 10:06:35 -04:00
free(silence);
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 11:32:40 -04:00
fc_consumer_close(c);
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 08:46:22 -04:00
fprintf(stderr, "[fc_pipe] done frames=%llu dropped=%llu audio_bytes=%llu gaps=%llu\n",
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 11:32:40 -04:00
(unsigned long long)frames_out,
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 08:46:22 -04:00
(unsigned long long)total_dropped,
(unsigned long long)audio_bytes,
(unsigned long long)audio_gaps);
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 11:32:40 -04:00
return 0;
}