Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/actions/environment-check/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Comment thread
roshan-ku marked this conversation as resolved.
Outdated

- name: Deploy cache to system
if: steps.env-cache.outputs.cache-hit == 'true'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
roshan-ku marked this conversation as resolved.
Outdated
make -j "$(nproc)"
make install DESTDIR="$RUNNER_TEMP/dvledtx-deps"
sudo make install
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Comment thread
roshan-ku marked this conversation as resolved.
Outdated
80 changes: 78 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -132,7 +146,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) |
Expand All @@ -141,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",
Expand All @@ -163,6 +180,65 @@ 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`).

#### Screen capture on a headless machine (no physical monitor)
Comment thread
roshan-ku marked this conversation as resolved.

`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): 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
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
```

#### 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": ":99.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: `<display>[+<x>,<y>]` (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
```

**Tear down** the virtual display when done:
```bash
pkill -f "gnome-session --session=ubuntu"
pkill -f "Xvfb :99"
```

## Logging

dvledtx includes a built-in logger with configurable output targets and log levels.
Expand Down
25 changes: 25 additions & 0 deletions config/tx_1session_12bit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"interfaces": [
{
"name": "0000:06:00.0",
"sip": "192.168.50.29",
"dip": "239.168.85.20"
}
],
"video": {
"width": 1920,
"height": 1080,
"tx_url": "bbb_sunflower_1080p_30fps_normal.mp4"
},
Comment thread
roshan-ku marked this conversation as resolved.
"tx_video": {
"fps": 30,
"fmt": "yuv422p12le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 }
}
]
}
26 changes: 26 additions & 0 deletions config/tx_fullhd_screen_capture.json
Original file line number Diff line number Diff line change
@@ -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 }
}
]
}
2 changes: 2 additions & 0 deletions include/app_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions include/util/config_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
8 changes: 5 additions & 3 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/core/session_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand Down
45 changes: 42 additions & 3 deletions src/ffmpeg/ffmpeg_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <libavutil/imgutils.h>
#include <libavutil/pixdesc.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>

/* =========================================================================
* Helpers
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -323,8 +352,13 @@ 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,
Comment thread
roshan-ku marked this conversation as resolved.
app->fmt, target_w, target_h,
&dec->fmt_ctx, &dec->codec_ctx, &dec->sws_ctx,
&dec->av_frame, &dec->yuv_frame, &dec->av_packet,
Expand All @@ -347,6 +381,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,
Expand All @@ -366,6 +401,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;
Expand Down
Loading
Loading