Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
14 changes: 5 additions & 9 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "c++",
"name": "pixelflux (rust)",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
"features": {
Expand All @@ -13,11 +13,8 @@
"vncPort": "5901"
}
},

"runArgs": ["--env-file", ".devcontainer/devcontainer.env"],

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
"runArgs": ["--env-file", ".devcontainer/devcontainer.env"],

// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [6080, 5901],
Expand All @@ -29,10 +26,9 @@
"customizations": {
"vscode": {
"extensions": [
// C++
"ms-vscode.cpptools",
"ms-vscode.cpptools-extension-pack",
"ms-vscode.cpptools-themes"
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb"
]
}
}
Expand Down
51 changes: 36 additions & 15 deletions .devcontainer/install-dependencies.sh
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
#!/bin/bash
# Dev setup for the pure-Rust pixelflux PyO3 extension (replaces the old C++ setup.py build).
set -euxo pipefail

sudo apt update
# dependencies
sudo apt-get update
# System C libraries the crate links against (x264-sys -> libx264, turbojpeg ->
# libjpeg-turbo, x11rb -> libxcb + shm + xfixes, VA-API, GBM/DRM, Wayland/xkb) plus
# the build toolchain (nasm is needed to build the vendored OpenH264 source).
sudo apt-get install -y \
g++ \
libjpeg-turbo8-dev \
libx11-dev \
libxfixes-dev \
libxext-dev \
libx264-dev \
python3-dev \
python3-pip \
python3-websockets
build-essential pkg-config nasm clang libclang-dev curl ca-certificates \
libjpeg-turbo8-dev libx264-dev \
libva-dev libdrm-dev libgbm-dev \
libwayland-dev libxkbcommon-dev \
libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev \
python3-dev python3-pip

# firefox-esr
# firefox-esr (for end-to-end testing the stream in a browser)
sudo apt install -y software-properties-common && sudo add-apt-repository ppa:mozillateam/ppa -y && sudo apt install -y firefox-esr

# setup
pip3 install setuptools
sudo python3 setup.py install
# Rust toolchain (the extension is built via setuptools-rust / cargo).
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# shellcheck source=/dev/null
source "$HOME/.cargo/env"
fi

# FFmpeg 8.1 is REQUIRED by ffmpeg-sys-next =8.1.0 (the VA-API encoder path) and is not
# in the Ubuntu archive, so pull it from conda-forge (Miniforge), matching the repo's
# build environment, and point pkg-config at it for the build.
if [ ! -d "$HOME/miniforge3" ]; then
curl -L -o /tmp/miniforge.sh \
"https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash /tmp/miniforge.sh -b -p "$HOME/miniforge3"
fi
source "$HOME/miniforge3/etc/profile.d/conda.sh"
conda create -y -n pixelflux -c conda-forge "ffmpeg=8.1"
conda activate pixelflux
export PKG_CONFIG_PATH="$CONDA_PREFIX/lib/pkgconfig:${PKG_CONFIG_PATH:-}"

# Build and install the extension from source.
pip3 install --upgrade pip setuptools-rust
pip3 install .
11 changes: 8 additions & 3 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
graft pixelflux
include pixelflux_wayland/Cargo.toml
recursive-include pixelflux_wayland/src *
include pixelflux/Cargo.toml
include pixelflux/Cargo.lock
recursive-include pixelflux/src *
recursive-include pixelflux/nvcodec-sys *
recursive-exclude pixelflux *.so *.pyc
prune pixelflux/target
prune pixelflux/nvcodec-sys/target
include pyproject.toml
include README.md
include setup.py
147 changes: 99 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,43 @@

This module provides a Python interface to a high-performance capture library supporting both **X11** and **Wayland** environments. It captures pixel data, detects changes, and encodes modified stripes into JPEG or H.264.

It supports CPU-based encoding (libx264, libjpeg-turbo) as well as hardware-accelerated H.264 encoding via NVIDIA's NVENC and VA-API for Intel/AMD GPUs. The Wayland backend features a **zero-copy pipeline**, passing GPU buffers directly to the encoder to minimize latency and CPU usage.
It supports CPU-based encoding (x264, JPEG) as well as hardware-accelerated H.264 encoding via NVIDIA's NVENC and VA-API for Intel/AMD GPUs. Both backends share a **zero-copy pipeline** that minimizes copies and latency end to end.

## Installation

This module relies on native C++ (X11) and Rust (Wayland) extensions that are compiled during installation.
pixelflux is a single self-contained **Rust** extension (no C/C++ sources) compiled during installation. Both the X11 and Wayland backends, all encoders, and the Python API live in it.

### 1. Prerequisites

Ensure you have a C++ compiler (`g++`), the Rust toolchain (`cargo`), and development files for Python and the underlying graphics libraries.
Ensure you have the Rust toolchain (`cargo`), Python development files, and the development libraries below.

**Base Dependencies (Debian/Ubuntu):**
```bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Build dependencies (Debian/Ubuntu)
sudo apt-get update && \
sudo apt-get install -y \
g++ \
git \
curl \
python3-dev \
cmake \
nasm \
libclang-dev \
libavcodec-dev \
libavutil-dev \
libjpeg-turbo8-dev \
libx264-dev \
libyuv-dev
```

**X11 Backend Dependencies:**
```bash
sudo apt-get install -y \
libx11-dev \
libxext-dev \
libxfixes-dev
```

**Wayland Backend Dependencies:**
To build the Rust-based Wayland backend, you need the Rust toolchain and Wayland/DRM libraries:

```bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install Libraries
sudo apt-get install -y \
libturbojpeg0-dev \
libgbm-dev \
libdrm-dev \
libwayland-dev \
libinput-dev \
libxkbcommon-dev \
libva-dev \
libclang-dev
libva-dev
```

> **Notes:** the FFmpeg bindings (`ffmpeg-next` 8.1) require **FFmpeg 8.1**; on distros shipping an older FFmpeg, install a newer build and point `PKG_CONFIG_PATH` at it. X11 capture uses pure-Rust XCB (no `libX11`/`libxcb`/`Xfixes` dev packages needed); colorspace conversion is pure-Rust and the NVENC/CUDA libraries are loaded at runtime (no compile-time NVIDIA packages).

### 2. Hardware Acceleration (Optional but Recommended)
* **NVIDIA (NVENC):** The library detects the NVIDIA driver at runtime. No extra compile-time packages are needed.
* **Intel/AMD (VA-API):** Ensure `libva-dev` and `libdrm-dev` are installed. You must also have the correct drivers (e.g., `intel-media-va-driver-non-free` or `mesa-va-drivers`).
Expand Down Expand Up @@ -91,6 +77,21 @@ To test launching programs into this backend simply add `WAYLAND_DISPLAY=wayland
WAYLAND_DISPLAY=wayland-1 glmark2-es2-wayland -s 1920x1080
```

### Automatic GPU Selection

Set `SELKIES_AUTO_GPU=true` (preferred, or the legacy `AUTO_GPU=true`) to let pixelflux pick a
render node automatically instead of supplying one. It enumerates `/sys/class/drm`, pairs each
`cardN` with its `renderD*` node by PCI device, and skips non-GPU cards (IPMI/VGA). Selection is
**driver-aware**: NVIDIA nodes are routed to NVENC, while Intel (`i915`) and AMD (`amdgpu`) nodes
take the VA-API path. Both the X11 and Wayland backends honor this.

```bash
export SELKIES_AUTO_GPU=true
```

When auto-selection is off and no node is supplied, an operator-set `DRINODE` (e.g.
`/dev/dri/renderD128`) is honored before falling back to the software renderer.

### Capture Settings

The `CaptureSettings` class configures both backends.
Expand Down Expand Up @@ -126,7 +127,7 @@ settings.paint_over_jpeg_quality = 90 # Quality for static "paint-over" stripe
settings.h264_crf = 25 # CRF value (0-51, lower is better quality/higher bitrate)
settings.h264_paintover_crf = 18 # CRF for H.264 paintover on static content. Must be lower than h264_crf to activate.
settings.h264_paintover_burst_frames = 5 # Number of high-quality frames to send in a burst when a paintover is triggered.
settings.h264_fullcolor = False # Use I444 (full color) instead of I420 for software encoding
settings.h264_fullcolor = False # Use I444/full color (High 4:4:4) instead of I420. Supported by software encoding and NVENC.
settings.h264_fullframe = True # Encode full frames (required for HW accel) instead of just changed stripes
settings.h264_streaming_mode = False # Bypass all VNC logic and work like a normal video encoder, higher constant CPU usage for fullscreen gaming/videos
settings.h264_cbr_mode = False # Switches to CBR mode and ignores CRF value. Used in conjunction with h264_bitrate_kbps.
Expand All @@ -138,6 +139,18 @@ settings.auto_adjust_screen_capture_size = True # Allow pixelflux to adjust it
# >= 0: Enable GPU Encoding on /dev/dri/renderD(128 + index)
# -1: Disable GPU Encoding (System will try NVENC if available when using the x11 backend, Wayland needs this set to a render node)
settings.vaapi_render_node_index = -1
# Explicit render node path (X11). Takes precedence over the positional index above and
# avoids the index ambiguity. Must be a bytes object, e.g. b"/dev/dri/renderD128".
settings.vaapi_render_node_path = None

# --- Wire Format / Zero-Copy (X11) ---
# False (default): prepend the per-stripe header to each packet (the WebSocket path).
# True: emit the raw encoded payload with no header (for a WebRTC path that frames itself).
settings.omit_stripe_headers = False
# Deprecated/ignored: the native frame handed to your callback always owns its buffer
# (zero-copy on every Python version, see below), so this flag no longer has any effect.
# Kept only for backward compatibility.
settings.deferred_free = False

# --- Change Detection & Optimization ---
settings.use_paint_over_quality = True # Enable paint-over/IDR requests for static regions
Expand Down Expand Up @@ -178,23 +191,40 @@ capture.inject_key(scancode=17, state=1)

### Stripe Callback

Your callback receives a `ctypes.POINTER(StripeEncodeResult)`.
Your callback receives a single **frame object** (`StripeFrame` on X11, `WaylandFrame` on
Wayland). Both support the buffer protocol — `bytes(frame)` / `memoryview(frame)` / `len(frame)`
— and expose the stripe metadata as attributes:

```python
def my_callback(frame):
# frame.data_type (0=Unknown, 1=JPEG, 2=H.264)
# frame.frame_id
# frame.stripe_y_start
# frame.stripe_height
encoded_data = bytes(frame) # copy out, or use memoryview(frame) zero-copy (below)
# Send encoded_data to the client...
```

### Zero-Copy Frames

`memoryview(frame)` aliases the native encoder buffer with **no copy**, on **every supported
Python version (3.9–3.14)**. The frame object owns its buffer and keeps it alive until every
consumer — including a transport that retained a slice during a partial write — has released its
view, so the hand-off is memory-safe. (The old `deferred_free` / `OwnedFrame` / PEP 688 /
Python-3.12-only path is gone; the native buffer protocol does this on all versions.) Hand the
view straight to an async socket; keep the frame referenced for the duration of the send.

```python
def my_callback(result_ptr, user_data):
result = result_ptr.contents

# Access data
# result.type (0=H264, 1=JPEG)
# result.frame_id
# result.stripe_y_start

# Copy data to Python bytes
encoded_data = ctypes.string_at(result.data, result.size)

# Send encoded_data to client...
def my_callback(frame):
if frame.data_type == 0 or len(frame) == 0: # nothing to send
return
# Hand BOTH the view and the frame to your sender (e.g. an asyncio.Queue) so the buffer
# outlives the send: the view pins the frame, which frees the buffer once the view drops.
queue.put_nowait({"data": memoryview(frame), "owner": frame})
```

See `example/screen_to_browser.py` for a complete queue-based usage.

## Zero-Copy Pipeline (Wayland)

The Wayland backend implements a **Zero-Copy** architecture for hardware encoding.
Expand All @@ -203,7 +233,7 @@ The Wayland backend implements a **Zero-Copy** architecture for hardware encodin
2. **Export:** This buffer is exported as a `Dmabuf` (file descriptor).
3. **Encoding:** The `Dmabuf` is imported directly into the encoder context (NVENC or VA-API) without ever copying pixel data to system RAM (CPU).

**Performance Note:** Enabling **watermarking** or utilizing a render node different from the encoding node will force a "Readback" fallback, copying pixels to the CPU and breaking the zero-copy chain. This increases latency and CPU load.
**Performance Note:** Software (Pixman) rendering, the absence of a hardware encoder, or utilizing a render node different from the encoding node will force a "Readback" fallback, copying pixels to the CPU and breaking the zero-copy chain (higher latency and CPU load). A watermark does **not** force readback — on the GPU path it is composited into the frame before encoding.

## Recording Sink (Wayland)

Expand All @@ -226,18 +256,39 @@ ffmpeg -f h264 -i unix:///tmp/pixelflux_record -c:v copy test.h264
ffmpeg -f h264 -framerate 60 -i unix:///tmp/pixelflux_record -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p test.mp4
```

## NVIDIA NVENC (X11)

* **Multi-GPU containers:** When several GPUs are exposed to a container, NVENC is filtered
in-process to the GPU you selected (no separate `LD_PRELOAD` shim is required). Verified on
NVIDIA drivers 570–595.
* **4:4:4 (High 4:4:4):** Set `h264_fullcolor = True` to encode full-chroma H.264 via NVENC
(`h264_fullcolor` codec), in addition to the software path.
* **Force a keyframe on demand:** `capture.request_idr_frame()` forces an IDR frame, e.g. when
a client reconnects or its decoder is reset. It routes to whichever encoder is active
(NVENC, VA-API, or software) and is a no-op while no capture is running.

### NVENC color conversion

NVENC encodes the captured ARGB directly (the driver's hardware does the ARGB→NV12 colorspace
conversion in BT.709), so there is **no CUDA Toolkit / NVRTC requirement** — only the NVIDIA
driver runtime (`libnvidia-encode`, `libcuda`), which is loaded at runtime. Nothing extra to
install at build or runtime beyond the driver.

## Features

* **Hybrid Backend:**
* **X11 (C++):** Legacy support using XShm.
* **Wayland (Rust):** Modern, secure, headless compositor based on [Smithay](https://github.com/Smithay/smithay).
* **Dual Backend (one Rust extension):**
* **X11:** XShm capture via pure-Rust XCB, with XFixes cursor and watermark compositing.
* **Wayland:** Modern, secure, headless compositor based on [Smithay](https://github.com/Smithay/smithay).
* **Flexible Encoding:**
* **Software:** libx264 (H.264) and libjpeg-turbo (JPEG) with multi-threaded striping.
* **Hardware:** NVIDIA NVENC and VA-API (Intel/AMD) with Zero-Copy support.
* **Software:** x264 (H.264, incl. 4:4:4) and JPEG with multi-threaded striping.
* **Hardware:** NVIDIA NVENC (incl. High 4:4:4, ARGB-direct BT.709, multi-GPU containers, API-version negotiation) and VA-API (Intel/AMD, VA-VPP convert) with Zero-Copy support.
* **Driver-aware GPU auto-selection** via `SELKIES_AUTO_GPU`.
* **Zero-Copy Frames (X11 & Wayland):** the native frame object (buffer protocol) hands the encoded buffer to Python with no copy, on every supported Python version (3.9–3.14).
* **Smart Bandwidth Management:**
* **Change Detection:** Encodes only changed stripes (Software/JPEG mode).
* **Paint-Over:** Automatically improves quality for static regions.
* **Damage Throttling:** Limits processing during high-motion scenes.
* **On-demand keyframes:** `request_idr_frame()` forces an IDR for reconnecting clients.
* **Input Handling:** Built-in input injection for mouse and keyboard (Wayland).
* **Cursor Compositing:** Hardware cursor planes or software rendering options.
* **Dynamic Watermarking:** Overlay PNGs with static positioning or DVD-screensaver style animation.
Expand Down
Loading