From a7c843aa59961c5886c26be1bd2bb1bd4890ad44 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 30 Jun 2026 11:41:46 +0530 Subject: [PATCH 01/10] Add config-driven screen capture input mode --- README.md | 26 +++++- config/tx_1session_12bit.json | 23 +++++ config/tx_fullhd_screen_capture.json | 26 ++++++ config/tx_fullhd_single_session.json | 1 - include/app_context.h | 2 + include/util/config_reader.h | 2 + meson_options.txt | 2 +- src/core/session_manager.c | 6 +- src/ffmpeg/ffmpeg_decoder.c | 39 ++++++++- src/util/config_reader.c | 59 ++++++++++--- tests/test_config_reader.c | 124 +++++++++++++++++++++++++++ tests/test_session_manager.c | 47 ++++++++++ 12 files changed, 339 insertions(+), 18 deletions(-) create mode 100644 config/tx_1session_12bit.json create mode 100644 config/tx_fullhd_screen_capture.json diff --git a/README.md b/README.md index 9178494..6e7a299 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,9 @@ dvledtx uses a JSON config file with three sections: | | `dip` | Destination multicast IP address | | **video** | `width` | Source frame width in pixels | | | `height` | Source frame height in pixels | -| | `tx_url` | Path to the source video file | +| | `input_mode` | (Optional) Video input mode: `file` (default) or `screen_capture` | +| | `tx_url` | Path to the source video file (used when `input_mode=file`) | +| | `screen_input` | (Optional) x11grab source string for screen capture (used when `input_mode=screen_capture`, default `:0.0+0,0`) | | **tx_video** | `scale_width` | (Optional) Output width after scaling | | | `scale_height` | (Optional) Output height after scaling | | | `fps` | Frames per second (25, 30, 50, 60) | @@ -162,6 +164,28 @@ Example (`config/tx_fullhd_single_session.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`). +Example screen-capture input (`config/tx_fullhd_screen_capture.json`): +```json +{ + "interfaces": [ + { "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" } + ], + "video": { + "width": 1920, + "height": 1080, + "input_mode": "screen_capture", + "screen_input": ":0.0+0,0" + }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p10le" + }, + "tx_sessions": [ + { "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } } + ] +} +``` + ## 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 new file mode 100644 index 0000000..1738448 --- /dev/null +++ b/config/tx_1session_12bit.json @@ -0,0 +1,23 @@ +{ + "interfaces": [ + { + "name": "0000:06:00.0", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 1920, + "height": 1080, + "fps": 30, + "fmt": "yuv422p12le", + "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" + }, + "tx_sessions": [ + { + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } + } + ] +} diff --git a/config/tx_fullhd_screen_capture.json b/config/tx_fullhd_screen_capture.json new file mode 100644 index 0000000..6a8d430 --- /dev/null +++ b/config/tx_fullhd_screen_capture.json @@ -0,0 +1,26 @@ +{ + "interfaces": [ + { + "name": "0000:06:00.0", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 1920, + "height": 1080, + "input_mode": "screen_capture", + "screen_input": ":0.0+0,0" + }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p10le" + }, + "tx_sessions": [ + { + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } + } + ] +} diff --git a/config/tx_fullhd_single_session.json b/config/tx_fullhd_single_session.json index 2939030..0d5369b 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -1,5 +1,4 @@ { - "log_file": "dvledtx.log", "interfaces": [ { "name": "0000:06:00.0", diff --git a/include/app_context.h b/include/app_context.h index f07def7..21aaedc 100644 --- a/include/app_context.h +++ b/include/app_context.h @@ -26,6 +26,7 @@ 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 */ char tx_url[256]; + char screen_input[128]; char config_file[256]; char sip_addr_str[INET_ADDRSTRLEN]; uint8_t sip_addr[4]; @@ -44,6 +45,7 @@ struct dvledtx_context { /* Session controls */ int st20p_sessions; + bool use_screen_capture; bool exit; bool force_dhcp; int test_time_s; diff --git a/include/util/config_reader.h b/include/util/config_reader.h index 5209fb3..249431a 100644 --- a/include/util/config_reader.h +++ b/include/util/config_reader.h @@ -34,6 +34,8 @@ struct dvledtx_config { uint32_t scale_height; /* 0 = no scaling (use source height) */ int fps; char fmt[32]; /* e.g. "yuv422p10le" */ + char input_mode[32]; /* "file" (default) or "screen_capture" */ + char screen_input[128]; /* x11grab source, e.g. ":0.0+0,0" */ char tx_url[256]; /* optional log file path (empty = console only) */ diff --git a/meson_options.txt b/meson_options.txt index 6909c5b..36a2ffa 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,2 +1,2 @@ -option('enable_mtl_tx', type: 'boolean', value: false, +option('enable_mtl_tx', type: 'boolean', value: true, description: 'Use MTL pipeline APIs (st20p_tx_get_frame / st20p_tx_put_frame) directly for TX instead of the FFmpeg mtl_st20p libavdevice muxer. When enabled: links libmtl, defines -DENABLE_MTL_TX, and bypasses avformat_write_header / av_write_frame on the TX path. When disabled (default): the existing FFmpeg mtl_st20p muxer path is used.') diff --git a/src/core/session_manager.c b/src/core/session_manager.c index 6246e82..a86fd02 100644 --- a/src/core/session_manager.c +++ b/src/core/session_manager.c @@ -253,7 +253,7 @@ int create_st20p_tx_session(session_manager_t* manager, struct dvledtx_context* if (manager->shared_dec) { ctx->shared_dec = manager->shared_dec; LOG_INFO("ST20P TX session %d: using shared decoder", session_idx); - } else if (strlen(app->tx_url) > 0) { + } else if (app->use_screen_capture == true || strlen(app->tx_url) > 0) { if (load_video_source(ctx, app->tx_url) < 0) { LOG_ERROR("ST20P TX session %d: load_video_source failed", session_idx); return -1; @@ -276,8 +276,8 @@ int session_manager_init(session_manager_t* manager, struct dvledtx_context* app /* Use shared decoder only when > 1 session and source needs decoding */ bool use_shared = (app->st20p_sessions > 1 && - strlen(app->tx_url) > 0 && - is_raw_yuv(app->tx_url) == false); + (app->use_screen_capture == true || + (strlen(app->tx_url) > 0 && is_raw_yuv(app->tx_url) == false))); if (use_shared) { manager->shared_dec = calloc(1, sizeof(struct shared_decode_ctx)); diff --git a/src/ffmpeg/ffmpeg_decoder.c b/src/ffmpeg/ffmpeg_decoder.c index 5274a5c..5319bbd 100644 --- a/src/ffmpeg/ffmpeg_decoder.c +++ b/src/ffmpeg/ffmpeg_decoder.c @@ -20,6 +20,7 @@ #include #include #include +#include /* ========================================================================= * Helpers @@ -199,6 +200,7 @@ void* shared_decode_thread(void* arg) { * ========================================================================= */ static int open_ffmpeg_decoder( const char* filename, const char* log_prefix, + bool use_screen_capture, const char* screen_input, int capture_w, int capture_h, int capture_fps, enum AVPixelFormat target_fmt, int target_w, int target_h, AVFormatContext** out_fmt_ctx, AVCodecContext** out_codec_ctx, struct SwsContext** out_sws_ctx, AVFrame** out_av_frame, @@ -207,10 +209,37 @@ static int open_ffmpeg_decoder( char errbuf[256]; int ret; - ret = avformat_open_input(out_fmt_ctx, filename, NULL, NULL); + if (use_screen_capture == true) { + const char* input_url = (screen_input && screen_input[0] != '\0') ? screen_input : ":0.0+0,0"; + char video_size[32]; + char framerate[16]; + snprintf(video_size, sizeof(video_size), "%dx%d", capture_w, capture_h); + snprintf(framerate, sizeof(framerate), "%d", capture_fps > 0 ? capture_fps : 30); + + const AVInputFormat* in_fmt = av_find_input_format("x11grab"); + if (in_fmt == NULL) { + LOG_ERROR("%s: x11grab input format not found", log_prefix); + return -1; + } + + AVDictionary* options = NULL; + av_dict_set(&options, "video_size", video_size, 0); + av_dict_set(&options, "framerate", framerate, 0); + + ret = avformat_open_input(out_fmt_ctx, input_url, in_fmt, &options); + av_dict_free(&options); + if (ret < 0) { + av_strerror(ret, errbuf, sizeof(errbuf)); + LOG_ERROR("%s: cannot open x11grab source %s: %s", log_prefix, input_url, errbuf); + return -1; + } + } else { + ret = avformat_open_input(out_fmt_ctx, filename, NULL, NULL); + } if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - LOG_ERROR("%s: cannot open %s: %s", log_prefix, filename, errbuf); + LOG_ERROR("%s: cannot open %s: %s", log_prefix, + use_screen_capture ? "x11grab input" : filename, errbuf); return -1; } ret = avformat_find_stream_info(*out_fmt_ctx, NULL); @@ -325,6 +354,7 @@ int open_shared_ffmpeg(struct shared_decode_ctx* dec, const char* filename) { int target_h = (int)(app->scale_height > 0 ? app->scale_height : app->height); return open_ffmpeg_decoder( filename, "Shared decode", + app->use_screen_capture, app->screen_input, (int)app->width, (int)app->height, app->fps, app->fmt, target_w, target_h, &dec->fmt_ctx, &dec->codec_ctx, &dec->sws_ctx, &dec->av_frame, &dec->yuv_frame, &dec->av_packet, @@ -347,6 +377,7 @@ static int open_ffmpeg_source(struct st20p_tx_ctx* ctx, const char* filename) { int target_h = (int)(ctx->app->scale_height > 0 ? ctx->app->scale_height : ctx->app->height); int ret = open_ffmpeg_decoder( filename, log_prefix, + ctx->app->use_screen_capture, ctx->app->screen_input, (int)ctx->app->width, (int)ctx->app->height, ctx->app->fps, ctx->app->fmt, target_w, target_h, &ctx->fmt_ctx, &ctx->codec_ctx, &ctx->sws_ctx, &ctx->av_frame, &ctx->yuv_frame, &ctx->av_packet, @@ -366,6 +397,10 @@ void close_ffmpeg_source(struct st20p_tx_ctx* ctx) { * Video source loading * ========================================================================= */ int load_video_source(struct st20p_tx_ctx* ctx, const char* filename) { + if (ctx->app->use_screen_capture == true) { + return open_ffmpeg_source(ctx, ctx->app->screen_input); + } + if (!filename || strlen(filename) == 0) { LOG_WARN("ST20P TX(%d): no source file configured", ctx->idx); return 0; diff --git a/src/util/config_reader.c b/src/util/config_reader.c index e9d7b48..4f2139e 100644 --- a/src/util/config_reader.c +++ b/src/util/config_reader.c @@ -231,6 +231,7 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { const char* buf_end = json + nread; memset(config, 0, sizeof(*config)); + strncpy(config->input_mode, "file", sizeof(config->input_mode) - 1); /* --- interfaces[0] --- */ const char* ifaces_arr = find_array(json, buf_end, "interfaces"); @@ -262,7 +263,13 @@ int parse_tx_config(const char* config_file, struct dvledtx_config* config) { int v; v = extract_json_int(video_obj, video_end, "width"); if (v > 0) config->width = v; v = extract_json_int(video_obj, video_end, "height"); if (v > 0) config->height = v; + extract_json_string(video_obj, video_end, "input_mode", config->input_mode, sizeof(config->input_mode)); + extract_json_string(video_obj, video_end, "screen_input", config->screen_input, sizeof(config->screen_input)); extract_json_string(video_obj, video_end, "tx_url", config->tx_url, sizeof(config->tx_url)); + + if (strcmp(config->input_mode, "screen_capture") == 0 && config->screen_input[0] == '\0') { + strncpy(config->screen_input, ":0.0+0,0", sizeof(config->screen_input) - 1); + } } /* --- tx_video block (transmission parameters) --- */ @@ -435,6 +442,20 @@ int validate_tx_config(const struct dvledtx_config* config) { return -1; } + /* Input mode validation */ + if (config->input_mode[0] == '\0' || strcmp(config->input_mode, "file") == 0) { + /* Default/normal mode */ + } else if (strcmp(config->input_mode, "screen_capture") == 0) { + if (config->screen_input[0] == '\0') { + LOG_ERROR("video.screen_input must be set when input_mode is 'screen_capture'"); + return -1; + } + } else { + LOG_ERROR("unsupported input_mode '%s' (supported: file, screen_capture)", + config->input_mode); + return -1; + } + /* Scale dimensions validation (optional — 0 means no scaling) */ if (config->scale_width != 0 || config->scale_height != 0) { if (config->scale_width == 0 || config->scale_height == 0) { @@ -495,14 +516,21 @@ int validate_tx_config(const struct dvledtx_config* config) { return -1; } - /* Video source file validation */ - if (config->tx_url[0] != '\0') { - FILE* f = fopen(config->tx_url, "rb"); - if (!f) { - LOG_ERROR("video source file not found: %s", config->tx_url); + /* Video source validation */ + if (strcmp(config->input_mode, "screen_capture") == 0) { + if (config->screen_input[0] == '\0') { + LOG_ERROR("video.screen_input must be non-empty for screen_capture mode"); return -1; } - fclose(f); + } else { + if (config->tx_url[0] != '\0') { + FILE* f = fopen(config->tx_url, "rb"); + if (!f) { + LOG_ERROR("video source file not found: %s", config->tx_url); + return -1; + } + fclose(f); + } } /* Session validation */ @@ -656,6 +684,11 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) strncpy(app->tx_url, config.tx_url, sizeof(app->tx_url) - 1); app->tx_url[sizeof(app->tx_url) - 1] = '\0'; } + if (config.screen_input[0] != '\0') { + strncpy(app->screen_input, config.screen_input, sizeof(app->screen_input) - 1); + app->screen_input[sizeof(app->screen_input) - 1] = '\0'; + } + app->use_screen_capture = (strcmp(config.input_mode, "screen_capture") == 0); /* Sessions — count drives how many TX sessions are created */ app->st20p_sessions = config.session_count; @@ -686,15 +719,21 @@ int load_and_apply_config(struct dvledtx_context* app, const char* config_file) config.interface_sip[0] ? config.interface_sip : "dhcp", config.interface_dip); if (config.scale_width > 0 && config.scale_height > 0) - LOG_INFO("Video: %ux%u -> scale %ux%u %dfps %s tx_url=%s", + LOG_INFO("Video: %ux%u -> scale %ux%u %dfps %s mode=%s source=%s", config.width, config.height, config.scale_width, config.scale_height, config.fps, config.fmt, - config.tx_url[0] ? config.tx_url : ""); + config.input_mode[0] ? config.input_mode : "file", + strcmp(config.input_mode, "screen_capture") == 0 + ? (config.screen_input[0] ? config.screen_input : "") + : (config.tx_url[0] ? config.tx_url : "")); else - LOG_INFO("Video: %ux%u %dfps %s tx_url=%s", + LOG_INFO("Video: %ux%u %dfps %s mode=%s source=%s", config.width, config.height, config.fps, config.fmt, - config.tx_url[0] ? config.tx_url : ""); + config.input_mode[0] ? config.input_mode : "file", + strcmp(config.input_mode, "screen_capture") == 0 + ? (config.screen_input[0] ? config.screen_input : "") + : (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, config.sessions[i].udp_port, config.sessions[i].payload_type, diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index ca9648a..7138f8b 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -67,6 +67,7 @@ static void fill_valid_config(struct dvledtx_config *cfg) cfg->height = 1080; cfg->fps = 30; strncpy(cfg->fmt, "yuv422p10le", sizeof(cfg->fmt) - 1); + strncpy(cfg->input_mode, "file", sizeof(cfg->input_mode) - 1); /* tx_url intentionally left empty — skips file-open check in validate */ cfg->session_count = 1; cfg->sessions[0].udp_port = 20000; @@ -243,6 +244,71 @@ 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); + assert_string_equal(cfg.input_mode, "file"); +} + +static void test_parse_input_mode_defaults_to_file_when_omitted(void **state) +{ + (void)state; + char *path = write_tmpfile( + "{" + " \"interfaces\": [{\"name\":\"eth0\",\"sip\":\"1.2.3.4\",\"dip\":\"239.1.1.1\"}]," + " \"video\": {\"width\":1920,\"height\":1080,\"tx_url\":\"/tmp/f.mp4\"}," + " \"tx_video\": {\"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_string_equal(cfg.input_mode, "file"); + assert_int_equal(cfg.screen_input[0], '\0'); +} + +static void test_parse_screen_capture_defaults_screen_input(void **state) +{ + (void)state; + char *path = write_tmpfile( + "{" + " \"interfaces\": [{\"name\":\"eth0\",\"sip\":\"1.2.3.4\",\"dip\":\"239.1.1.1\"}]," + " \"video\": {\"width\":1920,\"height\":1080,\"input_mode\":\"screen_capture\"}," + " \"tx_video\": {\"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_string_equal(cfg.input_mode, "screen_capture"); + assert_string_equal(cfg.screen_input, ":0.0+0,0"); +} + +static void test_parse_screen_capture_keeps_explicit_screen_input(void **state) +{ + (void)state; + char *path = write_tmpfile( + "{" + " \"interfaces\": [{\"name\":\"eth0\",\"sip\":\"1.2.3.4\",\"dip\":\"239.1.1.1\"}]," + " \"video\": {\"width\":1920,\"height\":1080," + " \"input_mode\":\"screen_capture\",\"screen_input\":\":1.0+100,200\"}," + " \"tx_video\": {\"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_string_equal(cfg.input_mode, "screen_capture"); + assert_string_equal(cfg.screen_input, ":1.0+100,200"); } /* ========================================================================== @@ -765,6 +831,35 @@ static void test_validate_no_scale_passes(void **state) assert_int_equal(validate_tx_config(&cfg), 0); } +static void test_validate_unknown_input_mode_fails(void **state) +{ + (void)state; + struct dvledtx_config cfg; + fill_valid_config(&cfg); + strncpy(cfg.input_mode, "camera", sizeof(cfg.input_mode) - 1); + assert_int_equal(validate_tx_config(&cfg), -1); +} + +static void test_validate_screen_capture_without_screen_input_fails(void **state) +{ + (void)state; + struct dvledtx_config cfg; + fill_valid_config(&cfg); + strncpy(cfg.input_mode, "screen_capture", sizeof(cfg.input_mode) - 1); + cfg.screen_input[0] = '\0'; + assert_int_equal(validate_tx_config(&cfg), -1); +} + +static void test_validate_screen_capture_with_screen_input_passes(void **state) +{ + (void)state; + struct dvledtx_config cfg; + fill_valid_config(&cfg); + strncpy(cfg.input_mode, "screen_capture", sizeof(cfg.input_mode) - 1); + strncpy(cfg.screen_input, ":0.0+0,0", sizeof(cfg.screen_input) - 1); + assert_int_equal(validate_tx_config(&cfg), 0); +} + static void test_validate_duplicate_udp_ports_fails(void **state) { (void)state; @@ -1250,6 +1345,28 @@ static void test_load_and_apply_config_copies_log_file(void **state) assert_string_equal(app.log_file, "myapp.log"); } +static void test_load_and_apply_config_maps_screen_capture_fields(void **state) +{ + (void)state; + char *path = write_tmpfile( + "{" + " \"interfaces\": [{\"name\":\"0000:06:00.0\",\"sip\":\"192.168.50.29\",\"dip\":\"239.168.85.20\"}]," + " \"video\": {\"width\":1920,\"height\":1080,\"input_mode\":\"screen_capture\",\"screen_input\":\":1.0+100,200\"}," + " \"tx_video\": {\"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_context app; + memset(&app, 0, sizeof(app)); + int ret = load_and_apply_config(&app, path); + unlink(path); free(path); + assert_int_equal(ret, 0); + assert_true(app.use_screen_capture); + assert_string_equal(app.screen_input, ":1.0+100,200"); +} + /* ========================================================================== * main * ========================================================================== */ @@ -1271,6 +1388,9 @@ int main(void) cmocka_unit_test(test_parse_returns_minus1_when_sessions_key_absent), cmocka_unit_test(test_parse_returns_minus1_when_sessions_array_empty), cmocka_unit_test(test_parse_returns_zero_fields_when_video_missing), + cmocka_unit_test(test_parse_input_mode_defaults_to_file_when_omitted), + cmocka_unit_test(test_parse_screen_capture_defaults_screen_input), + cmocka_unit_test(test_parse_screen_capture_keeps_explicit_screen_input), cmocka_unit_test(test_parse_session_missing_udp_port_fails), cmocka_unit_test(test_parse_session_udp_port_exceeds_65535_fails), cmocka_unit_test(test_parse_session_missing_payload_type_defaults_to_96), @@ -1315,6 +1435,9 @@ int main(void) cmocka_unit_test(test_validate_scale_crop_exceeds_scaled_dims_fails), cmocka_unit_test(test_validate_scale_crop_within_scaled_dims_passes), cmocka_unit_test(test_validate_no_scale_passes), + cmocka_unit_test(test_validate_unknown_input_mode_fails), + cmocka_unit_test(test_validate_screen_capture_without_screen_input_fails), + cmocka_unit_test(test_validate_screen_capture_with_screen_input_passes), 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), @@ -1360,6 +1483,7 @@ int main(void) cmocka_unit_test(test_load_and_apply_config_fmt_yuv420), cmocka_unit_test(test_load_and_apply_config_unknown_fmt_fails), cmocka_unit_test(test_load_and_apply_config_copies_log_file), + cmocka_unit_test(test_load_and_apply_config_maps_screen_capture_fields), }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/tests/test_session_manager.c b/tests/test_session_manager.c index a7821b7..3c5b22d 100644 --- a/tests/test_session_manager.c +++ b/tests/test_session_manager.c @@ -382,6 +382,51 @@ static void test_init_3sessions_raw_yuv_no_shared_decoder(void **state) session_manager_cleanup(&mgr); } +/* ========================================================================== + * session_manager_init — 3 sessions with screen capture -> shared decoder + * ========================================================================== */ + +static void test_init_3sessions_screen_capture_uses_shared_decoder(void **state) +{ + (void)state; + reset_mock_counters(); + struct dvledtx_context app; + fill_app(&app, 3, ""); + app.use_screen_capture = true; + strncpy(app.screen_input, ":0.0+0,0", sizeof(app.screen_input) - 1); + + session_manager_t mgr; + assert_int_equal(session_manager_init(&mgr, &app), 0); + + assert_non_null(mgr.shared_dec); + assert_int_equal(mock_open_shared_ffmpeg_calls, 1); + assert_int_equal(mock_load_video_source_calls, 0); + + session_manager_cleanup(&mgr); +} + +/* ========================================================================== + * session_manager_init — single session screen capture -> per-session source + * ========================================================================== */ + +static void test_init_single_session_screen_capture_loads_source(void **state) +{ + (void)state; + reset_mock_counters(); + struct dvledtx_context app; + fill_app(&app, 1, ""); + app.use_screen_capture = true; + strncpy(app.screen_input, ":0.0+0,0", sizeof(app.screen_input) - 1); + + session_manager_t mgr; + assert_int_equal(session_manager_init(&mgr, &app), 0); + + assert_null(mgr.shared_dec); + assert_int_equal(mock_load_video_source_calls, 1); + + session_manager_cleanup(&mgr); +} + /* ========================================================================== * session_manager_init — open_ffmpeg_tx failure * ========================================================================== */ @@ -934,6 +979,8 @@ int main(void) cmocka_unit_test(test_init_single_session_no_url), cmocka_unit_test(test_init_3sessions_with_url_uses_shared_decoder), cmocka_unit_test(test_init_3sessions_raw_yuv_no_shared_decoder), + cmocka_unit_test(test_init_3sessions_screen_capture_uses_shared_decoder), + cmocka_unit_test(test_init_single_session_screen_capture_loads_source), cmocka_unit_test(test_init_fails_when_open_output_fails), cmocka_unit_test(test_init_fails_when_open_shared_ffmpeg_fails), cmocka_unit_test(test_init_zero_sessions), From f74a727fa4bf5dd3a86e9e99d31a120c23767a1c Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Mon, 6 Jul 2026 14:31:40 +0530 Subject: [PATCH 02/10] fix(screen-capture): register avdevice unconditionally for x11grab support - src/main.c: call avdevice_register_all() unconditionally instead of only when ENABLE_MTL_TX is not defined. x11grab screen-capture input needs avdevice registered regardless of which TX path (FFmpeg muxer vs native MTL) is compiled in. Previously, builds with -Denable_mtl_tx=true (the flag used for the MTL TX pipeline) never registered avdevice, causing 'x11grab input format not found'. - meson.build: always link libavdevice regardless of enable_mtl_tx, since the x11grab decode path requires it independent of the TX path. - config/tx_fullhd_virtual_display.json: new example config demonstrating screen capture from a virtual (Xvfb) display instead of a physical one. - README.md: document how to set up a headless virtual display with a full GNOME desktop session (Xvfb + gnome-session) for screen capture testing/transmission on machines with no physical monitor. Verified end-to-end: transmitted both a physical display (:0) and a virtual Xvfb display (:99, running a real GNOME desktop) via x11grab and MTL ST20P TX. --- README.md | 47 +++++++++++++++++++++++++++ config/tx_fullhd_virtual_display.json | 26 +++++++++++++++ meson.build | 8 +++-- src/main.c | 13 ++++---- 4 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 config/tx_fullhd_virtual_display.json diff --git a/README.md b/README.md index 6e7a299..dd1a64e 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,53 @@ Example screen-capture input (`config/tx_fullhd_screen_capture.json`): } ``` +`screen_input` follows FFmpeg's `x11grab` URL syntax: `[+,]` (e.g. `:0.0+0,0` captures display `:0` starting at offset `0,0`). The capture resolution/framerate are taken from the `width`/`height`/`fps` fields above. + +#### Screen capture on a headless machine (no physical monitor) + +`x11grab` needs a real X11 display to attach to — it does not work against a raw framebuffer or DRM device. On a machine with no monitor connected, create a virtual display with `Xvfb` and run a desktop session on it so there's actual content to capture: + +1. **Install prerequisites** (once): + ```bash + sudo apt-get install -y xvfb ubuntu-desktop + ``` + FFmpeg itself must also be built with `x11grab` support — this requires `libxcb1-dev`, `libxcb-shm0-dev`, and `libxcb-xfixes0-dev` to be present when FFmpeg's `./configure` runs (it auto-detects them, no explicit `--enable-x11grab` flag needed). Verify with `ffmpeg -devices | grep x11grab`. + +2. **Start a virtual display** at the resolution you intend to transmit: + ```bash + Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp +extension GLX +extension RANDR & + ``` + +3. **Start a GNOME desktop session** on that display (software GL rendering is required since Xvfb has no GPU): + ```bash + mkdir -p /tmp/gnome99-runtime && chmod 700 /tmp/gnome99-runtime + DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 XDG_SESSION_TYPE=x11 \ + XDG_RUNTIME_DIR=/tmp/gnome99-runtime \ + dbus-run-session -- gnome-session --session=ubuntu & + ``` + Give it a few seconds to finish starting (`gnome-shell` and related services), then confirm with: + ```bash + DISPLAY=:99 ffmpeg -f x11grab -video_size 1920x1080 -i :99.0+0,0 -frames:v 1 -update 1 /tmp/check.png + ``` + +4. **Point `screen_input` at the virtual display** (see `config/tx_fullhd_virtual_display.json`): + ```json + "screen_input": ":99.0+0,0" + ``` + +5. **Run dvledtx** as usual — `x11grab` will capture whatever is rendered on `:99` (desktop, windows, applications) and transmit it, exactly as it would for a physical display: + ```bash + ./build/dvledtx --config config/tx_fullhd_virtual_display.json + ``` + +6. **Tear down** when done: + ```bash + pkill -f "gnome-session --session=ubuntu" + pkill -f "Xvfb :99" + ``` + +A lighter-weight alternative to a full GNOME session is a minimal window manager (e.g. `xfce4`), which starts faster and uses less memory if you only need a generic desktop background rather than the full Ubuntu shell. + ## Logging dvledtx includes a built-in logger with configurable output targets and log levels. diff --git a/config/tx_fullhd_virtual_display.json b/config/tx_fullhd_virtual_display.json new file mode 100644 index 0000000..d45a9ce --- /dev/null +++ b/config/tx_fullhd_virtual_display.json @@ -0,0 +1,26 @@ +{ + "interfaces": [ + { + "name": "0000:06:00.0", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 1920, + "height": 1080, + "input_mode": "screen_capture", + "screen_input": ":99.0+0,0" + }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p10le" + }, + "tx_sessions": [ + { + "udp_port": 20000, + "payload_type": 96, + "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } + } + ] +} diff --git a/meson.build b/meson.build index 6152843..80a39fb 100644 --- a/meson.build +++ b/meson.build @@ -17,16 +17,18 @@ libswscale_dep = dependency('libswscale', required: true) enable_mtl_tx = get_option('enable_mtl_tx') mtl_dep = [] -avdevice_dep = [] c_args = ['-DALLOW_EXPERIMENTAL_API', '-D_FORTIFY_SOURCE=2', '-O2'] mtl_inc_args = [] +# libavdevice is needed regardless of the TX path: the mtl_st20p muxer TX +# path uses it directly, and the MTL-native TX path still needs it for the +# x11grab screen-capture input decoder (avdevice_register_all()). +avdevice_dep = dependency('libavdevice', required: true) + 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 all_deps = [m_dep, pthread_dep, diff --git a/src/main.c b/src/main.c index 43746d5..cd87f54 100644 --- a/src/main.c +++ b/src/main.c @@ -14,10 +14,10 @@ #include #include -/* libavdevice is only needed for the FFmpeg mtl_st20p muxer TX path */ -#ifndef ENABLE_MTL_TX +/* libavdevice is needed for both TX paths: the FFmpeg mtl_st20p muxer TX + * path, and the x11grab screen-capture input decoder used by the MTL-native + * TX path (ENABLE_MTL_TX). */ #include -#endif #include #include "app_context.h" #include "util/config_reader.h" @@ -285,11 +285,10 @@ int main(int argc, char** argv) { if (signal(SIGTERM, dvledtx_sig_handler) == SIG_ERR) LOG_WARN("Failed to install SIGTERM handler"); - /* Register all FFmpeg devices (required for the MTL mtl_st20p muxer - * which lives in libavdevice, not libavformat) */ -#ifndef ENABLE_MTL_TX + /* Register all FFmpeg devices (required for the MTL mtl_st20p muxer, + * which lives in libavdevice, and for the x11grab screen-capture input + * decoder used by both TX paths) */ avdevice_register_all(); -#endif /* I-3: Suppress verbose FFmpeg internal logging in production to avoid * leaking internal paths or memory addresses via av_strerror output. */ From ca98309467d892f002cd8c19e11722a5bcfc30a4 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 7 Jul 2026 11:11:19 +0530 Subject: [PATCH 03/10] docs: document x11grab/screen-capture install prerequisites - Add note under FFmpeg build prerequisites that libx11-dev, libxcb1-dev, libxcb-shm0-dev, and libxcb-xfixes0-dev must be installed before building FFmpeg for x11grab (screen_capture mode) to be compiled in, plus a verification command (ffmpeg -devices | grep x11grab). - Add xvfb/ubuntu-desktop as prerequisites for headless machines and link to the existing virtual-display setup section. - Add the new headless screen-capture subsection to the table of contents. --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index efd5944..609d358 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [Usage](#usage) - [Binding Ethernet Controller to DPDK PMD and Hugepage Setup](#binding-ethernet-controller-to-dpdk-pmd-and-hugepage-setup) - [JSON Configuration](#json-configuration) + - [Screen capture on a headless machine (no physical monitor)](#screen-capture-on-a-headless-machine-no-physical-monitor) - [Logging](#logging) - [Command-Line Options](#command-line-options) - [Supported Formats](#supported-formats) @@ -93,6 +94,19 @@ FFmpeg is an open source project licensed under LGPL and GPL. See https://www.ff - [Build and install DPDK](https://github.com/OpenVisualCloud/Media-Transport-Library/blob/ffmpeg-plugin-extra-pixel-format/doc/build.md#2-dpdk-build-and-install) - [Build and install MTL](https://github.com/OpenVisualCloud/Media-Transport-Library/blob/ffmpeg-plugin-extra-pixel-format/doc/build.md#3-build-media-transport-library-and-app) - [FFmpeg 7.0 with MTL Plugin](https://github.com/OpenVisualCloud/Media-Transport-Library/blob/ffmpeg-plugin-extra-pixel-format/ecosystem/ffmpeg_plugin/README.md#1-build) + - **Screen capture support (`input_mode: screen_capture`) requires FFmpeg's `x11grab` device.** It is auto-detected and compiled in by FFmpeg's `./configure` script, but only if these packages are installed *before* building FFmpeg: + ```bash + sudo apt-get install -y libx11-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev + ``` + After building, verify support with: + ```bash + ffmpeg -devices | grep x11grab + ``` + If this prints nothing, FFmpeg needs to be reconfigured/rebuilt after installing the packages above — screen capture will otherwise fail at runtime with `x11grab input format not found`. + - **Headless machines (no physical monitor)** additionally need a virtual display to capture from — see [Screen capture on a headless machine](#screen-capture-on-a-headless-machine-no-physical-monitor) below, which requires: + ```bash + sudo apt-get install -y xvfb ubuntu-desktop + ``` ### Build Steps From 0d4ff6e866d4441c352a7c2d79745de9ad7f6bab Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 7 Jul 2026 11:33:27 +0530 Subject: [PATCH 04/10] test(screen-capture): add x11grab/avdevice regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_ffmpeg_decoder_mock.c: cover the previously-untested x11grab screen-capture branch of open_ffmpeg_decoder() via open_shared_ffmpeg() and load_video_source(). Verifies (a) x11grab is NOT resolvable before avdevice_register_all() runs, (b) it IS resolvable afterwards, and (c) both the shared-decoder and per-session paths fail gracefully (return -1, no crash) when pointed at a nonexistent display. - test_main.c: add test_main_registers_avdevice_for_x11grab as a direct regression guard for the bug fixed in f74a727 — avdevice_register_all() must be called unconditionally in tx_app_real_main(), not gated behind an ENABLE_MTL_TX ifdef. --- tests/test_ffmpeg_decoder_mock.c | 93 ++++++++++++++++++++++++++++++++ tests/test_main.c | 22 ++++++++ 2 files changed, 115 insertions(+) diff --git a/tests/test_ffmpeg_decoder_mock.c b/tests/test_ffmpeg_decoder_mock.c index d243da8..ba4bb3c 100644 --- a/tests/test_ffmpeg_decoder_mock.c +++ b/tests/test_ffmpeg_decoder_mock.c @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -283,6 +284,91 @@ static void test_load_video_source_nonexistent_mp4_returns_minus1(void **state) assert_null(ctx.fmt_ctx); } +/* ========================================================================= + * Test: screen capture (x11grab) input path + * + * Regression coverage for the bug fixed in commit f74a727: the x11grab + * demuxer is only discoverable via av_find_input_format("x11grab") after + * avdevice_register_all() has run. This test binary never calls that + * function on its own (only src/main.c does, which is not linked here), + * so ordering matters: the "before registration" test below MUST run + * before the "after registration" test registers it process-wide. + * ========================================================================= */ + +static void test_screen_capture_before_avdevice_register_fails(void **state) +{ + (void)state; + /* Sanity guard: at this point nothing in this test binary has called + * avdevice_register_all() yet, so x11grab must not be resolvable — + * mirrors the exact symptom of the original bug + * ("x11grab input format not found"). */ + assert_null(av_find_input_format("x11grab")); + + struct dvledtx_context app; + fill_app_16x16(&app, 1); + app.use_screen_capture = true; + strncpy(app.screen_input, ":0.0+0,0", sizeof(app.screen_input) - 1); + + struct shared_decode_ctx dec; + memset(&dec, 0, sizeof(dec)); + dec.app = &app; + dec.num_sessions = 1; + + int ret = open_shared_ffmpeg(&dec, "unused_filename.mp4"); + assert_int_equal(ret, -1); + assert_null(dec.fmt_ctx); +} + +static void test_screen_capture_after_avdevice_register_finds_x11grab(void **state) +{ + (void)state; + avdevice_register_all(); + assert_non_null(av_find_input_format("x11grab")); +} + +static void test_screen_capture_shared_invalid_display_fails_gracefully(void **state) +{ + (void)state; + /* x11grab is registered (previous test), but the configured display + * does not exist in this headless CI environment (or anywhere, + * given the absurd display number) — avformat_open_input() must fail + * and open_shared_ffmpeg() must propagate -1 without crashing. */ + struct dvledtx_context app; + fill_app_16x16(&app, 1); + app.use_screen_capture = true; + strncpy(app.screen_input, ":424242.0+0,0", sizeof(app.screen_input) - 1); + + struct shared_decode_ctx dec; + memset(&dec, 0, sizeof(dec)); + dec.app = &app; + dec.num_sessions = 1; + + int ret = open_shared_ffmpeg(&dec, "unused_filename.mp4"); + assert_int_equal(ret, -1); + assert_null(dec.fmt_ctx); +} + +static void test_screen_capture_per_session_invalid_display_fails_gracefully(void **state) +{ + (void)state; + /* Same as above but through the single-session load_video_source() -> + * open_ffmpeg_source() path used when st20p_sessions == 1. */ + struct dvledtx_context app; + fill_app_16x16(&app, 1); + app.use_screen_capture = true; + strncpy(app.screen_input, ":424242.0+0,0", sizeof(app.screen_input) - 1); + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.idx = 0; + ctx.app = &app; + + int ret = load_video_source(&ctx, app.screen_input); + assert_int_equal(ret, -1); + assert_false(ctx.use_ffmpeg); + assert_null(ctx.fmt_ctx); +} + /* ========================================================================= * Test: shared_decode_thread — runs with generated video + barriers * ========================================================================= */ @@ -419,6 +505,13 @@ int main(void) cmocka_unit_test(test_load_video_source_mp4_calls_open_ffmpeg_source), cmocka_unit_test(test_load_video_source_nonexistent_mp4_returns_minus1), + /* screen capture (x11grab) — order matters: avdevice_register_all() + * must not have run yet for the first test below. */ + cmocka_unit_test(test_screen_capture_before_avdevice_register_fails), + cmocka_unit_test(test_screen_capture_after_avdevice_register_finds_x11grab), + cmocka_unit_test(test_screen_capture_shared_invalid_display_fails_gracefully), + cmocka_unit_test(test_screen_capture_per_session_invalid_display_fails_gracefully), + /* shared_decode_thread */ cmocka_unit_test(test_shared_decode_thread_decodes_frames), diff --git a/tests/test_main.c b/tests/test_main.c index 7fa8e6e..2ee75e3 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -358,6 +358,27 @@ static void test_main_happy_path_exits_immediately(void **state) assert_int_equal(ret, 0); } +/* Regression test for commit f74a727: avdevice_register_all() must be + * called unconditionally in tx_app_real_main(), NOT gated behind an + * ENABLE_MTL_TX ifdef. Previously the MTL-native TX build path skipped + * avdevice registration entirely, so av_find_input_format("x11grab") + * always returned NULL and screen-capture input silently failed with + * "x11grab input format not found" — even though libavdevice itself was + * linked. Running the full main() body here and then checking that the + * x11grab demuxer becomes discoverable catches any future regression of + * that ifdef. */ +static void test_main_registers_avdevice_for_x11grab(void **state) +{ + (void)state; + reset_stubs(); + stub_load_config_set_exit = true; /* exit loop immediately */ + optind = 1; + char *argv[] = {"dvledtx", "--config", "test.json", NULL}; + int ret = tx_app_real_main(3, argv); + assert_int_equal(ret, 0); + assert_non_null(av_find_input_format("x11grab")); +} + /* Happy path with test_time_s = 1: exercises the for-loop branch. * app.exit is set true so the for loop exits after checking !app.exit. */ static void test_main_test_time_path(void **state) @@ -666,6 +687,7 @@ int main(void) /* tx_app_real_main (full main() body) */ cmocka_unit_test(test_main_no_config_returns_minus1), cmocka_unit_test(test_main_happy_path_exits_immediately), + cmocka_unit_test(test_main_registers_avdevice_for_x11grab), cmocka_unit_test(test_main_test_time_path), cmocka_unit_test(test_main_config_load_fails), cmocka_unit_test(test_main_session_init_fails), From 416a67fea300485db76b47060d2704cc3d0b30d3 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 7 Jul 2026 14:03:55 +0530 Subject: [PATCH 05/10] fix: address PR review comments on screen-capture feature - config/tx_fullhd_single_session.json: restore log_file field that was accidentally dropped - config/tx_1session_12bit.json: move fps/fmt into a tx_video block to match what the parser actually reads - config/tx_fullhd_screen_capture.json: use display :99 to match the virtual display documented in the README headless walkthrough; drop the now-redundant tx_fullhd_virtual_display.json - meson_options.txt: revert enable_mtl_tx default back to false so it matches the documented default behavior - src/util/config_reader.c: remove duplicate screen_input validation in validate_tx_config() (already validated earlier in the function) - src/ffmpeg/ffmpeg_decoder.c: open_shared_ffmpeg() now passes the effective source string (screen_input when screen capture is enabled) to open_ffmpeg_decoder() so log messages stay meaningful - README.md: move the headless/Xvfb/GNOME walkthrough before the screen-capture JSON example, turn the example blocks into proper headings, link back to Software Requirements instead of repeating install commands, and drop the xfce4 aside --- README.md | 79 ++++++++++++--------------- config/tx_1session_12bit.json | 6 +- config/tx_fullhd_screen_capture.json | 2 +- config/tx_fullhd_single_session.json | 1 + config/tx_fullhd_virtual_display.json | 26 --------- meson_options.txt | 2 +- src/ffmpeg/ffmpeg_decoder.c | 6 +- src/util/config_reader.c | 21 +++---- 8 files changed, 55 insertions(+), 88 deletions(-) delete mode 100644 config/tx_fullhd_virtual_display.json diff --git a/README.md b/README.md index 609d358..7cabef2 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ dvledtx uses a JSON config file with three sections: | | `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: MP4 file input (`config/tx_fullhd_single_session.json`) + ```json { "log_file": "dvledtx.log", @@ -179,39 +180,11 @@ Example (`config/tx_fullhd_single_session.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`). -Example screen-capture input (`config/tx_fullhd_screen_capture.json`): -```json -{ - "interfaces": [ - { "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" } - ], - "video": { - "width": 1920, - "height": 1080, - "input_mode": "screen_capture", - "screen_input": ":0.0+0,0" - }, - "tx_video": { - "fps": 30, - "fmt": "yuv422p10le" - }, - "tx_sessions": [ - { "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } } - ] -} -``` - -`screen_input` follows FFmpeg's `x11grab` URL syntax: `[+,]` (e.g. `:0.0+0,0` captures display `:0` starting at offset `0,0`). The capture resolution/framerate are taken from the `width`/`height`/`fps` fields above. - #### Screen capture on a headless machine (no physical monitor) `x11grab` needs a real X11 display to attach to — it does not work against a raw framebuffer or DRM device. On a machine with no monitor connected, create a virtual display with `Xvfb` and run a desktop session on it so there's actual content to capture: -1. **Install prerequisites** (once): - ```bash - sudo apt-get install -y xvfb ubuntu-desktop - ``` - FFmpeg itself must also be built with `x11grab` support — this requires `libxcb1-dev`, `libxcb-shm0-dev`, and `libxcb-xfixes0-dev` to be present when FFmpeg's `./configure` runs (it auto-detects them, no explicit `--enable-x11grab` flag needed). Verify with `ffmpeg -devices | grep x11grab`. +1. **Install prerequisites** (once): see [Software Requirements](#software-requirements) for the `xvfb`/`ubuntu-desktop` packages and the `x11grab`-enabled FFmpeg build. 2. **Start a virtual display** at the resolution you intend to transmit: ```bash @@ -230,23 +203,41 @@ Example screen-capture input (`config/tx_fullhd_screen_capture.json`): DISPLAY=:99 ffmpeg -f x11grab -video_size 1920x1080 -i :99.0+0,0 -frames:v 1 -update 1 /tmp/check.png ``` -4. **Point `screen_input` at the virtual display** (see `config/tx_fullhd_virtual_display.json`): - ```json - "screen_input": ":99.0+0,0" - ``` +#### Example: screen-capture input (`config/tx_fullhd_screen_capture.json`) -5. **Run dvledtx** as usual — `x11grab` will capture whatever is rendered on `:99` (desktop, windows, applications) and transmit it, exactly as it would for a physical display: - ```bash - ./build/dvledtx --config config/tx_fullhd_virtual_display.json - ``` +```json +{ + "interfaces": [ + { "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" } + ], + "video": { + "width": 1920, + "height": 1080, + "input_mode": "screen_capture", + "screen_input": ":99.0+0,0" + }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p10le" + }, + "tx_sessions": [ + { "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } } + ] +} +``` -6. **Tear down** when done: - ```bash - pkill -f "gnome-session --session=ubuntu" - pkill -f "Xvfb :99" - ``` +`screen_input` follows FFmpeg's `x11grab` URL syntax: `[+,]` (e.g. `:99.0+0,0` captures display `:99`, matching the virtual display created above, starting at offset `0,0`). The capture resolution/framerate are taken from the `width`/`height`/`fps` fields above. + +Run dvledtx as usual — `x11grab` will capture whatever is rendered on the display (desktop, windows, applications) and transmit it, exactly as it would for a physical display: +```bash +./build/dvledtx --config config/tx_fullhd_screen_capture.json +``` -A lighter-weight alternative to a full GNOME session is a minimal window manager (e.g. `xfce4`), which starts faster and uses less memory if you only need a generic desktop background rather than the full Ubuntu shell. +**Tear down** the virtual display when done: +```bash +pkill -f "gnome-session --session=ubuntu" +pkill -f "Xvfb :99" +``` ## Logging diff --git a/config/tx_1session_12bit.json b/config/tx_1session_12bit.json index 1738448..9154858 100644 --- a/config/tx_1session_12bit.json +++ b/config/tx_1session_12bit.json @@ -9,10 +9,12 @@ "video": { "width": 1920, "height": 1080, - "fps": 30, - "fmt": "yuv422p12le", "tx_url": "bbb_sunflower_1080p_30fps_normal.mp4" }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p12le" + }, "tx_sessions": [ { "udp_port": 20000, diff --git a/config/tx_fullhd_screen_capture.json b/config/tx_fullhd_screen_capture.json index 6a8d430..d45a9ce 100644 --- a/config/tx_fullhd_screen_capture.json +++ b/config/tx_fullhd_screen_capture.json @@ -10,7 +10,7 @@ "width": 1920, "height": 1080, "input_mode": "screen_capture", - "screen_input": ":0.0+0,0" + "screen_input": ":99.0+0,0" }, "tx_video": { "fps": 30, diff --git a/config/tx_fullhd_single_session.json b/config/tx_fullhd_single_session.json index 0d5369b..2939030 100644 --- a/config/tx_fullhd_single_session.json +++ b/config/tx_fullhd_single_session.json @@ -1,4 +1,5 @@ { + "log_file": "dvledtx.log", "interfaces": [ { "name": "0000:06:00.0", diff --git a/config/tx_fullhd_virtual_display.json b/config/tx_fullhd_virtual_display.json deleted file mode 100644 index d45a9ce..0000000 --- a/config/tx_fullhd_virtual_display.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "interfaces": [ - { - "name": "0000:06:00.0", - "sip": "192.168.50.29", - "dip": "239.168.85.20" - } - ], - "video": { - "width": 1920, - "height": 1080, - "input_mode": "screen_capture", - "screen_input": ":99.0+0,0" - }, - "tx_video": { - "fps": 30, - "fmt": "yuv422p10le" - }, - "tx_sessions": [ - { - "udp_port": 20000, - "payload_type": 96, - "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } - } - ] -} diff --git a/meson_options.txt b/meson_options.txt index 36a2ffa..6909c5b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,2 +1,2 @@ -option('enable_mtl_tx', type: 'boolean', value: true, +option('enable_mtl_tx', type: 'boolean', value: false, description: 'Use MTL pipeline APIs (st20p_tx_get_frame / st20p_tx_put_frame) directly for TX instead of the FFmpeg mtl_st20p libavdevice muxer. When enabled: links libmtl, defines -DENABLE_MTL_TX, and bypasses avformat_write_header / av_write_frame on the TX path. When disabled (default): the existing FFmpeg mtl_st20p muxer path is used.') diff --git a/src/ffmpeg/ffmpeg_decoder.c b/src/ffmpeg/ffmpeg_decoder.c index 5319bbd..718070c 100644 --- a/src/ffmpeg/ffmpeg_decoder.c +++ b/src/ffmpeg/ffmpeg_decoder.c @@ -352,8 +352,12 @@ int open_shared_ffmpeg(struct shared_decode_ctx* dec, const char* filename) { const struct dvledtx_context* app = dec->app; int target_w = (int)(app->scale_width > 0 ? app->scale_width : app->width); int target_h = (int)(app->scale_height > 0 ? app->scale_height : app->height); + /* filename may be empty when screen capture is enabled (app->tx_url is + * not required in that mode); use the screen_input descriptor instead so + * log messages stay meaningful. */ + const char* effective_source = app->use_screen_capture ? app->screen_input : filename; return open_ffmpeg_decoder( - filename, "Shared decode", + effective_source, "Shared decode", app->use_screen_capture, app->screen_input, (int)app->width, (int)app->height, app->fps, app->fmt, target_w, target_h, &dec->fmt_ctx, &dec->codec_ctx, &dec->sws_ctx, diff --git a/src/util/config_reader.c b/src/util/config_reader.c index 4f2139e..d06b70e 100644 --- a/src/util/config_reader.c +++ b/src/util/config_reader.c @@ -516,21 +516,16 @@ int validate_tx_config(const struct dvledtx_config* config) { return -1; } - /* Video source validation */ - if (strcmp(config->input_mode, "screen_capture") == 0) { - if (config->screen_input[0] == '\0') { - LOG_ERROR("video.screen_input must be non-empty for screen_capture mode"); + /* Video source file validation — screen_input was already validated + * above in the "Input mode validation" block, so only check the file + * path here when not in screen-capture mode. */ + if (strcmp(config->input_mode, "screen_capture") != 0 && config->tx_url[0] != '\0') { + FILE* f = fopen(config->tx_url, "rb"); + if (!f) { + LOG_ERROR("video source file not found: %s", config->tx_url); return -1; } - } else { - if (config->tx_url[0] != '\0') { - FILE* f = fopen(config->tx_url, "rb"); - if (!f) { - LOG_ERROR("video source file not found: %s", config->tx_url); - return -1; - } - fclose(f); - } + fclose(f); } /* Session validation */ From c9ead34dba9cd339e15b976d3382e14e1ecd30cf Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 7 Jul 2026 14:56:15 +0530 Subject: [PATCH 06/10] ci: build FFmpeg with x11grab support for screen-capture tests The CI FFmpeg build didn't install libxcb dev packages or pass --enable-x11grab, so av_find_input_format("x11grab") always returned NULL on runners, failing test_main_registers_avdevice_for_x11grab and test_screen_capture_after_avdevice_register_finds_x11grab. - install libx11-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev - pass --enable-x11grab explicitly to FFmpeg's ./configure - verify x11grab is registered after the build - bump the environment cache key so runners rebuild FFmpeg with the new flag instead of restoring a stale cached build --- .github/actions/environment-check/action.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/actions/environment-check/action.yml b/.github/actions/environment-check/action.yml index 4b10977..ee5eb3d 100644 --- a/.github/actions/environment-check/action.yml +++ b/.github/actions/environment-check/action.yml @@ -30,7 +30,7 @@ runs: path: | ${{ runner.temp }}/dvledtx-deps ${{ runner.temp }}/mtl-source - key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-${{ runner.os }} + key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-x11grab1-${{ runner.os }} - name: Deploy cache to system if: steps.env-cache.outputs.cache-hit == 'true' @@ -104,7 +104,7 @@ runs: shell: bash run: | echo "===== APT Package Setup =====" - PKGS="git gcc meson python3 python3-pip pkg-config libnuma-dev libjson-c-dev libpcap-dev libgtest-dev libssl-dev systemtap-sdt-dev llvm clang flex byacc libcmocka-dev nasm wget patch unzip shellcheck cppcheck" + PKGS="git gcc meson python3 python3-pip pkg-config libnuma-dev libjson-c-dev libpcap-dev libgtest-dev libssl-dev systemtap-sdt-dev llvm clang flex byacc libcmocka-dev nasm wget patch unzip shellcheck cppcheck libx11-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev" MISSING="" for pkg in $PKGS; do if dpkg -s "$pkg" &>/dev/null; then @@ -252,7 +252,8 @@ runs: cp "$MTL_SOURCE"/ecosystem/ffmpeg_plugin/mtl_*.c libavdevice/ cp "$MTL_SOURCE"/ecosystem/ffmpeg_plugin/mtl_*.h libavdevice/ ./configure --enable-shared --disable-static --enable-pic \ - --enable-libopenh264 --enable-encoder=libopenh264 --enable-mtl + --enable-libopenh264 --enable-encoder=libopenh264 --enable-mtl \ + --enable-x11grab make -j "$(nproc)" make install DESTDIR="$RUNNER_TEMP/dvledtx-deps" sudo make install @@ -318,6 +319,14 @@ runs: exit 1 fi + # Check x11grab device is registered (required for screen-capture input_mode) + if echo "$DEVICES" | grep -qi "x11grab"; then + echo " [OK] x11grab device registered in FFmpeg avdevices" + else + echo "ERROR: FFmpeg x11grab device not found after build." + exit 1 + fi + - name: Report cache size if: steps.env-cache.outputs.cache-hit != 'true' shell: bash @@ -332,4 +341,4 @@ runs: path: | ${{ runner.temp }}/dvledtx-deps ${{ runner.temp }}/mtl-source - key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-${{ runner.os }} + key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-x11grab1-${{ runner.os }} From eee77d09f16a81c240621bd1362a89888fa85943 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Wed, 8 Jul 2026 10:21:49 +0530 Subject: [PATCH 07/10] ci: drop --enable-x11grab flag and cache-key bump per review feedback x11grab is auto-detected by FFmpeg's ./configure once the libxcb dev packages are installed, so the explicit --enable-x11grab flag isn't needed on this system. Revert the flag and the accompanying cache-key suffix that was added to force a rebuild for it. --- .github/actions/environment-check/action.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/actions/environment-check/action.yml b/.github/actions/environment-check/action.yml index ee5eb3d..652159b 100644 --- a/.github/actions/environment-check/action.yml +++ b/.github/actions/environment-check/action.yml @@ -30,7 +30,7 @@ runs: path: | ${{ runner.temp }}/dvledtx-deps ${{ runner.temp }}/mtl-source - key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-x11grab1-${{ runner.os }} + key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-${{ runner.os }} - name: Deploy cache to system if: steps.env-cache.outputs.cache-hit == 'true' @@ -252,8 +252,7 @@ runs: cp "$MTL_SOURCE"/ecosystem/ffmpeg_plugin/mtl_*.c libavdevice/ cp "$MTL_SOURCE"/ecosystem/ffmpeg_plugin/mtl_*.h libavdevice/ ./configure --enable-shared --disable-static --enable-pic \ - --enable-libopenh264 --enable-encoder=libopenh264 --enable-mtl \ - --enable-x11grab + --enable-libopenh264 --enable-encoder=libopenh264 --enable-mtl make -j "$(nproc)" make install DESTDIR="$RUNNER_TEMP/dvledtx-deps" sudo make install @@ -341,4 +340,4 @@ runs: path: | ${{ runner.temp }}/dvledtx-deps ${{ runner.temp }}/mtl-source - key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-x11grab1-${{ runner.os }} + key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-${{ runner.os }} From facfcbd62937aec37a5a39f0c947a6aca000db87 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Wed, 8 Jul 2026 10:25:57 +0530 Subject: [PATCH 08/10] ci: bust stale environment cache so x11grab-enabled FFmpeg gets rebuilt The cache key only encoded the dpdk/mtl/ffmpeg version inputs, so adding the libxcb dev packages to the APT install list didn't invalidate the previously cached FFmpeg build (built before those packages existed), and the FFmpeg build step's pkg-config short-circuit kept restoring/reusing that stale build without x11grab. Add a generic env-cache-version input (default 2) to the cache key so runners rebuild the environment now, and document that it should be bumped in the future whenever a change alters what gets installed into the cache. --- .github/actions/environment-check/action.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/actions/environment-check/action.yml b/.github/actions/environment-check/action.yml index 652159b..955e319 100644 --- a/.github/actions/environment-check/action.yml +++ b/.github/actions/environment-check/action.yml @@ -19,6 +19,13 @@ inputs: description: 'FFmpeg release branch to build' required: false default: '7.0' + env-cache-version: + description: >- + Bump this whenever a change alters what gets built/installed into the + cached environment (e.g. new APT packages needed by the FFmpeg build) + so runners rebuild instead of restoring a stale cache. + required: false + default: '2' runs: using: composite @@ -30,7 +37,7 @@ runs: path: | ${{ runner.temp }}/dvledtx-deps ${{ runner.temp }}/mtl-source - key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-${{ runner.os }} + key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-v${{ inputs.env-cache-version }}-${{ runner.os }} - name: Deploy cache to system if: steps.env-cache.outputs.cache-hit == 'true' @@ -340,4 +347,4 @@ runs: path: | ${{ runner.temp }}/dvledtx-deps ${{ runner.temp }}/mtl-source - key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-${{ runner.os }} + key: env-dpdk-${{ inputs.dpdk-version }}-mtl-${{ inputs.mtl-version }}-ffmpeg-${{ inputs.ffmpeg-version }}-v${{ inputs.env-cache-version }}-${{ runner.os }} From c7c554dc2547572a734912529c7294fb421b62d6 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Thu, 9 Jul 2026 14:00:45 +0530 Subject: [PATCH 09/10] Add multi-session screen capture config and update README - Add config/tx_fullhd_screen_capture_multi_session.json (3 sessions, 640x1080 strips) - Update README: replace Xvfb with Xorg + dummy driver for physical input support - Document audio routing via PULSE_SERVER for headless virtual display - Document application launch with PULSE_SERVER (e.g. Chrome) - Add multi-session screen capture JSON example to README --- README.md | 97 +++++++++++++++++-- ...x_fullhd_screen_capture_multi_session.json | 36 +++++++ 2 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 config/tx_fullhd_screen_capture_multi_session.json diff --git a/README.md b/README.md index a0429ab..33ecd7e 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ FFmpeg is an open source project licensed under LGPL and GPL. See https://www.ff If this prints nothing, FFmpeg needs to be reconfigured/rebuilt after installing the packages above — screen capture will otherwise fail at runtime with `x11grab input format not found`. - **Headless machines (no physical monitor)** additionally need a virtual display to capture from — see [Screen capture on a headless machine](#screen-capture-on-a-headless-machine-no-physical-monitor) below, which requires: ```bash - sudo apt-get install -y xvfb ubuntu-desktop + sudo apt-get install -y xserver-xorg-video-dummy ubuntu-desktop ``` ### Build Steps @@ -186,27 +186,81 @@ Multiple sessions can be defined in `tx_sessions` to transmit different crop reg #### Screen capture on a headless machine (no physical monitor) -`x11grab` needs a real X11 display to attach to — it does not work against a raw framebuffer or DRM device. On a machine with no monitor connected, create a virtual display with `Xvfb` and run a desktop session on it so there's actual content to capture: +`x11grab` needs a real X11 display to attach to — it does not work against a raw framebuffer or DRM device. On a machine with no monitor connected, create a virtual display using Xorg with the `dummy` video driver and run a desktop session on it so there's actual content to capture. Unlike Xvfb, a real Xorg server claims physical input devices — your keyboard and mouse work directly on the virtual display. -1. **Install prerequisites** (once): see [Software Requirements](#software-requirements) for the `xvfb`/`ubuntu-desktop` packages and the `x11grab`-enabled FFmpeg build. +1. **Install prerequisites** (once): see [Software Requirements](#software-requirements) for the `xserver-xorg-video-dummy`/`ubuntu-desktop` packages and the `x11grab`-enabled FFmpeg build. -2. **Start a virtual display** at the resolution you intend to transmit: +2. **Create an Xorg config** for the dummy driver: ```bash - Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp +extension GLX +extension RANDR & + sudo tee /tmp/xorg-dummy.conf > /dev/null << 'EOF' + Section "Device" + Identifier "dummy" + Driver "dummy" + VideoRam 256000 + EndSection + + Section "Monitor" + Identifier "monitor" + HorizSync 28.0-80.0 + VertRefresh 48.0-75.0 + Modeline "1920x1080" 148.50 1920 2008 2052 2200 1080 1084 1089 1125 +hsync +vsync + EndSection + + Section "Screen" + Identifier "screen" + Device "dummy" + Monitor "monitor" + DefaultDepth 24 + SubSection "Display" + Depth 24 + Modes "1920x1080" + EndSubSection + EndSection + + Section "ServerLayout" + Identifier "layout" + Screen "screen" + Option "AllowEmptyInput" "false" + EndSection + EOF ``` -3. **Start a GNOME desktop session** on that display (software GL rendering is required since Xvfb has no GPU): +3. **Start Xorg** on display `:99`: ```bash + sudo Xorg :99 -config /tmp/xorg-dummy.conf -noreset & + ``` + +4. **Allow root access** and **start a GNOME desktop session** (software GL rendering is required since there is no GPU; `PULSE_SERVER` routes audio to the physical audio device): + ```bash + DISPLAY=:99 xhost +local:root mkdir -p /tmp/gnome99-runtime && chmod 700 /tmp/gnome99-runtime DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 XDG_SESSION_TYPE=x11 \ XDG_RUNTIME_DIR=/tmp/gnome99-runtime \ + PULSE_SERVER=unix:/run/user/$(id -u)/pulse/native \ dbus-run-session -- gnome-session --session=ubuntu & ``` - Give it a few seconds to finish starting (`gnome-shell` and related services), then confirm with: + +5. **Verify** the display is working (give GNOME a few seconds to start): ```bash DISPLAY=:99 ffmpeg -f x11grab -video_size 1920x1080 -i :99.0+0,0 -frames:v 1 -update 1 /tmp/check.png ``` +Your physical keyboard and mouse now control the virtual display directly. + +> **Audio:** If audio is not working (e.g. PulseAudio only has a `null` sink), load your physical audio device manually: +> ```bash +> # List available ALSA devices +> aplay -l +> # Load the desired device (e.g. USB headset on card 1) +> pactl load-module module-alsa-sink device=hw:1,0 sink_name=plantronics sink_properties=device.description="Plantronics_Headset" +> pactl set-default-sink plantronics +> ``` +> +> Applications launched on the virtual display (e.g. Chrome) must also have `PULSE_SERVER` set: +> ```bash +> DISPLAY=:99 PULSE_SERVER=unix:/run/user/$(id -u)/pulse/native google-chrome & +> ``` + #### Example: screen-capture input (`config/tx_fullhd_screen_capture.json`) ```json @@ -230,6 +284,33 @@ Multiple sessions can be defined in `tx_sessions` to transmit different crop reg } ``` +#### Example: multi-session screen capture (`config/tx_fullhd_screen_capture_multi_session.json`) + +Splits a 1920×1080 screen capture into 3 vertical strips, each transmitted as a separate ST2110 session: + +```json +{ + "interfaces": [ + { "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" } + ], + "video": { + "width": 1920, + "height": 1080, + "input_mode": "screen_capture", + "screen_input": ":99.0+0,0" + }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p10le" + }, + "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 } } + ] +} +``` + `screen_input` follows FFmpeg's `x11grab` URL syntax: `[+,]` (e.g. `:99.0+0,0` captures display `:99`, matching the virtual display created above, starting at offset `0,0`). The capture resolution/framerate are taken from the `width`/`height`/`fps` fields above. Run dvledtx as usual — `x11grab` will capture whatever is rendered on the display (desktop, windows, applications) and transmit it, exactly as it would for a physical display: @@ -240,7 +321,7 @@ Run dvledtx as usual — `x11grab` will capture whatever is rendered on the disp **Tear down** the virtual display when done: ```bash pkill -f "gnome-session --session=ubuntu" -pkill -f "Xvfb :99" +sudo pkill -f "Xorg :99" ``` ## Logging diff --git a/config/tx_fullhd_screen_capture_multi_session.json b/config/tx_fullhd_screen_capture_multi_session.json new file mode 100644 index 0000000..8a45e81 --- /dev/null +++ b/config/tx_fullhd_screen_capture_multi_session.json @@ -0,0 +1,36 @@ +{ + "interfaces": [ + { + "name": "0000:06:00.0", + "sip": "192.168.50.29", + "dip": "239.168.85.20" + } + ], + "video": { + "width": 1920, + "height": 1080, + "input_mode": "screen_capture", + "screen_input": ":99.0+0,0" + }, + "tx_video": { + "fps": 30, + "fmt": "yuv422p10le" + }, + "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 } + } + ] +} From ff02b6c4675bd4548159b0fb8605a02c5646441e Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 14 Jul 2026 14:24:48 +0530 Subject: [PATCH 10/10] docs: guide users to enable X11 (disable Wayland) for screen capture on physical monitors --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 33ecd7e..bc1a978 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [Usage](#usage) - [Binding Ethernet Controller to DPDK PMD and Hugepage Setup](#binding-ethernet-controller-to-dpdk-pmd-and-hugepage-setup) - [JSON Configuration](#json-configuration) + - [Ensuring an X11 session (required for screen capture)](#ensuring-an-x11-session-required-for-screen-capture) - [Screen capture on a headless machine (no physical monitor)](#screen-capture-on-a-headless-machine-no-physical-monitor) - [Logging](#logging) - [Command-Line Options](#command-line-options) @@ -103,6 +104,7 @@ FFmpeg is an open source project licensed under LGPL and GPL. See https://www.ff ffmpeg -devices | grep x11grab ``` If this prints nothing, FFmpeg needs to be reconfigured/rebuilt after installing the packages above — screen capture will otherwise fail at runtime with `x11grab input format not found`. + - **`x11grab` only works against an X11 (Xorg) display, not Wayland** — see [Ensuring an X11 session](#ensuring-an-x11-session-required-for-screen-capture) below if you're capturing from a machine's own physical desktop session. - **Headless machines (no physical monitor)** additionally need a virtual display to capture from — see [Screen capture on a headless machine](#screen-capture-on-a-headless-machine-no-physical-monitor) below, which requires: ```bash sudo apt-get install -y xserver-xorg-video-dummy ubuntu-desktop @@ -184,6 +186,28 @@ dvledtx uses a JSON config file with three sections: 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`). +#### Ensuring an X11 session (required for screen capture) + +`x11grab` can only capture from a native X11 (Xorg) display — it does not work against a Wayland compositor. This mainly affects **machines with a physical monitor** logging into their own desktop session: current Ubuntu releases (22.04 and later) default new GDM logins to a **Wayland** session, which causes `screen_capture` to fail (or silently capture a blank/incorrect frame) even though FFmpeg was built correctly with `x11grab` support. + +> The [headless setup below](#screen-capture-on-a-headless-machine-no-physical-monitor) is unaffected by this — it starts a real Xorg server directly (`Xorg :99 ...`) and forces the desktop session running on top of it with `XDG_SESSION_TYPE=x11`, so no GDM/Wayland session is ever involved. + +To capture from a machine's own physical display, make sure that desktop session is running X11, not Wayland: + +- **Per-login (manual)**: at the GDM login screen, click the gear icon next to the password field and choose **"Ubuntu on Xorg"** instead of the default **"Ubuntu"** (Wayland) before signing in. +- **System-wide (automatic)**: disable Wayland in GDM so every login defaults to X11 — edit `/etc/gdm3/custom.conf` and uncomment/set `WaylandEnable=false` under the `[daemon]` section, or apply it directly: + ```bash + sudo cp /etc/gdm3/custom.conf /etc/gdm3/custom.conf.bak + sudo sed -i 's/^#\?WaylandEnable=.*/WaylandEnable=false/' /etc/gdm3/custom.conf + grep -q '^WaylandEnable=false' /etc/gdm3/custom.conf || \ + sudo sed -i '/^\[daemon\]/a WaylandEnable=false' /etc/gdm3/custom.conf + sudo systemctl restart gdm3 # or: sudo reboot + ``` + Restarting `gdm3` logs out the current session — save your work first. After logging back in, verify the session type: + ```bash + echo $XDG_SESSION_TYPE # should print: x11 + ``` + #### Screen capture on a headless machine (no physical monitor) `x11grab` needs a real X11 display to attach to — it does not work against a raw framebuffer or DRM device. On a machine with no monitor connected, create a virtual display using Xorg with the `dummy` video driver and run a desktop session on it so there's actual content to capture. Unlike Xvfb, a real Xorg server claims physical input devices — your keyboard and mouse work directly on the virtual display.