From 3e268f3ef64364a58b543c8810fa87964cd127c6 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Tue, 21 Jul 2026 11:36:11 +0530 Subject: [PATCH 1/8] Support up to 8 NICs in FFmpeg TX path, remove PTP code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-NIC (FFmpeg avdevice mtl_st20p path): - Register up to DVLEDTX_MAX_NICS (8, MTL_PORT_MAX) NICs with the shared MTL device via p_port/r_port/p2_port..p7_port AVOptions, since mtl_init()/DPDK EAL is only initialised once per process (session 0 registers every configured NIC on first avformat_write_header()). - Set r_tx_ip on session 0's r_port slot so st20_tx_create()'s tv_ops_check() doesn't reject it with "invalid ip 0.0.0.0" (slot 1 aliases MTL_SESSION_PORT_R, so it needs a valid destination IP even though slots 2-7 are device-only registrations). - Requires a patched mtl_st20p muxer exposing p2_port..p7_port / p2_sip..p7_sip AVOptions (documented in README); the ENABLE_MTL_TX direct pipeline already supports 8 NICs without any FFmpeg patch. - Update config/tx_fullhd_multi_nic.json to an 8-NIC/8-session layout and tx_fullhd_single_session.json for this test environment. Remove PTP (hardware timing) support: - Drop ptp_enable/ptp_pi/ptp_unicast from app_context.h and config_reader.h/.c (was hardcoded, not JSON-configurable). - Remove the AVOption-setting block in ffmpeg_tx.c — these options were silently failing on every run (not exposed by the compiled mtl_st20p muxer), so this was dead/no-op code. - Remove MTL_FLAG_PTP_* setup and the mtl_tx_ptp_sync_notify_cb callback from the direct MTL TX path (mtl_tx.c). Validated end-to-end on hardware with 8 SR-IOV VFs: all 8 sessions open and transmit cleanly with no PTP-related warnings. --- README.md | 3 + config/tx_fullhd_multi_nic.json | 85 ++++++++++++++++------ config/tx_fullhd_single_session.json | 8 +-- include/ffmpeg/ffmpeg_tx.h | 8 +++ src/ffmpeg/ffmpeg_tx.c | 103 ++++++++++++++++++++++----- 5 files changed, 162 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index bc1a978..55f5c8f 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 @@ -348,6 +349,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 a 6-NIC 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_fullhd_multi_nic.json b/config/tx_fullhd_multi_nic.json index 4c2f286..58c3ca7 100644 --- a/config/tx_fullhd_multi_nic.json +++ b/config/tx_fullhd_multi_nic.json @@ -2,40 +2,50 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:03:10.0", - "sip": "192.168.50.30", - "dip": "239.168.85.20" + "name": "0000:01:10.1", + "sip": "192.168.50.40", + "dip": "239.168.85.30" }, { - "name": "0000:03:10.2", - "sip": "192.168.50.31", - "dip": "239.168.85.21" + "name": "0000:01:10.3", + "sip": "192.168.50.41", + "dip": "239.168.85.31" }, { - "name": "0000:03:10.4", - "sip": "192.168.50.32", - "dip": "239.168.85.22" + "name": "0000:01:10.5", + "sip": "192.168.50.42", + "dip": "239.168.85.32" }, { - "name": "0000:03:10.6", - "sip": "192.168.50.33", - "dip": "239.168.85.23" + "name": "0000:01:10.7", + "sip": "192.168.50.43", + "dip": "239.168.85.33" }, { - "name": "0000:03:11.0", - "sip": "192.168.50.34", - "dip": "239.168.85.24" + "name": "0000:01:11.1", + "sip": "192.168.50.44", + "dip": "239.168.85.34" }, { - "name": "0000:03:11.2", - "sip": "192.168.50.35", - "dip": "239.168.85.25" + "name": "0000:01:11.3", + "sip": "192.168.50.45", + "dip": "239.168.85.35" + }, + { + "name": "0000:01:11.5", + "sip": "192.168.50.46", + "dip": "239.168.85.36" + }, + { + "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": "/home/intel/workspace/bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { "scale_width": 1920, @@ -48,20 +58,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_single_session.json b/config/tx_fullhd_single_session.json index 7640ae6..6ed75bf 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -2,7 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:06:00.0", + "name": "0000:01:11.3", "sip": "192.168.50.29", "dip": "239.168.85.20" } @@ -10,11 +10,11 @@ "video": { "width": 3840, "height": 2160, - "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" + "tx_url": "/home/intel/workspace/bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { - "scale_width": 3840, - "scale_height": 2160, + "scale_width": 1920, + "scale_height": 1080, "fps": 30, "fmt": "yuv422p10le" }, 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..56f8be8 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 * ========================================================================= @@ -99,30 +115,79 @@ 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. + /* Multi-NIC (up to DVLEDTX_MAX_NICS physical ports, matching MTL's + * MTL_PORT_MAX): 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 referenced by ANY + * session must be registered with mtl_init() at that first call. + * + * The mtl_st20p muxer exposes one AVOption pair per device-level port + * slot: p_port/p_sip (slot 0), r_port/r_sip (slot 1), and + * p2_port/p2_sip .. p7_port/p7_sip (slots 2-7) — see DVLEDTX_NIC_SLOTS. + * These feed StDevArgs.port[]/sip[] in the plugin, which mtl_dev_get() + * scans up to MTL_PORT_MAX to build mtl_init_params for mtl_init(). + * + * This is primarily a *device*-level registration: it does not affect + * which physical port an individual session transmits on (that is + * still chosen per-session via this session's own p_port, set above + * from ctx->app->nics[nic].port). Slots 2-7 (p2_port..p7_port) are + * pure device-level additions — mtl_parse_tx_port() in the plugin only + * ever reads devArgs.port[0]/[1] (MTL_SESSION_PORT_MAX == 2) as a + * fallback for THIS session's own transport port(s). * - * 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. + * Slot 1 (r_port) is the exception: because it aliases + * MTL_SESSION_PORT_R, populating it here also makes session 0's own + * st20p stream a genuine ST 2022-7 dual-port (primary+redundant) + * transmission — i.e. session 0 additionally duplicates its own crop + * onto NIC[1]. r_tx_ip must therefore be set to a valid destination IP + * (that NIC's own configured dip) or st20_tx_create() fails with + * "invalid ip 0.0.0.0". This is unavoidable: mtl_dev_get()'s device + * port scan stops at the first unset slot, so slot 1 must be populated + * contiguously for slots 2-7 to be registered at all. * * 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); + * tests), so later runs could silently skip this registration. */ + 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; + } + + 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. */ From 4f4ca9d835d02649e6dca4a8df13cf7020639077 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Tue, 21 Jul 2026 13:29:55 +0530 Subject: [PATCH 2/8] Fix out-of-bounds session_net/nics indexing in open_ffmpeg_tx Guard ctx->idx against ctx->app->st20p_sessions and nic_index against nic_count before indexing session_net[]/nics[], falling back to app-level defaults / NIC 0 when out of range. Fixes a segfault in test_open_ffmpeg_tx_uses_app_defaults (pre-existing bug from upstream's Multiple NIC for Transmission commit). --- src/ffmpeg/ffmpeg_tx.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/ffmpeg/ffmpeg_tx.c b/src/ffmpeg/ffmpeg_tx.c index 56f8be8..3b40a07 100644 --- a/src/ffmpeg/ffmpeg_tx.c +++ b/src/ffmpeg/ffmpeg_tx.c @@ -95,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); From d05c25f739e1449c13df7b544580f3a1f9f5bc68 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Thu, 23 Jul 2026 08:54:42 +0530 Subject: [PATCH 3/8] Address PR review comments - Use relative filenames (not absolute paths) for tx_url in example configs - Fix stale DVLEDTX_NIC_SLOTS reference in ffmpeg_tx.c comment - Minimize the multi-NIC registration comment in ffmpeg_tx.c - Fail fast with a clear error if the mtl_st20p muxer lacks p2_port when more than 2 NICs are configured, instead of continuing with only warnings - Fix README's tx_fullhd_multi_nic.json description (was '6-NIC', now '8-NIC/8-session') --- README.md | 2 +- config/tx_fullhd_multi_nic.json | 2 +- config/tx_fullhd_single_session.json | 2 +- src/ffmpeg/ffmpeg_tx.c | 56 +++++++++++----------------- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 55f5c8f..e34a4d9 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,7 @@ 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 a 6-NIC example). Using more than 2 NICs with the default FFmpeg TX path requires the patched `mtl_st20p` muxer described above. +`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 diff --git a/config/tx_fullhd_multi_nic.json b/config/tx_fullhd_multi_nic.json index 58c3ca7..7db86e5 100644 --- a/config/tx_fullhd_multi_nic.json +++ b/config/tx_fullhd_multi_nic.json @@ -45,7 +45,7 @@ "video": { "width": 1920, "height": 1080, - "tx_url": "/home/intel/workspace/bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { "scale_width": 1920, diff --git a/config/tx_fullhd_single_session.json b/config/tx_fullhd_single_session.json index 6ed75bf..8d66328 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": "/home/intel/workspace/bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" }, "tx_video": { "scale_width": 1920, diff --git a/src/ffmpeg/ffmpeg_tx.c b/src/ffmpeg/ffmpeg_tx.c index 3b40a07..f457a29 100644 --- a/src/ffmpeg/ffmpeg_tx.c +++ b/src/ffmpeg/ffmpeg_tx.c @@ -128,41 +128,14 @@ 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 (up to DVLEDTX_MAX_NICS physical ports, matching MTL's - * MTL_PORT_MAX): 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 referenced by ANY - * session must be registered with mtl_init() at that first call. - * - * The mtl_st20p muxer exposes one AVOption pair per device-level port - * slot: p_port/p_sip (slot 0), r_port/r_sip (slot 1), and - * p2_port/p2_sip .. p7_port/p7_sip (slots 2-7) — see DVLEDTX_NIC_SLOTS. - * These feed StDevArgs.port[]/sip[] in the plugin, which mtl_dev_get() - * scans up to MTL_PORT_MAX to build mtl_init_params for mtl_init(). - * - * This is primarily a *device*-level registration: it does not affect - * which physical port an individual session transmits on (that is - * still chosen per-session via this session's own p_port, set above - * from ctx->app->nics[nic].port). Slots 2-7 (p2_port..p7_port) are - * pure device-level additions — mtl_parse_tx_port() in the plugin only - * ever reads devArgs.port[0]/[1] (MTL_SESSION_PORT_MAX == 2) as a - * fallback for THIS session's own transport port(s). - * - * Slot 1 (r_port) is the exception: because it aliases - * MTL_SESSION_PORT_R, populating it here also makes session 0's own - * st20p stream a genuine ST 2022-7 dual-port (primary+redundant) - * transmission — i.e. session 0 additionally duplicates its own crop - * onto NIC[1]. r_tx_ip must therefore be set to a valid destination IP - * (that NIC's own configured dip) or st20_tx_create() fails with - * "invalid ip 0.0.0.0". This is unavoidable: mtl_dev_get()'s device - * port scan stops at the first unset slot, so slot 1 must be populated - * contiguously for slots 2-7 to be registered at all. - * - * 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 registration. */ + /* 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) { @@ -172,6 +145,19 @@ int open_ffmpeg_tx(struct st20p_tx_ctx* ctx) { 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 */ From a5a06cf50c874b31aac2911cb9977cf3fa4be7bc Mon Sep 17 00:00:00 2001 From: sunilnom Date: Thu, 23 Jul 2026 09:45:44 +0530 Subject: [PATCH 4/8] config: add explicit nic_index to interfaces[] and tx_sessions[] Add an explicit "nic_index" field to every interfaces[] entry and every tx_sessions[] entry across all sample config files, matching each entry's array position. This makes the schema consistent for single-NIC and multi-NIC configs alike (tx_fullhd_multi_nic.json already used this convention for tx_sessions[]). --- config/tx_1session_12bit.json | 2 ++ config/tx_2k_multi_session.json | 4 ++++ config/tx_2k_single_session.json | 4 +++- config/tx_4k_multi_session.json | 4 ++++ config/tx_4k_single_session.json | 2 ++ config/tx_fullhd_multi_nic.json | 8 ++++++++ config/tx_fullhd_multi_session.json | 8 ++++++-- config/tx_fullhd_screen_capture.json | 2 ++ config/tx_fullhd_screen_capture_multi_session.json | 4 ++++ config/tx_fullhd_single_session.json | 2 ++ 10 files changed, 37 insertions(+), 3 deletions(-) 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 7db86e5..cdc4d8c 100644 --- a/config/tx_fullhd_multi_nic.json +++ b/config/tx_fullhd_multi_nic.json @@ -2,41 +2,49 @@ "log_file": "dvledtx.log", "interfaces": [ { + "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" }, { + "nic_index": 3, "name": "0000:01:10.7", "sip": "192.168.50.43", "dip": "239.168.85.33" }, { + "nic_index": 4, "name": "0000:01:11.1", "sip": "192.168.50.44", "dip": "239.168.85.34" }, { + "nic_index": 5, "name": "0000:01:11.3", "sip": "192.168.50.45", "dip": "239.168.85.35" }, { + "nic_index": 6, "name": "0000:01:11.5", "sip": "192.168.50.46", "dip": "239.168.85.36" }, { + "nic_index": 7, "name": "0000:01:11.7", "sip": "192.168.50.47", "dip": "239.168.85.37" diff --git a/config/tx_fullhd_multi_session.json b/config/tx_fullhd_multi_session.json index 8e5d1bf..f10494a 100644 --- a/config/tx_fullhd_multi_session.json +++ b/config/tx_fullhd_multi_session.json @@ -2,7 +2,8 @@ "log_file": "dvledtx.log", "interfaces": [ { - "name": "0000:03:10.1", + "nic_index": 0, + "name": "0000:01:11.3", "sip": "192.168.50.29", "dip": "239.168.85.21" } @@ -10,7 +11,7 @@ "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, @@ -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 8d66328..de38033 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -2,6 +2,7 @@ "log_file": "dvledtx.log", "interfaces": [ { + "nic_index": 0, "name": "0000:01:11.3", "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": 1920, "h": 1080 } From ec96fda64b9a170de1ab8b030a3bf437d8b75365 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Thu, 23 Jul 2026 09:50:04 +0530 Subject: [PATCH 5/8] config: restore original interface/tx_url in tx_fullhd_multi_session.json Local ad-hoc edits (swapping in an available NIC/video file for this dev environment) were accidentally included in the previous commit, breaking test_parse_3sessions_interface_fields which asserts the fixture's original interface name (0000:03:10.1) and tx_url. Restore both to their committed values; nic_index additions are unaffected. --- config/tx_fullhd_multi_session.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/tx_fullhd_multi_session.json b/config/tx_fullhd_multi_session.json index f10494a..87df774 100644 --- a/config/tx_fullhd_multi_session.json +++ b/config/tx_fullhd_multi_session.json @@ -3,7 +3,7 @@ "interfaces": [ { "nic_index": 0, - "name": "0000:01:11.3", + "name": "0000:03:10.1", "sip": "192.168.50.29", "dip": "239.168.85.21" } @@ -11,7 +11,7 @@ "video": { "width": 1920, "height": 1080, - "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "ball_fullhd_yuv422p10_30fps_5min.mp4" }, "tx_video": { "scale_width": 1920, From 2dea57c6b88762d6cbbdedbaacf7a1ed2906ecb9 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Thu, 23 Jul 2026 10:11:57 +0530 Subject: [PATCH 6/8] Validate interfaces[].nic_index matches its array position Parsing now fails fast with a clear error if an interfaces[] entry's optional nic_index does not equal its actual position in the array, catching config authoring mistakes (reordered/duplicated entries) before they silently misroute a tx_session to the wrong NIC. Interfaces are still stored positionally; nic_index is not otherwise used to control storage. --- src/util/config_reader.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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; } From 6c04ac70cb71d9ac5c975e1a27afc19073c30102 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Thu, 23 Jul 2026 11:34:45 +0530 Subject: [PATCH 7/8] Add unit tests for interfaces[].nic_index position validation Covers the new validation added in config_reader.c: - nic_index omitted (backward compatible) - nic_index matches actual array position (multi-interface) - nic_index mismatch on a non-first entry fails parsing - nic_index mismatch on the first (only) entry fails parsing --- tests/test_config_reader.c | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) 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), From 9946f82a1a7fc097cbc36b021080a6c84048c662 Mon Sep 17 00:00:00 2001 From: sunilnom Date: Thu, 23 Jul 2026 11:37:22 +0530 Subject: [PATCH 8/8] docs: document interfaces[].nic_index in README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e34a4d9..ac43049 100644 --- a/README.md +++ b/README.md @@ -144,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 | @@ -166,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,