diff --git a/README.md b/README.md index a62771f..7798477 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ dvledtx uses a JSON config file with three sections: | | `payload_type` | RTP payload type (typically 96) | | | `crop` | Region to transmit: `x`, `y`, `w`, `h` in pixels | -Example (`config/tx_1session.json`): +Example (`config/tx_fullhd_single_session.json`): ```json { "log_file": "dvledtx.log", @@ -156,7 +156,7 @@ Example (`config/tx_1session.json`): } ``` -Multiple sessions can be defined in `tx_sessions` to transmit different crop regions of the same video simultaneously (see `config/tx_3sessions.json`). +Multiple sessions can be defined in `tx_sessions` to transmit different crop regions of the same video simultaneously (see `config/tx_fullhd_multi_session.json`). ## Logging @@ -188,7 +188,7 @@ When `log_file` is set, log output is written to that file in addition to the co #### Using JSON Configuration (recommended) ```bash -./build/dvledtx --config config/tx_1session.json +./build/dvledtx --config config/tx_fullhd_single_session.json ``` ## Command-Line Options @@ -227,7 +227,14 @@ When `log_file` is set, log output is written to that file in addition to the co - 60 fps ### Resolutions -- Tested with 1920x1080 + +| Resolution | Dimensions | Description | +|------------|------------|-------------| +| **1080p** (Full HD) | 1920x1080 | Standard HD resolution | +| **2K** (QHD) | 2560x1440 | Quad HD / 2K resolution | +| **4K** (UHD) | 3840x2160 | Ultra HD / 4K resolution | + +> **Note:** Maximum supported resolution is 3840x2160. Width must be even for YUV format alignment. ## Performance Considerations diff --git a/config/tx_2k_multi_session.json b/config/tx_2k_multi_session.json new file mode 100644 index 0000000..2b4c9f3 --- /dev/null +++ b/config/tx_2k_multi_session.json @@ -0,0 +1,34 @@ +{ + "log_file": "dvledtx.log", + "interfaces": [ + { + "name": "0000:03:00.1", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 2560, + "height": 1440, + "fps": 30, + "fmt": "yuv422p10le", + "tx_url": "ball_2k_yuv420p_30fps_5min.mp4" + }, + "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_1session.json b/config/tx_2k_single_session.json similarity index 60% rename from config/tx_1session.json rename to config/tx_2k_single_session.json index ef34df0..a2a5c86 100644 --- a/config/tx_1session.json +++ b/config/tx_2k_single_session.json @@ -1,24 +1,24 @@ -{ +{ "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" } ], "video": { - "width": 1920, - "height": 1080, + "width": 2560, + "height": 1440, "fps": 30, "fmt": "yuv422p10le", - "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" + "tx_url": "ball_2k_yuv420p_30fps_5min.mp4" }, "tx_sessions": [ { "udp_port": 20000, "payload_type": 96, - "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } + "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 new file mode 100644 index 0000000..bb83a02 --- /dev/null +++ b/config/tx_4k_multi_session.json @@ -0,0 +1,34 @@ +{ + "log_file": "dvledtx.log", + "interfaces": [ + { + "name": "0000:03:00.1", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 3840, + "height": 2160, + "fps": 30, + "fmt": "yuv422p10le", + "tx_url": "ball_4k_yuv420p_30fps_5min.mp4" + }, + "tx_sessions": [ + { + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 1280, "h": 2160 } + }, + { + "udp_port": 20002, + "payload_type": 96, + "crop": { "x": 1280, "y": 0, "w": 1280, "h": 2160 } + }, + { + "udp_port": 20004, + "payload_type": 96, + "crop": { "x": 2560, "y": 0, "w": 1280, "h": 2160 } + } + ] +} diff --git a/config/tx_4k_single_session.json b/config/tx_4k_single_session.json new file mode 100644 index 0000000..df69353 --- /dev/null +++ b/config/tx_4k_single_session.json @@ -0,0 +1,24 @@ +{ + "log_file": "dvledtx.log", + "interfaces": [ + { + "name": "0000:03:00.1", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 3840, + "height": 2160, + "fps": 30, + "fmt": "yuv422p10le", + "tx_url": "ball_4k_yuv420p_30fps_5min.mp4" + }, + "tx_sessions": [ + { + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 } + } + ] +} diff --git a/config/tx_3sessions.json b/config/tx_fullhd_multi_session.json similarity index 100% rename from config/tx_3sessions.json rename to config/tx_fullhd_multi_session.json diff --git a/config/tx_fullhd_single_session.json b/config/tx_fullhd_single_session.json new file mode 100644 index 0000000..82bcdff --- /dev/null +++ b/config/tx_fullhd_single_session.json @@ -0,0 +1,24 @@ +{ + "log_file": "dvledtx.log", + "interfaces": [ + { + "name": "0000:03:00.1", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 3840, + "height": 2160, + "fps": 60, + "fmt": "gbrp10le", + "tx_url": "ball_4k_gbrp10le_30fps_5min.mp4" + }, + "tx_sessions": [ + { + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 } + } + ] +} diff --git a/src/util/config_reader.c b/src/util/config_reader.c index 09b6841..c475e07 100644 --- a/src/util/config_reader.c +++ b/src/util/config_reader.c @@ -417,13 +417,13 @@ int validate_tx_config(const struct dvledtx_config* config) { LOG_ERROR("video width/height must be non-zero"); return -1; } - if (config->width > 1920 || config->height > 1080) { - LOG_ERROR("video resolution %dx%d exceeds maximum 1920x1080", - config->width, config->height); + if (config->width > 3840 || config->height > 2160) { + LOG_ERROR("video resolution %ux%u exceeds maximum 3840x2160", + (unsigned)config->width, (unsigned)config->height); return -1; } if (config->width % 2 != 0) { - LOG_ERROR("video width %d must be even for YUV formats", config->width); + LOG_ERROR("video width %u must be even for YUV formats", (unsigned)config->width); return -1; } diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index 8bea230..241536b 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -32,8 +32,8 @@ # error "FIXTURE_DIR must be defined by the build system (-DFIXTURE_DIR=...)" #endif -#define FIXTURE_3SESSIONS FIXTURE_DIR "/tx_3sessions.json" -#define FIXTURE_1SESSION FIXTURE_DIR "/tx_1session.json" +#define FIXTURE_3SESSIONS FIXTURE_DIR "/tx_fullhd_multi_session.json" +#define FIXTURE_1SESSION FIXTURE_DIR "/tx_fullhd_single_session.json" /* -------------------------------------------------------------------------- * Helper: write content to a new temp file, return heap-allocated path. @@ -449,7 +449,7 @@ static void test_validate_zero_session_count_fails(void **state) static void test_validate_3sessions_tiled_layout_passes(void **state) { (void)state; - /* Mirrors tx_3sessions.json: three 640-wide horizontal tiles */ + /* 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); @@ -602,7 +602,41 @@ static void test_validate_resolution_exceeds_max_fails(void **state) (void)state; struct dvledtx_config cfg; fill_valid_config(&cfg); - cfg.width = 2000; /* > 1920 limit */ + cfg.width = 4000; /* > 3840 limit */ + assert_int_equal(validate_tx_config(&cfg), -1); +} + +static void test_validate_2k_resolution_passes(void **state) +{ + (void)state; + struct dvledtx_config cfg; + fill_valid_config(&cfg); + cfg.width = 2560; + cfg.height = 1440; + cfg.sessions[0].crop_w = 2560; + cfg.sessions[0].crop_h = 1440; + assert_int_equal(validate_tx_config(&cfg), 0); +} + +static void test_validate_4k_resolution_passes(void **state) +{ + (void)state; + struct dvledtx_config cfg; + fill_valid_config(&cfg); + cfg.width = 3840; + cfg.height = 2160; + cfg.sessions[0].crop_w = 3840; + cfg.sessions[0].crop_h = 2160; + assert_int_equal(validate_tx_config(&cfg), 0); +} + +static void test_validate_4k_height_exceeds_max_fails(void **state) +{ + (void)state; + struct dvledtx_config cfg; + fill_valid_config(&cfg); + cfg.width = 3840; + cfg.height = 2162; /* > 2160 limit */ assert_int_equal(validate_tx_config(&cfg), -1); } @@ -1141,6 +1175,9 @@ int main(void) cmocka_unit_test(test_validate_zero_session_count_fails), cmocka_unit_test(test_validate_3sessions_tiled_layout_passes), cmocka_unit_test(test_validate_resolution_exceeds_max_fails), + cmocka_unit_test(test_validate_2k_resolution_passes), + cmocka_unit_test(test_validate_4k_resolution_passes), + cmocka_unit_test(test_validate_4k_height_exceeds_max_fails), cmocka_unit_test(test_validate_duplicate_udp_ports_fails), cmocka_unit_test(test_validate_crop_x_misaligned_for_yuv422_fails), cmocka_unit_test(test_validate_tx_url_nonexistent_file_fails),