From 4816381a907632b90bd9d83cbed03304dd812d92 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Tue, 7 Jul 2026 14:37:25 +0530 Subject: [PATCH 1/3] feat: multi-NIC TX support (squashed for rebase onto upstream main) Squashes the following multiple_nic branch commits prior to rebasing onto upstream/main (which independently merged the scaling_tx feature as #32): 24c825d feat: add 2K/4K resolution support and rename config files 6df149d feat: add video scaling support (upscale/downscale) 7ba414c fix: address PR review comments 084b8cd feat: multi-NIC support for FFmpeg and direct MTL TX paths 72418ec fix: use shared frame counter for RTP timestamps in multi-session TX 5313ed5 feat: multi-NIC and scaling TX support from scaling_tx branch 3cc5866 Remove hardcoded MAX_TX_SESSIONS/MAX_INTERFACES limits f88c35c fix: address PR review comments (round 2) --- README.md | 19 +- config/tx_2k_multi_session.json | 7 +- config/tx_4k_multi_session.json | 3 + config/tx_4k_single_session.json | 5 +- config/tx_fullhd_multi_nic.json | 47 +++++ config/tx_fullhd_multi_session.json | 9 +- config/tx_fullhd_single_session.json | 1 + include/app_context.h | 68 ++++++- include/core/session_manager.h | 8 +- include/mtl/mtl_tx.h | 56 +++-- include/util/config_reader.h | 19 +- meson.build | 17 +- src/core/session_manager.c | 4 +- src/ffmpeg/ffmpeg_tx.c | 36 +++- src/main.c | 5 +- src/mtl/mtl_tx.c | 77 +++++-- src/util/config_reader.c | 294 +++++++++++++++++++-------- tests/meson.build | 5 +- tests/test_config_reader.c | 135 +++++++++--- tests/test_ffmpeg_decoder_mock.c | 12 +- tests/test_ffmpeg_tx.c | 10 +- tests/test_ffmpeg_tx_mock.c | 14 +- tests/test_ffmpeg_watchdog.c | 12 +- tests/test_main.c | 55 +++-- tests/test_session_manager.c | 17 +- 25 files changed, 685 insertions(+), 250 deletions(-) create mode 100644 config/tx_fullhd_multi_nic.json diff --git a/README.md b/README.md index ff5ddf7..ef53b65 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ dvledtx uses a JSON config file with three sections: | Section | Field | Description | |---------|-------|-------------| | **log_file** | `log_file` | (Optional) Path/name of the log output file (e.g. `dvledtx.log`). If omitted, logging goes to console only. | -| **interfaces** | `name` | PCI BDF address of the NIC (e.g. `0000:06:00.0`) | +| **interfaces[]** | `name` | PCI BDF address of the NIC (e.g. `0000:06:00.0`) | | | `sip` | Source IP address | | | `dip` | Destination multicast IP address | | **video** | `width` | Source frame width in pixels | @@ -137,26 +137,31 @@ dvledtx uses a JSON config file with three sections: | | `scale_height` | (Optional) Output height after scaling | | | `fps` | Frames per second (25, 30, 50, 60) | | | `fmt` | Pixel format (see [Supported Formats](#supported-formats)) | -| **tx_sessions[]** | `udp_port` | UDP port for the session | +| **tx_sessions[]** | `nic_index` | (Optional) Index into `interfaces[]` selecting which NIC this session uses (default: `0`) | +| | `udp_port` | UDP port for the session | | | `payload_type` | (Optional) RTP payload type — defaults to `96` if not present | | | `crop` | Region to transmit: `x`, `y`, `w`, `h` in pixels | -Example (`config/tx_fullhd_single_session.json`): +Example (`config/tx_fullhd_multi_nic.json`): ```json { "log_file": "dvledtx.log", "interfaces": [ - { "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" } + { "name": "0000:03:10.0", "sip": "192.168.50.30", "dip": "239.168.85.20" }, + { "name": "0000:03:10.2", "sip": "192.168.50.29", "dip": "239.168.85.21" } ], "video": { "width": 1920, "height": 1080, "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { - "fps": 30, "fmt": "yuv422p10le" + "scale_width": 1920, "scale_height": 1080, + "fps": 30, + "fmt": "yuv422p10le" }, "tx_sessions": [ - { "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } } + { "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 960, "h": 1080 } }, + { "nic_index": 1, "udp_port": 20002, "payload_type": 96, "crop": { "x": 960, "y": 0, "w": 960, "h": 1080 } } ] } ``` @@ -265,7 +270,7 @@ Example — upscale 1080p source to 4K output: "fps": 30, "fmt": "yuv422p10le" }, "tx_sessions": [ - { "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 } } + { "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 } } ] } ``` diff --git a/config/tx_2k_multi_session.json b/config/tx_2k_multi_session.json index ebaea16..8a8820a 100644 --- a/config/tx_2k_multi_session.json +++ b/config/tx_2k_multi_session.json @@ -2,7 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:06:00.0", + "name": "0000:03:00.1", "sip": "192.168.50.29", "dip": "239.168.85.20" } @@ -10,7 +10,7 @@ "video": { "width": 2560, "height": 1440, - "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "ball_2k_yuv420p_30fps_5min.mp4" }, "tx_video": { "scale_width": 2560, @@ -21,14 +21,17 @@ "tx_sessions": [ { "udp_port": 20000, + "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 854, "h": 1440 } }, { "udp_port": 20002, + "payload_type": 96, "crop": { "x": 854, "y": 0, "w": 854, "h": 1440 } }, { "udp_port": 20004, + "payload_type": 96, "crop": { "x": 1708, "y": 0, "w": 852, "h": 1440 } } ] diff --git a/config/tx_4k_multi_session.json b/config/tx_4k_multi_session.json index 63800f2..0b14087 100644 --- a/config/tx_4k_multi_session.json +++ b/config/tx_4k_multi_session.json @@ -21,14 +21,17 @@ "tx_sessions": [ { "udp_port": 20000, + "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } }, { "udp_port": 20002, + "payload_type": 96, "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } }, { "udp_port": 20004, + "payload_type": 96, "crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 } } ] diff --git a/config/tx_4k_single_session.json b/config/tx_4k_single_session.json index 5fa2bbc..004de34 100644 --- a/config/tx_4k_single_session.json +++ b/config/tx_4k_single_session.json @@ -15,12 +15,13 @@ "tx_video": { "scale_width": 900, "scale_height": 600, - "fps": 30, - "fmt": "yuv422p10le" + "fps": 60, + "fmt": "gbrp10le" }, "tx_sessions": [ { "udp_port": 20000, + "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 900, "h": 600 } } ] diff --git a/config/tx_fullhd_multi_nic.json b/config/tx_fullhd_multi_nic.json new file mode 100644 index 0000000..0b30ea8 --- /dev/null +++ b/config/tx_fullhd_multi_nic.json @@ -0,0 +1,47 @@ +{ + "log_file": "dvledtx.log", + "interfaces": [ + { + "name": "0000:03:00.0", + "sip": "192.168.50.30", + "dip": "239.168.85.20" + }, + { + "name": "0000:03:10.2", + "sip": "192.168.50.29", + "dip": "239.168.85.21" + } + ], + "video": { + "width": 1920, + "height": 1080, + "tx_url": "/home/intel/workspace/sample/ball_1080p_yuv420p_30fps_15min.mp4" + }, + "tx_video": { + "scale_width": 1920, + "scale_height": 1080, + "fps": 30, + "fmt": "gbrp10le" + }, + "tx_sessions": [ + { + "nic_index": 0, + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } + }, + { + "nic_index": 1, + "udp_port": 20002, + "payload_type": 96, + "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } + }, + { + "nic_index": 1, + "udp_port": 20004, + "payload_type": 96, + "crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 } + } + ] +} + diff --git a/config/tx_fullhd_multi_session.json b/config/tx_fullhd_multi_session.json index 3e98b9e..3fd5015 100644 --- a/config/tx_fullhd_multi_session.json +++ b/config/tx_fullhd_multi_session.json @@ -2,15 +2,15 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:06:00.0", + "name": "0000:03:10.1", "sip": "192.168.50.29", - "dip": "239.168.85.20" + "dip": "239.168.85.21" } ], "video": { "width": 1920, "height": 1080, - "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "/home/intel/workspace/sample/ball_1080p_yuv420p_30fps_15min.mp4" }, "tx_video": { "scale_width": 1920, @@ -21,14 +21,17 @@ "tx_sessions": [ { "udp_port": 20000, + "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } }, { "udp_port": 20002, + "payload_type": 96, "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } }, { "udp_port": 20004, + "payload_type": 96, "crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 } } ] diff --git a/config/tx_fullhd_single_session.json b/config/tx_fullhd_single_session.json index 2939030..32b9925 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -21,6 +21,7 @@ "tx_sessions": [ { "udp_port": 20000, + "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } } ] diff --git a/include/app_context.h b/include/app_context.h index f07def7..78fcbc3 100644 --- a/include/app_context.h +++ b/include/app_context.h @@ -6,10 +6,24 @@ #include #include +#include +#include #include #include -#define MAX_TX_SESSIONS (8) +/* Fixed field-width constants (not array size limits) */ +#define PORT_NAME_LEN 64 +#define IP_STR_LEN INET_ADDRSTRLEN +#define IP_ADDR_BYTES 4 + +/* Per-NIC configuration (one element per interface) */ +struct nic_config { + char port[PORT_NAME_LEN]; /* DPDK NIC PCI BDF */ + char sip_addr_str[IP_STR_LEN]; /* source IP string */ + uint8_t sip_addr[IP_ADDR_BYTES]; /* source IP binary */ + char dip_addr_str[IP_STR_LEN]; /* destination IP string */ + uint8_t dip_addr[IP_ADDR_BYTES]; /* destination IP binary */ +}; /* Per-session network and crop parameters (populated from JSON tx_sessions[]) */ struct tx_session_net { @@ -19,18 +33,17 @@ struct tx_session_net { int crop_y; int crop_w; int crop_h; + int nic_index; /* which NIC this session uses (index into nics[]) */ }; /* Application context for TX sessions */ struct dvledtx_context { - /* Configuration */ - char port[64]; /* DPDK NIC PCI BDF/address for MTL output (e.g. 0000:af:00.0), not a Linux interface name */ + /* NIC configuration — dynamically allocated array [0..nic_count-1] */ + int nic_count; /* number of active NICs */ + struct nic_config* nics; /* heap-allocated, nic_count elements */ + char tx_url[256]; char config_file[256]; - char sip_addr_str[INET_ADDRSTRLEN]; - uint8_t sip_addr[4]; - char dip_addr_str[INET_ADDRSTRLEN]; - uint8_t dip_addr[4]; uint16_t udp_port; uint8_t payload_type; /* RTP dynamic payload type (default: 96) */ @@ -48,10 +61,47 @@ struct dvledtx_context { bool force_dhcp; int test_time_s; - /* Per-session network + crop config (from JSON tx_sessions[]) */ - struct tx_session_net session_net[MAX_TX_SESSIONS]; + /* Per-session network + crop config — dynamically allocated, st20p_sessions elements */ + struct tx_session_net* session_net; /* Optional log file path from config (empty = console logging only) */ char log_file[256]; }; + +/* Free dynamically allocated members of dvledtx_context (nics, session_net). + * Does NOT free the struct itself (it may be stack-allocated). */ +static inline void dvledtx_context_free(struct dvledtx_context* ctx) { + if (ctx == NULL) return; + free(ctx->nics); ctx->nics = NULL; + free(ctx->session_net); ctx->session_net = NULL; + ctx->nic_count = 0; + ctx->st20p_sessions = 0; +} + +/* Allocate NIC and session arrays inside an already zeroed context. + * calloc(0, ...) is permitted by the C standard to return NULL, which is + * not an allocation failure, so a zero count is treated as "nothing to + * allocate" rather than an error (avoids spurious failures, e.g. in tests + * that call dvledtx_context_alloc(ctx, 1, 0)). On failure, ctx is left + * fully zeroed (no dangling nics pointer with a stale nic_count) so it + * remains safe to pass to dvledtx_context_free(). + * Returns 0 on success, -1 on allocation failure. */ +static inline int dvledtx_context_alloc(struct dvledtx_context* ctx, + int nic_count, int session_count) { + if (nic_count > 0) { + ctx->nics = calloc((size_t)nic_count, sizeof(struct nic_config)); + if (ctx->nics == NULL) return -1; + } + ctx->nic_count = nic_count; + + if (session_count > 0) { + ctx->session_net = calloc((size_t)session_count, sizeof(struct tx_session_net)); + if (ctx->session_net == NULL) { + free(ctx->nics); ctx->nics = NULL; ctx->nic_count = 0; + return -1; + } + } + ctx->st20p_sessions = session_count; + return 0; +} diff --git a/include/core/session_manager.h b/include/core/session_manager.h index 5f98448..e325c9c 100644 --- a/include/core/session_manager.h +++ b/include/core/session_manager.h @@ -16,9 +16,11 @@ #include #include -/* MTL pipeline API headers — only when building the direct MTL TX path */ -#ifdef ENABLE_MTL_TX +/* MTL API headers — always included; mtl_tx_init/uninit are always compiled + * to allow pre-initialising all NIC ports regardless of TX path. */ #include "mtl_api.h" + +#ifdef ENABLE_MTL_TX #include "st_pipeline_api.h" #endif @@ -120,9 +122,7 @@ typedef struct session_manager_s { struct shared_decode_ctx* shared_dec; -#ifdef ENABLE_MTL_TX mtl_handle mtl; /* MTL library instance — owns all st20p_tx sessions */ -#endif bool running; } session_manager_t; diff --git a/include/mtl/mtl_tx.h b/include/mtl/mtl_tx.h index 7c5756a..4acefd7 100644 --- a/include/mtl/mtl_tx.h +++ b/include/mtl/mtl_tx.h @@ -7,25 +7,18 @@ /* * mtl_tx.h — public API for the direct MTL pipeline TX path. * - * When -DENABLE_MTL_TX is set, dvledtx transmits frames by calling the MTL - * pipeline API directly (st20p_tx_get_frame / st20p_tx_put_frame) instead - * of going through the FFmpeg libavdevice mtl_st20p muxer. + * mtl_tx_init() / mtl_tx_uninit() are declared unconditionally so that the + * FFmpeg avdevice path can also pre-initialise MTL with ALL NIC ports before + * opening any mtl_st20p session. Without this, DPDK EAL is initialised by + * the first avformat_write_header() call with only one port, causing + * "mt_port_by_name … is not valid" for every subsequent NIC. * - * This file declares: - * - Format mapping helpers (AVPixelFormat → MTL enums). - * - mtl_copy_crop_to_frame(): copies a crop rectangle from a decoded - * AVFrame into an MTL st_frame DMA buffer. - * - mtl_tx_init() / mtl_tx_uninit(): MTL library lifecycle. - * - mtl_tx_session_create() / mtl_tx_session_free(): per-session lifecycle. - * - mtl_tx_send_yuv_frame(): decoded frame crop-and-send via MTL API. - * - mtl_tx_send_raw_yuv(): raw YUV buffer send via MTL API. + * All other symbols (format helpers, session create/free, send functions) + * are only needed when ENABLE_MTL_TX is defined. * * Implementation: src/mtl/mtl_tx.c - * Only compiled when ENABLE_MTL_TX is defined. */ -#ifdef ENABLE_MTL_TX - #include "mtl_api.h" #include "st_pipeline_api.h" #include @@ -36,6 +29,25 @@ struct dvledtx_context; struct st20p_tx_ctx; typedef struct session_manager_s session_manager_t; +/* ------------------------------------------------------------------------- + * MTL library lifecycle — always available (FFmpeg and direct-MTL paths) + * ---------------------------------------------------------------------- */ + +/* + * mtl_tx_init() — initialise the MTL/DPDK library with ALL ports from app. + * Must be called once before any TX session (FFmpeg or direct) is opened. + * Stores the handle in manager->mtl. + * Returns 0 on success, -1 on failure. + */ +int mtl_tx_init(session_manager_t* manager, struct dvledtx_context* app); + +/* + * mtl_tx_uninit() — release the MTL library instance stored in manager->mtl. + */ +void mtl_tx_uninit(session_manager_t* manager); + +#ifdef ENABLE_MTL_TX + /* ------------------------------------------------------------------------- * Format mapping * ---------------------------------------------------------------------- */ @@ -74,22 +86,6 @@ void mtl_copy_crop_to_frame(struct st_frame* dst, const AVFrame* src, int crop_w, int crop_h, enum AVPixelFormat fmt); -/* ------------------------------------------------------------------------- - * MTL library lifecycle - * ---------------------------------------------------------------------- */ - -/* - * mtl_tx_init() — initialise the MTL library with parameters from app. - * Stores the handle in manager->mtl. - * Returns 0 on success, -1 on failure. - */ -int mtl_tx_init(session_manager_t* manager, struct dvledtx_context* app); - -/* - * mtl_tx_uninit() — release the MTL library instance stored in manager->mtl. - */ -void mtl_tx_uninit(session_manager_t* manager); - /* ------------------------------------------------------------------------- * Per-session TX session lifecycle * ---------------------------------------------------------------------- */ diff --git a/include/util/config_reader.h b/include/util/config_reader.h index 5209fb3..d0c0d08 100644 --- a/include/util/config_reader.h +++ b/include/util/config_reader.h @@ -18,14 +18,17 @@ struct tx_session_config { int crop_y; int crop_w; int crop_h; + int nic_index; /* which NIC (index into interface arrays); default 0 */ }; /* Full application configuration parsed from JSON */ struct dvledtx_config { - /* interfaces[0] */ - char interface_name[64]; /* PCI address, e.g. "0000:02:00.0" */ - char interface_sip[32]; /* source IP, e.g. "192.168.50.29" */ - char interface_dip[32]; /* destination IP, e.g. "239.168.85.20" */ + /* interfaces array — dynamically allocated */ + int nic_count; /* number of interfaces parsed */ + int nic_cap; /* allocated capacity */ + char (*interface_name)[64]; /* PCI address per NIC */ + char (*interface_sip)[32]; /* source IP per NIC */ + char (*interface_dip)[32]; /* destination multicast IP per NIC */ /* video block */ uint32_t width; @@ -39,9 +42,10 @@ struct dvledtx_config { /* optional log file path (empty = console only) */ char log_file[256]; - /* tx_sessions array — count drives st20p_sessions */ + /* tx_sessions array — dynamically allocated */ int session_count; - struct tx_session_config sessions[MAX_TX_SESSIONS]; + int session_cap; /* allocated capacity */ + struct tx_session_config* sessions; }; /* Quickly extract only the "log_file" value from a config file without a full parse. @@ -60,3 +64,6 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file); /* Convert sip_addr_str / dip_addr_str to packed binary (struct in_addr). * Call once after load_and_apply_config(). Returns 0 on success, -1 on error. */ int resolve_ip_addrs(struct dvledtx_context* ctx); + +/* Free dynamically allocated members of dvledtx_config (interface arrays, sessions). */ +void dvledtx_config_free(struct dvledtx_config* config); diff --git a/meson.build b/meson.build index 6152843..c7d8527 100644 --- a/meson.build +++ b/meson.build @@ -16,15 +16,22 @@ libswscale_dep = dependency('libswscale', required: true) # TX path selection — see meson_options.txt for details. enable_mtl_tx = get_option('enable_mtl_tx') -mtl_dep = [] +# mtl_tx.c always calls mtl_init()/mtl_uninit() (unconditionally, not just +# under ENABLE_MTL_TX) so that the FFmpeg avdevice path can pre-register all +# NIC ports with DPDK EAL before the mtl_st20p muxer opens its own session — +# see the comment above mtl_tx_init() in src/mtl/mtl_tx.c. MTL is therefore a +# required dependency for both TX paths, not just the direct-MTL one. +# +# Its include path comes entirely from `dependency('mtl')`'s own pkg-config +# Cflags (which already resolves to wherever MTL headers are installed, e.g. +# `-I/usr/local/include -I/usr/local/include/mtl`) — no hardcoded include +# path is added here, so this works regardless of MTL's install location. +mtl_dep = dependency('mtl', required: true) avdevice_dep = [] c_args = ['-DALLOW_EXPERIMENTAL_API', '-D_FORTIFY_SOURCE=2', '-O2'] -mtl_inc_args = [] if enable_mtl_tx - mtl_dep = dependency('mtl', required: true) c_args += ['-DENABLE_MTL_TX'] - mtl_inc_args = ['-I/usr/local/include/mtl'] else avdevice_dep = dependency('libavdevice', required: true) endif @@ -46,7 +53,7 @@ executable('dvledtx', ], include_directories: include_directories('include'), dependencies: all_deps, - c_args: c_args + mtl_inc_args, + c_args: c_args, link_args: ['-s'], build_rpath: '', install: false diff --git a/src/core/session_manager.c b/src/core/session_manager.c index 6246e82..6b82085 100644 --- a/src/core/session_manager.c +++ b/src/core/session_manager.c @@ -270,6 +270,8 @@ int session_manager_init(session_manager_t* manager, struct dvledtx_context* app memset(manager, 0, sizeof(*manager)); #ifdef ENABLE_MTL_TX + /* Direct MTL path: initialise the MTL library instance here so that + * st20p_tx_session_create() can call st20p_tx_create() immediately. */ if (mtl_tx_init(manager, app) < 0) return -1; #endif /* ENABLE_MTL_TX */ @@ -459,9 +461,7 @@ void session_manager_cleanup(session_manager_t* manager) { manager->shared_dec = NULL; } -#ifdef ENABLE_MTL_TX mtl_tx_uninit(manager); -#endif manager->st20p_count = 0; } diff --git a/src/ffmpeg/ffmpeg_tx.c b/src/ffmpeg/ffmpeg_tx.c index a37aa38..bb1b8f0 100644 --- a/src/ffmpeg/ffmpeg_tx.c +++ b/src/ffmpeg/ffmpeg_tx.c @@ -86,17 +86,45 @@ int open_ffmpeg_tx(struct st20p_tx_ctx* ctx) { int payload_type = ctx->app->session_net[ctx->idx].payload_type; if (payload_type == 0) payload_type = ctx->app->payload_type; - ret = av_opt_set (ctx->out_fmt_ctx->priv_data, "p_port", ctx->app->port, 0); + int nic = ctx->app->session_net[ctx->idx].nic_index; + + ret = av_opt_set (ctx->out_fmt_ctx->priv_data, "p_port", ctx->app->nics[nic].port, 0); if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set p_port failed (ret=%d)", ctx->idx, ret); - ret = av_opt_set (ctx->out_fmt_ctx->priv_data, "p_sip", ctx->app->sip_addr_str, 0); + ret = av_opt_set (ctx->out_fmt_ctx->priv_data, "p_sip", ctx->app->nics[nic].sip_addr_str, 0); if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set p_sip failed (ret=%d)", ctx->idx, ret); - ret = av_opt_set (ctx->out_fmt_ctx->priv_data, "p_tx_ip", ctx->app->dip_addr_str, 0); + ret = av_opt_set (ctx->out_fmt_ctx->priv_data, "p_tx_ip", ctx->app->nics[nic].dip_addr_str, 0); if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set p_tx_ip failed (ret=%d)", ctx->idx, ret); ret = av_opt_set_int(ctx->out_fmt_ctx->priv_data, "udp_port", (int64_t)udp_port, 0); if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set_int udp_port failed (ret=%d)", ctx->idx, ret); ret = av_opt_set_int(ctx->out_fmt_ctx->priv_data, "payload_type", (int64_t)payload_type, 0); if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set_int payload_type failed (ret=%d)", ctx->idx, ret); + /* Multi-NIC: The FFmpeg mtl_st20p plugin's mtl_dev_get() uses a singleton + * shared MTL handle — the FIRST avformat_write_header() call creates the + * MTL instance via mtl_init(), which initialises DPDK EAL. EAL cannot be + * re-initialised, so all NIC ports must be registered at that first call. + * + * Pass the second NIC as the "redundant" port (r_port / r_sip) on the + * first session so mtl_init() registers both ports with EAL. Subsequent + * sessions on either NIC will reuse the shared handle. + * + * ctx->idx == 0 identifies the first session deterministically. A + * function-static flag would never reset across multiple start/stop + * cycles or repeated init calls within the same process (e.g. unit + * tests), so later runs could silently skip this redundant-port + * registration even when nic_count == 2. */ + if (ctx->idx == 0 && ctx->app->nic_count == 2) { + int other = (nic == 0) ? 1 : 0; + ret = av_opt_set(ctx->out_fmt_ctx->priv_data, "r_port", ctx->app->nics[other].port, 0); + if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set r_port failed (ret=%d)", ctx->idx, ret); + ret = av_opt_set(ctx->out_fmt_ctx->priv_data, "r_sip", ctx->app->nics[other].sip_addr_str, 0); + if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set r_sip failed (ret=%d)", ctx->idx, ret); + ret = av_opt_set(ctx->out_fmt_ctx->priv_data, "r_tx_ip", ctx->app->nics[other].dip_addr_str, 0); + if (ret < 0) LOG_WARN("ST20P TX(%d): av_opt_set r_tx_ip failed (ret=%d)", ctx->idx, ret); + LOG_INFO("ST20P TX(%d): registering second NIC %s with MTL for multi-NIC support", + ctx->idx, ctx->app->nics[other].port); + } + /* RAWVIDEO stream — no encoder; MTL accepts raw packed pixel data directly. */ ctx->out_stream = avformat_new_stream(ctx->out_fmt_ctx, NULL); if (ctx->out_stream == NULL) { @@ -168,7 +196,7 @@ int open_ffmpeg_tx(struct st20p_tx_ctx* ctx) { ctx->pts = 0; LOG_INFO("ST20P TX(%d): ffmpeg_tx opened (%dx%d %s @ %dfps) -> %s:%u via %s", ctx->idx, out_w, height, ffmpeg_fmt_name(ctx->app->fmt), ctx->app->fps, - ctx->app->dip_addr_str, (unsigned)udp_port, ctx->app->port); + ctx->app->nics[nic].dip_addr_str, (unsigned)udp_port, ctx->app->nics[nic].port); return 0; } diff --git a/src/main.c b/src/main.c index 43746d5..2609e14 100644 --- a/src/main.c +++ b/src/main.c @@ -310,7 +310,9 @@ int main(int argc, char** argv) { } LOG_INFO("dvledtx started successfully"); - LOG_INFO("Port: %s, DIP: %s, UDP: %d", app.port, app.dip_addr_str, app.udp_port); + if (app.nic_count > 0) + LOG_INFO("Port[0]: %s, DIP[0]: %s, UDP: %d", + app.nics[0].port, app.nics[0].dip_addr_str, app.udp_port); LOG_INFO("Video: %dx%d, ST20P sessions: %d", app.width, app.height, app.st20p_sessions); @@ -339,6 +341,7 @@ int main(int argc, char** argv) { cleanup: /* Stop and cleanup session manager */ session_manager_cleanup(&session_manager); + dvledtx_context_free(&app); LOG_INFO("dvledtx shutdown complete"); cleanup_logger: /* Cleanup logger */ diff --git a/src/mtl/mtl_tx.c b/src/mtl/mtl_tx.c index fdff0a7..a39d74d 100644 --- a/src/mtl/mtl_tx.c +++ b/src/mtl/mtl_tx.c @@ -22,17 +22,21 @@ * mtl_tx_send_raw_yuv() — get MTL frame, memcpy raw buffer, put frame */ -#ifdef ENABLE_MTL_TX - +/* mtl_tx_init / mtl_tx_uninit are compiled unconditionally so that the + * FFmpeg avdevice path can also pre-initialise DPDK EAL with ALL NIC ports + * before opening any mtl_st20p session (fixes multi-NIC support). + * All other functions remain guarded by ENABLE_MTL_TX. */ #include "mtl/mtl_tx.h" #include "app_context.h" #include "core/session_manager.h" #include "util/logger.h" -#include #include #include #include +#ifdef ENABLE_MTL_TX +#include + /* ========================================================================= * get_input_format / get_transport_format * ========================================================================= @@ -181,8 +185,10 @@ void mtl_copy_crop_to_frame(struct st_frame* dst, const AVFrame* src, } } +#endif /* ENABLE_MTL_TX — mtl_copy_crop_to_frame and format helpers */ + /* ========================================================================= - * mtl_tx_init / mtl_tx_uninit + * mtl_tx_init / mtl_tx_uninit — compiled unconditionally (FFmpeg + direct) * ========================================================================= * * mtl_tx_init() — initialise the MTL library using parameters from app. @@ -196,26 +202,43 @@ int mtl_tx_init(session_manager_t* manager, struct dvledtx_context* app) { struct mtl_init_params mtl_params; memset(&mtl_params, 0, sizeof(mtl_params)); - mtl_params.flags = MTL_FLAG_BIND_NUMA | MTL_FLAG_DEV_AUTO_START_STOP; - mtl_params.num_ports = 1; - snprintf(mtl_params.port[MTL_PORT_P], MTL_PORT_MAX_LEN, "%s", app->port); - memcpy(mtl_params.sip_addr[MTL_PORT_P], app->sip_addr, MTL_IP_ADDR_LEN); - mtl_params.pmd[MTL_PORT_P] = mtl_pmd_by_port_name(app->port); + mtl_params.flags = MTL_FLAG_BIND_NUMA | MTL_FLAG_DEV_AUTO_START_STOP; + mtl_params.num_ports = app->nic_count; + + /* Count sessions assigned to each NIC for queue allocation */ + int* sessions_per_nic = calloc((size_t)app->nic_count, sizeof(int)); + if (sessions_per_nic == NULL) { + LOG_ERROR("Failed to allocate sessions_per_nic array"); + return -1; + } + for (int i = 0; i < app->st20p_sessions; i++) { + int ni = app->session_net[i].nic_index; + if (ni >= 0 && ni < app->nic_count) + sessions_per_nic[ni]++; + } + + for (int ni = 0; ni < app->nic_count; ni++) { + snprintf(mtl_params.port[ni], MTL_PORT_MAX_LEN, "%s", app->nics[ni].port); + memcpy(mtl_params.sip_addr[ni], app->nics[ni].sip_addr, MTL_IP_ADDR_LEN); + mtl_params.pmd[ni] = mtl_pmd_by_port_name(app->nics[ni].port); - uint16_t tx_queues = (uint16_t)(app->st20p_sessions + 2); - uint16_t rx_queues = 2; - mtl_params.tx_queues_cnt[MTL_PORT_P] = tx_queues; - mtl_params.rx_queues_cnt[MTL_PORT_P] = rx_queues; + uint16_t tx_queues = (uint16_t)(sessions_per_nic[ni] + 2); + uint16_t rx_queues = 1; /* minimal RX for control traffic; MTL adds 1 system queue */ + mtl_params.tx_queues_cnt[ni] = tx_queues; + mtl_params.rx_queues_cnt[ni] = rx_queues; + + LOG_INFO("MTL init: port[%d]=%s pmd=%d tx_queues=%d rx_queues=%d", + ni, app->nics[ni].port, mtl_params.pmd[ni], tx_queues, rx_queues); + } - LOG_INFO("MTL init: port=%s pmd=%d tx_queues=%d rx_queues=%d", - app->port, mtl_params.pmd[MTL_PORT_P], tx_queues, rx_queues); + free(sessions_per_nic); manager->mtl = mtl_init(&mtl_params); if (!manager->mtl) { LOG_ERROR("Failed to initialise MTL library"); return -1; } - LOG_INFO("MTL library initialised successfully"); + LOG_INFO("MTL library initialised successfully (%d port(s))", app->nic_count); return 0; } @@ -226,6 +249,11 @@ void mtl_tx_uninit(session_manager_t* manager) { } } +/* ========================================================================= + * Everything below requires the direct MTL TX path (ENABLE_MTL_TX). + * ========================================================================= */ +#ifdef ENABLE_MTL_TX + /* ========================================================================= * mtl_tx_session_create / mtl_tx_session_free * ========================================================================= @@ -244,8 +272,9 @@ int mtl_tx_session_create(session_manager_t* manager, struct st20p_tx_ctx* ctx, ops.priv = ctx; ops.port.num_port = 1; - memcpy(ops.port.dip_addr[MTL_SESSION_PORT_P], app->dip_addr, MTL_IP_ADDR_LEN); - snprintf(ops.port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", app->port); + int nic = app->session_net[session_idx].nic_index; + memcpy(ops.port.dip_addr[MTL_SESSION_PORT_P], app->nics[nic].dip_addr, MTL_IP_ADDR_LEN); + snprintf(ops.port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", app->nics[nic].port); int udp_port = app->session_net[session_idx].udp_port; if (udp_port == 0) udp_port = (int)app->udp_port + (session_idx * 2); @@ -315,8 +344,16 @@ int mtl_tx_send_yuv_frame(struct st20p_tx_ctx* ctx, const AVFrame* src, mtl_copy_crop_to_frame(frame, src, crop_x, crop_y, crop_w, crop_h, ctx->app->fmt); + /* Use the shared frame counter for RTP timestamp so ALL sessions stamp the + * same video frame identically — prevents inter-session clock drift. + * Per-session frames_sent diverges under thread scheduling jitter. + * frame_counter is incremented by the decode thread after all TX threads + * have consumed the frame (post barrier_copied), so it is stable here. */ + uint32_t frame_num = ctx->shared_dec + ? (uint32_t)atomic_load(&ctx->shared_dec->frame_counter) + : ctx->frames_sent; frame->tfmt = ST10_TIMESTAMP_FMT_MEDIA_CLK; - frame->timestamp = ctx->frames_sent * 90000 / (uint32_t)ctx->app->fps; + frame->timestamp = frame_num * 90000 / (uint32_t)ctx->app->fps; int ret = st20p_tx_put_frame(ctx->handle, frame); if (ret < 0) { @@ -357,6 +394,8 @@ int mtl_tx_send_raw_yuv(struct st20p_tx_ctx* ctx) { memcpy(frame->addr[0], ctx->source_buffer + ctx->current_pos, frame_bytes); ctx->current_pos += frame_bytes; + /* raw YUV path is always single-session (no shared_dec); per-session + * frames_sent is the correct counter here. */ frame->tfmt = ST10_TIMESTAMP_FMT_MEDIA_CLK; frame->timestamp = ctx->frames_sent * 90000 / (uint32_t)ctx->app->fps; diff --git a/src/util/config_reader.c b/src/util/config_reader.c index e9d7b48..f480ea8 100644 --- a/src/util/config_reader.c +++ b/src/util/config_reader.c @@ -232,24 +232,72 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { const char* buf_end = json + nread; memset(config, 0, sizeof(*config)); - /* --- interfaces[0] --- */ + /* --- interfaces[] — parse all entries, growing arrays as needed --- */ const char* ifaces_arr = find_array(json, buf_end, "interfaces"); if (ifaces_arr != NULL) { const char* ifaces_end = find_array_end(ifaces_arr, buf_end); if (ifaces_end != NULL) { - const char* first_brace = ifaces_arr + 1; - while (first_brace < ifaces_end && *first_brace != '{') first_brace++; - if (first_brace < ifaces_end) { - const char* iface_obj = first_brace; + const char* icursor = ifaces_arr + 1; + config->nic_count = 0; + config->nic_cap = 0; + while (icursor < ifaces_end) { + while (icursor < ifaces_end && *icursor != '{') icursor++; + if (icursor >= ifaces_end) break; + const char* iface_obj = icursor; const char* iface_end = find_object_end(iface_obj, ifaces_end); if (iface_end == NULL) { - LOG_WARN("interfaces[0] object not properly closed; " - "truncating parse at array end"); - iface_end = ifaces_end; + LOG_WARN("interfaces[%d]: object not properly closed; stopping parse", + config->nic_count); + break; } - extract_json_string(iface_obj, iface_end, "name", config->interface_name, sizeof(config->interface_name)); - extract_json_string(iface_obj, iface_end, "sip", config->interface_sip, sizeof(config->interface_sip)); - extract_json_string(iface_obj, iface_end, "dip", config->interface_dip, sizeof(config->interface_dip)); + /* Grow arrays if needed. Each realloc() is assigned back + * to config-> immediately after it succeeds, rather than + * only after all three succeed. Otherwise, if e.g. the + * first realloc() succeeds (freeing/moving the old buffer) + * but a later one fails, the successfully-reallocated + * buffer would never be stored in config and would leak, + * while config's stale copy of that pointer would be a + * dangling reference that dvledtx_config_free() would then + * double-free. */ + if (config->nic_count >= config->nic_cap) { + int old_cap = config->nic_cap; + int new_cap = old_cap == 0 ? 4 : old_cap * 2; + + char (*nn)[64] = realloc(config->interface_name, (size_t)new_cap * sizeof(*nn)); + if (!nn) { + LOG_ERROR("realloc failed for interface_name array"); + free(json); dvledtx_config_free(config); return -1; + } + config->interface_name = nn; + memset(nn + old_cap, 0, (size_t)(new_cap - old_cap) * sizeof(*nn)); + + char (*ns)[32] = realloc(config->interface_sip, (size_t)new_cap * sizeof(*ns)); + if (!ns) { + LOG_ERROR("realloc failed for interface_sip array"); + free(json); dvledtx_config_free(config); return -1; + } + config->interface_sip = ns; + memset(ns + old_cap, 0, (size_t)(new_cap - old_cap) * sizeof(*ns)); + + char (*nd)[32] = realloc(config->interface_dip, (size_t)new_cap * sizeof(*nd)); + if (!nd) { + LOG_ERROR("realloc failed for interface_dip array"); + free(json); dvledtx_config_free(config); return -1; + } + config->interface_dip = nd; + memset(nd + old_cap, 0, (size_t)(new_cap - old_cap) * sizeof(*nd)); + + config->nic_cap = new_cap; + } + int n = config->nic_count; + extract_json_string(iface_obj, iface_end, "name", + config->interface_name[n], sizeof(config->interface_name[n])); + extract_json_string(iface_obj, iface_end, "sip", + config->interface_sip[n], sizeof(config->interface_sip[n])); + extract_json_string(iface_obj, iface_end, "dip", + config->interface_dip[n], sizeof(config->interface_dip[n])); + config->nic_count++; + icursor = iface_end + 1; } } } @@ -293,8 +341,9 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { /* Walk the array, extracting each '{...}' object */ const char* cursor = sessions_arr + 1; /* skip '[' */ config->session_count = 0; + config->session_cap = 0; - while (cursor < sessions_end && config->session_count < MAX_TX_SESSIONS) { + while (cursor < sessions_end) { /* advance to next '{' */ while (cursor < sessions_end && *cursor != '{') cursor++; if (cursor >= sessions_end) break; @@ -303,6 +352,21 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { const char* sess_end = find_object_end(sess_obj, sessions_end); if (sess_end == NULL) break; + /* Grow sessions array if needed */ + if (config->session_count >= config->session_cap) { + int new_cap = config->session_cap == 0 ? 8 : config->session_cap * 2; + struct tx_session_config* ns = realloc(config->sessions, + (size_t)new_cap * sizeof(*ns)); + if (!ns) { + LOG_ERROR("realloc failed for sessions array"); + free(json); dvledtx_config_free(config); return -1; + } + memset(ns + config->session_cap, 0, + (size_t)(new_cap - config->session_cap) * sizeof(*ns)); + config->sessions = ns; + config->session_cap = new_cap; + } + struct tx_session_config* s = &config->sessions[config->session_count]; int v; @@ -323,6 +387,9 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { s->payload_type = 96; /* default to 96 (first dynamic RTP payload type) */ } + v = extract_json_int(sess_obj, sess_end, "nic_index"); + s->nic_index = (v >= 0) ? v : 0; /* default to NIC 0 for backward compat */ + /* crop sub-object */ const char* crop_obj = find_object(sess_obj, sess_end, "crop"); if (crop_obj == NULL) { @@ -361,64 +428,90 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { return 0; } +void dvledtx_config_free(struct dvledtx_config* config) { + if (config == NULL) return; + free(config->interface_name); config->interface_name = NULL; + free(config->interface_sip); config->interface_sip = NULL; + free(config->interface_dip); config->interface_dip = NULL; + free(config->sessions); config->sessions = NULL; + config->nic_count = config->nic_cap = 0; + config->session_count = config->session_cap = 0; +} + int validate_tx_config(const struct dvledtx_config* config) { - /* Interface validation */ - if (config->interface_name[0] == '\0') { - LOG_ERROR("interfaces[0].name is required"); + /* Interface validation — loop over all parsed NICs */ + if (config->nic_count == 0) { + LOG_ERROR("'interfaces' array is empty or missing"); return -1; } - if (config->interface_dip[0] == '\0') { - LOG_ERROR("interfaces[0].dip is required"); + + regex_t bdf_regex; + int reti = regcomp(&bdf_regex, + "^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\\.[0-9]$", + REG_EXTENDED | REG_NOSUB); + if (reti != 0) { + LOG_ERROR("Internal error: PCI BDF regex compilation failed"); return -1; } - /* Validate IP address format (basic dotted-quad check) */ - if (config->interface_sip[0] != '\0') { - struct in_addr tmp; - if (inet_pton(AF_INET, config->interface_sip, &tmp) != 1) { - LOG_ERROR("Invalid source IP address '%s'", config->interface_sip); - return -1; - } - } - { - struct in_addr tmp; - if (inet_pton(AF_INET, config->interface_dip, &tmp) != 1) { - LOG_ERROR("Invalid destination IP address '%s'", config->interface_dip); + for (int ni = 0; ni < config->nic_count; ni++) { + if (config->interface_name[ni][0] == '\0') { + LOG_ERROR("interfaces[%d].name is required", ni); + regfree(&bdf_regex); return -1; } - /* D-3: Validate DIP is within multicast range (224.0.0.0/4) */ - uint32_t dip_host = ntohl(tmp.s_addr); - if ((dip_host & 0xF0000000U) != 0xE0000000U) { - LOG_ERROR("Destination IP '%s' is not a valid multicast address " - "(must be in 224.0.0.0/4)", config->interface_dip); + if (config->interface_dip[ni][0] == '\0') { + LOG_ERROR("interfaces[%d].dip is required", ni); + regfree(&bdf_regex); return -1; } - /* Warn if not in administratively-scoped range 239.0.0.0/8 */ - if ((dip_host >> 24) != 239) { - LOG_WARN("Destination IP '%s' is outside the administratively-scoped " - "multicast range (239.0.0.0/8)", config->interface_dip); + + /* Validate source IP format */ + if (config->interface_sip[ni][0] != '\0') { + struct in_addr tmp; + if (inet_pton(AF_INET, config->interface_sip[ni], &tmp) != 1) { + LOG_ERROR("interfaces[%d]: invalid source IP address '%s'", + ni, config->interface_sip[ni]); + regfree(&bdf_regex); + return -1; + } } - } - /* S-2: Validate PCI BDF format (e.g. "0000:06:00.0") */ - if (config->interface_name[0] != '\0') { - regex_t regex; - int reti = regcomp(®ex, - "^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\\.[0-9]$", - REG_EXTENDED | REG_NOSUB); - if (reti != 0) { - LOG_ERROR("Internal error: PCI BDF regex compilation failed"); - return -1; + /* Validate destination IP — must be multicast */ + { + struct in_addr tmp; + if (inet_pton(AF_INET, config->interface_dip[ni], &tmp) != 1) { + LOG_ERROR("interfaces[%d]: invalid destination IP address '%s'", + ni, config->interface_dip[ni]); + regfree(&bdf_regex); + return -1; + } + uint32_t dip_host = ntohl(tmp.s_addr); + if ((dip_host & 0xF0000000U) != 0xE0000000U) { + LOG_ERROR("interfaces[%d]: destination IP '%s' is not a valid " + "multicast address (must be in 224.0.0.0/4)", + ni, config->interface_dip[ni]); + regfree(&bdf_regex); + return -1; + } + if ((dip_host >> 24) != 239) { + LOG_WARN("interfaces[%d]: destination IP '%s' is outside the " + "administratively-scoped multicast range (239.0.0.0/8)", + ni, config->interface_dip[ni]); + } } - reti = regexec(®ex, config->interface_name, 0, NULL, 0); - regfree(®ex); + + /* S-2: Validate PCI BDF format (e.g. "0000:06:00.0") */ + reti = regexec(&bdf_regex, config->interface_name[ni], 0, NULL, 0); if (reti != 0) { - LOG_ERROR("Invalid PCI BDF format '%s' " - "(expected DDDD:DD:DD.D hex pattern)", - config->interface_name); + LOG_ERROR("interfaces[%d]: invalid PCI BDF format '%s' " + "(expected DDDD:DD:DD.D hex pattern)", + ni, config->interface_name[ni]); + regfree(&bdf_regex); return -1; } } + regfree(&bdf_regex); /* Video resolution validation */ if (config->width == 0 || config->height == 0) { @@ -514,6 +607,13 @@ int validate_tx_config(const struct dvledtx_config* config) { for (int i = 0; i < config->session_count; i++) { const struct tx_session_config* s = &config->sessions[i]; + /* nic_index must reference a parsed interface */ + if (s->nic_index < 0 || s->nic_index >= config->nic_count) { + LOG_ERROR("session %d: nic_index %d is out of range [0..%d]", + i, s->nic_index, config->nic_count - 1); + return -1; + } + /* UDP port range */ if (s->udp_port == 0) { LOG_ERROR("session %d: udp_port must be non-zero", i); @@ -592,11 +692,13 @@ int validate_tx_config(const struct dvledtx_config* config) { } } - /* Check for duplicate UDP ports */ + /* Check for duplicate UDP ports on the same NIC + * (same port on different NICs is valid — separate interfaces) */ for (int j = 0; j < i; j++) { - if (config->sessions[j].udp_port == s->udp_port) { - LOG_ERROR("session %d and %d have duplicate udp_port %d", - j, i, s->udp_port); + if (config->sessions[j].udp_port == s->udp_port && + config->sessions[j].nic_index == s->nic_index) { + LOG_ERROR("session %d and %d have duplicate udp_port %d on NIC %d", + j, i, s->udp_port, s->nic_index); return -1; } } @@ -612,26 +714,42 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) return 0; /* no config file — keep CLI defaults */ struct dvledtx_config config; + memset(&config, 0, sizeof(config)); if (parse_tx_config(config_file, &config) != 0) { LOG_WARN("Failed to parse config file %s", config_file); + dvledtx_config_free(&config); return -1; } if (validate_tx_config(&config) != 0) { LOG_WARN("Invalid config file %s", config_file); + dvledtx_config_free(&config); return -1; } - /* Interface */ - strncpy(app->port, config.interface_name, sizeof(app->port) - 1); - app->port[sizeof(app->port) - 1] = '\0'; - - if (config.interface_sip[0] != '\0') { - strncpy(app->sip_addr_str, config.interface_sip, sizeof(app->sip_addr_str) - 1); - app->sip_addr_str[sizeof(app->sip_addr_str) - 1] = '\0'; + /* Allocate dynamic arrays in app context */ + dvledtx_context_free(app); /* free any previous allocation */ + if (dvledtx_context_alloc(app, config.nic_count, config.session_count) < 0) { + LOG_ERROR("Failed to allocate app context arrays"); + dvledtx_config_free(&config); + return -1; } - strncpy(app->dip_addr_str, config.interface_dip, sizeof(app->dip_addr_str) - 1); - app->dip_addr_str[sizeof(app->dip_addr_str) - 1] = '\0'; + /* Interface — copy all NICs */ + for (int ni = 0; ni < config.nic_count; ni++) { + strncpy(app->nics[ni].port, config.interface_name[ni], + sizeof(app->nics[ni].port) - 1); + app->nics[ni].port[sizeof(app->nics[ni].port) - 1] = '\0'; + + if (config.interface_sip[ni][0] != '\0') { + strncpy(app->nics[ni].sip_addr_str, config.interface_sip[ni], + sizeof(app->nics[ni].sip_addr_str) - 1); + app->nics[ni].sip_addr_str[sizeof(app->nics[ni].sip_addr_str) - 1] = '\0'; + } + + strncpy(app->nics[ni].dip_addr_str, config.interface_dip[ni], + sizeof(app->nics[ni].dip_addr_str) - 1); + app->nics[ni].dip_addr_str[sizeof(app->nics[ni].dip_addr_str) - 1] = '\0'; + } /* Video */ app->width = config.width; @@ -649,6 +767,7 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) else if (strcmp(config.fmt, "gbrp12le") == 0) app->fmt = AV_PIX_FMT_GBRP12LE; else { LOG_ERROR("Unsupported pixel format '%s'", config.fmt); + dvledtx_config_free(&config); return -1; } @@ -657,9 +776,6 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) app->tx_url[sizeof(app->tx_url) - 1] = '\0'; } - /* Sessions — count drives how many TX sessions are created */ - app->st20p_sessions = config.session_count; - /* Copy per-session network + crop into app->session_net[] */ for (int i = 0; i < config.session_count; i++) { app->session_net[i].udp_port = config.sessions[i].udp_port; @@ -668,6 +784,7 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) app->session_net[i].crop_y = config.sessions[i].crop_y; app->session_net[i].crop_w = config.sessions[i].crop_w; app->session_net[i].crop_h = config.sessions[i].crop_h; + app->session_net[i].nic_index = config.sessions[i].nic_index; } /* Use first session's udp_port as the legacy app->udp_port @@ -681,10 +798,13 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) app->log_file[sizeof(app->log_file) - 1] = '\0'; } - LOG_INFO("Config loaded: %s (interface=%s sip=%s dip=%s)", - config_file, config.interface_name, - config.interface_sip[0] ? config.interface_sip : "dhcp", - config.interface_dip); + LOG_INFO("Config loaded: %s (%d NIC(s), %d session(s))", + config_file, config.nic_count, config.session_count); + for (int ni = 0; ni < config.nic_count; ni++) + LOG_INFO(" NIC[%d]: interface=%s sip=%s dip=%s", ni, + config.interface_name[ni], + config.interface_sip[ni][0] ? config.interface_sip[ni] : "dhcp", + config.interface_dip[ni]); if (config.scale_width > 0 && config.scale_height > 0) LOG_INFO("Video: %ux%u -> scale %ux%u %dfps %s tx_url=%s", config.width, config.height, @@ -696,11 +816,13 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) config.width, config.height, config.fps, config.fmt, config.tx_url[0] ? config.tx_url : ""); for (int i = 0; i < config.session_count; i++) - LOG_INFO(" Session %d: udp_port=%u pt=%u crop=[%d,%d %dx%d]", i, + LOG_INFO(" Session %d: udp_port=%u pt=%u nic=%d crop=[%d,%d %dx%d]", i, config.sessions[i].udp_port, config.sessions[i].payload_type, + config.sessions[i].nic_index, config.sessions[i].crop_x, config.sessions[i].crop_y, config.sessions[i].crop_w, config.sessions[i].crop_h); + dvledtx_config_free(&config); return 0; } @@ -715,17 +837,23 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) * dip_addr_str is mandatory — the multicast destination IP must be valid. * -------------------------------------------------------------------------*/ int resolve_ip_addrs(struct dvledtx_context* ctx) { - if (ctx->sip_addr_str[0] != '\0') { - if (inet_pton(AF_INET, ctx->sip_addr_str, ctx->sip_addr) != 1) { - LOG_ERROR("Invalid source IP address %s", ctx->sip_addr_str); + for (int ni = 0; ni < ctx->nic_count; ni++) { + if (ctx->nics[ni].sip_addr_str[0] != '\0') { + if (inet_pton(AF_INET, ctx->nics[ni].sip_addr_str, + ctx->nics[ni].sip_addr) != 1) { + LOG_ERROR("NIC[%d]: invalid source IP address %s", + ni, ctx->nics[ni].sip_addr_str); + return -1; + } + } else { + LOG_INFO("NIC[%d]: no source IP provided, DHCP mode", ni); + } + if (inet_pton(AF_INET, ctx->nics[ni].dip_addr_str, + ctx->nics[ni].dip_addr) != 1) { + LOG_ERROR("NIC[%d]: invalid destination IP address %s", + ni, ctx->nics[ni].dip_addr_str); return -1; } - } else { - LOG_INFO("No source IP provided, DHCP mode"); - } - if (inet_pton(AF_INET, ctx->dip_addr_str, ctx->dip_addr) != 1) { - LOG_ERROR("Invalid destination IP address %s", ctx->dip_addr_str); - return -1; } return 0; } diff --git a/tests/meson.build b/tests/meson.build index 61763c2..90cb745 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -25,7 +25,7 @@ all_deps = [m_dep, pthread_dep, libavformat_dep, libavcodec_dep, libavutil_dep, libswscale_dep, avdevice_dep] fixture_dir = meson.project_source_root() / '../config' -test_inc = include_directories('../include') +test_inc = include_directories('../include', '/usr/local/include/mtl') test_deps = all_deps + [cmocka_dep] test_c_args = ['-DALLOW_EXPERIMENTAL_API'] @@ -100,10 +100,11 @@ test_session_manager = executable('test_session_manager', sources: [ 'test_session_manager.c', '../src/core/session_manager.c', + '../src/mtl/mtl_tx.c', '../src/util/logger.c', ], include_directories: test_inc, - dependencies: test_deps, + dependencies: test_deps + (mtl_dep.found() ? [mtl_dep] : []), c_args: test_c_args, install: false ) diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index ca9648a..bb9609c 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -60,14 +60,24 @@ static char *write_tmpfile(const char *content) static void fill_valid_config(struct dvledtx_config *cfg) { memset(cfg, 0, sizeof(*cfg)); - strncpy(cfg->interface_name, "0000:06:00.0", sizeof(cfg->interface_name) - 1); - strncpy(cfg->interface_sip, "192.168.50.29", sizeof(cfg->interface_sip) - 1); - strncpy(cfg->interface_dip, "239.168.85.20", sizeof(cfg->interface_dip) - 1); + + /* Allocate dynamic arrays */ + cfg->nic_cap = 1; + cfg->interface_name = calloc(1, sizeof(*cfg->interface_name)); + cfg->interface_sip = calloc(1, sizeof(*cfg->interface_sip)); + cfg->interface_dip = calloc(1, sizeof(*cfg->interface_dip)); + cfg->nic_count = 1; + + strncpy(cfg->interface_name[0], "0000:06:00.0", sizeof(cfg->interface_name[0]) - 1); + strncpy(cfg->interface_sip[0], "192.168.50.29", sizeof(cfg->interface_sip[0]) - 1); + strncpy(cfg->interface_dip[0], "239.168.85.20", sizeof(cfg->interface_dip[0]) - 1); cfg->width = 1920; cfg->height = 1080; cfg->fps = 30; strncpy(cfg->fmt, "yuv422p10le", sizeof(cfg->fmt) - 1); /* tx_url intentionally left empty — skips file-open check in validate */ + cfg->session_cap = 1; + cfg->sessions = calloc(1, sizeof(*cfg->sessions)); cfg->session_count = 1; cfg->sessions[0].udp_port = 20000; cfg->sessions[0].payload_type = 96; @@ -119,9 +129,9 @@ static void test_parse_3sessions_interface_fields(void **state) (void)state; struct dvledtx_config cfg; assert_int_equal(parse_tx_config(FIXTURE_3SESSIONS, &cfg), 0); - assert_string_equal(cfg.interface_name, "0000:06:00.0"); - assert_string_equal(cfg.interface_sip, "192.168.50.29"); - assert_string_equal(cfg.interface_dip, "239.168.85.20"); + assert_string_equal(cfg.interface_name[0], "0000:03:10.1"); + assert_string_equal(cfg.interface_sip[0], "192.168.50.29"); + assert_string_equal(cfg.interface_dip[0], "239.168.85.21"); } static void test_parse_3sessions_video_params(void **state) @@ -255,6 +265,7 @@ static void test_validate_valid_config_passes(void **state) struct dvledtx_config cfg; fill_valid_config(&cfg); assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_missing_interface_name_fails(void **state) @@ -262,8 +273,9 @@ static void test_validate_missing_interface_name_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - cfg.interface_name[0] = '\0'; + cfg.interface_name[0][0] = '\0'; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_missing_dip_fails(void **state) @@ -271,8 +283,9 @@ static void test_validate_missing_dip_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - cfg.interface_dip[0] = '\0'; + cfg.interface_dip[0][0] = '\0'; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_invalid_sip_fails(void **state) @@ -280,8 +293,9 @@ static void test_validate_invalid_sip_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_sip, "not.an.ip.address", sizeof(cfg.interface_sip) - 1); + strncpy(cfg.interface_sip[0], "not.an.ip.address", sizeof(cfg.interface_sip[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_invalid_dip_fails(void **state) @@ -289,8 +303,9 @@ static void test_validate_invalid_dip_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_dip, "999.999.999.999", sizeof(cfg.interface_dip) - 1); + strncpy(cfg.interface_dip[0], "999.999.999.999", sizeof(cfg.interface_dip[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_zero_width_fails(void **state) @@ -300,6 +315,7 @@ static void test_validate_zero_width_fails(void **state) fill_valid_config(&cfg); cfg.width = 0; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_zero_height_fails(void **state) @@ -309,6 +325,7 @@ static void test_validate_zero_height_fails(void **state) fill_valid_config(&cfg); cfg.height = 0; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_odd_width_fails(void **state) @@ -318,6 +335,7 @@ static void test_validate_odd_width_fails(void **state) fill_valid_config(&cfg); cfg.width = 1921; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_supported_fps_values_all_pass(void **state) @@ -329,6 +347,7 @@ static void test_validate_supported_fps_values_all_pass(void **state) fill_valid_config(&cfg); cfg.fps = valid_fps[i]; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } } @@ -339,6 +358,7 @@ static void test_validate_unsupported_fps_fails(void **state) fill_valid_config(&cfg); cfg.fps = 24; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_unsupported_fmt_fails(void **state) @@ -348,6 +368,7 @@ static void test_validate_unsupported_fmt_fails(void **state) fill_valid_config(&cfg); strncpy(cfg.fmt, "rgb24", sizeof(cfg.fmt) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_all_supported_fmts_pass(void **state) @@ -360,6 +381,7 @@ static void test_validate_all_supported_fmts_pass(void **state) fill_valid_config(&cfg); strncpy(cfg.fmt, fmts[i], sizeof(cfg.fmt) - 1); assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } } @@ -370,6 +392,7 @@ static void test_validate_zero_udp_port_fails(void **state) fill_valid_config(&cfg); cfg.sessions[0].udp_port = 0; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_payload_type_below_96_fails(void **state) @@ -379,6 +402,7 @@ static void test_validate_payload_type_below_96_fails(void **state) fill_valid_config(&cfg); cfg.sessions[0].payload_type = 95; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_payload_type_above_127_fails(void **state) @@ -388,6 +412,7 @@ static void test_validate_payload_type_above_127_fails(void **state) fill_valid_config(&cfg); cfg.sessions[0].payload_type = 128; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_payload_type_boundary_96_passes(void **state) @@ -397,6 +422,7 @@ static void test_validate_payload_type_boundary_96_passes(void **state) fill_valid_config(&cfg); cfg.sessions[0].payload_type = 96; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_payload_type_boundary_127_passes(void **state) @@ -406,6 +432,7 @@ static void test_validate_payload_type_boundary_127_passes(void **state) fill_valid_config(&cfg); cfg.sessions[0].payload_type = 127; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_crop_x_plus_w_exceeds_width_fails(void **state) @@ -416,6 +443,7 @@ static void test_validate_crop_x_plus_w_exceeds_width_fails(void **state) cfg.sessions[0].crop_x = 100; cfg.sessions[0].crop_w = 1920; /* 100 + 1920 = 2020 > 1920 */ assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_crop_y_plus_h_exceeds_height_fails(void **state) @@ -426,6 +454,7 @@ static void test_validate_crop_y_plus_h_exceeds_height_fails(void **state) cfg.sessions[0].crop_y = 100; cfg.sessions[0].crop_h = 1080; /* 100 + 1080 = 1180 > 1080 */ assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_odd_crop_w_fails(void **state) @@ -435,6 +464,7 @@ static void test_validate_odd_crop_w_fails(void **state) fill_valid_config(&cfg); cfg.sessions[0].crop_w = 641; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_zero_session_count_fails(void **state) @@ -444,6 +474,7 @@ static void test_validate_zero_session_count_fails(void **state) fill_valid_config(&cfg); cfg.session_count = 0; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_3sessions_tiled_layout_passes(void **state) @@ -452,13 +483,23 @@ static void test_validate_3sessions_tiled_layout_passes(void **state) /* Mirrors tx_fullhd_multi_session.json: three 640-wide horizontal tiles */ struct dvledtx_config cfg; memset(&cfg, 0, sizeof(cfg)); - strncpy(cfg.interface_name, "0000:06:00.0", sizeof(cfg.interface_name) - 1); - strncpy(cfg.interface_sip, "192.168.50.29", sizeof(cfg.interface_sip) - 1); - strncpy(cfg.interface_dip, "239.168.85.20", sizeof(cfg.interface_dip) - 1); + + cfg.nic_cap = 1; + cfg.interface_name = calloc(1, sizeof(*cfg.interface_name)); + cfg.interface_sip = calloc(1, sizeof(*cfg.interface_sip)); + cfg.interface_dip = calloc(1, sizeof(*cfg.interface_dip)); + cfg.nic_count = 1; + + strncpy(cfg.interface_name[0], "0000:06:00.0", sizeof(cfg.interface_name[0]) - 1); + strncpy(cfg.interface_sip[0], "192.168.50.29", sizeof(cfg.interface_sip[0]) - 1); + strncpy(cfg.interface_dip[0], "239.168.85.20", sizeof(cfg.interface_dip[0]) - 1); cfg.width = 1920; cfg.height = 1080; cfg.fps = 30; strncpy(cfg.fmt, "yuv420", sizeof(cfg.fmt) - 1); + + cfg.session_cap = 3; + cfg.sessions = calloc(3, sizeof(*cfg.sessions)); cfg.session_count = 3; const uint16_t ports[] = {20000, 20002, 20004}; @@ -472,6 +513,7 @@ static void test_validate_3sessions_tiled_layout_passes(void **state) cfg.sessions[i].crop_h = 1080; } assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } /* ========================================================================== @@ -605,6 +647,7 @@ static void test_validate_resolution_exceeds_max_fails(void **state) fill_valid_config(&cfg); cfg.width = 4000; /* > 3840 limit */ assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_2k_resolution_passes(void **state) @@ -617,6 +660,7 @@ static void test_validate_2k_resolution_passes(void **state) cfg.sessions[0].crop_w = 2560; cfg.sessions[0].crop_h = 1440; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_4k_resolution_passes(void **state) @@ -629,6 +673,7 @@ static void test_validate_4k_resolution_passes(void **state) cfg.sessions[0].crop_w = 3840; cfg.sessions[0].crop_h = 2160; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_4k_height_exceeds_max_fails(void **state) @@ -639,6 +684,7 @@ static void test_validate_4k_height_exceeds_max_fails(void **state) cfg.width = 3840; cfg.height = 2162; /* > 2160 limit */ assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* ========================================================================== @@ -657,6 +703,7 @@ static void test_validate_scale_upscale_1080p_to_4k_passes(void **state) cfg.sessions[0].crop_w = 3840; cfg.sessions[0].crop_h = 2160; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_scale_downscale_4k_to_1080p_passes(void **state) @@ -671,6 +718,7 @@ static void test_validate_scale_downscale_4k_to_1080p_passes(void **state) cfg.sessions[0].crop_w = 1920; cfg.sessions[0].crop_h = 1080; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_scale_width_only_fails(void **state) @@ -681,6 +729,7 @@ static void test_validate_scale_width_only_fails(void **state) cfg.scale_width = 3840; cfg.scale_height = 0; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_scale_height_only_fails(void **state) @@ -691,6 +740,7 @@ static void test_validate_scale_height_only_fails(void **state) cfg.scale_width = 0; cfg.scale_height = 2160; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_scale_exceeds_max_fails(void **state) @@ -701,6 +751,7 @@ static void test_validate_scale_exceeds_max_fails(void **state) cfg.scale_width = 4096; cfg.scale_height = 2160; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_scale_odd_width_fails(void **state) @@ -711,6 +762,7 @@ static void test_validate_scale_odd_width_fails(void **state) cfg.scale_width = 1921; cfg.scale_height = 1080; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_scale_odd_height_yuv420_fails(void **state) @@ -724,6 +776,7 @@ static void test_validate_scale_odd_height_yuv420_fails(void **state) cfg.sessions[0].crop_w = 1920; cfg.sessions[0].crop_h = 1080; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_scale_crop_exceeds_scaled_dims_fails(void **state) @@ -739,6 +792,7 @@ static void test_validate_scale_crop_exceeds_scaled_dims_fails(void **state) cfg.sessions[0].crop_w = 3840; cfg.sessions[0].crop_h = 2160; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_scale_crop_within_scaled_dims_passes(void **state) @@ -753,6 +807,7 @@ static void test_validate_scale_crop_within_scaled_dims_passes(void **state) cfg.sessions[0].crop_w = 2560; cfg.sessions[0].crop_h = 1440; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_no_scale_passes(void **state) @@ -763,6 +818,7 @@ static void test_validate_no_scale_passes(void **state) cfg.scale_width = 0; cfg.scale_height = 0; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } static void test_validate_duplicate_udp_ports_fails(void **state) @@ -770,11 +826,21 @@ static void test_validate_duplicate_udp_ports_fails(void **state) (void)state; struct dvledtx_config cfg; memset(&cfg, 0, sizeof(cfg)); - strncpy(cfg.interface_name, "0000:06:00.0", sizeof(cfg.interface_name) - 1); - strncpy(cfg.interface_sip, "192.168.1.1", sizeof(cfg.interface_sip) - 1); - strncpy(cfg.interface_dip, "239.0.0.1", sizeof(cfg.interface_dip) - 1); + + cfg.nic_cap = 1; + cfg.interface_name = calloc(1, sizeof(*cfg.interface_name)); + cfg.interface_sip = calloc(1, sizeof(*cfg.interface_sip)); + cfg.interface_dip = calloc(1, sizeof(*cfg.interface_dip)); + cfg.nic_count = 1; + + strncpy(cfg.interface_name[0], "0000:06:00.0", sizeof(cfg.interface_name[0]) - 1); + strncpy(cfg.interface_sip[0], "192.168.1.1", sizeof(cfg.interface_sip[0]) - 1); + strncpy(cfg.interface_dip[0], "239.0.0.1", sizeof(cfg.interface_dip[0]) - 1); cfg.width = 1920; cfg.height = 1080; cfg.fps = 25; strncpy(cfg.fmt, "yuv422p10le", sizeof(cfg.fmt) - 1); + + cfg.session_cap = 2; + cfg.sessions = calloc(2, sizeof(*cfg.sessions)); cfg.session_count = 2; cfg.sessions[0].udp_port = 20000; cfg.sessions[0].payload_type = 96; cfg.sessions[0].crop_x = 0; cfg.sessions[0].crop_y = 0; @@ -784,6 +850,7 @@ static void test_validate_duplicate_udp_ports_fails(void **state) cfg.sessions[1].crop_x = 960; cfg.sessions[1].crop_y = 0; cfg.sessions[1].crop_w = 960; cfg.sessions[1].crop_h = 1080; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_crop_x_misaligned_for_yuv422_fails(void **state) @@ -795,6 +862,7 @@ static void test_validate_crop_x_misaligned_for_yuv422_fails(void **state) cfg.sessions[0].crop_x = 1; cfg.sessions[0].crop_w = 1918; /* crop_x + crop_w = 1919 < 1920; crop_w even */ assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_tx_url_nonexistent_file_fails(void **state) @@ -804,6 +872,7 @@ static void test_validate_tx_url_nonexistent_file_fails(void **state) fill_valid_config(&cfg); strncpy(cfg.tx_url, "/tmp/dvledtx_no_such_video_xyz.mp4", sizeof(cfg.tx_url) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_tx_url_existing_file_passes(void **state) @@ -816,6 +885,7 @@ static void test_validate_tx_url_existing_file_passes(void **state) strncpy(cfg.tx_url, path, sizeof(cfg.tx_url) - 1); int ret = validate_tx_config(&cfg); unlink(path); free(path); + dvledtx_config_free(&cfg); assert_int_equal(ret, 0); } @@ -829,8 +899,9 @@ static void test_validate_non_multicast_dip_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_dip, "192.168.1.100", sizeof(cfg.interface_dip) - 1); + strncpy(cfg.interface_dip[0], "192.168.1.100", sizeof(cfg.interface_dip[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* S-2: PCI BDF must match DDDD:DD:DD.D hex pattern */ @@ -839,8 +910,9 @@ static void test_validate_invalid_bdf_format_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_name, "eth0", sizeof(cfg.interface_name) - 1); + strncpy(cfg.interface_name[0], "eth0", sizeof(cfg.interface_name[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_valid_bdf_passes(void **state) @@ -848,8 +920,9 @@ static void test_validate_valid_bdf_passes(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_name, "0000:af:00.1", sizeof(cfg.interface_name) - 1); + strncpy(cfg.interface_name[0], "0000:af:00.1", sizeof(cfg.interface_name[0]) - 1); assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } /* D-3: UDP port must be >= 1024 (above privileged range) */ @@ -860,6 +933,7 @@ static void test_validate_privileged_udp_port_fails(void **state) fill_valid_config(&cfg); cfg.sessions[0].udp_port = 80; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* ========================================================================== @@ -997,6 +1071,7 @@ static void test_validate_crop_x_plus_w_unsigned_overflow_safe(void **state) cfg.sessions[0].crop_x = 2147483647; /* INT_MAX */ cfg.sessions[0].crop_w = 2; /* even, so passes alignment */ assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } static void test_validate_crop_y_plus_h_unsigned_overflow_safe(void **state) @@ -1007,6 +1082,7 @@ static void test_validate_crop_y_plus_h_unsigned_overflow_safe(void **state) cfg.sessions[0].crop_y = 2147483647; /* INT_MAX */ cfg.sessions[0].crop_h = 2; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* D-3: 224.x.x.x is in the multicast range — passes (with a warning) */ @@ -1015,10 +1091,11 @@ static void test_validate_low_multicast_dip_passes_with_warning(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_dip, "224.0.0.50", sizeof(cfg.interface_dip) - 1); + strncpy(cfg.interface_dip[0], "224.0.0.50", sizeof(cfg.interface_dip[0]) - 1); /* In multicast range but outside the 239.0.0.0/8 administratively-scoped * range — must still validate (a warning is logged). */ assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } /* D-3: 240.x.x.x is OUTSIDE the multicast range (224.0.0.0/4) — must fail */ @@ -1027,8 +1104,9 @@ static void test_validate_above_multicast_dip_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_dip, "240.0.0.1", sizeof(cfg.interface_dip) - 1); + strncpy(cfg.interface_dip[0], "240.0.0.1", sizeof(cfg.interface_dip[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* D-3: UDP port at the boundary (1024) must pass */ @@ -1039,6 +1117,7 @@ static void test_validate_udp_port_boundary_1024_passes(void **state) fill_valid_config(&cfg); cfg.sessions[0].udp_port = 1024; assert_int_equal(validate_tx_config(&cfg), 0); + dvledtx_config_free(&cfg); } /* D-3: UDP port 1023 (just below boundary) must fail */ @@ -1049,6 +1128,7 @@ static void test_validate_udp_port_boundary_1023_fails(void **state) fill_valid_config(&cfg); cfg.sessions[0].udp_port = 1023; assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* S-2: BDF with too few digits in domain part must fail */ @@ -1058,8 +1138,9 @@ static void test_validate_short_bdf_format_fails(void **state) struct dvledtx_config cfg; fill_valid_config(&cfg); /* Missing leading "00" in domain — DDD:DD:DD.D instead of DDDD:DD:DD.D */ - strncpy(cfg.interface_name, "000:06:00.0", sizeof(cfg.interface_name) - 1); + strncpy(cfg.interface_name[0], "000:06:00.0", sizeof(cfg.interface_name[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* S-2: BDF with non-hex chars must fail */ @@ -1068,8 +1149,9 @@ static void test_validate_nonhex_bdf_format_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - strncpy(cfg.interface_name, "ZZZZ:06:00.0", sizeof(cfg.interface_name) - 1); + strncpy(cfg.interface_name[0], "ZZZZ:06:00.0", sizeof(cfg.interface_name[0]) - 1); assert_int_equal(validate_tx_config(&cfg), -1); + dvledtx_config_free(&cfg); } /* ========================================================================== @@ -1168,7 +1250,7 @@ static void test_load_and_apply_config_populates_app_context(void **state) int ret = load_and_apply_config(&app, path); unlink(path); free(path); assert_int_equal(ret, 0); - assert_string_equal(app.port, "0000:06:00.0"); + assert_string_equal(app.nics[0].port, "0000:06:00.0"); assert_int_equal((int)app.width, 1920); assert_int_equal((int)app.height, 1080); assert_int_equal(app.fps, 30); @@ -1176,6 +1258,7 @@ static void test_load_and_apply_config_populates_app_context(void **state) assert_int_equal(app.st20p_sessions, 1); assert_int_equal(app.session_net[0].udp_port, 20000); assert_int_equal(app.session_net[0].payload_type, 96); + dvledtx_context_free(&app); } static void test_load_and_apply_config_fmt_yuv444p(void **state) @@ -1189,6 +1272,7 @@ static void test_load_and_apply_config_fmt_yuv444p(void **state) unlink(path); free(path); assert_int_equal(ret, 0); assert_int_equal(app.fmt, AV_PIX_FMT_YUV444P10LE); + dvledtx_context_free(&app); } static void test_load_and_apply_config_fmt_gbrp10le(void **state) @@ -1202,6 +1286,7 @@ static void test_load_and_apply_config_fmt_gbrp10le(void **state) unlink(path); free(path); assert_int_equal(ret, 0); assert_int_equal(app.fmt, AV_PIX_FMT_GBRP10LE); + dvledtx_context_free(&app); } static void test_load_and_apply_config_fmt_yuv420(void **state) @@ -1215,6 +1300,7 @@ static void test_load_and_apply_config_fmt_yuv420(void **state) unlink(path); free(path); assert_int_equal(ret, 0); assert_int_equal(app.fmt, AV_PIX_FMT_YUV420P); + dvledtx_context_free(&app); } static void test_load_and_apply_config_unknown_fmt_fails(void **state) @@ -1248,6 +1334,7 @@ static void test_load_and_apply_config_copies_log_file(void **state) unlink(path); free(path); assert_int_equal(ret, 0); assert_string_equal(app.log_file, "myapp.log"); + dvledtx_context_free(&app); } /* ========================================================================== diff --git a/tests/test_ffmpeg_decoder_mock.c b/tests/test_ffmpeg_decoder_mock.c index d243da8..c15543a 100644 --- a/tests/test_ffmpeg_decoder_mock.c +++ b/tests/test_ffmpeg_decoder_mock.c @@ -178,17 +178,19 @@ static int generate_test_video(void) static void fill_app_16x16(struct dvledtx_context* app, int sessions) { memset(app, 0, sizeof(*app)); - strncpy(app->port, "0000:06:00.0", sizeof(app->port) - 1); - strncpy(app->sip_addr_str, "192.168.50.29", sizeof(app->sip_addr_str) - 1); - strncpy(app->dip_addr_str, "239.168.85.20", sizeof(app->dip_addr_str) - 1); + + dvledtx_context_alloc(app, 1, sessions); + + strncpy(app->nics[0].port, "0000:06:00.0", sizeof(app->nics[0].port) - 1); + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); app->width = 16; app->height = 16; app->fps = 25; app->fmt = AV_PIX_FMT_YUV422P10LE; app->udp_port = 20000; app->payload_type = 96; - app->st20p_sessions = sessions; - for (int i = 0; i < sessions && i < MAX_TX_SESSIONS; i++) { + for (int i = 0; i < sessions; i++) { app->session_net[i].udp_port = 20000 + i * 2; app->session_net[i].payload_type = 96; app->session_net[i].crop_x = 0; diff --git a/tests/test_ffmpeg_tx.c b/tests/test_ffmpeg_tx.c index a50488a..f8073c7 100644 --- a/tests/test_ffmpeg_tx.c +++ b/tests/test_ffmpeg_tx.c @@ -64,16 +64,18 @@ const AVOutputFormat* __wrap_av_guess_format(const char* short_name, static void fill_app(struct dvledtx_context* app) { memset(app, 0, sizeof(*app)); - strncpy(app->port, "0000:06:00.0", sizeof(app->port) - 1); - strncpy(app->sip_addr_str, "192.168.50.29", sizeof(app->sip_addr_str) - 1); - strncpy(app->dip_addr_str, "239.168.85.20", sizeof(app->dip_addr_str) - 1); + + dvledtx_context_alloc(app, 1, 1); + + strncpy(app->nics[0].port, "0000:06:00.0", sizeof(app->nics[0].port) - 1); + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); app->width = 640; app->height = 1080; app->fps = 30; app->fmt = AV_PIX_FMT_YUV422P10LE; app->udp_port = 20000; app->payload_type = 96; - app->st20p_sessions = 1; app->session_net[0].udp_port = 20000; app->session_net[0].payload_type = 96; app->session_net[0].crop_x = 0; diff --git a/tests/test_ffmpeg_tx_mock.c b/tests/test_ffmpeg_tx_mock.c index d97b7c6..3eebcb1 100644 --- a/tests/test_ffmpeg_tx_mock.c +++ b/tests/test_ffmpeg_tx_mock.c @@ -158,17 +158,19 @@ static int generate_test_video(void) static void fill_app_16x16(struct dvledtx_context* app, int sessions) { memset(app, 0, sizeof(*app)); - strncpy(app->port, "0000:06:00.0", sizeof(app->port) - 1); - strncpy(app->sip_addr_str, "192.168.50.29", sizeof(app->sip_addr_str) - 1); - strncpy(app->dip_addr_str, "239.168.85.20", sizeof(app->dip_addr_str) - 1); + + dvledtx_context_alloc(app, 1, sessions); + + strncpy(app->nics[0].port, "0000:06:00.0", sizeof(app->nics[0].port) - 1); + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); app->width = 16; app->height = 16; app->fps = 25; app->fmt = AV_PIX_FMT_YUV422P10LE; app->udp_port = 20000; app->payload_type = 96; - app->st20p_sessions = sessions; - for (int i = 0; i < sessions && i < MAX_TX_SESSIONS; i++) { + for (int i = 0; i < sessions; i++) { app->session_net[i].udp_port = 20000 + i * 2; app->session_net[i].payload_type = 96; app->session_net[i].crop_w = 16; @@ -216,7 +218,7 @@ static void test_open_ffmpeg_tx_uses_app_defaults(void **state) fill_app_16x16(&app, 1); /* Clear per-session network config — open_ffmpeg_tx must fall back to * app-level width/height. */ - memset(app.session_net, 0, sizeof(app.session_net)); + memset(app.session_net, 0, (size_t)app.st20p_sessions * sizeof(*app.session_net)); struct st20p_tx_ctx ctx; memset(&ctx, 0, sizeof(ctx)); diff --git a/tests/test_ffmpeg_watchdog.c b/tests/test_ffmpeg_watchdog.c index 8ff4656..363a16f 100644 --- a/tests/test_ffmpeg_watchdog.c +++ b/tests/test_ffmpeg_watchdog.c @@ -177,17 +177,19 @@ static int generate_test_video(void) static void fill_app_16x16(struct dvledtx_context* app, int sessions) { memset(app, 0, sizeof(*app)); - strncpy(app->port, "0000:06:00.0", sizeof(app->port) - 1); - strncpy(app->sip_addr_str, "192.168.50.29", sizeof(app->sip_addr_str) - 1); - strncpy(app->dip_addr_str, "239.168.85.20", sizeof(app->dip_addr_str) - 1); + + dvledtx_context_alloc(app, 1, sessions); + + strncpy(app->nics[0].port, "0000:06:00.0", sizeof(app->nics[0].port) - 1); + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); app->width = 16; app->height = 16; app->fps = 25; app->fmt = AV_PIX_FMT_YUV422P10LE; app->udp_port = 20000; app->payload_type = 96; - app->st20p_sessions = sessions; - for (int i = 0; i < sessions && i < MAX_TX_SESSIONS; i++) { + for (int i = 0; i < sessions; i++) { app->session_net[i].udp_port = 20000 + i * 2; app->session_net[i].payload_type = 96; app->session_net[i].crop_x = 0; diff --git a/tests/test_main.c b/tests/test_main.c index 7fa8e6e..24a9c42 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -108,8 +108,11 @@ int load_and_apply_config(struct dvledtx_context* app, const char* f) { (void)f; if (stub_load_and_apply_config_ret == 0 && stub_load_config_set_ips) { - strncpy(app->sip_addr_str, "192.168.50.29", sizeof(app->sip_addr_str) - 1); - strncpy(app->dip_addr_str, "239.168.85.20", sizeof(app->dip_addr_str) - 1); + /* Allocate dynamic arrays for the stub */ + dvledtx_context_free(app); + dvledtx_context_alloc(app, 1, 1); + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); app->test_time_s = stub_load_config_test_time_s; app->exit = stub_load_config_set_exit; } @@ -122,17 +125,21 @@ int load_and_apply_config(struct dvledtx_context* app, const char* f) * stubs above. Provide the real implementation directly here so that the * existing resolve_ip_addrs test cases continue to work correctly. */ int resolve_ip_addrs(struct dvledtx_context* ctx) { - if (ctx->sip_addr_str[0] != '\0') { - if (inet_pton(AF_INET, ctx->sip_addr_str, ctx->sip_addr) != 1) { - LOG_ERROR("Invalid source IP address %s", ctx->sip_addr_str); + for (int ni = 0; ni < ctx->nic_count; ni++) { + if (ctx->nics[ni].sip_addr_str[0] != '\0') { + if (inet_pton(AF_INET, ctx->nics[ni].sip_addr_str, + ctx->nics[ni].sip_addr) != 1) { + LOG_ERROR("Invalid source IP address %s", ctx->nics[ni].sip_addr_str); + return -1; + } + } else { + LOG_INFO("NIC[%d]: no source IP provided, DHCP mode", ni); + } + if (inet_pton(AF_INET, ctx->nics[ni].dip_addr_str, + ctx->nics[ni].dip_addr) != 1) { + LOG_ERROR("Invalid destination IP address %s", ctx->nics[ni].dip_addr_str); return -1; } - } else { - LOG_INFO("No source IP provided, DHCP mode"); - } - if (inet_pton(AF_INET, ctx->dip_addr_str, ctx->dip_addr) != 1) { - LOG_ERROR("Invalid destination IP address %s", ctx->dip_addr_str); - return -1; } return 0; } @@ -236,12 +243,14 @@ static void test_resolve_ip_valid_sip_and_dip(void **state) (void)state; struct dvledtx_context ctx; memset(&ctx, 0, sizeof(ctx)); - strncpy(ctx.sip_addr_str, "192.168.50.29", sizeof(ctx.sip_addr_str) - 1); - strncpy(ctx.dip_addr_str, "239.168.85.20", sizeof(ctx.dip_addr_str) - 1); + dvledtx_context_alloc(&ctx, 1, 0); + strncpy(ctx.nics[0].sip_addr_str, "192.168.50.29", sizeof(ctx.nics[0].sip_addr_str) - 1); + strncpy(ctx.nics[0].dip_addr_str, "239.168.85.20", sizeof(ctx.nics[0].dip_addr_str) - 1); assert_int_equal(resolve_ip_addrs(&ctx), 0); /* Verify binary addresses were written */ - assert_int_equal(ctx.sip_addr[0], 192); - assert_int_equal(ctx.dip_addr[0], 239); + assert_int_equal(ctx.nics[0].sip_addr[0], 192); + assert_int_equal(ctx.nics[0].dip_addr[0], 239); + dvledtx_context_free(&ctx); } static void test_resolve_ip_empty_sip_dhcp_mode(void **state) @@ -249,10 +258,12 @@ static void test_resolve_ip_empty_sip_dhcp_mode(void **state) (void)state; struct dvledtx_context ctx; memset(&ctx, 0, sizeof(ctx)); + dvledtx_context_alloc(&ctx, 1, 0); /* sip_addr_str is empty → DHCP mode branch; only dip is resolved */ - strncpy(ctx.dip_addr_str, "239.168.85.20", sizeof(ctx.dip_addr_str) - 1); + strncpy(ctx.nics[0].dip_addr_str, "239.168.85.20", sizeof(ctx.nics[0].dip_addr_str) - 1); assert_int_equal(resolve_ip_addrs(&ctx), 0); - assert_int_equal(ctx.dip_addr[0], 239); + assert_int_equal(ctx.nics[0].dip_addr[0], 239); + dvledtx_context_free(&ctx); } static void test_resolve_ip_invalid_sip_fails(void **state) @@ -260,9 +271,11 @@ static void test_resolve_ip_invalid_sip_fails(void **state) (void)state; struct dvledtx_context ctx; memset(&ctx, 0, sizeof(ctx)); - strncpy(ctx.sip_addr_str, "not.an.ip.addr", sizeof(ctx.sip_addr_str) - 1); - strncpy(ctx.dip_addr_str, "239.168.85.20", sizeof(ctx.dip_addr_str) - 1); + dvledtx_context_alloc(&ctx, 1, 0); + strncpy(ctx.nics[0].sip_addr_str, "not.an.ip.addr", sizeof(ctx.nics[0].sip_addr_str) - 1); + strncpy(ctx.nics[0].dip_addr_str, "239.168.85.20", sizeof(ctx.nics[0].dip_addr_str) - 1); assert_int_equal(resolve_ip_addrs(&ctx), -1); + dvledtx_context_free(&ctx); } static void test_resolve_ip_invalid_dip_fails(void **state) @@ -270,8 +283,10 @@ static void test_resolve_ip_invalid_dip_fails(void **state) (void)state; struct dvledtx_context ctx; memset(&ctx, 0, sizeof(ctx)); - strncpy(ctx.dip_addr_str, "999.999.999.999", sizeof(ctx.dip_addr_str)); + dvledtx_context_alloc(&ctx, 1, 0); + strncpy(ctx.nics[0].dip_addr_str, "999.999.999.999", sizeof(ctx.nics[0].dip_addr_str)); assert_int_equal(resolve_ip_addrs(&ctx), -1); + dvledtx_context_free(&ctx); } /* =========================================================================== diff --git a/tests/test_session_manager.c b/tests/test_session_manager.c index a7821b7..8c81efc 100644 --- a/tests/test_session_manager.c +++ b/tests/test_session_manager.c @@ -281,9 +281,13 @@ static void reset_mock_counters(void) static void fill_app(struct dvledtx_context* app, int sessions, const char* url) { memset(app, 0, sizeof(*app)); - strncpy(app->port, "0000:06:00.0", sizeof(app->port) - 1); - strncpy(app->sip_addr_str, "192.168.50.29", sizeof(app->sip_addr_str) - 1); - strncpy(app->dip_addr_str, "239.168.85.20", sizeof(app->dip_addr_str) - 1); + + /* Allocate dynamic arrays */ + dvledtx_context_alloc(app, 1, sessions); + + strncpy(app->nics[0].port, "0000:06:00.0", sizeof(app->nics[0].port) - 1); + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); strncpy(app->tx_url, url, sizeof(app->tx_url) - 1); app->width = 1920; app->height = 1080; @@ -291,10 +295,9 @@ static void fill_app(struct dvledtx_context* app, int sessions, const char* url) app->fmt = AV_PIX_FMT_YUV422P10LE; app->udp_port = 20000; app->payload_type = 96; - app->st20p_sessions = sessions; /* Distribute 1920px evenly across sessions — 640px each */ - for (int i = 0; i < sessions && i < MAX_TX_SESSIONS; i++) { + for (int i = 0; i < sessions; i++) { app->session_net[i].udp_port = 20000 + i * 2; app->session_net[i].payload_type = 96; app->session_net[i].crop_x = i * 640; @@ -473,7 +476,7 @@ static void test_create_session_crop_fallback_3sessions(void **state) reset_mock_counters(); struct dvledtx_context app; fill_app(&app, 3, ""); - memset(app.session_net, 0, sizeof(app.session_net)); + memset(app.session_net, 0, (size_t)app.st20p_sessions * sizeof(*app.session_net)); session_manager_t mgr; assert_int_equal(session_manager_init(&mgr, &app), 0); @@ -498,7 +501,7 @@ static void test_create_session_crop_fallback_last_gets_remainder(void **state) struct dvledtx_context app; fill_app(&app, 3, ""); app.width = 1920; - memset(app.session_net, 0, sizeof(app.session_net)); + memset(app.session_net, 0, (size_t)app.st20p_sessions * sizeof(*app.session_net)); session_manager_t mgr; assert_int_equal(session_manager_init(&mgr, &app), 0); From ccbca8e872ae19abe10c2027fb08922ba4e86f3d Mon Sep 17 00:00:00 2001 From: sunilnom Date: Wed, 8 Jul 2026 08:55:12 +0530 Subject: [PATCH 2/3] Standardize config video fmt/tx_url naming; fix test_main hang in resolve_ip_fails stub - config/*.json: unify tx_video.fmt to yuv422p10le and video.tx_url to the ball__yuv422p10_fps_.mp4 naming pattern; drop hardcoded absolute sample paths in favor of relative filenames. - tests/test_main.c: load_and_apply_config stub now always allocates the nic array so resolve_ip_addrs() is properly exercised when stub_load_config_set_ips is false, instead of leaving nic_count == 0 which let tx_app_real_main() fall into the infinite transmit loop and hang/timeout the whole test binary. --- config/tx_2k_single_session.json | 2 +- config/tx_4k_multi_session.json | 2 +- config/tx_4k_single_session.json | 4 ++-- config/tx_fullhd_multi_nic.json | 30 +++++++++++++++++++++++----- config/tx_fullhd_multi_session.json | 2 +- config/tx_fullhd_single_session.json | 2 +- tests/test_main.c | 18 +++++++++++++---- 7 files changed, 45 insertions(+), 15 deletions(-) diff --git a/config/tx_2k_single_session.json b/config/tx_2k_single_session.json index 0be6239..2961470 100644 --- a/config/tx_2k_single_session.json +++ b/config/tx_2k_single_session.json @@ -10,7 +10,7 @@ "video": { "width": 2560, "height": 1440, - "tx_url": "ball_2k_yuv420p_30fps_5min.mp4" + "tx_url": "ball_2k_yuv422p10_30fps_5min.mp4" }, "tx_video": { "scale_width": 2560, diff --git a/config/tx_4k_multi_session.json b/config/tx_4k_multi_session.json index 0b14087..ee6c0f6 100644 --- a/config/tx_4k_multi_session.json +++ b/config/tx_4k_multi_session.json @@ -10,7 +10,7 @@ "video": { "width": 3840, "height": 2160, - "tx_url": "ball_4k_gbrp10le_60fps_5min.mp4" + "tx_url": "ball_4k_yuv422p10_60fps_5min.mp4" }, "tx_video": { "scale_width": 1920, diff --git a/config/tx_4k_single_session.json b/config/tx_4k_single_session.json index 004de34..d901e6b 100644 --- a/config/tx_4k_single_session.json +++ b/config/tx_4k_single_session.json @@ -10,13 +10,13 @@ "video": { "width": 3840, "height": 2160, - "tx_url": "ball_4k_gbrp10le_60fps_5min.mp4" + "tx_url": "ball_4k_yuv422p10_60fps_5min.mp4" }, "tx_video": { "scale_width": 900, "scale_height": 600, "fps": 60, - "fmt": "gbrp10le" + "fmt": "yuv422p10le" }, "tx_sessions": [ { diff --git a/config/tx_fullhd_multi_nic.json b/config/tx_fullhd_multi_nic.json index 0b30ea8..4c2f286 100644 --- a/config/tx_fullhd_multi_nic.json +++ b/config/tx_fullhd_multi_nic.json @@ -2,26 +2,46 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:03:00.0", + "name": "0000:03:10.0", "sip": "192.168.50.30", "dip": "239.168.85.20" }, { "name": "0000:03:10.2", - "sip": "192.168.50.29", + "sip": "192.168.50.31", "dip": "239.168.85.21" + }, + { + "name": "0000:03:10.4", + "sip": "192.168.50.32", + "dip": "239.168.85.22" + }, + { + "name": "0000:03:10.6", + "sip": "192.168.50.33", + "dip": "239.168.85.23" + }, + { + "name": "0000:03:11.0", + "sip": "192.168.50.34", + "dip": "239.168.85.24" + }, + { + "name": "0000:03:11.2", + "sip": "192.168.50.35", + "dip": "239.168.85.25" } ], "video": { "width": 1920, "height": 1080, - "tx_url": "/home/intel/workspace/sample/ball_1080p_yuv420p_30fps_15min.mp4" + "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" }, "tx_video": { "scale_width": 1920, "scale_height": 1080, "fps": 30, - "fmt": "gbrp10le" + "fmt": "yuv422p10le" }, "tx_sessions": [ { @@ -37,7 +57,7 @@ "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } }, { - "nic_index": 1, + "nic_index": 2, "udp_port": 20004, "payload_type": 96, "crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 } diff --git a/config/tx_fullhd_multi_session.json b/config/tx_fullhd_multi_session.json index 3fd5015..8e5d1bf 100644 --- a/config/tx_fullhd_multi_session.json +++ b/config/tx_fullhd_multi_session.json @@ -10,7 +10,7 @@ "video": { "width": 1920, "height": 1080, - "tx_url": "/home/intel/workspace/sample/ball_1080p_yuv420p_30fps_15min.mp4" + "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" }, "tx_video": { "scale_width": 1920, diff --git a/config/tx_fullhd_single_session.json b/config/tx_fullhd_single_session.json index 32b9925..7640ae6 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -10,7 +10,7 @@ "video": { "width": 3840, "height": 2160, - "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" }, "tx_video": { "scale_width": 3840, diff --git a/tests/test_main.c b/tests/test_main.c index 24a9c42..a4f529f 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -107,12 +107,22 @@ int peek_config_log_file(const char* f, char* buf, size_t sz) int load_and_apply_config(struct dvledtx_context* app, const char* f) { (void)f; - if (stub_load_and_apply_config_ret == 0 && stub_load_config_set_ips) { - /* Allocate dynamic arrays for the stub */ + if (stub_load_and_apply_config_ret == 0) { + /* Allocate dynamic arrays for the stub — always, so that nic_count > 0 + * and resolve_ip_addrs() actually exercises its validation logic + * instead of trivially "succeeding" on a zero-length loop (which + * previously let tx_app_real_main() fall through into the + * infinite transmit loop and hang the whole test binary). */ dvledtx_context_free(app); dvledtx_context_alloc(app, 1, 1); - strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); - strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); + if (stub_load_config_set_ips) { + strncpy(app->nics[0].sip_addr_str, "192.168.50.29", sizeof(app->nics[0].sip_addr_str) - 1); + strncpy(app->nics[0].dip_addr_str, "239.168.85.20", sizeof(app->nics[0].dip_addr_str) - 1); + } else { + /* Leave dip_addr_str empty → resolve_ip_addrs() must fail. */ + app->nics[0].sip_addr_str[0] = '\0'; + app->nics[0].dip_addr_str[0] = '\0'; + } app->test_time_s = stub_load_config_test_time_s; app->exit = stub_load_config_set_exit; } From a12a1a64c5cb8e7fc2fe58142ad459d71171ed90 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Fri, 10 Jul 2026 10:31:33 +0530 Subject: [PATCH 3/3] Fix memory leaks in config_reader fuzz harnesses and tests - Add dvledtx_config_free() calls after parse_tx_config() in fuzz harnesses (AFL and libFuzzer) to prevent per-iteration leaks that could cause OOM during long fuzzing runs. - Add missing dvledtx_config_free(&cfg) calls in test_config_reader.c test functions that call parse_tx_config() directly without freeing the resulting dynamically-allocated arrays. Confirmed via AddressSanitizer: parse_tx_config()/config_reader.c itself is correct (its only production caller, load_and_apply_config(), already frees on every path); the leaks were only in callers that never freed the parsed config. --- fuzz/fuzz_config_reader.c | 2 ++ fuzz/fuzz_config_reader_libfuzzer.c | 1 + tests/test_config_reader.c | 17 +++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/fuzz/fuzz_config_reader.c b/fuzz/fuzz_config_reader.c index 880e594..9363d26 100644 --- a/fuzz/fuzz_config_reader.c +++ b/fuzz/fuzz_config_reader.c @@ -37,6 +37,7 @@ int main(int argc, char *argv[]) { memset(&config, 0, sizeof(config)); parse_tx_config(tmpfile, &config); validate_tx_config(&config); + dvledtx_config_free(&config); unlink(tmpfile); } @@ -51,6 +52,7 @@ int main(int argc, char *argv[]) { memset(&config, 0, sizeof(config)); parse_tx_config(argv[1], &config); validate_tx_config(&config); + dvledtx_config_free(&config); #endif return 0; diff --git a/fuzz/fuzz_config_reader_libfuzzer.c b/fuzz/fuzz_config_reader_libfuzzer.c index 8d236e4..febeac6 100644 --- a/fuzz/fuzz_config_reader_libfuzzer.c +++ b/fuzz/fuzz_config_reader_libfuzzer.c @@ -40,6 +40,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { memset(&config, 0, sizeof(config)); parse_tx_config(tmpfile, &config); validate_tx_config(&config); + dvledtx_config_free(&config); unlink(tmpfile); return 0; diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index bb9609c..a7d2f2f 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -114,6 +114,7 @@ static void test_parse_3sessions_session_count(void **state) struct dvledtx_config cfg; assert_int_equal(parse_tx_config(FIXTURE_3SESSIONS, &cfg), 0); assert_int_equal(cfg.session_count, 3); + dvledtx_config_free(&cfg); } static void test_parse_1session_session_count(void **state) @@ -122,6 +123,7 @@ static void test_parse_1session_session_count(void **state) struct dvledtx_config cfg; assert_int_equal(parse_tx_config(FIXTURE_1SESSION, &cfg), 0); assert_int_equal(cfg.session_count, 1); + dvledtx_config_free(&cfg); } static void test_parse_3sessions_interface_fields(void **state) @@ -132,6 +134,7 @@ static void test_parse_3sessions_interface_fields(void **state) assert_string_equal(cfg.interface_name[0], "0000:03:10.1"); assert_string_equal(cfg.interface_sip[0], "192.168.50.29"); assert_string_equal(cfg.interface_dip[0], "239.168.85.21"); + dvledtx_config_free(&cfg); } static void test_parse_3sessions_video_params(void **state) @@ -143,6 +146,7 @@ static void test_parse_3sessions_video_params(void **state) assert_int_equal((int)cfg.height, 1080); assert_int_equal(cfg.fps, 30); assert_string_equal(cfg.fmt, "yuv422p10le"); + dvledtx_config_free(&cfg); } static void test_parse_3sessions_log_file(void **state) @@ -151,6 +155,7 @@ static void test_parse_3sessions_log_file(void **state) struct dvledtx_config cfg; assert_int_equal(parse_tx_config(FIXTURE_3SESSIONS, &cfg), 0); assert_string_equal(cfg.log_file, "dvledtx.log"); + dvledtx_config_free(&cfg); } static void test_parse_3sessions_session0_crop(void **state) @@ -166,6 +171,7 @@ static void test_parse_3sessions_session0_crop(void **state) assert_int_equal(s->crop_y, 0); assert_int_equal(s->crop_w, 640); assert_int_equal(s->crop_h, 1080); + dvledtx_config_free(&cfg); } static void test_parse_3sessions_session1_crop(void **state) @@ -181,6 +187,7 @@ static void test_parse_3sessions_session1_crop(void **state) assert_int_equal(s->crop_y, 0); assert_int_equal(s->crop_w, 640); assert_int_equal(s->crop_h, 1080); + dvledtx_config_free(&cfg); } static void test_parse_3sessions_session2_crop(void **state) @@ -196,6 +203,7 @@ static void test_parse_3sessions_session2_crop(void **state) assert_int_equal(s->crop_y, 0); assert_int_equal(s->crop_w, 640); assert_int_equal(s->crop_h, 1080); + dvledtx_config_free(&cfg); } static void test_parse_returns_minus1_when_sessions_key_absent(void **state) @@ -213,6 +221,7 @@ static void test_parse_returns_minus1_when_sessions_key_absent(void **state) unlink(path); free(path); assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } static void test_parse_returns_minus1_when_sessions_array_empty(void **state) @@ -231,6 +240,7 @@ static void test_parse_returns_minus1_when_sessions_array_empty(void **state) unlink(path); free(path); assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } static void test_parse_returns_zero_fields_when_video_missing(void **state) @@ -253,6 +263,7 @@ static void test_parse_returns_zero_fields_when_video_missing(void **state) assert_int_equal((int)cfg.width, 0); assert_int_equal((int)cfg.height, 0); assert_int_equal(cfg.fps, 0); + dvledtx_config_free(&cfg); } /* ========================================================================== @@ -583,6 +594,7 @@ static void test_parse_session_missing_udp_port_fails(void **state) int ret = parse_tx_config(path, &cfg); unlink(path); free(path); assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } static void test_parse_session_udp_port_exceeds_65535_fails(void **state) @@ -600,6 +612,7 @@ static void test_parse_session_udp_port_exceeds_65535_fails(void **state) int ret = parse_tx_config(path, &cfg); unlink(path); free(path); assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } static void test_parse_session_missing_payload_type_defaults_to_96(void **state) @@ -618,6 +631,7 @@ static void test_parse_session_missing_payload_type_defaults_to_96(void **state) unlink(path); free(path); assert_int_equal(ret, 0); assert_int_equal(cfg.sessions[0].payload_type, 96); + dvledtx_config_free(&cfg); } static void test_parse_session_no_crop_object_fails(void **state) @@ -634,6 +648,7 @@ static void test_parse_session_no_crop_object_fails(void **state) int ret = parse_tx_config(path, &cfg); unlink(path); free(path); assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } /* ========================================================================== @@ -1175,6 +1190,7 @@ static void test_parse_session_negative_crop_x_fails(void **state) unlink(path); free(path); /* Missing crop "x" — extract_json_int returns -1, parse must fail */ assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } static void test_parse_session_zero_crop_w_fails(void **state) @@ -1192,6 +1208,7 @@ static void test_parse_session_zero_crop_w_fails(void **state) int ret = parse_tx_config(path, &cfg); unlink(path); free(path); assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); } /* ==========================================================================