Skip to content
Open
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
4 changes: 2 additions & 2 deletions docs/selkies/components/baseimages.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ There is deliberately **no `latest` tag** for base images. Downstream images pin
- **[Selkies Desktop](selkies-desktop.md)** at `/usr/bin/selkies-desktop`, activated by env var.
- **Nginx** with the fancyindex module, serving the client, proxying the WebSocket, handling basic auth, subfolder support, and the `/files` download index.
- **PulseAudio** with null sinks (`output` and `input`) wired for stream audio and microphone return.
- **Gamepad plumbing**: the joystick interposer and fake udev libraries, preloaded globally, with device nodes created at init.
- **Gamepad plumbing**: the input interposer and fake udev libraries, preloaded globally, with device nodes created at init.
- **Quality of life**: passwordless sudo for the desktop user, all system locales prebuilt for `LC_ALL`, `proot-apps` synced into the user home for persistent app installs, Docker in Docker support for privileged containers, and notification support.

## The runtime in one diagram
Expand All @@ -47,7 +47,7 @@ graph TD
SELKIES -->|starts in process| PF[pixelflux Wayland compositor, socket wayland-1]
DE -->|waits for wayland-1| LABWC[labwc or a full DE, exposes wayland-0]
LABWC --> APP[autostart application]
NGINX -->|3000 / 3001| WEB[web client, /websocket proxy, /files, /pelorus]
NGINX -->|3000 / 3001| WEB[web client, /api proxy, /pelorus]
```

At startup a chain of one shot init scripts configures everything from environment variables: Nginx substitution (ports, auth, subfolder, title), Wayland or X11 mode selection, first run copy of the autostart and menu defaults into `/config`, hardening (the `HARDEN_*` and `DISABLE_*` family), GPU detection and permission fixes, and gamepad device setup. Then the long running services above come up in dependency order.
Expand Down
2 changes: 1 addition & 1 deletion docs/selkies/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ graph TD
To make the layering concrete, here is the life of one frame in a Wayland mode container:

1. The application renders into a buffer belonging to **labwc** (or KWin on KDE), which is itself a client of the headless Smithay compositor that **pixelflux** hosts in process.
2. Pixelflux composites the output. If a GPU holds the framebuffer and the encoder lives on the same GPU, the frame is passed as a DMA-BUF straight into NVENC or VA-API, zero copy. Otherwise it is read back and encoded on CPU, in parallel stripes if the software encoder is in use.
2. Pixelflux composites the output. If a GPU holds the framebuffer and the encoder lives on the same GPU, the frame is passed as a DMA-BUF straight into NVENC or VA-API, zero copy, for any codec the card carries. Otherwise it is read back and encoded on CPU, in parallel stripes for H.264 and JPEG and full frame for the other codecs.
3. Only regions that changed get encoded at all; a static screen costs almost nothing, and after motion stops a high quality paint over pass restores perfect detail.
4. The encoded frame, with a small binary header, is handed to **Selkies**, which broadcasts it over the WebSocket to every connected viewer with backpressure control per client.
5. The container's **Nginx** carries that WebSocket alongside the static web client, file downloads, basic auth, and the optional Pelorus API, all on one HTTPS port.
Expand Down
45 changes: 26 additions & 19 deletions docs/selkies/components/pixelflux.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Pixelflux and Pcmflux

**Repository:** [linuxserver/pixelflux](https://github.com/linuxserver/pixelflux) · **PyPI:** `pip install pixelflux` · **License:** MPL-2.0
**Repositories:** [selkies-project/pixelflux](https://github.com/selkies-project/pixelflux) and [selkies-project/pcmflux](https://github.com/selkies-project/pcmflux) · **PyPI:** `pip install pixelflux pcmflux` · **License:** MPL-2.0 · **Upstream docs:** [docs.selkies.io](https://docs.selkies.io/) · **Rustdocs:** [pixelflux](https://pixelflux.selkies.io/pixelflux/index.html), [pcmflux](https://pcmflux.selkies.io/pcmflux/index.html)

Pixelflux is the rendering and capture pipeline at the core of the entire platform. It captures a Linux framebuffer, decides what changed, encodes it as JPEG or H.264, and hands the encoded frames to your code through a Python callback. Selkies is its primary consumer, but it is a standalone library you can embed in anything.
Pixelflux is the rendering and capture pipeline at the core of the entire platform. It captures a Linux framebuffer, decides what changed, encodes it as JPEG, H.264, H.265, VP8, VP9, or AV1, and hands the encoded frames to your code through a Python callback. Selkies is its primary consumer, but it is a standalone library you can embed in anything.

Pcmflux is its audio sibling: it captures PulseAudio output and encodes Opus frames for delivery to the browser, and handles the microphone return path. Both ship as prebuilt wheels for x86_64 and aarch64 on glibc and musl.

Expand All @@ -12,23 +12,28 @@ As of the 2.0.0 release, pixelflux is a **Rust** library exposed to Python throu

- An **X11 backend**: XShm screen capture with XFixes cursor tracking, for the legacy X11 stack.
- A **Wayland backend**: a full headless Wayland compositor built on [Smithay](https://github.com/Smithay/smithay), running in process. This is the key architectural point of the modern stack: **pixelflux does not capture a Wayland compositor, it is the compositor.** It synthesizes the output, seat, and clipboard itself, which is why the framebuffer can live directly on a GPU and why input is injected through its API rather than tools like xdotool.
- Four encoders and the damage tracking, paint over, and rate control logic shared between them.
- Six codecs across software and hardware encoders, and the damage tracking, paint over, and rate control logic shared between them.

## Encoders

| Encoder | Hardware | Mode | Notes |
| --- | --- | --- | --- |
| JPEG | CPU | Striped | Stateless, per stripe quality. Kept as the compatibility path for browsers that cannot decode video frames; 4:4:4 H.264 with paint over matches it visually everywhere else |
| x264 | CPU | Striped or full frame | Default software H.264, ultrafast zerolatency, 4:4:4 capable |
| OpenH264 | CPU | Full frame only | Opt in alternative software encoder, 4:2:0 only |
| NVENC | Nvidia GPU | Full frame | Direct NVENC via runtime library loading, no CUDA toolkit needed, supports High 4:4:4, zero copy from DMA-BUF |
| VA-API | Intel and AMD GPU | Full frame | Through FFmpeg's `h264_vaapi`, zero copy from DMA-BUF, no 4:4:4 (falls back to CPU) |
Every codec is selected by name (`codec = "h264"` and so on) and pixelflux resolves the encoder behind it: the GPU engine when the encoding device has one, the software encoder the build carries otherwise. `pixelflux.SOFTWARE_ENCODERS` names the software encoder per codec and `pixelflux.hardware_encoders(node)` the codecs a render node's NVENC or VA-API serves, which is how Selkies trims its menu at startup.

| Codec | Software | NVENC (Nvidia) | VA-API (Intel and AMD) | Shape | 4:4:4 |
| --- | --- | --- | --- | --- | --- |
| JPEG | libjpeg-turbo, vendored | No | No | Striped | Always, JFIF is full color |
| H.264 | x264, or OpenH264 in a GPL free build | Yes | Yes | Striped or full frame | x264 and NVENC. VA-API has no 4:4:4 H.264 profile on current drivers and hands the request to x264. OpenH264 is 4:2:0 only |
| H.265 | x265, or kvazaar in a GPL free build | Yes | Yes | Full frame | x265 and NVENC, VA-API negotiates a 4:4:4 surface per device |
| VP8 | libvpx | No | Where the GPU has an engine | Full frame | No, and VP8 declares BT.601 because its bitstream can name nothing else |
| VP9 | libvpx | No | Yes | Full frame | Profile 1 on libvpx and VA-API |
| AV1 | SVT-AV1 | Ada and newer | Where the GPU has an engine | Full frame | No |

Hardware encoders are loaded at runtime: NVENC through the driver's `libnvidia-encode` and `libcuda` with no CUDA toolkit, VA-API and the software H.265, VP8, VP9, and AV1 encoders through the system FFmpeg's `libavcodec`. The official wheels are GPL builds with x264 and x265; a `PIXELFLUX_ENABLE_GPL=0` source build swaps in OpenH264 and kvazaar and keeps everything else.

### Striped encoding

On the CPU paths the screen is divided into horizontal stripes, one per CPU core (each at least 64 rows). Each stripe has its own encoder instance and its own damage history, and stripes are encoded in parallel across a thread pool. Only stripes that changed are encoded and sent. This is the "hybrid VNC and video codec" idea that lets a CPU only server idle at nearly zero cost and still deliver 60fps where the screen is actually moving.
For JPEG and H.264 on the CPU the screen is divided into horizontal stripes, one per CPU core (each at least 64 rows). Each stripe has its own encoder instance and its own damage history, and stripes are encoded in parallel across a thread pool. Only stripes that changed are encoded and sent. This is the "hybrid VNC and video codec" idea that lets a CPU only server idle at nearly zero cost and still deliver 60fps where the screen is actually moving.

Hardware encoders always operate full frame, delivered as a single full height stripe, since the GPU encodes the whole surface in one shot anyway.
Hardware encoders always operate full frame, delivered as a single full height stripe, since the GPU encodes the whole surface in one shot anyway. H.265, VP8, VP9, and AV1 are full frame on the CPU too, so a software session on those codecs gets neither the per core parallelism nor the dirty stripe savings, and costs several times the CPU of striped x264.

### Damage detection

Expand All @@ -38,7 +43,7 @@ Hardware encoders always operate full frame, delivered as a single full height s

### Paint over

The signature quality feature. After a region has been static for a configurable number of frames (`paint_over_trigger_frames`), it is re sent at high quality: a higher quality JPEG, or for H.264 a burst of frames at a lower CRF. Motion cancels an in flight burst. The result is video efficiency during motion and pixel perfect text the moment you stop scrolling.
The signature quality feature. After a region has been static for a configurable number of frames (`paint_over_trigger_frames`), it is re sent at high quality: a higher quality JPEG, or for the video codecs a burst of frames at a lower CRF. Motion cancels an in flight burst. The result is video efficiency during motion and pixel perfect text the moment you stop scrolling.

### Rate control

Expand All @@ -47,7 +52,9 @@ The signature quality feature. After a region has been static for a configurable

## Zero copy on Wayland

When the compositor renders on a GPU and the encoder is on the same GPU, frames flow as DMA-BUF handles from the render buffer straight into NVENC or VA-API. The pixels never touch system RAM and the CPU never sees them. If the render and encode devices differ, or a software encoder is selected, pixelflux falls back to a readback path automatically and logs which decision it made.
When the compositor renders on a GPU and the encoder is on the same GPU, frames flow as DMA-BUF handles from the render buffer straight into NVENC or VA-API. The pixels never touch system RAM and the CPU never sees them. If the render and encode devices differ, or a software encoder is selected, pixelflux falls back to a readback path automatically and logs which decision it made. Zero copy is a property of the capture path, not the codec: any codec the GPU carries takes it.

On X11 there is one zero copy path as well. When the session encodes on NVENC, capture goes through NvFBC, the Nvidia X driver composites the screen into a buffer in video memory, and that buffer is registered with the encoder in place. Every other X11 session copies each frame once into shared memory. Nothing selects this, the driver decides and the log says which path was taken.

GPU selection is automatic: it walks `/sys/class/drm`, identifies cards by driver (`nvidia` goes to NVENC, `i915` and `amdgpu` to VA-API), and can be pinned by device path, index, or an `auto_gpu` token matching a driver or vendor ID.

Expand All @@ -60,28 +67,28 @@ settings = CaptureSettings()
settings.capture_width = 1920
settings.capture_height = 1080
settings.target_fps = 60.0
settings.output_mode = 1 # 0 = JPEG, 1 = H.264
settings.codec = "h264" # "jpeg", "h264", "h265", "vp8", "vp9", or "av1"
settings.video_crf = 25
settings.use_paint_over_quality = True

def on_frame(frame):
# frame is a StripeFrame: zero copy buffer protocol object
# frame.data_type: 1 = JPEG, 2 = H.264
# frame.data_type: 0 = JPEG, 1 = H.264, 2 = VP8, 3 = VP9, 4 = AV1, 5 = H.265
# bytes(frame) or memoryview(frame) for the payload
ws.send(bytes(frame))

capture = ScreenCapture()
capture.start_capture(on_frame, settings)
```

Each encoded stripe arrives with a compact binary header (6 bytes for JPEG, 10 for H.264 carrying frame type, frame number, stripe offset, and dimensions) that the web client parses to place stripes on the canvas. Headers can be omitted for embedding in your own protocol.
Each encoded stripe arrives with a compact binary header (6 bytes for JPEG, 10 for the video codecs carrying frame type and codec id, frame number, stripe offset, and dimensions) that the web client parses to place stripes on the canvas. Headers can be omitted for embedding in your own protocol.

The Wayland backend additionally exposes input injection (keyboard by scancode with a hot swappable XKB keymap, absolute and relative pointer, buttons, scroll), clipboard get and set, cursor callbacks delivering PNG cursor images out of band, and live rate updates. `example/screen_to_browser.py` in the repository is a complete working WebSocket streaming server in one file, with a matching browser client in `example/index.html`.

## Extras worth knowing about

- **Computer Use API**: setting the `PIXELFLUX_CU=<port>` environment variable starts a small HTTP server with a `POST /computer-use` endpoint accepting JSON actions (`screenshot`, `left_click`, `type`, `key`, `scroll`, `zoom`, and friends). This is the raw input and vision layer that [Pelorus](pelorus.md) builds on.
- **Recording sink**: point `recording_socket` (or `PIXELFLUX_RECORDING_SOCKET`) at a Unix socket path and pixelflux serves the raw Annex-B H.264 bitstream to any connected client, forcing a keyframe when someone connects. Requires a full frame H.264 mode.
- **Recording sink**: point `recording_socket` (or `PIXELFLUX_RECORDING_SOCKET`) at a Unix socket path and pixelflux serves the raw elementary stream to any connected client, forcing a keyframe when someone connects: Annex B for H.264 and H.265, an OBU stream for AV1, IVF for VP8 and VP9. Requires a full frame codec, striped H.264 and JPEG are not served.
- **Watermarking**: composite a PNG over the stream at any corner, centered, or animated (it bounces). On the GPU path the watermark is composited before encode with no readback penalty.
- **Fractional scaling**, HiDPI support, and cursor theme control on the Wayland backend.

Expand All @@ -91,4 +98,4 @@ The library itself is configured purely through `CaptureSettings`. The familiar

## Building from source

`pip install .` drives a `setuptools-rust` build. On Debian or Ubuntu the build dependencies are roughly: `python3-dev cmake nasm libclang-dev libavcodec-dev libavutil-dev libx264-dev libturbojpeg0-dev libgbm-dev libdrm-dev libwayland-dev libinput-dev libxkbcommon-dev libva-dev` plus a Rust toolchain. Any system FFmpeg from 6.0 through 8.1 works (only used for VA-API). Most users should just take the prebuilt wheels.
`pip install .` drives a `setuptools-rust` build. On Debian or Ubuntu the build dependencies are roughly: `python3-dev cmake nasm libclang-dev libavcodec-dev libavutil-dev libx264-dev libturbojpeg0-dev libgbm-dev libdrm-dev libwayland-dev libinput-dev libxkbcommon-dev libva-dev` plus a Rust toolchain. Any system FFmpeg from 6.0 through 9.0 works, it supplies the VA-API encoders and the software H.265, VP8, VP9, and AV1 encoders, so a codec whose encoder that FFmpeg lacks has no software path. Most users should just take the prebuilt wheels.
Loading