- Overview
- Notices
- Features
- Building
- Usage
- Logging
- Command-Line Options
- Supported Formats
- Performance Considerations
- Running Unit Tests
- Troubleshooting
- Contributing
- License
Direct View LED Software Toolkit is a simplified, standalone transmitter application using FFmpeg APIs and Media Transport Library (MTL) plugin.
dvledtx reads a video source file (e.g., MP4), decodes it using FFmpeg, and transmits uncompressed video frames over an IP network using the SMPTE ST 2110-20 standard. Multiple crop regions of the same source video can be transmitted simultaneously as independent RTP streams — enabling tiled LED wall configurations where each panel receives its own portion of the frame.
- LED Video Walls: Drive multiple LED panels from a single source, each panel receiving a cropped region of the full frame.
| Component | Requirement |
|---|---|
| OS | Validated on Ubuntu 22.04 LTS; should work on Ubuntu 24.04 and higher |
| CPU | Intel 12th Generation (Alder Lake) or above |
| NIC | Intel Ethernet Controller I225 or above |
| Memory | Hugepages configured (typically 2GB+) |
| Kernel | IOMMU and VFIO support enabled |
FFmpeg is an open source project licensed under LGPL and GPL. See https://www.ffmpeg.org/legal.html. You are solely responsible for determining if your use of FFmpeg requires any additional licenses. Intel is not responsible for obtaining any such licenses, nor liable for any licensing fees due, in connection with your use of FFmpeg.
- ST20P Video Transmission: Uncompressed video over SMPTE ST 2110-20
- Multi-session Support: Multiple concurrent video streams
- JSON Configuration: Per-session crop and network settings via JSON config
- Memory Efficient: Uses hugepages for optimal performance
- Intel 12th Generation CPU or above
- Intel Ethernet Controller I225 or above
Note: This toolkit has been validated against Ubuntu 22.04 LTS but should work on Ubuntu 24.04 LTS and higher versions.
- Ubuntu 22.04 or 24.04 LTS
- Media Transport Library (MTL) v26.01+
- Follow these steps
- Install APT packages
- Clone Media-Transport-Library
git clone https://github.com/OpenVisualCloud/Media-Transport-Library.git cd Media-Transport-Library git checkout ffmpeg-plugin-extra-pixel-format cd .. export mtl_source_code=${PWD}/Media-Transport-Library - Build and install DPDK
- Build and install MTL
- Follow these steps
- FFmpeg 7.0 with MTL Plugin
- Screen capture support (
input_mode: screen_capture) requires FFmpeg'sx11grabdevice. It is auto-detected and compiled in by FFmpeg's./configurescript, but only if these packages are installed before building FFmpeg:After building, verify support with:sudo apt-get install -y libx11-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
If this prints nothing, FFmpeg needs to be reconfigured/rebuilt after installing the packages above — screen capture will otherwise fail at runtime withffmpeg -devices | grep x11grabx11grab input format not found. x11grabonly works against an X11 (Xorg) display, not Wayland — see Ensuring an X11 session 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 below, which requires:
sudo apt-get install -y xserver-xorg-video-dummy ubuntu-desktop
- Screen capture support (
Once all dependencies are installed, clone this repository and run the build script:
git clone https://github.com/OpenVisualCloud/directview-led-software-toolkit
cd directview-led-software-toolkit
bash scripts/build.shTo build with MTL TX support enabled:
bash scripts/build.sh -Denable_mtl_tx=trueThe built binary will be available at build/dvledtx.
- Ensure VFIO group exists follow
- DPDK PMD Setup
- Hugepage Setup
dvledtx uses a JSON config file with three sections:
| Section | Field | Description |
|---|---|---|
| log_file | log_file |
(Optional) Path/name of the log output file (e.g. dvledtx.log). If omitted, logging goes to console only. |
| interfaces[] | name |
PCI BDF address of the NIC (e.g. 0000:06:00.0) |
sip |
Source IP address | |
dip |
Destination multicast IP address | |
| video | width |
Source frame width in pixels |
height |
Source frame height in pixels | |
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) | |
fmt |
Pixel format (see Supported Formats) | |
| tx_sessions[] | nic_index |
(Optional) Index into interfaces[] selecting which NIC this session uses (default: 0) |
udp_port |
UDP port for the session | |
payload_type |
(Optional) RTP payload type — defaults to 96 if not present |
|
crop |
Region to transmit: x, y, w, h in pixels |
{
"log_file": "dvledtx.log",
"interfaces": [
{ "name": "0000:03:10.0", "sip": "192.168.50.30", "dip": "239.168.85.20" },
{ "name": "0000:03:10.2", "sip": "192.168.50.29", "dip": "239.168.85.21" }
],
"video": {
"width": 1920, "height": 1080,
"tx_url": "bbb_sunflower_1080p_30fps_normal.mp4"
},
"tx_video": {
"scale_width": 1920, "scale_height": 1080,
"fps": 30,
"fmt": "yuv422p10le"
},
"tx_sessions": [
{ "nic_index": 0, "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 960, "h": 1080 } },
{ "nic_index": 1, "udp_port": 20002, "payload_type": 96, "crop": { "x": 960, "y": 0, "w": 960, "h": 1080 } }
]
}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).
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 is unaffected by this — it starts a real Xorg server directly (
Xorg :99 ...) and forces the desktop session running on top of it withXDG_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.confand uncomment/setWaylandEnable=falseunder the[daemon]section, or apply it directly:Restartingsudo 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
gdm3logs out the current session — save your work first. After logging back in, verify the session type:echo $XDG_SESSION_TYPE # should print: x11
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.
-
Install prerequisites (once): see Software Requirements for the
xserver-xorg-video-dummy/ubuntu-desktoppackages and thex11grab-enabled FFmpeg build. -
Create an Xorg config for the dummy driver:
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
-
Start Xorg on display
:99:sudo Xorg :99 -config /tmp/xorg-dummy.conf -noreset & -
Allow root access and start a GNOME desktop session (software GL rendering is required since there is no GPU;
PULSE_SERVERroutes audio to the physical audio device):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 &
-
Verify the display is working (give GNOME a few seconds to start):
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
nullsink), load your physical audio device manually:# 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 plantronicsApplications launched on the virtual display (e.g. Chrome) must also have
PULSE_SERVERset:DISPLAY=:99 PULSE_SERVER=unix:/run/user/$(id -u)/pulse/native google-chrome &
{
"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 } }
]
}Splits a 1920×1080 screen capture into 3 vertical strips, each transmitted as a separate ST2110 session:
{
"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: <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:
./build/dvledtx --config config/tx_fullhd_screen_capture.jsonTear down the virtual display when done:
pkill -f "gnome-session --session=ubuntu"
sudo pkill -f "Xorg :99"dvledtx includes a built-in logger with configurable output targets and log levels.
Specify a log file in the JSON config with the top-level log_file field:
{
"log_file": "dvledtx.log",
...
}When log_file is set, log output is written to that file in addition to the console. If the field is omitted, output goes to the console only.
| Level | Description |
|---|---|
ERROR |
Critical failures only |
WARN |
Non-fatal warnings |
INFO |
General operational messages (default) |
DEBUG |
Verbose diagnostic output |
./build/dvledtx --config config/tx_fullhd_single_session.json| Option | Description |
|---|---|
-C, --config <file> |
JSON config file (required) |
-v, --version |
Show version and exit |
--help |
Show help message |
./build/dvledtx --version
# or
./build/dvledtx -v| Format | Chroma | Bit Depth | Color Space |
|---|---|---|---|
yuv422p10le |
4:2:2 | 10-bit | YUV |
yuv420 |
4:2:0 | 8-bit | YUV |
yuv444p10le |
4:4:4 | 10-bit | YUV |
gbrp10le |
4:4:4 | 10-bit | RGB |
yuv422p12le |
4:2:2 | 12-bit | YUV |
yuv444p12le |
4:4:4 | 12-bit | YUV |
gbrp12le |
4:4:4 | 12-bit | RGB |
- 25 fps
- 30 fps
- 50 fps
- 60 fps
| 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.
dvledtx supports upscaling and downscaling via optional scale_width and scale_height fields in the video configuration block. When set, the decoded source video is scaled to the specified dimensions before crop regions are applied.
| Feature | Description |
|---|---|
| Upscale | Scale smaller source (e.g. 1080p) to larger output (e.g. 4K) |
| Downscale | Scale larger source (e.g. 4K) to smaller output (e.g. 1080p) |
| Single session | Full scaled frame transmitted as one stream |
| Multi-session | Crop regions applied to scaled frame for tiled LED walls |
| Max output | 3840x2160 (4K UHD) |
Example — upscale 1080p source to 4K output:
{
"video": {
"width": 1920, "height": 1080,
"tx_url": "source_1080p.mp4"
},
"tx_video": {
"scale_width": 3840, "scale_height": 2160,
"fps": 30, "fmt": "yuv422p10le"
},
"tx_sessions": [
{ "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 } }
]
}Log output when scaling is active:
[INFO ] Video: 1920x1080 -> scale 3840x2160 30fps yuv422p10le tx_url=source_1080p.mp4
[INFO ] Session 0: udp_port=20000 pt=96 crop=[0,0 3840x2160]
Note: When
scale_width/scale_heightare set, crop bounds are validated against the scaled dimensions, not the source dimensions. Both fields must be specified together, and the scaled dimensions must satisfy the pixel format's chroma-alignment constraints (e.g.yuv420requires even width and height;yuv422*requires even width).
- Hugepage Memory: All buffers allocated on hugepages
- Zero-copy Design: Minimal memory copying
- Hardware Acceleration: Uses MTL's hardware features
- Efficient Threading: Minimal context switching
Install dependencies if not present
sudo apt install libcmocka-dev
pip install gcovr
To build and run all unit tests with coverage:
bash scripts/test.shTo run tests without generating a coverage report:
bash scripts/test.sh --no-coverageMTL requires IOMMU and VFIO kernel parameters to be set. If MTL fails to initialize or VFIO devices are not accessible, check that the following parameters are present in /etc/default/grub:
intel_iommu=on iommu=pt pcie_aspm=off pcie_port_pm=off vfio-pci.disable_idle_d3=1
To add them manually:
-
Open
/etc/default/grubin a text editor:sudo nano /etc/default/grub
-
Append the missing parameters to
GRUB_CMDLINE_LINUX_DEFAULT, for example:GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on iommu=pt pcie_aspm=off pcie_port_pm=off vfio-pci.disable_idle_d3=1" -
Update GRUB and reboot:
sudo update-grub sudo reboot
Note: A reboot is required for the IOMMU/VFIO parameters to take effect.
MTL uses VFIO to access the NIC. The current user must belong to the vfio group.
-
Create the
vfiogroup if it does not exist:sudo groupadd vfio
-
Add your user to the group:
sudo usermod -aG vfio $USER -
Apply the group membership without logging out:
newgrp vfio
Or log out and back in for the change to take effect permanently.
-
Verify group membership:
id -nG $USER
If dvledtx becomes unresponsive or needs to be force-stopped:
sudo pkill -9 -f dvledtx-
MTL Initialization Failed
- Check network port PCI address matches your hardware:
lspci | grep Ethernet # Example output: 06:00.0 Ethernet controller: Intel Corporation ... # Use BDF format in config: "0000:06:00.0"
- Verify MTL library is properly installed:
# Check MTL shared library is present ldconfig -p | grep mtl # Expected: libmtl.so (libc6,x86-64) => /usr/local/lib/x86_64-linux-gnu/libmtl.so # Check MTL pkg-config file is available pkg-config --modversion mtl # Expected: 26.01.0.REL (or higher) # Check MTL header files exist ls /usr/local/include/mtl/mtl_api.h
- Verify DPDK is installed and the NIC is bound to VFIO:
# Check DPDK installation pkg-config --modversion libdpdk # Check NIC binding status sudo $mtl_source_code/script/nicctl.sh list # Or use dpdk-devbind.py: dpdk-devbind.py --status # The target NIC should show "drv=vfio-pci"
- Ensure IOMMU is enabled (see IOMMU/VFIO Kernel Parameters):
dmesg | grep -i iommu # Should show "DMAR: IOMMU enabled" cat /proc/cmdline | grep intel_iommu
- Verify network card is supported (Intel I225, I226, E810 series)
- Check network port PCI address matches your hardware:
-
Cannot Load Source File
- Check file exists and is readable:
ls -la /path/to/video.mp4
- Verify the file is a valid video (not corrupted):
ffprobe /path/to/video.mp4 # Should show video stream info without errors - Ensure sufficient disk space:
df -h . - Verify file format matches config parameters (resolution, pixel format)
- Note: Symlinked files are rejected for security reasons
- Check file exists and is readable:
-
Network Transmission Issues
- Ensure network card supports required bandwidth (uncompressed 1080p30 ≈ 1.8 Gbps)
- Verify hugepages are allocated:
cat /proc/meminfo | grep HugePages # HugePages_Free should be > 0
-
Hugepage Allocation Failure
- Check current hugepage status:
cat /proc/meminfo | grep Huge - Allocate hugepages (requires root):
sudo sysctl -w vm.nr_hugepages=2048 # Or make persistent in /etc/sysctl.conf
- Check current hugepage status:
-
Permission Denied (VFIO)
- Ensure user is in the
vfiogroup (see VFIO Group Setup) - Check VFIO device permissions:
ls -la /dev/vfio/ # Group should be 'vfio' with rw permissions
- Ensure user is in the
Contributions are welcome. Please open an issue or submit a pull request on GitHub.
This project is licensed under the BSD 3-Clause License. See LICENSE for details.
