feat(framecache): phase 5 — network ingest (RTMP/SRT) via framecache
- services/framecache/src/net_ingest.c: new network ingest process
- Spawns ffmpeg to decode SRT/RTMP → raw UYVY422 stdout
- Reads decoded frames and writes into framecache slot via shm
- Registers slot with framecache HTTP API on startup
- Deregisters slot on clean exit (SIGTERM)
- Reconnect loop for listener mode (stays alive between sessions)
- --url, --slot-id, --fc-url, --width, --height, --fps-num/den,
--source-type, --listen, --listen-port, --stream-key args
- Emits format JSON to stderr on first frame
- services/framecache/CMakeLists.txt: add net_ingest target
- services/framecache/Dockerfile: copy net_ingest to runtime image
- services/node-agent/index.js:
- startNetIngest() / stopNetIngest(): lifecycle management per recorder
- Spawns net_ingest before sidecar start for srt/rtmp sourceTypes
- Injects FC_SLOT_ID=net-<containerId> into sidecar env
- Sets IpcMode=host for network sidecars using framecache
- Maps temp id → real containerId after container create
- stopNetIngest() called on sidecar stop
- NET_INGEST_BIN env var (default: docker exec framecache net_ingest)
- services/capture/src/capture-manager.js:
- _buildInputArgs srt/rtmp: framecache path when FC_SLOT_ID set
(spawns fc_pipe, uses pipe:0 rawvideo input — same as SDI path)
- Falls back to direct URL when FC_SLOT_ID not set (legacy path)
- audioMap: network via framecache uses '0🅰️0?' (video-only fc_pipe,
no audio FIFO — audio-in-shm is roadmap)
- HLS tee: sdiHlsDir covers network-via-framecache; legacy tee gated
on !FC_SLOT_ID to avoid duplicate HLS outputs
- fc_pipe piped to ffmpeg stdin for network framecache path
- docker-compose.worker.yml: FC_URL + NET_INGEST_BIN in node-agent env
This commit is contained in:
parent
b700902200
commit
99723da00f
6 changed files with 593 additions and 15 deletions
|
|
@ -60,6 +60,12 @@ services:
|
||||||
BMD_MODEL: ${BMD_MODEL:-}
|
BMD_MODEL: ${BMD_MODEL:-}
|
||||||
BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv}
|
BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv}
|
||||||
LIVE_DIR: ${LIVE_DIR:-/mnt/NVME/MAM/wild-dragon-live}
|
LIVE_DIR: ${LIVE_DIR:-/mnt/NVME/MAM/wild-dragon-live}
|
||||||
|
# Framecache service URL (on the wild-dragon-worker network)
|
||||||
|
FC_URL: ${FC_URL:-http://framecache:7435}
|
||||||
|
# net_ingest binary — runs inside the framecache container via docker exec.
|
||||||
|
# node-agent has docker.sock so it can exec into the framecache container.
|
||||||
|
# Override with a host-installed path if preferred.
|
||||||
|
NET_INGEST_BIN: ${NET_INGEST_BIN:-docker exec framecache net_ingest}
|
||||||
# REPO_DIR: host path to the checked-out repo. The agent passes this to the
|
# REPO_DIR: host path to the checked-out repo. The agent passes this to the
|
||||||
# one-shot driver-install container so install-driver.sh can read
|
# one-shot driver-install container so install-driver.sh can read
|
||||||
# sdk/<vendor>/ and run deploy/install-driver.sh. Must match the host path
|
# sdk/<vendor>/ and run deploy/install-driver.sh. Must match the host path
|
||||||
|
|
|
||||||
|
|
@ -521,6 +521,49 @@ class CaptureManager {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _buildInputArgs({ sourceType, sourceBackend = 'blackmagic', device, port, board, sourceUrl, listen, listenPort, streamKey }) {
|
async _buildInputArgs({ sourceType, sourceBackend = 'blackmagic', device, port, board, sourceUrl, listen, listenPort, streamKey }) {
|
||||||
|
// ── Network sources via framecache (primary when FC_SLOT_ID is set) ──────
|
||||||
|
// node-agent starts net_ingest before the sidecar, which decodes the stream
|
||||||
|
// to raw UYVY422 and registers a framecache slot. We read from that slot via
|
||||||
|
// fc_pipe — same zero-copy path as SDI sources — enabling simultaneous
|
||||||
|
// growing + proxy + HLS from any network source.
|
||||||
|
if ((sourceType === 'srt' || sourceType === 'rtmp') && process.env.FC_SLOT_ID) {
|
||||||
|
const slotId = process.env.FC_SLOT_ID;
|
||||||
|
const fcPipeBin = process.env.FC_PIPE_BIN || 'fc_pipe';
|
||||||
|
const WAIT_MS = 60_000; /* network sources may take longer to connect */
|
||||||
|
|
||||||
|
const fcSize = process.env.DELTACAST_VIDEO_SIZE || '1920x1080';
|
||||||
|
const fcFps = process.env.DELTACAST_FRAMERATE || '30000/1001';
|
||||||
|
|
||||||
|
console.log(`[framecache] net slot=${slotId} size=${fcSize} fps=${fcFps}`);
|
||||||
|
|
||||||
|
const fcPipeProcess = spawn(fcPipeBin, [slotId, String(WAIT_MS)], {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
fcPipeProcess.stderr.on('data', chunk => {
|
||||||
|
process.stderr.write(`[fc_pipe:${slotId}] ${chunk}`);
|
||||||
|
});
|
||||||
|
fcPipeProcess.on('error', err =>
|
||||||
|
console.error(`[fc_pipe:${slotId}] spawn error: ${err.message}`));
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputArgs: [
|
||||||
|
'-use_wallclock_as_timestamps', '1',
|
||||||
|
'-thread_queue_size', '512',
|
||||||
|
'-f', 'rawvideo',
|
||||||
|
'-pix_fmt', 'uyvy422',
|
||||||
|
'-video_size', fcSize,
|
||||||
|
'-framerate', fcFps,
|
||||||
|
'-i', 'pipe:0',
|
||||||
|
],
|
||||||
|
isNetwork: false, /* treat as raw source — no -map 0:v:0? needed */
|
||||||
|
bridgeProcess: fcPipeProcess,
|
||||||
|
audioFifo: null,
|
||||||
|
interlaced: false,
|
||||||
|
_fcPipeProcess: fcPipeProcess,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Legacy direct network paths (no framecache / net_ingest not running) ──
|
||||||
if (sourceType === 'srt') {
|
if (sourceType === 'srt') {
|
||||||
let url;
|
let url;
|
||||||
if (listen) {
|
if (listen) {
|
||||||
|
|
@ -998,13 +1041,16 @@ exit "$BMXRC"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Audio input index:
|
// Audio input index:
|
||||||
// - deltacast + blackmagic via framecache (fc_pipe): video on input 0
|
// - deltacast via framecache: video pipe:0, audio FIFO input 1 → '1:a:0?'
|
||||||
// (pipe:0 from fc_pipe), audio FIFO on input 1 → audioMap = '1:a:0?'
|
// - blackmagic via framecache: same → '1:a:0?'
|
||||||
// - DeckLink legacy (ffmpeg -f decklink): audio embedded in input 0
|
// - network via framecache (fc_pipe): video-only pipe, no audio input → '0:a:0?'
|
||||||
// - network sources: audio in input 0
|
// (net_ingest only writes video; audio from network stream not yet in shm)
|
||||||
const audioMap = (sourceType === 'deltacast' ||
|
// - DeckLink legacy / network legacy: audio in input 0 → '0:a:0?'
|
||||||
((sourceType === 'sdi' || sourceType === 'blackmagic') && process.env.FC_SLOT_ID))
|
const _viaFcPipe = !!process.env.FC_SLOT_ID;
|
||||||
? '1:a:0?' : '0:a:0?';
|
const _hasAudioFifo = _viaFcPipe && (sourceType === 'deltacast' ||
|
||||||
|
sourceType === 'sdi' ||
|
||||||
|
sourceType === 'blackmagic');
|
||||||
|
const audioMap = _hasAudioFifo ? '1:a:0?' : '0:a:0?';
|
||||||
|
|
||||||
// Non-growing master: ffmpeg muxes the finalized MOV directly. Growing
|
// Non-growing master: ffmpeg muxes the finalized MOV directly. Growing
|
||||||
// master: raw2bmx muxes the OP1a from elementary FIFOs (handled below via
|
// master: raw2bmx muxes the OP1a from elementary FIFOs (handled below via
|
||||||
|
|
@ -1051,10 +1097,12 @@ exit "$BMXRC"
|
||||||
// stdin (pipe:0 input). For all other sources stdin is ignored.
|
// stdin (pipe:0 input). For all other sources stdin is ignored.
|
||||||
const hiresStdio = bridgeProcess ? ['pipe', 'ignore', 'pipe'] : ['ignore', 'ignore', 'pipe'];
|
const hiresStdio = bridgeProcess ? ['pipe', 'ignore', 'pipe'] : ['ignore', 'ignore', 'pipe'];
|
||||||
|
|
||||||
// For SDI/framecache sources the live HLS preview is a SECOND OUTPUT of
|
// For SDI/framecache sources (including network via framecache) the live
|
||||||
// the hires ffmpeg (one read → split → [master] + [HLS preview]).
|
// HLS preview is a SECOND OUTPUT of the hires ffmpeg.
|
||||||
|
const _viaFcPipeHls = !!process.env.FC_SLOT_ID;
|
||||||
let sdiHlsDir = null;
|
let sdiHlsDir = null;
|
||||||
if ((sourceType === 'sdi' || sourceType === 'deltacast' || sourceType === 'blackmagic')
|
if ((sourceType === 'sdi' || sourceType === 'deltacast' || sourceType === 'blackmagic'
|
||||||
|
|| (_viaFcPipeHls && (sourceType === 'srt' || sourceType === 'rtmp')))
|
||||||
&& this._assetIdForHls) {
|
&& this._assetIdForHls) {
|
||||||
const fsMod = await import('node:fs');
|
const fsMod = await import('node:fs');
|
||||||
sdiHlsDir = '/live/' + this._assetIdForHls;
|
sdiHlsDir = '/live/' + this._assetIdForHls;
|
||||||
|
|
@ -1130,7 +1178,6 @@ exit "$BMXRC"
|
||||||
hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio });
|
hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio });
|
||||||
|
|
||||||
// When video comes from fc_pipe, pipe its stdout to ffmpeg stdin.
|
// When video comes from fc_pipe, pipe its stdout to ffmpeg stdin.
|
||||||
// fc_pipe writes raw UYVY422 frames; ffmpeg reads them as rawvideo pipe:0.
|
|
||||||
if (bridgeProcess && bridgeProcess.stdout && hiresProcess.stdin) {
|
if (bridgeProcess && bridgeProcess.stdout && hiresProcess.stdin) {
|
||||||
bridgeProcess.stdout.pipe(hiresProcess.stdin);
|
bridgeProcess.stdout.pipe(hiresProcess.stdin);
|
||||||
bridgeProcess.on('exit', () => {
|
bridgeProcess.on('exit', () => {
|
||||||
|
|
@ -1147,10 +1194,13 @@ exit "$BMXRC"
|
||||||
const processes = { hires: hiresProcess };
|
const processes = { hires: hiresProcess };
|
||||||
const uploads = { hires: growingPath ? Promise.resolve({ growingPath }) : null };
|
const uploads = { hires: growingPath ? Promise.resolve({ growingPath }) : null };
|
||||||
|
|
||||||
// ── HLS tee for network sources (live preview in the UI) ──────────
|
// ── HLS tee for legacy network sources (live preview in the UI) ──────────
|
||||||
|
// When network sources come via framecache (FC_SLOT_ID set), HLS preview is
|
||||||
|
// handled as a 2nd ffmpeg output in the hires process above (sdiHlsDir path).
|
||||||
|
// This tee is only for the legacy direct-URL network path (no framecache).
|
||||||
let hlsProcess = null;
|
let hlsProcess = null;
|
||||||
let hlsDir = null;
|
let hlsDir = null;
|
||||||
if (isNetwork && this._assetIdForHls) {
|
if (isNetwork && !process.env.FC_SLOT_ID && this._assetIdForHls) {
|
||||||
try {
|
try {
|
||||||
const fs = await import('node:fs');
|
const fs = await import('node:fs');
|
||||||
hlsDir = '/live/' + this._assetIdForHls;
|
hlsDir = '/live/' + this._assetIdForHls;
|
||||||
|
|
@ -1158,7 +1208,6 @@ exit "$BMXRC"
|
||||||
const hlsArgs = [
|
const hlsArgs = [
|
||||||
...inputArgs,
|
...inputArgs,
|
||||||
'-map', '0:v:0?', '-map', '0:a:0?',
|
'-map', '0:v:0?', '-map', '0:a:0?',
|
||||||
// GPU-gated preview encode, same as the SDI 2nd-output path (#164).
|
|
||||||
...buildHlsVideoArgs(videoCodec, framerate),
|
...buildHlsVideoArgs(videoCodec, framerate),
|
||||||
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
||||||
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
||||||
|
|
@ -1170,7 +1219,7 @@ exit "$BMXRC"
|
||||||
hlsProcess.stderr.on('data', (d) => { console.error('[HLS] ' + d); });
|
hlsProcess.stderr.on('data', (d) => { console.error('[HLS] ' + d); });
|
||||||
hlsProcess.on('exit', (c) => console.log('[HLS] exited ' + c));
|
hlsProcess.on('exit', (c) => console.log('[HLS] exited ' + c));
|
||||||
processes.hls = hlsProcess;
|
processes.hls = hlsProcess;
|
||||||
console.log('[HLS] tee started -> ' + hlsDir);
|
console.log('[HLS] legacy-net tee started -> ' + hlsDir);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[HLS] tee failed:', err.message);
|
console.error('[HLS] tee failed:', err.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,19 @@ add_library(fc_client STATIC
|
||||||
target_include_directories(fc_client PUBLIC src client)
|
target_include_directories(fc_client PUBLIC src client)
|
||||||
target_link_libraries(fc_client rt pthread)
|
target_link_libraries(fc_client rt pthread)
|
||||||
|
|
||||||
|
# ── net_ingest — network source (RTMP/SRT) → framecache slot ─────────
|
||||||
|
# Spawned by node-agent when a network recorder starts.
|
||||||
|
# Decodes the network stream to raw UYVY422 via ffmpeg and writes frames
|
||||||
|
# into a framecache slot, giving capture-manager the same fc_pipe consumer
|
||||||
|
# interface as SDI sources.
|
||||||
|
add_executable(net_ingest
|
||||||
|
src/net_ingest.c
|
||||||
|
src/slot.c
|
||||||
|
)
|
||||||
|
target_include_directories(net_ingest PRIVATE src)
|
||||||
|
target_link_libraries(net_ingest rt pthread)
|
||||||
|
install(TARGETS net_ingest DESTINATION bin)
|
||||||
|
|
||||||
# ── fc_pipe — slot → stdout adapter (used by capture-manager.js) ─────
|
# ── fc_pipe — slot → stdout adapter (used by capture-manager.js) ─────
|
||||||
# Spawned by capture-manager as a child process; writes raw UYVY422
|
# Spawned by capture-manager as a child process; writes raw UYVY422
|
||||||
# frames from a framecache slot to stdout so ffmpeg reads them as
|
# frames from a framecache slot to stdout so ffmpeg reads them as
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
|
||||||
COPY --from=builder /build/framecache /usr/local/bin/framecache
|
COPY --from=builder /build/framecache /usr/local/bin/framecache
|
||||||
COPY --from=builder /build/fc_pipe /usr/local/bin/fc_pipe
|
COPY --from=builder /build/fc_pipe /usr/local/bin/fc_pipe
|
||||||
|
COPY --from=builder /build/net_ingest /usr/local/bin/net_ingest
|
||||||
COPY --from=builder /build/fc_test_consumer /usr/local/bin/fc_test_consumer 2>/dev/null || true
|
COPY --from=builder /build/fc_test_consumer /usr/local/bin/fc_test_consumer 2>/dev/null || true
|
||||||
|
|
||||||
# /dev/shm/framecache is created at runtime (tmpfs)
|
# /dev/shm/framecache is created at runtime (tmpfs)
|
||||||
|
|
|
||||||
402
services/framecache/src/net_ingest.c
Normal file
402
services/framecache/src/net_ingest.c
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
/**
|
||||||
|
* net_ingest.c — Network source (RTMP/SRT) → framecache slot ingest.
|
||||||
|
*
|
||||||
|
* Spawns ffmpeg to decode a network stream to raw UYVY422 on stdout, then
|
||||||
|
* reads those frames and writes them into a framecache slot via the shm
|
||||||
|
* ring buffer. Registers the slot with the framecache HTTP API on startup
|
||||||
|
* and deregisters on clean exit.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* net_ingest --url <srt://...|rtmp://...>
|
||||||
|
* --slot-id <recorder-uuid>
|
||||||
|
* --fc-url http://framecache:7435
|
||||||
|
* --width <W> --height <H>
|
||||||
|
* --fps-num <N> --fps-den <D>
|
||||||
|
* [--source-type srt|rtmp]
|
||||||
|
* [--listen] # SRT/RTMP listener mode
|
||||||
|
* [--listen-port <N>] # listener port (SRT default 9000, RTMP 1935)
|
||||||
|
* [--stream-key <k>] # RTMP stream key (default "stream")
|
||||||
|
*
|
||||||
|
* Emits one JSON line to stderr on first frame:
|
||||||
|
* {"slot_id":"<id>","width":W,"height":H,"fps_num":N,"fps_den":D,
|
||||||
|
* "source_type":"srt","pix_fmt":"uyvy422"}
|
||||||
|
*
|
||||||
|
* Exits 0 on clean stop (SIGTERM), 1 on error.
|
||||||
|
*
|
||||||
|
* The framecache slot stays alive between ffmpeg reconnects (listener mode):
|
||||||
|
* net_ingest keeps the slot open and restarts ffmpeg on disconnect.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "slot.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
|
||||||
|
/* Re-use fc_writer helpers inline (no external dep) */
|
||||||
|
#define FC_URL_DEFAULT "http://localhost:7435"
|
||||||
|
|
||||||
|
static volatile int g_stop = 0;
|
||||||
|
static void on_signal(int s) { (void)s; g_stop = 1; }
|
||||||
|
|
||||||
|
/* ── Tiny HTTP POST/DELETE (same approach as fc_writer.c) ─────────── */
|
||||||
|
static int http_req(const char *method, const char *host, int port,
|
||||||
|
const char *path, const char *body,
|
||||||
|
char *resp, size_t resp_len)
|
||||||
|
{
|
||||||
|
struct sockaddr_in sa;
|
||||||
|
memset(&sa, 0, sizeof sa);
|
||||||
|
sa.sin_family = AF_INET;
|
||||||
|
sa.sin_port = htons((uint16_t)port);
|
||||||
|
struct hostent *he = gethostbyname(host);
|
||||||
|
if (!he) return -1;
|
||||||
|
memcpy(&sa.sin_addr, he->h_addr_list[0], (size_t)he->h_length);
|
||||||
|
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
struct timeval tv = { .tv_sec = 5 };
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||||
|
if (connect(fd, (struct sockaddr *)&sa, sizeof sa) < 0) { close(fd); return -1; }
|
||||||
|
|
||||||
|
char req[4096];
|
||||||
|
int rlen;
|
||||||
|
if (body)
|
||||||
|
rlen = snprintf(req, sizeof req,
|
||||||
|
"%s %s HTTP/1.0\r\nHost: %s:%d\r\n"
|
||||||
|
"Content-Type: application/json\r\nContent-Length: %zu\r\n"
|
||||||
|
"Connection: close\r\n\r\n%s",
|
||||||
|
method, path, host, port, strlen(body), body);
|
||||||
|
else
|
||||||
|
rlen = snprintf(req, sizeof req,
|
||||||
|
"%s %s HTTP/1.0\r\nHost: %s:%d\r\nConnection: close\r\n\r\n",
|
||||||
|
method, path, host, port);
|
||||||
|
|
||||||
|
send(fd, req, (size_t)rlen, 0);
|
||||||
|
|
||||||
|
int status = -1;
|
||||||
|
size_t got = 0;
|
||||||
|
char buf[8192];
|
||||||
|
ssize_t n;
|
||||||
|
while ((n = recv(fd, buf + got, sizeof buf - got - 1, 0)) > 0) got += (size_t)n;
|
||||||
|
buf[got] = '\0';
|
||||||
|
sscanf(buf, "HTTP/%*s %d", &status);
|
||||||
|
if (resp && resp_len) {
|
||||||
|
const char *b = strstr(buf, "\r\n\r\n");
|
||||||
|
if (b) { strncpy(resp, b + 4, resp_len - 1); resp[resp_len-1] = '\0'; }
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_url(const char *url, char *host, size_t hl, int *port) {
|
||||||
|
const char *p = url;
|
||||||
|
if (!strncmp(p, "http://", 7)) p += 7;
|
||||||
|
*port = 7435;
|
||||||
|
const char *colon = strchr(p, ':');
|
||||||
|
if (colon) {
|
||||||
|
size_t n = (size_t)(colon - p) < hl ? (size_t)(colon - p) : hl - 1;
|
||||||
|
strncpy(host, p, n); host[n] = '\0';
|
||||||
|
*port = atoi(colon + 1);
|
||||||
|
} else { strncpy(host, p, hl - 1); host[hl-1] = '\0'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int json_str(const char *j, const char *k, char *out, size_t len) {
|
||||||
|
char pat[128]; snprintf(pat, sizeof pat, "\"%s\":", k);
|
||||||
|
const char *p = strstr(j, pat); if (!p) return -1;
|
||||||
|
p += strlen(pat); while (*p == ' ') p++;
|
||||||
|
if (*p != '"') return -1; p++;
|
||||||
|
size_t i = 0;
|
||||||
|
while (*p && *p != '"' && i < len - 1) out[i++] = *p++;
|
||||||
|
out[i] = '\0'; return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Frame size helpers ────────────────────────────────────────────── */
|
||||||
|
static inline size_t frame_bytes(uint32_t w, uint32_t h) {
|
||||||
|
return (size_t)w * h * 2; /* UYVY422 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Register slot with framecache ────────────────────────────────── */
|
||||||
|
static int register_slot(const char *fc_url, const char *slot_id,
|
||||||
|
uint32_t w, uint32_t h,
|
||||||
|
uint32_t fps_num, uint32_t fps_den,
|
||||||
|
const char *source_type,
|
||||||
|
char *shm_path, size_t sp_len,
|
||||||
|
char *sem_name, size_t sn_len)
|
||||||
|
{
|
||||||
|
char host[128]; int port;
|
||||||
|
parse_url(fc_url, host, sizeof host, &port);
|
||||||
|
|
||||||
|
char body[512];
|
||||||
|
snprintf(body, sizeof body,
|
||||||
|
"{\"slot_id\":\"%s\",\"width\":%u,\"height\":%u,"
|
||||||
|
"\"fps_num\":%u,\"fps_den\":%u,\"source_type\":\"%s\"}",
|
||||||
|
slot_id, w, h, fps_num, fps_den, source_type);
|
||||||
|
|
||||||
|
char resp[1024] = {0};
|
||||||
|
int st = http_req("POST", host, port, "/slots", body, resp, sizeof resp);
|
||||||
|
if (st != 201) {
|
||||||
|
fprintf(stderr, "[net_ingest] POST /slots failed HTTP %d: %s\n", st, resp);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
json_str(resp, "shm_path", shm_path, sp_len);
|
||||||
|
json_str(resp, "sem_name", sem_name, sn_len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void deregister_slot(const char *fc_url, const char *slot_id) {
|
||||||
|
char host[128]; int port;
|
||||||
|
parse_url(fc_url, host, sizeof host, &port);
|
||||||
|
char path[192]; snprintf(path, sizeof path, "/slots/%s", slot_id);
|
||||||
|
http_req("DELETE", host, port, path, NULL, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Open shm + semaphore for writing ─────────────────────────────── */
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <semaphore.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *base;
|
||||||
|
size_t size;
|
||||||
|
int fd;
|
||||||
|
sem_t *sem;
|
||||||
|
} ShmWriter;
|
||||||
|
|
||||||
|
static int shm_writer_open(const char *shm_path, const char *sem_name,
|
||||||
|
ShmWriter *sw)
|
||||||
|
{
|
||||||
|
sw->fd = open(shm_path, O_RDWR);
|
||||||
|
if (sw->fd < 0) return -1;
|
||||||
|
fc_header_t hdr;
|
||||||
|
if (pread(sw->fd, &hdr, sizeof hdr, 0) != sizeof hdr || hdr.magic != FC_MAGIC) {
|
||||||
|
close(sw->fd); return -1;
|
||||||
|
}
|
||||||
|
sw->size = fc_slot_shm_size(hdr.frame_size);
|
||||||
|
sw->base = mmap(NULL, sw->size, PROT_READ | PROT_WRITE, MAP_SHARED, sw->fd, 0);
|
||||||
|
if (sw->base == MAP_FAILED) { close(sw->fd); return -1; }
|
||||||
|
sw->sem = sem_open(sem_name, 0);
|
||||||
|
if (sw->sem == SEM_FAILED) { munmap(sw->base, sw->size); close(sw->fd); return -1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void shm_write_frame(ShmWriter *sw, const uint8_t *data,
|
||||||
|
uint32_t size, uint64_t pts_us)
|
||||||
|
{
|
||||||
|
fc_header_t *hdr = (fc_header_t *)sw->base;
|
||||||
|
uint64_t cur = atomic_load_explicit(&hdr->write_cursor, memory_order_relaxed);
|
||||||
|
fc_frame_t *frame = fc_frame_at(sw->base, hdr->frame_size, cur);
|
||||||
|
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
|
frame->pts_us = pts_us;
|
||||||
|
frame->wall_us = (uint64_t)ts.tv_sec * 1000000ULL + ts.tv_nsec / 1000ULL;
|
||||||
|
frame->size = size < hdr->frame_size ? size : hdr->frame_size;
|
||||||
|
memcpy(frame->data, data, frame->size);
|
||||||
|
atomic_store_explicit(&hdr->write_cursor, cur + 1, memory_order_release);
|
||||||
|
sem_post(sw->sem);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void shm_writer_close(ShmWriter *sw) {
|
||||||
|
if (sw->sem) { sem_close(sw->sem); sw->sem = NULL; }
|
||||||
|
if (sw->base) { munmap(sw->base, sw->size); sw->base = NULL; }
|
||||||
|
if (sw->fd >= 0) { close(sw->fd); sw->fd = -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Build ffmpeg args for network decode → rawvideo stdout ────────── */
|
||||||
|
static int build_ffmpeg_args(
|
||||||
|
char **argv, int max_args,
|
||||||
|
const char *url, const char *source_type,
|
||||||
|
int listen, int listen_port, const char *stream_key,
|
||||||
|
uint32_t w, uint32_t h)
|
||||||
|
{
|
||||||
|
char size_str[32];
|
||||||
|
snprintf(size_str, sizeof size_str, "%ux%u", w, h);
|
||||||
|
char port_str[16];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
argv[i++] = "ffmpeg";
|
||||||
|
argv[i++] = "-hide_banner";
|
||||||
|
argv[i++] = "-loglevel"; argv[i++] = "warning";
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
argv[i++] = "-probesize"; argv[i++] = "32M";
|
||||||
|
argv[i++] = "-analyzeduration"; argv[i++] = "10M";
|
||||||
|
argv[i++] = "-fflags"; argv[i++] = "+genpts";
|
||||||
|
|
||||||
|
if (!strcmp(source_type, "srt") && listen) {
|
||||||
|
snprintf(port_str, sizeof port_str, "%d", listen_port ? listen_port : 9000);
|
||||||
|
char srt_url[256];
|
||||||
|
snprintf(srt_url, sizeof srt_url, "srt://0.0.0.0:%s?mode=listener", port_str);
|
||||||
|
argv[i++] = "-i"; argv[i++] = strdup(srt_url);
|
||||||
|
} else if (!strcmp(source_type, "rtmp") && listen) {
|
||||||
|
snprintf(port_str, sizeof port_str, "%d", listen_port ? listen_port : 1935);
|
||||||
|
char rtmp_url[256];
|
||||||
|
snprintf(rtmp_url, sizeof rtmp_url, "rtmp://0.0.0.0:%s/live/%s",
|
||||||
|
port_str, stream_key ? stream_key : "stream");
|
||||||
|
argv[i++] = "-listen"; argv[i++] = "1";
|
||||||
|
argv[i++] = "-i"; argv[i++] = strdup(rtmp_url);
|
||||||
|
} else {
|
||||||
|
argv[i++] = "-i"; argv[i++] = (char *)url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video output: raw UYVY422 to stdout */
|
||||||
|
argv[i++] = "-map"; argv[i++] = "0:v:0";
|
||||||
|
argv[i++] = "-vf"; argv[i++] = "scale=iw:ih,format=uyvy422";
|
||||||
|
argv[i++] = "-f"; argv[i++] = "rawvideo";
|
||||||
|
argv[i++] = "-pix_fmt"; argv[i++] = "uyvy422";
|
||||||
|
argv[i++] = "-s"; argv[i++] = strdup(size_str);
|
||||||
|
argv[i++] = "pipe:1";
|
||||||
|
|
||||||
|
argv[i] = NULL;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main ──────────────────────────────────────────────────────────── */
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
const char *url = NULL;
|
||||||
|
const char *slot_id = NULL;
|
||||||
|
const char *fc_url = getenv("FC_URL") ? getenv("FC_URL") : FC_URL_DEFAULT;
|
||||||
|
const char *source_type = "srt";
|
||||||
|
uint32_t width = 1920, height = 1080;
|
||||||
|
uint32_t fps_num = 30000, fps_den = 1001;
|
||||||
|
int listen = 0, listen_port = 0;
|
||||||
|
const char *stream_key = "stream";
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (!strcmp(argv[i], "--url") && i+1 < argc) url = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--slot-id") && i+1 < argc) slot_id = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--fc-url") && i+1 < argc) fc_url = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--source-type") && i+1 < argc) source_type = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--width") && i+1 < argc) width = (uint32_t)atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--height") && i+1 < argc) height = (uint32_t)atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--fps-num") && i+1 < argc) fps_num = (uint32_t)atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--fps-den") && i+1 < argc) fps_den = (uint32_t)atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--listen")) listen = 1;
|
||||||
|
else if (!strcmp(argv[i], "--listen-port") && i+1 < argc) listen_port = atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--stream-key") && i+1 < argc) stream_key = argv[++i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slot_id) {
|
||||||
|
fprintf(stderr, "[net_ingest] --slot-id required\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!url && !listen) {
|
||||||
|
fprintf(stderr, "[net_ingest] --url or --listen required\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGTERM, on_signal);
|
||||||
|
signal(SIGINT, on_signal);
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
signal(SIGCHLD, SIG_DFL);
|
||||||
|
|
||||||
|
/* ── Register slot ──────────────────────────────────────────────── */
|
||||||
|
char shm_path[128] = {0}, sem_name[128] = {0};
|
||||||
|
if (register_slot(fc_url, slot_id, width, height, fps_num, fps_den,
|
||||||
|
source_type, shm_path, sizeof shm_path,
|
||||||
|
sem_name, sizeof sem_name) < 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShmWriter sw = { .fd = -1 };
|
||||||
|
if (shm_writer_open(shm_path, sem_name, &sw) < 0) {
|
||||||
|
fprintf(stderr, "[net_ingest] failed to open shm %s\n", shm_path);
|
||||||
|
deregister_slot(fc_url, slot_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t fsz = frame_bytes(width, height);
|
||||||
|
uint8_t *frame_buf = malloc(fsz);
|
||||||
|
if (!frame_buf) { shm_writer_close(&sw); deregister_slot(fc_url, slot_id); return 1; }
|
||||||
|
|
||||||
|
uint64_t frame_seq = 0;
|
||||||
|
int reported = 0;
|
||||||
|
|
||||||
|
fprintf(stderr, "[net_ingest] slot=%s %ux%u %.2ffps source=%s%s\n",
|
||||||
|
slot_id, width, height,
|
||||||
|
fps_den ? (double)fps_num / fps_den : 0.0,
|
||||||
|
source_type, listen ? " (listener)" : "");
|
||||||
|
|
||||||
|
/* ── Outer reconnect loop (listener mode stays alive between sessions) */
|
||||||
|
while (!g_stop) {
|
||||||
|
/* Build ffmpeg argv */
|
||||||
|
char *ff_argv[64];
|
||||||
|
build_ffmpeg_args(ff_argv, 64, url, source_type,
|
||||||
|
listen, listen_port, stream_key, width, height);
|
||||||
|
|
||||||
|
/* Spawn ffmpeg with stdout pipe */
|
||||||
|
int pfd[2];
|
||||||
|
if (pipe(pfd) < 0) break;
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { close(pfd[0]); close(pfd[1]); break; }
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
/* Child: redirect stdout to pipe write end */
|
||||||
|
dup2(pfd[1], STDOUT_FILENO);
|
||||||
|
close(pfd[0]); close(pfd[1]);
|
||||||
|
execvp("ffmpeg", ff_argv);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parent: read from pipe read end */
|
||||||
|
close(pfd[1]);
|
||||||
|
int rfd = pfd[0];
|
||||||
|
|
||||||
|
size_t buf_off = 0;
|
||||||
|
while (!g_stop) {
|
||||||
|
ssize_t n = read(rfd, frame_buf + buf_off, fsz - buf_off);
|
||||||
|
if (n <= 0) break; /* ffmpeg exited or pipe closed */
|
||||||
|
buf_off += (size_t)n;
|
||||||
|
if (buf_off < fsz) continue; /* incomplete frame — keep reading */
|
||||||
|
|
||||||
|
/* Full frame assembled */
|
||||||
|
uint64_t pts_us = fps_num > 0
|
||||||
|
? frame_seq * 1000000ULL * fps_den / fps_num
|
||||||
|
: 0;
|
||||||
|
shm_write_frame(&sw, frame_buf, (uint32_t)fsz, pts_us);
|
||||||
|
frame_seq++;
|
||||||
|
buf_off = 0;
|
||||||
|
|
||||||
|
if (!reported) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"{\"slot_id\":\"%s\",\"width\":%u,\"height\":%u,"
|
||||||
|
"\"fps_num\":%u,\"fps_den\":%u,"
|
||||||
|
"\"source_type\":\"%s\",\"pix_fmt\":\"uyvy422\"}\n",
|
||||||
|
slot_id, width, height, fps_num, fps_den, source_type);
|
||||||
|
fflush(stderr);
|
||||||
|
reported = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(rfd);
|
||||||
|
/* Reap ffmpeg child */
|
||||||
|
int wstatus;
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, &wstatus, 0);
|
||||||
|
|
||||||
|
if (!listen || g_stop) break;
|
||||||
|
|
||||||
|
/* Listener mode: wait 1s then reconnect */
|
||||||
|
fprintf(stderr, "[net_ingest] listener: waiting for next connection\n");
|
||||||
|
struct timespec ts = { .tv_sec = 1 };
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(frame_buf);
|
||||||
|
shm_writer_close(&sw);
|
||||||
|
deregister_slot(fc_url, slot_id);
|
||||||
|
fprintf(stderr, "[net_ingest] done frames=%llu\n", (unsigned long long)frame_seq);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -86,6 +86,64 @@ const _containerSourceType = new Map();
|
||||||
// port -> fmt JSON from bridge stderr (inject into sidecar env + slot_id)
|
// port -> fmt JSON from bridge stderr (inject into sidecar env + slot_id)
|
||||||
const _dcPortFmt = new Map();
|
const _dcPortFmt = new Map();
|
||||||
|
|
||||||
|
// ── Network ingest ────────────────────────────────────────────────────────
|
||||||
|
// One net_ingest process per active network recorder (SRT/RTMP).
|
||||||
|
// Decodes the stream to raw UYVY422 and writes into a framecache slot so
|
||||||
|
// capture-manager can use fc_pipe — the same consumer path as SDI sources.
|
||||||
|
const NET_INGEST_BIN = process.env.NET_INGEST_BIN || 'net_ingest';
|
||||||
|
// containerId → ChildProcess for cleanup on sidecar stop
|
||||||
|
const _netIngestProcs = new Map();
|
||||||
|
|
||||||
|
function startNetIngest(containerId, { sourceType, sourceUrl, listen, listenPort, streamKey,
|
||||||
|
width = 1920, height = 1080,
|
||||||
|
fpsNum = 30000, fpsDen = 1001 }) {
|
||||||
|
const slotId = `net-${containerId}`;
|
||||||
|
const args = [
|
||||||
|
'--slot-id', slotId,
|
||||||
|
'--fc-url', FC_URL,
|
||||||
|
'--source-type', sourceType,
|
||||||
|
'--width', String(width),
|
||||||
|
'--height', String(height),
|
||||||
|
'--fps-num', String(fpsNum),
|
||||||
|
'--fps-den', String(fpsDen),
|
||||||
|
];
|
||||||
|
if (listen) {
|
||||||
|
args.push('--listen');
|
||||||
|
if (listenPort) args.push('--listen-port', String(listenPort));
|
||||||
|
if (streamKey) args.push('--stream-key', streamKey);
|
||||||
|
} else if (sourceUrl) {
|
||||||
|
args.push('--url', sourceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[net-ingest:${slotId}] launching: ${NET_INGEST_BIN} ${args.join(' ')}`);
|
||||||
|
const proc = spawn(NET_INGEST_BIN, args, {
|
||||||
|
stdio: ['ignore', 'ignore', 'pipe'],
|
||||||
|
env: { ...process.env, FC_URL },
|
||||||
|
});
|
||||||
|
proc.stderr.setEncoding('utf8');
|
||||||
|
proc.stderr.on('data', chunk => {
|
||||||
|
for (const line of chunk.split('\n')) {
|
||||||
|
const t = line.trim();
|
||||||
|
if (t) console.log(`[net-ingest:${slotId}] ${t}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
proc.on('error', err => console.error(`[net-ingest:${slotId}] spawn error: ${err.message}`));
|
||||||
|
proc.on('exit', (c, s) => {
|
||||||
|
console.log(`[net-ingest:${slotId}] exited code=${c} signal=${s}`);
|
||||||
|
_netIngestProcs.delete(containerId);
|
||||||
|
});
|
||||||
|
_netIngestProcs.set(containerId, { proc, slotId });
|
||||||
|
return slotId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopNetIngest(containerId) {
|
||||||
|
const entry = _netIngestProcs.get(containerId);
|
||||||
|
if (!entry) return;
|
||||||
|
console.log(`[net-ingest:${entry.slotId}] stopping`);
|
||||||
|
try { entry.proc.kill('SIGTERM'); } catch (_) {}
|
||||||
|
_netIngestProcs.delete(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
// ── DeckLink bridge ───────────────────────────────────────────────────────
|
// ── DeckLink bridge ───────────────────────────────────────────────────────
|
||||||
// One decklink-bridge process per node, managing all DeckLink devices.
|
// One decklink-bridge process per node, managing all DeckLink devices.
|
||||||
// Mirrors the deltacast-bridge singleton pattern.
|
// Mirrors the deltacast-bridge singleton pattern.
|
||||||
|
|
@ -396,6 +454,44 @@ async function handleSidecarStart(body, res) {
|
||||||
// Always inject FC_URL so capture-manager can find the framecache service.
|
// Always inject FC_URL so capture-manager can find the framecache service.
|
||||||
sidecarEnv.push(`FC_URL=${FC_URL}`);
|
sidecarEnv.push(`FC_URL=${FC_URL}`);
|
||||||
|
|
||||||
|
// Network sources (SRT/RTMP): launch net_ingest to decode stream into
|
||||||
|
// a framecache slot, then inject FC_SLOT_ID so capture-manager reads
|
||||||
|
// from the slot via fc_pipe (same path as SDI sources).
|
||||||
|
if (sourceType === 'srt' || sourceType === 'rtmp') {
|
||||||
|
const _srcCfg = (env.find(e => e.startsWith('SOURCE_CONFIG=')) || '').slice(14);
|
||||||
|
let _netCfg = {};
|
||||||
|
try { _netCfg = JSON.parse(_srcCfg); } catch (_) {}
|
||||||
|
const _listen = !!(body.listen || _netCfg.listen);
|
||||||
|
const _listenPort = body.listenPort || _netCfg.listenPort || 0;
|
||||||
|
const _streamKey = body.streamKey || _netCfg.streamKey || 'stream';
|
||||||
|
const _srcUrl = body.sourceUrl || _netCfg.url || '';
|
||||||
|
// Width/height/fps from recorder config if available; defaults used otherwise.
|
||||||
|
// net_ingest will auto-scale via ffmpeg -vf scale=iw:ih.
|
||||||
|
const _w = _netCfg.width || 1920;
|
||||||
|
const _h = _netCfg.height || 1080;
|
||||||
|
const _fpsNum = _netCfg.fps_num || 30000;
|
||||||
|
const _fpsDen = _netCfg.fps_den || 1001;
|
||||||
|
|
||||||
|
// containerId not known yet — we start net_ingest just before container
|
||||||
|
// start and use a temporary slot ID based on a timestamp.
|
||||||
|
const _tempId = `${sourceType}-${Date.now()}`;
|
||||||
|
const _slotId = startNetIngest(_tempId, {
|
||||||
|
sourceType: sourceType,
|
||||||
|
sourceUrl: _srcUrl,
|
||||||
|
listen: _listen,
|
||||||
|
listenPort: _listenPort,
|
||||||
|
streamKey: _streamKey,
|
||||||
|
width: _w,
|
||||||
|
height: _h,
|
||||||
|
fpsNum: _fpsNum,
|
||||||
|
fpsDen: _fpsDen,
|
||||||
|
});
|
||||||
|
sidecarEnv.push(`FC_SLOT_ID=${_slotId}`);
|
||||||
|
hostConfig.IpcMode = 'host';
|
||||||
|
// Store temp id so we can remap to real containerId on create success
|
||||||
|
body._netIngestTempId = _tempId;
|
||||||
|
}
|
||||||
|
|
||||||
// Deltacast: ensure the shared bridge daemon is running on the HOST before
|
// Deltacast: ensure the shared bridge daemon is running on the HOST before
|
||||||
// starting the sidecar. The bridge writes frames to the framecache shm ring;
|
// starting the sidecar. The bridge writes frames to the framecache shm ring;
|
||||||
// the sidecar reads via the consumer library (fc_client).
|
// the sidecar reads via the consumer library (fc_client).
|
||||||
|
|
@ -471,6 +567,15 @@ async function handleSidecarStart(body, res) {
|
||||||
|
|
||||||
if (sourceType === 'deltacast') _containerSourceType.set(containerId, 'deltacast');
|
if (sourceType === 'deltacast') _containerSourceType.set(containerId, 'deltacast');
|
||||||
if (sourceType === 'sdi' || sourceType === 'blackmagic') _containerSourceType.set(containerId, 'blackmagic');
|
if (sourceType === 'sdi' || sourceType === 'blackmagic') _containerSourceType.set(containerId, 'blackmagic');
|
||||||
|
if (sourceType === 'srt' || sourceType === 'rtmp') {
|
||||||
|
_containerSourceType.set(containerId, sourceType);
|
||||||
|
// Remap net_ingest from temp id to real containerId
|
||||||
|
if (body._netIngestTempId && _netIngestProcs.has(body._netIngestTempId)) {
|
||||||
|
const entry = _netIngestProcs.get(body._netIngestTempId);
|
||||||
|
_netIngestProcs.delete(body._netIngestTempId);
|
||||||
|
_netIngestProcs.set(containerId, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
jsonResponse(res, 201, { containerId, capturePort });
|
jsonResponse(res, 201, { containerId, capturePort });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (sourceType === 'deltacast') {
|
if (sourceType === 'deltacast') {
|
||||||
|
|
@ -535,6 +640,8 @@ async function handleSidecarStop(containerId, res) {
|
||||||
_dlSidecarCount = 0;
|
_dlSidecarCount = 0;
|
||||||
stopDecklinkBridge();
|
stopDecklinkBridge();
|
||||||
}
|
}
|
||||||
|
} else if (_srcType === 'srt' || _srcType === 'rtmp') {
|
||||||
|
stopNetIngest(containerId);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[sidecar-stop] background cleanup failed for ${containerId}:`, err.message);
|
console.error(`[sidecar-stop] background cleanup failed for ${containerId}:`, err.message);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue