diff --git a/README.md b/README.md index bc1a978..ac43049 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ FFmpeg is an open source project licensed under LGPL and GPL. See https://www.ff ```bash sudo apt-get install -y xserver-xorg-video-dummy ubuntu-desktop ``` + - The stock `mtl_st20p` muxer only exposes `p_port`/`r_port` (2 physical NIC ports). To use more than 2 NICs with the default (non-`ENABLE_MTL_TX`) build, the plugin's `libavdevice/mtl_common.h` must be patched to add `p2_port`..`p7_port` (and matching `p2_sip`..`p7_sip`) AVOptions mapped to `devArgs.port[MTL_PORT_2..MTL_PORT_7]` / `devArgs.sip[...]`, then FFmpeg rebuilt and reinstalled. Without this patch, `nic_count` is effectively capped at 2 for the FFmpeg TX path (the `ENABLE_MTL_TX` direct-pipeline build already supports up to 8 NICs without any patch). ### Build Steps @@ -143,7 +144,8 @@ 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[]** | `nic_index` | (Optional) Must equal this entry's position in the `interfaces[]` array (0, 1, 2, ...); parsing fails if it doesn't match. Informational/self-documenting only — interfaces are always resolved by array position. | +| | `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 | @@ -165,8 +167,8 @@ dvledtx uses a JSON config file with three sections: { "log_file": "dvledtx.log", "interfaces": [ - { "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" } + { "nic_index": 0, "name": "0000:03:10.0", "sip": "192.168.50.30", "dip": "239.168.85.20" }, + { "nic_index": 1, "name": "0000:03:10.2", "sip": "192.168.50.29", "dip": "239.168.85.21" } ], "video": { "width": 1920, "height": 1080, @@ -348,6 +350,8 @@ pkill -f "gnome-session --session=ubuntu" sudo pkill -f "Xorg :99" ``` +`interfaces[]` supports up to 8 NICs (MTL's port limit); each `tx_sessions[]` entry picks its NIC via `nic_index` (see `config/tx_fullhd_multi_nic.json` for an 8-NIC/8-session example). Using more than 2 NICs with the default FFmpeg TX path requires the patched `mtl_st20p` muxer described above. + ## Logging dvledtx includes a built-in logger with configurable output targets and log levels. diff --git a/config/tx_1session_12bit.json b/config/tx_1session_12bit.json index 9154858..69d5a39 100644 --- a/config/tx_1session_12bit.json +++ b/config/tx_1session_12bit.json @@ -1,6 +1,7 @@ { "interfaces": [ { + "nic_index": 0, "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -17,6 +18,7 @@ }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } diff --git a/config/tx_2k_multi_session.json b/config/tx_2k_multi_session.json index 8a8820a..ac8f9f7 100644 --- a/config/tx_2k_multi_session.json +++ b/config/tx_2k_multi_session.json @@ -2,6 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { + "nic_index": 0, "name": "0000:03:00.1", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -20,16 +21,19 @@ }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 854, "h": 1440 } }, { + "nic_index": 0, "udp_port": 20002, "payload_type": 96, "crop": { "x": 854, "y": 0, "w": 854, "h": 1440 } }, { + "nic_index": 0, "udp_port": 20004, "payload_type": 96, "crop": { "x": 1708, "y": 0, "w": 852, "h": 1440 } diff --git a/config/tx_2k_single_session.json b/config/tx_2k_single_session.json index 2961470..75bee99 100644 --- a/config/tx_2k_single_session.json +++ b/config/tx_2k_single_session.json @@ -2,6 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { + "nic_index": 0, "name": "0000:03:00.1", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -20,7 +21,8 @@ }, "tx_sessions": [ { - "udp_port": 20000, + "nic_index": 0, + "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 2560, "h": 1440 } } ] diff --git a/config/tx_4k_multi_session.json b/config/tx_4k_multi_session.json index ee6c0f6..ffce21a 100644 --- a/config/tx_4k_multi_session.json +++ b/config/tx_4k_multi_session.json @@ -2,6 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { + "nic_index": 0, "name": "0000:03:00.1", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -20,16 +21,19 @@ }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } }, { + "nic_index": 0, "udp_port": 20002, "payload_type": 96, "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } }, { + "nic_index": 0, "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 d901e6b..3d6e542 100644 --- a/config/tx_4k_single_session.json +++ b/config/tx_4k_single_session.json @@ -2,6 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { + "nic_index": 0, "name": "0000:03:00.1", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -20,6 +21,7 @@ }, "tx_sessions": [ { + "nic_index": 0, "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 index 4c2f286..cdc4d8c 100644 --- a/config/tx_fullhd_multi_nic.json +++ b/config/tx_fullhd_multi_nic.json @@ -2,40 +2,58 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:03:10.0", - "sip": "192.168.50.30", - "dip": "239.168.85.20" + "nic_index": 0, + "name": "0000:01:10.1", + "sip": "192.168.50.40", + "dip": "239.168.85.30" + }, + { + "nic_index": 1, + "name": "0000:01:10.3", + "sip": "192.168.50.41", + "dip": "239.168.85.31" + }, + { + "nic_index": 2, + "name": "0000:01:10.5", + "sip": "192.168.50.42", + "dip": "239.168.85.32" }, { - "name": "0000:03:10.2", - "sip": "192.168.50.31", - "dip": "239.168.85.21" + "nic_index": 3, + "name": "0000:01:10.7", + "sip": "192.168.50.43", + "dip": "239.168.85.33" }, { - "name": "0000:03:10.4", - "sip": "192.168.50.32", - "dip": "239.168.85.22" + "nic_index": 4, + "name": "0000:01:11.1", + "sip": "192.168.50.44", + "dip": "239.168.85.34" }, { - "name": "0000:03:10.6", - "sip": "192.168.50.33", - "dip": "239.168.85.23" + "nic_index": 5, + "name": "0000:01:11.3", + "sip": "192.168.50.45", + "dip": "239.168.85.35" }, { - "name": "0000:03:11.0", - "sip": "192.168.50.34", - "dip": "239.168.85.24" + "nic_index": 6, + "name": "0000:01:11.5", + "sip": "192.168.50.46", + "dip": "239.168.85.36" }, { - "name": "0000:03:11.2", - "sip": "192.168.50.35", - "dip": "239.168.85.25" + "nic_index": 7, + "name": "0000:01:11.7", + "sip": "192.168.50.47", + "dip": "239.168.85.37" } ], "video": { "width": 1920, "height": 1080, - "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" + "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { "scale_width": 1920, @@ -48,20 +66,51 @@ "nic_index": 0, "udp_port": 20000, "payload_type": 96, - "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } + "crop": { "x": 0, "y": 0, "w": 240, "h": 1080 } }, { "nic_index": 1, "udp_port": 20002, "payload_type": 96, - "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } + "crop": { "x": 240, "y": 0, "w": 240, "h": 1080 } }, { "nic_index": 2, "udp_port": 20004, "payload_type": 96, - "crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 } + "crop": { "x": 480, "y": 0, "w": 240, "h": 1080 } + }, + { + "nic_index": 3, + "udp_port": 20006, + "payload_type": 96, + "crop": { "x": 720, "y": 0, "w": 240, "h": 1080 } + }, + { + "nic_index": 4, + "udp_port": 20008, + "payload_type": 96, + "crop": { "x": 960, "y": 0, "w": 240, "h": 1080 } + }, + { + "nic_index": 5, + "udp_port": 20010, + "payload_type": 96, + "crop": { "x": 1200, "y": 0, "w": 240, "h": 1080 } + }, + { + "nic_index": 6, + "udp_port": 20012, + "payload_type": 96, + "crop": { "x": 1440, "y": 0, "w": 240, "h": 1080 } + }, + { + "nic_index": 7, + "udp_port": 20014, + "payload_type": 96, + "crop": { "x": 1680, "y": 0, "w": 240, "h": 1080 } } ] } + diff --git a/config/tx_fullhd_multi_session.json b/config/tx_fullhd_multi_session.json index 8e5d1bf..87df774 100644 --- a/config/tx_fullhd_multi_session.json +++ b/config/tx_fullhd_multi_session.json @@ -2,6 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { + "nic_index": 0, "name": "0000:03:10.1", "sip": "192.168.50.29", "dip": "239.168.85.21" @@ -20,16 +21,19 @@ }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } }, { + "nic_index": 0, "udp_port": 20002, "payload_type": 96, "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } }, { + "nic_index": 0, "udp_port": 20004, "payload_type": 96, "crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 } diff --git a/config/tx_fullhd_screen_capture.json b/config/tx_fullhd_screen_capture.json index d45a9ce..4b7e484 100644 --- a/config/tx_fullhd_screen_capture.json +++ b/config/tx_fullhd_screen_capture.json @@ -1,6 +1,7 @@ { "interfaces": [ { + "nic_index": 0, "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -18,6 +19,7 @@ }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } diff --git a/config/tx_fullhd_screen_capture_multi_session.json b/config/tx_fullhd_screen_capture_multi_session.json index 8a45e81..8aa9071 100644 --- a/config/tx_fullhd_screen_capture_multi_session.json +++ b/config/tx_fullhd_screen_capture_multi_session.json @@ -1,6 +1,7 @@ { "interfaces": [ { + "nic_index": 0, "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" @@ -18,16 +19,19 @@ }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 640, "h": 1080 } }, { + "nic_index": 0, "udp_port": 20002, "payload_type": 96, "crop": { "x": 640, "y": 0, "w": 640, "h": 1080 } }, { + "nic_index": 0, "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 7640ae6..de38033 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -2,7 +2,8 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:06:00.0", + "nic_index": 0, + "name": "0000:01:11.3", "sip": "192.168.50.29", "dip": "239.168.85.20" } @@ -10,16 +11,17 @@ "video": { "width": 3840, "height": 2160, - "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" + "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { - "scale_width": 3840, - "scale_height": 2160, + "scale_width": 1920, + "scale_height": 1080, "fps": 30, "fmt": "yuv422p10le" }, "tx_sessions": [ { + "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } diff --git a/include/ffmpeg/ffmpeg_tx.h b/include/ffmpeg/ffmpeg_tx.h index 8ca3127..9a40cc8 100644 --- a/include/ffmpeg/ffmpeg_tx.h +++ b/include/ffmpeg/ffmpeg_tx.h @@ -38,6 +38,14 @@ * Also pre-allocates enc_frame (crop-width scratch) and enc_pkt * (contiguous packed frame buffer) whose size must equal MTL's frame_size. * + * Multi-NIC: on the session that opens first (idx 0), also registers + * every other configured NIC (up to DVLEDTX_MAX_NICS = 8, MTL's port + * limit) with the mtl_st20p plugin's singleton MTL device via the + * p_port/r_port/p2_port..p7_port AVOptions, since mtl_init()/DPDK EAL is + * only ever invoked once per process. Requires a patched mtl_st20p muxer + * exposing p2_port..p7_port (see README's FFmpeg prerequisite section); + * without it, only 2 NICs (p_port/r_port) can be registered. + * * close_ffmpeg_tx(): * Writes the AVFormat trailer (mtl_st20p: no-op), frees the AVFormatContext * (this destroys the MTL TX session internally), and releases enc_frame/pkt. diff --git a/src/ffmpeg/ffmpeg_tx.c b/src/ffmpeg/ffmpeg_tx.c index bb1b8f0..f457a29 100644 --- a/src/ffmpeg/ffmpeg_tx.c +++ b/src/ffmpeg/ffmpeg_tx.c @@ -41,6 +41,22 @@ * Internal helpers * ========================================================================= */ +/* + * Device-level NIC port AVOptions exposed by the mtl_st20p muxer, one per + * physical port slot (matches MTL's MTL_PORT_MAX = 8: primary, redundant, + * plus 6 additional ports p2..p7 — see mtl_common.h's MTL_TX_DEV_ARGS in + * the FFmpeg MTL plugin). Slot 0 (p_port/p_sip) is always set unconditionally + * further down in open_ffmpeg_tx() for this session's own NIC; slots 1-7 + * are only used once, by session 0, to register every other configured NIC + * with mtl_dev_get()/mtl_init() before DPDK EAL is initialised. */ +#define DVLEDTX_MAX_NICS 8 +static const char* const dvledtx_nic_port_opt[DVLEDTX_MAX_NICS] = { + "p_port", "r_port", "p2_port", "p3_port", "p4_port", "p5_port", "p6_port", "p7_port" +}; +static const char* const dvledtx_nic_sip_opt[DVLEDTX_MAX_NICS] = { + "p_sip", "r_sip", "p2_sip", "p3_sip", "p4_sip", "p5_sip", "p6_sip", "p7_sip" +}; + /* ========================================================================= * open_ffmpeg_tx * ========================================================================= @@ -79,14 +95,27 @@ int open_ffmpeg_tx(struct st20p_tx_ctx* ctx) { } /* Resolve per-session network params from JSON config; fall back to - * app-level defaults for CLI-only runs (session_net[] is zero-initialised). */ - int udp_port = ctx->app->session_net[ctx->idx].udp_port; - if (udp_port == 0) udp_port = (int)ctx->app->udp_port + (ctx->idx * 2); - - int payload_type = ctx->app->session_net[ctx->idx].payload_type; - if (payload_type == 0) payload_type = ctx->app->payload_type; - - int nic = ctx->app->session_net[ctx->idx].nic_index; + * app-level defaults for CLI-only runs (session_net[] is zero-initialised) + * or when idx is out of range for the allocated session_net[] array. */ + int udp_port, payload_type, nic; + if (ctx->idx >= 0 && ctx->idx < ctx->app->st20p_sessions) { + udp_port = ctx->app->session_net[ctx->idx].udp_port; + if (udp_port == 0) udp_port = (int)ctx->app->udp_port + (ctx->idx * 2); + + payload_type = ctx->app->session_net[ctx->idx].payload_type; + if (payload_type == 0) payload_type = ctx->app->payload_type; + + nic = ctx->app->session_net[ctx->idx].nic_index; + } else { + udp_port = (int)ctx->app->udp_port + (ctx->idx * 2); + payload_type = ctx->app->payload_type; + nic = 0; + } + if (nic < 0 || nic >= ctx->app->nic_count) { + LOG_WARN("ST20P TX(%d): nic_index=%d out of range (nic_count=%d); using NIC 0", + ctx->idx, nic, ctx->app->nic_count); + nic = 0; + } 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); @@ -99,30 +128,65 @@ int open_ffmpeg_tx(struct st20p_tx_ctx* ctx) { 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); + /* Multi-NIC: mtl_init()/DPDK EAL is only ever initialised once per + * process (on the FIRST avformat_write_header()), so session 0 + * registers every configured NIC (up to DVLEDTX_MAX_NICS) as a + * device-level port via the p_port/r_port/p2_port..p7_port AVOptions + * (see dvledtx_nic_*_opt[] above). Slot 1 (r_port) aliases this + * session's own MTL_SESSION_PORT_R, so it also needs a valid r_tx_ip + * or st20_tx_create() rejects it with "invalid ip 0.0.0.0". Requires a + * patched mtl_st20p muxer exposing p2_port..p7_port (see README). */ + if (ctx->idx == 0) { + int nic_total = ctx->app->nic_count; + if (nic_total > DVLEDTX_MAX_NICS) { + LOG_WARN("ST20P TX(%d): nic_count=%d exceeds MTL's %d-port limit; " + "only the first %d NICs will be registered", + ctx->idx, nic_total, DVLEDTX_MAX_NICS, DVLEDTX_MAX_NICS); + nic_total = DVLEDTX_MAX_NICS; + } + + /* Fail fast if the muxer lacks p2_port: without it, sessions beyond + * the first 2 NICs would silently only get a WARN from av_opt_set() + * below, then fail later in a much harder-to-debug way once MTL/EAL + * is already initialised. */ + if (nic_total > 2 && + av_opt_find(ctx->out_fmt_ctx->priv_data, "p2_port", NULL, 0, 0) == NULL) { + LOG_ERROR("ST20P TX(%d): mtl_st20p muxer lacks p2_port AVOption; " + "rebuild FFmpeg with the patched mtl_common.h (see README) " + "to support more than 2 NICs", ctx->idx); + avformat_free_context(ctx->out_fmt_ctx); ctx->out_fmt_ctx = NULL; + return -1; + } + + int slot = 1; /* slot 0 (p_port/p_sip) already carries this session's own NIC */ + for (int i = 0; i < nic_total && slot < DVLEDTX_MAX_NICS; i++) { + if (i == nic) continue; /* already registered in slot 0 */ + + ret = av_opt_set(ctx->out_fmt_ctx->priv_data, dvledtx_nic_port_opt[slot], + ctx->app->nics[i].port, 0); + if (ret < 0) + LOG_WARN("ST20P TX(%d): av_opt_set %s failed (ret=%d)", + ctx->idx, dvledtx_nic_port_opt[slot], ret); + ret = av_opt_set(ctx->out_fmt_ctx->priv_data, dvledtx_nic_sip_opt[slot], + ctx->app->nics[i].sip_addr_str, 0); + if (ret < 0) + LOG_WARN("ST20P TX(%d): av_opt_set %s failed (ret=%d)", + ctx->idx, dvledtx_nic_sip_opt[slot], ret); + + /* Slot 1 (r_port) aliases this session's own MTL_SESSION_PORT_R — + * give it a valid dip so st20_tx_create()'s tv_ops_check() doesn't + * reject it (see comment above). Slots 2-7 are never read as this + * session's own transport port, so no dip is needed for them. */ + if (slot == 1) { + ret = av_opt_set(ctx->out_fmt_ctx->priv_data, "r_tx_ip", + ctx->app->nics[i].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 NIC %s (%s) with MTL for multi-NIC support", + ctx->idx, ctx->app->nics[i].port, dvledtx_nic_port_opt[slot]); + slot++; + } } /* RAWVIDEO stream — no encoder; MTL accepts raw packed pixel data directly. */ diff --git a/src/util/config_reader.c b/src/util/config_reader.c index 52bc5cb..c8cdc05 100644 --- a/src/util/config_reader.c +++ b/src/util/config_reader.c @@ -198,7 +198,9 @@ int peek_config_log_file(const char* config_file, char* out_buf, size_t out_size * * Expected JSON structure: * { - * "interfaces": [ { "name": "...", "sip": "...", "dip": "..." } ], + * "interfaces": [ { "nic_index": N, "name": "...", "sip": "...", "dip": "..." } ], + * -- "nic_index" is optional but, if present, must equal the entry's + * position in the array (0, 1, 2, ...); parsing fails otherwise. * "video": { "width": N, "height": N, "tx_url": "..." }, * "tx_video": { "scale_width": N, "scale_height": N, "fps": N, "fmt": "..." }, * "log_file": "/path/to/dvledtx.log", (optional — omit for console-only logging) @@ -297,6 +299,25 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { 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])); + + /* "nic_index" (if present) is informational only — interfaces + * are always stored at their position in the array (n). It + * must match that position so the JSON stays self-consistent + * with how tx_sessions[].nic_index actually resolves to this + * interface; catch authoring mistakes (e.g. reordered or + * duplicated entries) early instead of silently misrouting + * sessions to the wrong NIC. */ + int declared_idx = extract_json_int(iface_obj, iface_end, "nic_index"); + if (declared_idx >= 0 && declared_idx != n) { + LOG_ERROR("interfaces[%d]: nic_index %d does not match its array " + "position %d; nic_index must equal the interface's " + "position within the interfaces array", + n, declared_idx, n); + free(json); + dvledtx_config_free(config); + return -1; + } + config->nic_count++; icursor = iface_end + 1; } diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index 8d248ab..c521c8c 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -720,6 +720,96 @@ static void test_parse_session_no_crop_object_fails(void **state) dvledtx_config_free(&cfg); } +/* ========================================================================== + * parse_tx_config — interfaces[].nic_index validation + * ========================================================================== */ + +static void test_parse_interfaces_nic_index_omitted_passes(void **state) +{ + (void)state; + /* No "nic_index" field at all — backward compatible, no validation. */ + char *path = write_tmpfile( + "{" + " \"interfaces\": [{\"name\":\"eth0\",\"sip\":\"1.2.3.4\",\"dip\":\"5.6.7.8\"}]," + " \"video\": {\"width\":1920,\"height\":1080,\"fps\":25,\"fmt\":\"yuv422p10le\"}," + " \"tx_sessions\": [{\"udp_port\":20000,\"payload_type\":96," + " \"crop\":{\"x\":0,\"y\":0,\"w\":1920,\"h\":1080}}]" + "}"); + assert_non_null(path); + struct dvledtx_config cfg; + int ret = parse_tx_config(path, &cfg); + unlink(path); free(path); + assert_int_equal(ret, 0); + assert_int_equal(cfg.nic_count, 1); + dvledtx_config_free(&cfg); +} + +static void test_parse_interfaces_nic_index_matches_position_passes(void **state) +{ + (void)state; + /* nic_index values match each entry's actual array position (0, 1). */ + char *path = write_tmpfile( + "{" + " \"interfaces\": [" + " {\"nic_index\":0,\"name\":\"0000:01:10.1\",\"sip\":\"1.2.3.4\",\"dip\":\"239.1.1.1\"}," + " {\"nic_index\":1,\"name\":\"0000:01:10.3\",\"sip\":\"1.2.3.5\",\"dip\":\"239.1.1.2\"}" + " ]," + " \"video\": {\"width\":1920,\"height\":1080,\"fps\":25,\"fmt\":\"yuv422p10le\"}," + " \"tx_sessions\": [{\"udp_port\":20000,\"payload_type\":96,\"nic_index\":1," + " \"crop\":{\"x\":0,\"y\":0,\"w\":1920,\"h\":1080}}]" + "}"); + assert_non_null(path); + struct dvledtx_config cfg; + int ret = parse_tx_config(path, &cfg); + unlink(path); free(path); + assert_int_equal(ret, 0); + assert_int_equal(cfg.nic_count, 2); + assert_string_equal(cfg.interface_name[0], "0000:01:10.1"); + assert_string_equal(cfg.interface_name[1], "0000:01:10.3"); + dvledtx_config_free(&cfg); +} + +static void test_parse_interfaces_nic_index_mismatch_fails(void **state) +{ + (void)state; + /* Second entry claims nic_index 5, but its actual array position is 1. */ + char *path = write_tmpfile( + "{" + " \"interfaces\": [" + " {\"nic_index\":0,\"name\":\"0000:01:10.1\",\"sip\":\"1.2.3.4\",\"dip\":\"239.1.1.1\"}," + " {\"nic_index\":5,\"name\":\"0000:01:10.3\",\"sip\":\"1.2.3.5\",\"dip\":\"239.1.1.2\"}" + " ]," + " \"video\": {\"width\":1920,\"height\":1080,\"fps\":25,\"fmt\":\"yuv422p10le\"}," + " \"tx_sessions\": [{\"udp_port\":20000,\"payload_type\":96," + " \"crop\":{\"x\":0,\"y\":0,\"w\":1920,\"h\":1080}}]" + "}"); + assert_non_null(path); + struct dvledtx_config cfg; + int ret = parse_tx_config(path, &cfg); + unlink(path); free(path); + assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); +} + +static void test_parse_interfaces_nic_index_first_entry_mismatch_fails(void **state) +{ + (void)state; + /* Single interface but nic_index declared as 1 instead of its position 0. */ + char *path = write_tmpfile( + "{" + " \"interfaces\": [{\"nic_index\":1,\"name\":\"eth0\",\"sip\":\"1.2.3.4\",\"dip\":\"5.6.7.8\"}]," + " \"video\": {\"width\":1920,\"height\":1080,\"fps\":25,\"fmt\":\"yuv422p10le\"}," + " \"tx_sessions\": [{\"udp_port\":20000,\"payload_type\":96," + " \"crop\":{\"x\":0,\"y\":0,\"w\":1920,\"h\":1080}}]" + "}"); + assert_non_null(path); + struct dvledtx_config cfg; + int ret = parse_tx_config(path, &cfg); + unlink(path); free(path); + assert_int_equal(ret, -1); + dvledtx_config_free(&cfg); +} + /* ========================================================================== * validate_tx_config — additional failure / edge-case paths * ========================================================================== */ @@ -1504,6 +1594,10 @@ int main(void) cmocka_unit_test(test_parse_session_no_crop_object_fails), cmocka_unit_test(test_parse_session_negative_crop_x_fails), cmocka_unit_test(test_parse_session_zero_crop_w_fails), + cmocka_unit_test(test_parse_interfaces_nic_index_omitted_passes), + cmocka_unit_test(test_parse_interfaces_nic_index_matches_position_passes), + cmocka_unit_test(test_parse_interfaces_nic_index_mismatch_fails), + cmocka_unit_test(test_parse_interfaces_nic_index_first_entry_mismatch_fails), /* --- validate_tx_config --- */ cmocka_unit_test(test_validate_valid_config_passes),