diff --git a/docs/selkies/components/baseimages.md b/docs/selkies/components/baseimages.md index 6b94b7b52..e24c6d651 100644 --- a/docs/selkies/components/baseimages.md +++ b/docs/selkies/components/baseimages.md @@ -24,13 +24,14 @@ There is deliberately **no `latest` tag** for base images. Downstream images pin ## What is inside - **The LSIO foundation**: each tag builds on the corresponding LinuxServer.io distro baseimage, inheriting s6-overlay init, the `abc` user with PUID and PGID remapping, `TZ`, Docker mods, and the `/custom-cont-init.d` and `/custom-services.d` hooks. -- **Selkies** (pinned commit) installed into the `/lsiopy` virtualenv, with pixelflux 2.x and pcmflux 2.x, plus [Pelorus](pelorus.md) preinstalled. +- **Selkies** (pinned release) installed into the `/lsiopy` virtualenv, with pixelflux 2.x and pcmflux 2.x, plus [Pelorus](pelorus.md) preinstalled. - **The web client**: prebuilt dashboards under `/usr/share/selkies/`, selected at runtime by the `DASHBOARD` variable. -- **Display servers for both stacks**: labwc (built from source with a small IPC patch that adds the window query socket Pelorus uses) for Wayland, and an XLibre Xvfb built with glamor and DRI3 plus Openbox for X11. The Xvfb carries a LinuxServer patch that keeps the screen pixmap on the GPU and lets the framebuffer follow RandR resizes, which is what makes zero copy capture possible on X11. Both are prebuilt per distro in [selkies-layers](https://github.com/linuxserver/selkies-layers) and copied into the image. A patched wlroots build makes the compositor survive pixman rendering faults instead of crashing. +- **Display servers for both stacks**: labwc (built from source with two small patches: the IPC socket Pelorus queries windows over, and a control socket Selkies asks for a second screen over) for Wayland, and an XLibre Xvfb built with glamor and DRI3 plus Openbox for X11. The Xvfb carries four LinuxServer patches: the screen pixmap lives on the GPU and the framebuffer follows RandR resizes, which is what makes zero copy capture possible on X11; it starts with spare outputs a second display is plugged into as a real monitor; DRI3 presents synchronize explicitly through timeline syncobjs where the driver supports them; and its fake vblank runs at the rate the session is captured at. Both are prebuilt per distro in [selkies-layers](https://github.com/linuxserver/selkies-layers) and copied into the image, along with wlroots 0.19.3 on distros that do not package it. - **[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. +- **Nginx** serving the client, proxying everything under `/api` (the WebSocket, WebRTC signaling, files, printing, and the operator API) to Selkies, handling basic auth, the subfolder prefix, and the `/pelorus/` proxy. - **PulseAudio** with null sinks (`output` and `input`) wired for stream audio and microphone return. - **Gamepad plumbing**: the input interposer and fake udev libraries, preloaded globally, with device nodes created at init. +- **Printing**: `cups-daemon` and `cups-filters`, which Selkies runs as the session user so a printed document reaches the browser. - **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 diff --git a/docs/selkies/components/pelorus.md b/docs/selkies/components/pelorus.md index e52c14159..162c70dd1 100644 --- a/docs/selkies/components/pelorus.md +++ b/docs/selkies/components/pelorus.md @@ -19,7 +19,7 @@ docker run --rm -it \ `PELORUS=true` in Wayland mode does all of this automatically: - Starts the Pelorus FastAPI server (port 5100 internally, proxied by the container's Nginx at `/pelorus/`) -- Enables the pixelflux **Computer Use API** on port 5000 internally (`PIXELFLUX_CU=5000`), the raw input injection and screenshot layer +- Enables the pixelflux **Computer Use API** on port 5000 internally (`PIXELFLUX_CU=5000`), the raw input injection and screenshot layer. That server binds loopback and expects a bearer token (`PIXELFLUX_CU_TOKEN`) on every request - Starts the AT-SPI accessibility registry and forces the accessibility bridge on for GTK and Qt applications so their UI trees are readable - On labwc, starts the compositor with its IPC socket (`labwc -i`) so windows can be enumerated; on KDE, window data comes from KWin over D-Bus @@ -65,6 +65,7 @@ Provider configuration lives at `/config/agent/config.toml` (auto created, seede | `PELORUS_MODEL` | `gemma4:12b` | Model name | | `PELORUS_API_KEY` | empty | API key where the provider needs one | | `PIXELFLUX_CU` | `5000` when `PELORUS=true` | Computer Use API port, set automatically | +| `PIXELFLUX_CU_TOKEN` | generated when `PELORUS=true` | Bearer token the Computer Use server requires on every request. Set your own to pin it | ## Security diff --git a/docs/selkies/components/pixelflux.md b/docs/selkies/components/pixelflux.md index 7ffe410a6..6195c4182 100644 --- a/docs/selkies/components/pixelflux.md +++ b/docs/selkies/components/pixelflux.md @@ -8,11 +8,11 @@ Pcmflux is its audio sibling: it captures PulseAudio output and encodes Opus fra ## What it actually is -As of the 2.0.0 release, pixelflux is a **Rust** library exposed to Python through PyO3 (earlier versions were C++ with ctypes). One extension module contains: +pixelflux is a **Rust** library exposed to Python through PyO3. One extension module contains: - An **X11 backend**: DRI3 capture that blits the GPU resident screen into DMA-BUFs the encoder imports in place, and XShm capture with XFixes cursor tracking where that does not apply. - 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 Wayland 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. -- Six codecs across software and hardware 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. Beyond NVENC and VA-API the hardware side also drives a Jetson's Tegra encoder through the vendor's V4L2 libraries and stateful V4L2 memory to memory encoders on other boards. ## Encoders @@ -27,7 +27,7 @@ Every codec is selected by name (`codec = "h264"` and so on) and pixelflux resol | 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. +Hardware encoders are loaded at runtime: NVENC through the driver's `libnvidia-encode` and `libcuda` with no CUDA toolkit, VA-API through the host's `libva` with the session writing the codec headers itself. The software encoders are linked directly, libvpx, x265, and SVT-AV1 built into the wheel, so nothing links or loads FFmpeg. 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. Every encoder also reports which formats it codes (`420-8`, `444-8`, `420-10`, `444-10`), which is how Selkies knows where FullColor and 10-bit reach the stream. ### Striped encoding @@ -47,8 +47,9 @@ The signature quality feature. After a region has been static for a configurable ### Rate control -- **CRF/CQP mode** (default): constant quality, bits go wherever needed. -- Infinite GOP by default: keyframes are only sent on demand (client join, recovery, or an optional periodic interval). Bitrate, framerate, and quality are all adjustable live without restarting the capture. +- **CRF/CQP mode**, the containers' default: constant quality on one quality index mapped onto each codec's quantizer, bits go wherever needed. +- **CBR mode**, upstream's default: a bitrate target with a one and a half frame VBV, which Selkies' congestion control steers down to what the link carries. A still screen is still refined to the paint over quality, through the rate control rather than a burst. +- Infinite GOP by default: keyframes are only sent on demand (client join, recovery, or an optional periodic interval), and a frame a client lost is predicted past rather than answered with a keyframe on every encoder that lets the session name its references. Bitrate, framerate, and quality are all adjustable live without restarting the capture. ## Zero copy @@ -81,14 +82,16 @@ capture = ScreenCapture() capture.start_capture(on_frame, settings) ``` -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. +Each encoded stripe arrives with a compact binary header (6 bytes for JPEG, 12 for the video codecs carrying frame kind and codec id, frame number, stripe offset, dimensions, and the id of the frame it predicts from) 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=` 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. +- **Computer Use API**: setting the `PIXELFLUX_CU=` 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). A bare port binds loopback only, and every request has to carry the bearer token `PIXELFLUX_CU_TOKEN` names. 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 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. +- **MP4 recorder**: a crash safe fragmented MP4 recorder of its own, H.264 with the session's audio muxed in from pcmflux's Ogg Opus socket, which the Selkies operator API starts and stops. +- **Virtual camera**: decodes a browser's webcam uplink into the V4L2 device the [web client](../user-guide/web-client.md#webcam) feature rides on. - **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. @@ -98,4 +101,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 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. +`pip install .` drives a `setuptools-rust` build. On Debian or Ubuntu the build dependencies are roughly: `python3-dev cmake nasm libclang-dev libx264-dev libx265-dev libvpx-dev libsvtav1enc-dev libdav1d-dev libde265-dev libgbm-dev libdrm-dev libwayland-dev libinput-dev libudev-dev libxkbcommon-dev libpixman-1-dev` plus a Rust toolchain of 1.89 or newer. No FFmpeg and no libva development package is needed: the codec libraries are linked directly and libva is opened at runtime. libjpeg-turbo is vendored. Most users should just take the prebuilt wheels. diff --git a/docs/selkies/components/sealskin-apps.md b/docs/selkies/components/sealskin-apps.md index 08b78205d..7110b9bf0 100644 --- a/docs/selkies/components/sealskin-apps.md +++ b/docs/selkies/components/sealskin-apps.md @@ -72,6 +72,8 @@ Scripts can also do first run setup, seeding emulator configs, installing deskto Stores are cached server side with ETag revalidation and refreshed hourly, so updates propagate automatically. Custom store apps can point at any image, including your own images built with the [Developer Guide](../developer-guide/building-images.md), which makes the registry the natural distribution channel for in house apps on a SealSkin deployment. +A second kind of catalog sits beside the app stores: **PRoot Apps catalogs**, folders of portable applications built with `proot-apps localrepo`, which SealSkin fetches on every node, assigns to users and groups, and mounts into sessions as `PA_REPO_FOLDER`, so the sidebar's apps section installs from your catalog rather than the public one. See [Installing Applications](../user-guide/installing-apps.md). + ## Related lists The website app showcase at [sealskin.app](https://sealskin.app) maintains a compact JavaScript list of the fleet (`apps.js`) for display purposes; the YAML manifest in this repository is the operational source of truth that servers actually consume. diff --git a/docs/selkies/components/sealskin.md b/docs/selkies/components/sealskin.md index d48e26f43..83f3aa3f3 100644 --- a/docs/selkies/components/sealskin.md +++ b/docs/selkies/components/sealskin.md @@ -1,74 +1,92 @@ # SealSkin -**Repository:** [selkies-project/sealskin](https://github.com/selkies-project/sealskin) · **Server image:** [linuxserver/docker-sealskin](https://github.com/linuxserver/docker-sealskin) · **Site:** [sealskin.app](https://sealskin.app) +**Repository:** [selkies-project/sealskin](https://github.com/selkies-project/sealskin) · **Server image:** [linuxserver/docker-sealskin](https://github.com/linuxserver/docker-sealskin) · **Site:** [sealskin.app](https://sealskin.app) · **Docs:** [sealskin.selkies.io](https://sealskin.selkies.io) -SealSkin is the orchestration layer of the platform: a self hosted client server system that manages users and launches Selkies containers on demand. It is two things at once: +SealSkin is the orchestration layer of the platform: a self hosted server that manages users and launches Selkies containers on demand, with a web app, a browser extension, and mobile apps as clients. It is two things at once: -1. **A turnkey VDI platform.** Install the server, install the browser extension or mobile app, and you have multi user, on demand, GPU aware application streaming with authentication, file management, and collaboration built in. -2. **A reference implementation.** If you want to build your own VDI product, kiosk fleet, or internal tool on the Selkies images, SealSkin's source shows exactly how to launch, configure, proxy, and reap these containers correctly. Every trick it uses is documented below. +1. **A turnkey VDI platform.** Install the server, open it in a browser or install the extension or mobile app, and you have multi user, on demand, GPU aware application streaming with sign in, file management, collaboration, and clustering built in. +2. **A reference implementation.** If you want to build your own VDI product, kiosk fleet, or internal tool on the Selkies images, SealSkin's source shows exactly how to launch, configure, proxy, secure, and reap these containers correctly. Every trick it uses is documented below, and its `server/app/collaboration.py` is the reference driver of Selkies' secure mode. -The philosophy: **the browser is your operating system.** The extension intercepts what you do, clicking a link, downloading a file, right clicking an image, and redirects the action into an isolated container on your server. Suspicious files never touch your device, heavyweight apps run on server hardware, and sessions are ephemeral by default. +The philosophy: **the browser is your operating system.** Click a link, download a file, or open an app and the action lands in an isolated container on your server. Suspicious files never touch your device, heavyweight apps run on server hardware, and sessions are ephemeral by default. ## Architecture ```mermaid graph LR - E[Browser extension / mobile app] -->|E2EE REST API| A[API server, FastAPI] - E -->|HTTPS sessions| C[Caddy session proxy] - A -->|docker socket| D[Docker Engine] - D --> S1[Selkies app container] - D --> S2[Selkies app container] - C -->|per session auth| S1 - C -->|per session auth| S2 + W[Web app, extension, mobile app] -->|HTTPS :8443| C[Caddy data plane] + C -->|/api, /ui| A[API server, FastAPI :8000] + C -->|per session auth| S1[Selkies app container] + C -->|per session auth| S2[Selkies app container] + A -->|docker socket or Kubernetes API| D[Provider] + D --> S1 + D --> S2 + A -->|:8444 signed peer calls| N[Other nodes of a cluster] ``` -- **Control plane:** a Python FastAPI server (default port 8000). Handles the handshake, auth, app and user management, and session orchestration. All API request and response bodies are end to end encrypted with an AES-GCM session key exchanged via RSA, on top of whatever transport is in play. -- **Data plane:** a bundled Caddy instance (default port 8443, TLS with your certificate) proxies live session traffic. Containers are never exposed directly; Caddy asks the API server to resolve each session path to the right container IP and injects the container's per session basic auth credentials upstream. -- **Providers:** container launching is behind a provider abstraction, Docker via the socket is the implemented provider. This is the seam you would extend for Kubernetes or remote hosts. +- **Control plane:** a Python FastAPI server on port 8000. Handles sign in, the key based handshake, app and user management, session orchestration, files, shares, and the collaboration WebSocket. +- **Data plane:** a bundled Caddy instance on port 8443 with your certificate. It serves the web app, forwards the API, and proxies every live session path to the right container after asking the API server whether the request may pass. Containers are never published on a host port; only Caddy reaches them. +- **Providers:** container launching is behind a provider abstraction with two implementations, Docker through the socket and Kubernetes, where every session is a pod in the server's own namespace. +- **Nodes:** one server is a cluster of one. Several servers sharing an object store become nodes of the same cluster, calling each other over a signed peer listener on port 8444. -## Authentication: no passwords, anywhere +## Clients and sign in -SealSkin uses RSA key pairs instead of passwords. Each user has a public key on the server; the client signs a short lived JWT (5 minute expiry) with its private key for every API interaction. The server verifies against the stored public key. If the server is compromised there are no password hashes to steal. On first start the server generates an `admin` user and writes a ready to import `admin.json` client configuration containing the key material. +SealSkin takes three kinds of client, and no passwords anywhere: -Users and groups are plain files on disk with per user settings: persistent storage on or off, GPU access, public sharing, hardening flags. Group settings override user settings. Admins get a full management UI inside the extension's options page. +| Client | Who the user is | Reaches | +| --- | --- | --- | +| **Web app** at `https://:8443/` | A cookie from the root key, an OpenID Connect or SAML identity provider, or a reverse proxy that signs users in through forward auth (Authelia, Authentik, Keycloak, oauth2-proxy, Pocket ID, Tinyauth) | Every node of a cluster | +| **Extension and mobile app** with an application key | A per device key the user made in the web app, shown as text or a QR code, which an administrator can expire, scope, or revoke | Every node of a cluster | +| **Extension and mobile app** with a key file | An RSA key pair, the client signing a short lived JWT for every request over an end to end encrypted API | The one server the file names | + +The first start writes two files for the built in `admin` account: `/config/admin.json`, the key file of the companion apps, and `/config/root_key`, the root key that signs the web app in. Groups from the identity provider make administrators and set limits, and a new user in no group is held until an administrator approves them. Key file sign in stays on under `SEALSKIN_LEGACY_AUTH` for existing deployments and can be switched off once the provider is in place. + +The web app installs as a PWA, and every application has a pinned address, `/app//`, that attaches to its running session or launches one, so a bookmark or a home screen icon lands straight in a desktop. ## How it launches Selkies containers This is the part to study if you are building your own platform. For each session SealSkin: 1. Generates a session UUID and random `CUSTOM_USER` and `PASSWORD` credentials (a fresh pair per session). -2. Injects environment variables: `SUBFOLDER=//` (so the container serves under its session path), `PUID`, `PGID`, `PIXELFLUX_WAYLAND`, `LC_ALL` for the user's language, and `DRINODE` plus `DRI_NODE` when a GPU is assigned. -3. Applies the app template: an admin defined bundle of the standard Selkies environment variables (UI lockdown, hardening, encoder settings, watermarks), plus `DOCKER_*` pseudo variables that translate into Docker options (privileged, devices, memory limits, network mode, and so on). -4. Mounts the home directory: a persistent per user home under `/storage//`, or an ephemeral directory that is deleted when the session ends. A shared files mount lands at `/config/Desktop/files`. +2. Injects environment variables: `SUBFOLDER=//` so the container serves under its session path, `PUID`, `PGID`, `PIXELFLUX_WAYLAND`, `LC_ALL` for the user's language, and `DRINODE` plus `DRI_NODE` when a GPU is assigned. +3. Applies the app template: an admin defined bundle of the standard Selkies environment variables (UI lockdown, hardening, encoders, start policies, watermarks, the connection indicator, 10-bit video, the print spool, the Computer Use token, and so on), plus `DOCKER_*` pseudo variables that translate into Docker options (privileged, devices, memory limits, network mode). The variables the editor offers are declared in the server's `template_schema.yml`, which mirrors what the baseimage and Selkies read, with the defaults the container effectively runs with. +4. Mounts the home directory: a persistent per user home under `/storage//`, or an ephemeral directory that is deleted when the session ends. A shared files mount lands at `/config/Desktop/files`, and any PRoot Apps catalogs assigned to the user are mounted read only and named through `PA_REPO_FOLDER`. 5. Writes the app's **autostart script** into the mounted home before boot (`.config/labwc/autostart` for Wayland), which is how "open this URL" and "open this file" work: the script consumes `SEALSKIN_URL` or `SEALSKIN_FILE` variables. Scripts come from the [apps registry](sealskin-apps.md). -6. Starts the container with no published ports on the server's Docker network, waits for the web interface to answer, then hands the client a session URL through the Caddy proxy. +6. Starts the container with no published ports on the server's Docker network (or as a pod), waits for the web interface to answer, then hands the client a session URL through the Caddy proxy. 7. On session end, stops the container (containers run with auto remove) and deletes ephemeral storage. -GPU allocation is automatic: the server detects render nodes and drivers at startup, users with GPU permission pick a device at launch, Nvidia cards get the runtime and device requests wired up, Intel and AMD get the DRI device mapped. +For a web sign in, SealSkin serves the Selkies web client itself, taken out of each image after it is pulled, so sessions live at `//` on the web app's own origin: one certificate, permissions granted once, no wildcard DNS, and no code from the image running next to the sign in cookie. The container then answers only the session's API. `SEALSKIN_SESSION_ISOLATION` keeps every session on a name of its own instead, for images you do not trust. + +GPU allocation is automatic: the server detects render nodes and drivers at startup, users with GPU permission pick a device at launch, Nvidia cards get the runtime and device requests wired up (the NVIDIA Container Toolkit 1.20.1 or newer), Intel and AMD get the DRI device mapped. An app's `nvidia_support` and `dri3_support` flags in the registry gate what the launcher offers. ## Features beyond launching -- **Collaboration rooms:** launch a session in room mode and invite participants with links, full control, read only, or gamepad player slots, with A/V chat signalling and up to four physical gamepads passed through. Built on the Selkies token control plane (port 8083 inside the container). -- **File manager:** browse, upload, and download files in your server side homes from the extension, with chunked transfers for large files. -- **Public shares:** password protectable, expiring public download links for files in your storage. +- **Collaboration rooms:** launch a session in room mode and invite participants with links, full control, read only, or gamepad player slots, with A/V chat signalling and up to four physical gamepads passed through, one slot per controller. Built on Selkies' secure mode: SealSkin provisions the token table through `/api/tokens` and opens each participant's page with its token in the URL fragment, where no server log or Referer sees it. +- **Clusters:** servers share users, groups, applications, templates, and settings through an object store, an S3 bucket or the built in file store, with pools of nodes, placement of sessions, GPU slots, home directories pinned to a node and moved between them, shared files encrypted under `SEALSKIN_STORE_KEY`, limits that hold across nodes, and an append only audit log. `SEALSKIN_NODE_ROLES` makes a node a frontend, a runtime, or both. +- **Kubernetes:** one manifest installs the server in any namespace you administer, with no cluster scoped objects; every session is a pod beside it, with GPUs, pod templates, resource quotas, and pull secrets honored. +- **VPN exits:** administrators import WireGuard and OpenVPN profiles; a user picks one at launch and the session leaves through that tunnel and nothing else, with a gateway per endpoint on Docker and a hub per endpoint on Kubernetes. The application image knows nothing about it. +- **Registries, mirrors, and proxies:** private registries through a Docker `config.json` on each node (`SEALSKIN_REGISTRY_AUTH_PATH`), internal hosts through a netrc file (`SEALSKIN_NETRC_PATH`), `SEALSKIN_IMAGE_MIRRORS` for every pull, and `HTTPS_PROXY` with `SSL_CERT_FILE` for the server's own fetches. A deployment without internet access runs on these alone. +- **File manager:** browse, upload, and download files in your server side homes, with chunked transfers for large files, and password protectable, expiring public download links. - **App Lab (meta apps):** launch a base app in customize mode, install software and tweak settings interactively, then commit the home directory as a golden template. New "meta apps" launch from copies of that template, no Docker knowledge required. -- **App templates:** reusable environment variable bundles for hardening and UI lockdown, applied per app from the admin UI. +- **App templates:** reusable environment variable bundles for hardening and UI lockdown, applied per app from the admin UI. A user with the right permission edits templates short of the `DOCKER_*` settings and the identity variables, which stay an administrator's. +- **PRoot Apps catalogs:** catalogs of portable applications assigned to users and groups, fetched on every node and mounted into sessions. +- **Branding:** a name, logo, accent, wallpaper, home links, and the store links of the companion apps, set from the dashboard with a live preview. - **Download interception:** the extension can intercept your next browser download and reroute the file into an isolated container instead of your disk. - **Internationalization:** the client is localized in 18 languages, and sessions launch with the user's locale via `LC_ALL`. ## Clients -- **Browser extension** ([Chrome](https://chromewebstore.google.com/detail/sealskin-isolation/lclgfmnljgacfdpmmmjmfpdelndbbfhk), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/sealskin-isolation/)): the primary client. Context menu entries (open link isolated, open file isolated, send media, search selection), download interception, the launcher popup, the admin UI, and the file manager. -- **Mobile apps** ([iOS](https://apps.apple.com/us/app/sealskin/id6758210210), [Android](https://play.google.com/store/apps/details?id=io.linuxserver.sealskin)): Capacitor apps wrapping the same client code. Sessions open in the system browser for full performance; context menu and download interception are extension only. +- **Web app:** served by the server at `/`, the launcher, the file manager, and the admin pages, with nothing to install. +- **Browser extension** ([Chrome](https://chromewebstore.google.com/detail/sealskin-isolation/lclgfmnljgacfdpmmmjmfpdelndbbfhk), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/sealskin-isolation/)): a thin shell around the served UI that adds the privileged pieces: context menu entries (open link isolated, open file isolated, send media, search selection), download interception, and the E2EE session for key files. +- **Mobile apps** ([iOS](https://apps.apple.com/us/app/sealskin/id6758210210), [Android](https://play.google.com/store/apps/details?id=io.linuxserver.sealskin)): Capacitor shells wrapping the same client code, with native file open and a QR scanner for application keys. Sessions open in the system browser for full performance. - Best experience is on desktop Chromium family browsers. Firefox works with some performance trade offs. ## Deployment notes -- The supported deployment is the LinuxServer.io server image from [linuxserver/docker-sealskin](https://github.com/linuxserver/docker-sealskin). The server needs the Docker socket, a `/config` volume, a `/storage` volume for user homes, and a TLS certificate and key for the session proxy. -- Configuration is via `SEALSKIN_*` environment variables (`SEALSKIN_API_PORT`, `SEALSKIN_SESSION_PORT`, `SEALSKIN_APP_RESOURCE_PATH` for the app store URL, `SEALSKIN_PUID` and `SEALSKIN_PGID` for launched containers, storage paths, auto update interval, and more). -- SealSkin expects to own its ports; it does not currently sit behind a traditional reverse proxy, the bundled Caddy is the proxy. +- The supported deployment is the LinuxServer.io server image from [linuxserver/docker-sealskin](https://github.com/linuxserver/docker-sealskin), whose installer script also obtains a certificate. The server needs the Docker socket (or a Kubernetes service account), a `/config` volume, a `/storage` volume for user homes, a TLS certificate and key for the session proxy, and a stable container name, which is also its identity among a cluster's records (`SEALSKIN_NODE_NAME` otherwise). +- Configuration is via `SEALSKIN_*` environment variables, listed in the upstream settings reference: ports, the app store URL, `SEALSKIN_PUID` and `SEALSKIN_PGID` for launched containers, storage paths, the identity provider, the object store, and more. +- It runs behind a reverse proxy as well as directly on the internet: set `SEALSKIN_PUBLIC_URL` and `SEALSKIN_TRUSTED_PROXIES`, and `SEALSKIN_HTTP_PORT` for a proxy that terminates TLS. The upstream docs carry a page per proxy, including SWAG. - App images update automatically in the background when enabled, and the app store cache refreshes hourly by default. ## Privacy -The clients talk only to the server you configure. No analytics, no tracking, no third parties; the developers have no access to your server or data. Full policy in [PRIVACY.md](https://github.com/selkies-project/sealskin/blob/master/PRIVACY.md). +The clients talk only to the server you configure. No analytics, no tracking, no third parties; every picture a page shows is served by your own server. Full policy in [PRIVACY.md](https://github.com/selkies-project/sealskin/blob/main/PRIVACY.md). diff --git a/docs/selkies/components/selkies.md b/docs/selkies/components/selkies.md index f152e9b8a..f95ce1158 100644 --- a/docs/selkies/components/selkies.md +++ b/docs/selkies/components/selkies.md @@ -9,13 +9,15 @@ Selkies is the heart of the platform: a ground up, web native remote desktop pro `selkies` is a Python asyncio application (console script `selkies`, installed in the baseimages at `/lsiopy/bin/selkies`) that owns the session: - **Video**: drives [pixelflux](pixelflux.md) capture and encoding, and broadcasts encoded frames to all connected clients over the WebSocket, with per client backpressure (frame acknowledgements, RTT smoothing, and stall detection) so one slow viewer does not degrade the rest. An opt in [WebRTC transport](../user-guide/webrtc.md) carries the same video and Opus streams over UDP with a priority pacer and optional congestion control; clients can switch transports at runtime. -- **Audio out**: drives pcmflux, which captures the PulseAudio `output.monitor` source and Opus encodes at up to 320kbps. +- **Audio out**: drives pcmflux, which captures the PulseAudio `output.monitor` source and Opus encodes at up to 510kbps, with Opus RED redundancy against packet loss. - **Microphone in**: receives PCM from the browser and plays it into a virtual PulseAudio source (`SelkiesVirtualMic`) that session apps consume as a normal mic. -- **Input**: injects keyboard, mouse, touch, and scroll. On Wayland, injection goes through pixelflux's compositor APIs with an xkbcommon keymap (plus [waylandtyper](https://github.com/linuxserver/waylandtyper), our maintained fork of `wtype`, for unicode text batches). On X11, through pynput, xdotool, and python-xlib. Gamepads are handled by per slot Unix socket servers feeding the input interposer (below). -- **Clipboard**: bidirectional sync via `wl-clipboard` on Wayland or `xclip` on X11, with optional binary (image) clipboard support and chunked transfer for large payloads. -- **Files**: receives chunked uploads over the socket into the session; downloads are served by the container's Nginx file index. +- **Input**: injects keyboard, mouse, touch, and scroll. On Wayland, injection goes through pixelflux's compositor seat with an xkbcommon keymap, falling back to an in process virtual keyboard client for text the layout lacks, with no forked tools. On X11, through in process XTEST over a vendored python-xlib, with a spare keycode overlay for unicode and IME text. Gamepads are handled by per slot Unix socket servers feeding the input interposer (below), or by kernel uinput devices where `/dev/uinput` is writable. +- **Clipboard**: bidirectional, event driven sync, XFixes selection events on X11 and the compositor's data control on Wayland, with formatted text, optional binary (image) clipboard support, and chunked transfer for large payloads. +- **Files**: receives chunked uploads over the socket into the session and serves the file browser and downloads itself under `/api/files/`, paced against the video stream. +- **Printing**: runs a CUPS queue as the session user and hands each printed document to the browser as a PDF. +- **Operator API**: lists and closes connected pages, starts and stops a fragmented MP4 recording with audio, and takes screenshots, under `/api`, with an audit webhook that reports every transfer, print, connection, and recording. - **Settings and stats**: pushes the sanitized settings schema to the client (this is what builds the sidebar UI, including which controls are locked), and streams CPU, GPU, memory, and network stats. -- **Sharing and roles**: manages primary, collaborator, view only, and player 2 to 4 roles, either via URL fragments (`#shared`, `#collab`, `#player2`) or, in secure deployments, via a token control plane on an internal port (`POST /tokens` authorized by `SELKIES_MASTER_TOKEN`), which is what [SealSkin](sealskin.md) uses for its collaboration rooms. +- **Sharing and roles**: manages controller, view only, and player 2 to 4 roles, either via URL fragments (`#shared`, `#player2`, with a second page on the plain address joining as a controller) or, in secure deployments, via a token table posted to `/api/tokens` with `SELKIES_MASTER_TOKEN` or read from `SELKIES_TOKENS_FILE`, which is what [SealSkin](sealskin.md) uses for its collaboration rooms. A page holds its token in the URL fragment, where no proxy log sees it. Configuration is uniform: every setting is simultaneously a CLI flag (`--framerate`) and an environment variable (`SELKIES_FRAMERATE`), with the value syntax (ranges, enums, `|locked`) described in the [Configuration Reference](../user-guide/configuration.md). The protocol itself is documented in [The Streaming Protocol](../developer-guide/protocol.md). @@ -24,7 +26,7 @@ Configuration is uniform: every setting is simultaneously a CLI flag (`--framera The client lives in the same repository under `addons/`: - **selkies-web-core**: the engine. Connects the WebSocket, demultiplexes binary frames, decodes H.264, H.265, VP8, VP9, or AV1 with WebCodecs `VideoDecoder`, JPEG stripes with `createImageBitmap`, and Opus in a worker, renders to canvas, plays audio through an AudioWorklet, and captures all input including trackpad gestures and pointer lock. It exposes a documented `postMessage` API so any UI can be built on top of it. -- **selkies-dashboard**: the standard React sidebar UI described in [Using the Web Client](../user-guide/web-client.md), fully internationalized. Variants exist (`selkies-dashboard-wish` is a newer TypeScript and shadcn based rewrite), and the container's `DASHBOARD` variable selects between built dashboards. The postMessage split means a white label or kiosk frontend is just another dashboard. +- **selkies-dashboard**: the standard React sidebar UI described in [Using the Web Client](../user-guide/web-client.md), fully internationalized. Variants exist (`selkies-dashboard-wish` is the TypeScript rewrite with the same sections in an accordion layout), and the container's `DASHBOARD` variable selects between built dashboards. The postMessage split means a white label or kiosk frontend is just another dashboard. Browsers without WebCodecs audio get Opus decoded by a WebAssembly build of libopus, so Safari and older Firefox still carry sound, the microphone, and the webcam over WebSockets. - **universal-touch-gamepad**: an on screen touch gamepad that injects itself into the browser's Gamepad API with configurable layouts. ## The gamepad trick @@ -34,14 +36,13 @@ Two small C addons make plug and play gamepads work in unprivileged containers w - **input-interposer**: an `LD_PRELOAD` library that intercepts `open()` calls to `/dev/input/js*` and `/dev/input/event*` and transparently redirects them to Unix sockets served by Selkies, faithfully emulating the Linux joystick and evdev APIs (ioctls included). Apps think they opened a real Xbox 360 pad. It works in the other direction too: a preloaded application that opens `/dev/uinput` to create its own virtual device (Steam Input, a remapper) gets a socket backed device that sibling applications discover through fake udev, and with `SELKIES_PUBLISH_INPUT_DEVICES` the same pool carries a copy of the session keyboard and pointer for applications that enumerate evdev directly. - **fake-udev**: a stub `libudev` that makes device enumeration APIs report the synthetic pads with a plausible sysfs hierarchy, for apps and engines (SDL, browsers) that discover devices through udev rather than by opening device nodes. -Both are preloaded automatically in the baseimages, and `NO_GAMEPAD=true` turns the whole mechanism off. +Both are preloaded automatically in the baseimages, and `NO_GAMEPAD=true` turns the whole mechanism off. Where `/dev/uinput` is writable Selkies registers real kernel devices instead, which need no preload, and a game's force feedback on either path is played back on the controller in the browser. ## Ports (inside a container) | Port | What | | --- | --- | -| 8082 | The Selkies server (`SELKIES_PORT`, upstream default is 8080, the baseimages set 8082 via `CUSTOM_WS_PORT`). Nginx proxies everything under `/api` to it: the data WebSocket at `/api/websockets`, WebRTC signaling at `/api/webrtc/signaling`, the transport switch, and the file browser at `/api/files/` | -| 8083 | Token control plane for secure sharing mode, never expose it | +| 8082 | The Selkies server (`SELKIES_PORT`, upstream default is 8080, the baseimages set 8082 via `CUSTOM_WS_PORT`). Nginx proxies everything under `/api` to it: the data WebSocket at `/api/websockets`, WebRTC signaling at `/api/webrtc/signaling`, the transport switch, the file browser at `/api/files/`, printed documents, the operator API, and the token control plane at `/api/tokens`, which only the master token unlocks | | 3000 / 3001 | Nginx HTTP and HTTPS in front of everything ([baseimage](baseimages.md) territory) | ## Relationship to the rest of the stack diff --git a/docs/selkies/developer-guide/architecture.md b/docs/selkies/developer-guide/architecture.md index 827785d47..9d0a4b791 100644 --- a/docs/selkies/developer-guide/architecture.md +++ b/docs/selkies/developer-guide/architecture.md @@ -42,7 +42,7 @@ Six codecs behind one policy layer, each resolved to a GPU engine or a software | CPU full frame | H.265 (x265), VP8 and VP9 (libvpx), AV1 (SVT-AV1) | Whole frame per encode, no striping | | GPU | NVENC: H.264, H.265, AV1. VA-API: all five | Full frame, zero copy from DMA-BUF when render and encode share a device | -Quality logic is shared: infinite GOP with on demand IDR frames, CRF rate control with live retuning on one quality scale mapped onto each codec's quantizer, and the **paint over** system, after N static frames, resend at high quality (better JPEG quality, or a video burst at lower CRF), cancelled instantly by motion. Selkies probes the encoding device at startup and only offers the codecs the host can serve. +Quality logic is shared: infinite GOP with on demand IDR frames and reference invalidation so a lost frame costs no keyframe, CRF or CBR rate control with live retuning on one quality scale mapped onto each codec's quantizer, congestion control that steers a CBR stream to what each page's link carries, 4:4:4 and 10-bit where the codec and encoder carry them, and the **paint over** system: once the screen holds still it is resent at high quality (better JPEG quality, a video burst at lower CRF, or under CBR a refinement through the rate control), cancelled instantly by motion. Selkies probes the encoding device at startup and only offers the codecs the host can serve. The full encoder and settings detail lives on the [Pixelflux page](../components/pixelflux.md). @@ -58,22 +58,22 @@ PulseAudio runs with two null sinks: `output` (what apps play into) and `input` Browser events become terse text messages over the same socket (`kd,`, `m,,,,`, and friends). Injection differs by stack: -- **Wayland**: injected through pixelflux's compositor seat, keyboard by scancode against a hot swappable xkb keymap, pointer absolute or relative, with unicode text batched through [waylandtyper](https://github.com/linuxserver/waylandtyper) (our maintained fork of `wtype` that fixes many bugs in the old codebase) for IME correctness. Because the compositor is headless and synthetic, external tools like xdotool do not work here, the API is the only door. -- **X11**: pynput, xdotool, and XTEST, with a keysym map and Cyrillic to QWERTY remapping so shortcuts work across layouts. +- **Wayland**: injected through pixelflux's compositor seat, keyboard by scancode against a hot swappable xkb keymap, pointer absolute or relative, with unicode and IME text batched through an in process virtual keyboard client. The path forks no tools: the compositor is headless and synthetic, so the API is the only door. Under a nested KWin, relative motion for games goes through KWin's fake input device. +- **X11**: in process XTEST over a vendored python-xlib, with a dynamic spare keycode overlay for keysyms the layout lacks and an XKB group lock so shortcuts work across layouts; xdotool is only a last resort fallback. Gamepads bypass the display server entirely: Selkies serves the Linux joystick and evdev protocols over Unix sockets, and the `LD_PRELOAD` interposer plus fake udev library make applications believe `/dev/input/js0` is a real Xbox 360 pad. Four slots exist, mappable to remote players. ## The web layer -Nginx inside the container is the single front door: it serves the static client (a React dashboard over the `selkies-web-core` engine), proxies `/api` (the data WebSocket, WebRTC signaling, and the `/api/files/` browser) to the Selkies server, optionally enforces basic auth, applies the `SUBFOLDER` prefix, and proxies `/pelorus/` when the agent layer is on. The dashboard and the engine communicate over a documented `postMessage` API, which is the extension point for custom frontends. +Nginx inside the container is the single front door: it serves the static client (a React dashboard over the `selkies-web-core` engine), proxies `/api` (the data WebSocket, WebRTC signaling, the `/api/files/` browser, printed documents, the operator API, and the token control plane) to the Selkies server, optionally enforces basic auth, applies the `SUBFOLDER` prefix, which Selkies reads too, and proxies `/pelorus/` when the agent layer is on. The dashboard and the engine communicate over a documented `postMessage` API, which is the extension point for custom frontends. ## Sharing and multi user -One session, many sockets. Every connected client gets the same broadcast frames; roles (primary, collab, view only, player N) gate which input messages are honored. In secure mode (used by SealSkin), access requires per user tokens registered through a control plane endpoint on an internal port, and roles can be re assigned live, this is what powers collaboration rooms with granular permissions. +One session, many sockets. Every connected client gets the same broadcast frames; roles (controller, view only, player N) gate which input messages are honored, and a page on a narrower link is sent fewer frames with the encoder predicting past the ones it skipped. In secure mode (used by SealSkin), access requires per user tokens registered through `POST /api/tokens` with the master token or read from a tokens file, and roles and gamepad slots can be re assigned live, this is what powers collaboration rooms with granular permissions. ## Where the orchestration layer plugs in -Nothing in the container knows about SealSkin. The orchestration contract is entirely environment variables and volumes at launch: `SUBFOLDER` for path routing, `CUSTOM_USER` and `PASSWORD` for per session auth injected by the proxy, `SELKIES_MASTER_TOKEN` to switch on token mode, `DRINODE` and `DRI_NODE` for GPU placement, and a bind mounted home with an autostart script written before boot. Any scheduler that can do those five things can orchestrate the platform, that is the entire integration surface, and [SealSkin](../components/sealskin.md) is the reference implementation of it. +Nothing in the container knows about SealSkin. The orchestration contract is entirely environment variables and volumes at launch: `SUBFOLDER` for path routing, `CUSTOM_USER` and `PASSWORD` for per session auth injected by the proxy, `SELKIES_MASTER_TOKEN` (and optionally `SELKIES_TOKENS_FILE`) to switch on token mode, `DRINODE` and `DRI_NODE` for GPU placement, and a bind mounted home with an autostart script written before boot. Any scheduler that can do those five things can orchestrate the platform, that is the entire integration surface, and [SealSkin](../components/sealskin.md) is the reference implementation of it. ## Performance characteristics diff --git a/docs/selkies/developer-guide/baseimage-internals.md b/docs/selkies/developer-guide/baseimage-internals.md index 307b743f6..e14e42a79 100644 --- a/docs/selkies/developer-guide/baseimage-internals.md +++ b/docs/selkies/developer-guide/baseimage-internals.md @@ -6,16 +6,15 @@ What actually happens inside a Selkies container, boot order, services, and the The baseimage Dockerfile is a multi stage build assembling, onto a LinuxServer.io distro base (Debian, Ubuntu, Alpine, Fedora, Arch, or Kali): -- An **XLibre Xvfb** built with glamor and DRI3 from [selkies-layers](https://github.com/linuxserver/selkies-layers), patched so the screen pixmap lives on the GPU (`-glamor -dri /dev/dri/renderD###`) and the framebuffer follows RandR resizes -- The **web frontend**: the Selkies repository pinned to an exact commit, with `selkies-web-core` and the dashboards built and dropped in `/usr/share/selkies/` -- **labwc 0.9.7** built from source with the IPC patch (adds `labwc -i`, a read only JSON window query socket at `$XDG_RUNTIME_DIR/labwc.sock`, consumed by Pelorus) -- A rebuilt **wlroots** with a defensive patch that catches SIGSEGV and SIGBUS inside pixman draw calls and skips the frame instead of crashing the compositor -- **selkies-desktop** and **[waylandtyper](https://github.com/linuxserver/waylandtyper)** (our maintained fork of wtype, fixing many bugs in the old codebase) built from source +- An **XLibre Xvfb** (`xlibre-xserver-25.2.2`) built with glamor and DRI3 from [selkies-layers](https://github.com/linuxserver/selkies-layers) with four patches: the screen pixmap lives on the GPU (`-glamor -dri /dev/dri/renderD###`) and the framebuffer follows RandR resizes; the server starts with four CRTCs whose spare outputs stay disconnected until Selkies plugs a second display in; DRI3 presents synchronize through timeline syncobjs where the driver supports them; and the fake vblank follows the rate pixelflux publishes on the root window +- The **web frontend**: the Selkies repository pinned to the release, with `selkies-web-core` and the dashboards built and dropped in `/usr/share/selkies/` +- **labwc 0.9.7** built from source in selkies-layers with two patches: the IPC socket (`labwc -i`, a read only JSON window query socket at `$XDG_RUNTIME_DIR/labwc.sock`, consumed by Pelorus) and a control socket Selkies grows and removes a second screen over. On distros without a packaged wlroots 0.19 the layer carries wlroots 0.19.3 too +- **selkies-desktop** and **[waylandtyper](https://github.com/linuxserver/waylandtyper)** (our maintained fork of wtype) from their selkies-layers builds; the KDE Webtops additionally take a patched KWin from the same place for their second display - The **selkies** Python package installed into the `/lsiopy` virtualenv (pulling in pixelflux and pcmflux wheels), plus **pelorus** - The **input interposer** (`/usr/lib/selkies_input_interposer.so`) and **fake udev** (`/opt/lib/libudev.so.1.0.0-fake`) compiled from the Selkies addons -- Nginx with fancyindex, PulseAudio, mesa and VA-API userspace, Vulkan loaders, all system locales, proot-apps, Docker in Docker machinery, and passwordless sudo for `abc` +- Nginx, PulseAudio, CUPS (`cups-daemon` and `cups-filters` for the session's print queue), mesa and VA-API userspace, Vulkan loaders, all system locales, proot-apps, Docker in Docker machinery, and passwordless sudo for `abc` -Baked ENV defaults worth knowing: `HOME=/config`, `DISPLAY=:1`, `TITLE=Selkies`, `SELKIES_ENCODER="h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg"`, `SELKIES_ENABLE_BASIC_AUTH=false`, `SELKIES_VIDEO_STREAMING_MODE=false`, `SELKIES_ALLOWED_ORIGINS="*"`, `START_DOCKER=true`, `DISABLE_DRI3=false`, `NVIDIA_DRIVER_CAPABILITIES=all`, and the interposer path in `SELKIES_INTERPOSER`. +Baked ENV defaults worth knowing: `HOME=/config`, `DISPLAY=:1`, `TITLE=Selkies`, `SELKIES_ENCODER="h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg"`, `SELKIES_RATE_CONTROL_MODE="crf,cbr"`, `SELKIES_ENABLE_BASIC_AUTH=false`, `SELKIES_VIDEO_STREAMING_MODE=false`, `SELKIES_ALLOWED_ORIGINS="*"`, `START_DOCKER=true`, `DISABLE_DRI3=false`, `NVIDIA_DRIVER_CAPABILITIES=all`, `__GL_SYNC_TO_VBLANK=0`, and the interposer paths in `SELKIES_INTERPOSER` and `SELKIES_WEBCAM_INTERPOSER`. ## Boot: the init chain @@ -30,15 +29,15 @@ init-os-end └─ init-selkies-end → init-config → ... → init-services ``` -**`init-nginx`** renders `/defaults/default.conf` into the live Nginx config with plain string substitution: ports (`CUSTOM_PORT`, `CUSTOM_HTTPS_PORT`, `CUSTOM_WS_PORT`), `SUBFOLDER` tokens, the download path, basic auth (generates `.htpasswd` and uncomments the auth lines when `PASSWORD` is set), IPv6 removal, dev mode rewiring, dashboard selection into `/usr/share/selkies/web`, and the PWA `manifest.json` with your `TITLE`. On first run it also generates the ten year self signed certificate into `/config/ssl/`. +**`init-nginx`** renders `/defaults/default.conf` into the live Nginx config with plain string substitution: ports (`CUSTOM_PORT`, `CUSTOM_HTTPS_PORT`, `CUSTOM_WS_PORT`), `SUBFOLDER` tokens, basic auth (generates `.htpasswd` and uncomments the auth lines when `PASSWORD` is set), IPv6 removal, dev mode rewiring, dashboard selection into `/usr/share/selkies/web`, and the PWA `manifest.json` with your `TITLE`. It creates `FILE_MANAGER_PATH` and, on first run, generates the ten year self signed certificate into `/config/ssl/`. **`init-selkies-config`** is the big one: - Chooses the mode: `PIXELFLUX_WAYLAND=true` selects labwc paths (`$HOME/.config/labwc`, `/defaults/autostart_wayland`, `/defaults/menu_wayland.xml`) otherwise Openbox paths. Multi monitor on Wayland is Selkies' call, not the init script's: at startup it probes the compositor, the labwc IPC socket or a KWin capability check, and enables the second screen only where it finds support. Both compositors carry LinuxServer patches for this, the labwc ones ship in the baseimage. - First run copies of `autostart` and `menu.xml` into the config dir (persistent, user editable); `rc.xml` for labwc is regenerated from the template every boot. -- Recreates `$HOME/.XDG` as `XDG_RUNTIME_DIR` and clears stale PulseAudio state, so unclean shutdowns recover. +- Recreates `$HOME/.XDG` as `XDG_RUNTIME_DIR`, which is where the interposer sockets and the print queue's socket live, points `CUPS_SERVER` at that queue unless you set it or turned printing off, and clears stale PulseAudio state, so unclean shutdowns recover. - Applies every hardening variable (permission stripping, sudoers corruption, menu and keybind editing, locking rc.xml and autostart when watchdog mode is on), the details are in the [Security guide](../user-guide/security.md). -- GPU env: with exactly one render node and nothing set, points `DRINODE` and `DRI_NODE` at it; wires `PIXELFLUX_CU=5000` and `ROOT_PATH=/pelorus` when `PELORUS=true`. +- GPU env: with exactly one render node and nothing set, points `DRINODE` and `DRI_NODE` at it; wires `PIXELFLUX_CU=5000`, a generated `PIXELFLUX_CU_TOKEN` unless you set one, and `ROOT_PATH=/pelorus` when `PELORUS=true`. - Creates the gamepad device nodes (`/dev/input/js0-3` and event nodes) and sets the global `LD_PRELOAD` for the interposer and fake udev, unless `NO_GAMEPAD` is set. - Syncs proot-apps into the user home and handles `LC_ALL` locale derivation. - Steam shim: links `/usr/local/bin/steam` (marked `#SELKIESSHIM`) to `/usr/bin/steam` when nothing is there, so `steam` runs the installer in `/steam.sh` until a real launcher exists. `NO_STEAM=true` removes both. @@ -52,7 +51,7 @@ init-os-end | `svc-nginx` | Reaps zombie workers then runs Nginx in the foreground | | `svc-pulseaudio` | PulseAudio as `abc`, never idle exits, runtime dir under `/defaults` | | `svc-xorg` | X11 mode: picks the render node (`DRINODE`, else the Nvidia node when one exists, else the first) and starts Xvfb on `:1` with `-glamor -dri ` so the screen lives on the GPU, unless `DISABLE_DRI3=true` or no node exists. Wayland mode: sleeps | -| `svc-selkies` | Loads the `output` and `input` null sinks once PulseAudio is up, then runs `selkies --addr=localhost --mode=websockets` as `abc`. In Wayland mode this is what brings up the pixelflux compositor on `wayland-1` | +| `svc-selkies` | Loads the `output` and `input` null sinks once PulseAudio is up, then runs `selkies --addr=localhost` as `abc`, every other setting coming from the environment. In Wayland mode this is what brings up the pixelflux compositor on `wayland-1` | | `svc-de` | Waits for the display (the `wayland-1` socket, or `xset q` on X11), then executes `/defaults/startwm_wayland.sh` or `/defaults/startwm.sh` as `abc`, recording the PID for clean teardown. On X11 it also sets the initial 1024x768 mode (or `SELKIES_MANUAL_WIDTH` and `HEIGHT`) via xrandr | | `svc-xsettingsd` | X11 only, DPI hinting for legacy toolkits | | `svc-watchdog` | Only active with `RESTART_APP=true`: polls for the autostart process and relaunches it if it dies | @@ -68,20 +67,18 @@ One config, two identical server blocks (HTTP 3000, HTTPS 3001): | Location | Purpose | | --- | --- | | `SUBFOLDER` (default `/`) | The web client static files from `/usr/share/selkies/web/` | -| `SUBFOLDERwebsocket` | Proxy to the Selkies data WebSocket on 127.0.0.1:8082 | -| `SUBFOLDERfiles` | fancyindex download listing of `FILE_MANAGER_PATH` (default `/config/Desktop`), removed entirely when downloads are disabled or `HARDEN_DESKTOP` is on | +| `SUBFOLDERapi` | Proxy to the Selkies server on 127.0.0.1:8082 with the prefix kept, which Selkies also reads from `SUBFOLDER`: the data WebSocket, WebRTC signaling, the file browser and uploads, printed documents, the operator API, and the token control plane. Unlimited body size and no request buffering, so uploads stream through | | `SUBFOLDERpelorus/` | Proxy to the Pelorus API on 127.0.0.1:5100 | | `/devmode` | Proxy to a Vite dev server, see [Development Environment](development.md) | -All proxy locations use hour long timeouts, no buffering, and a 10MB body cap. Because substitution is plain `sed`, exotic characters in `PASSWORD` or `SUBFOLDER` can break the config, keep them simple. +All proxy locations use hour long timeouts and no response buffering. Because substitution is plain `sed`, exotic characters in `PASSWORD` or `SUBFOLDER` can break the config, keep them simple. ## Internal port map | Port | Owner | Exposure | | --- | --- | --- | | 3000, 3001 | Nginx | Published, everything user facing | -| 8082 | Selkies WebSocket | localhost only, via Nginx | -| 8083 | Selkies token control plane | localhost only, orchestrators call it, never expose | +| 8082 | Selkies server, everything under `/api` including the token control plane | localhost only, via Nginx | | 5100 | Pelorus API | localhost only, via Nginx at `/pelorus/` | | 5000 | pixelflux Computer Use API | localhost only | | 5173 | Vite dev server | localhost only, via `/devmode` | diff --git a/docs/selkies/developer-guide/development.md b/docs/selkies/developer-guide/development.md index db9e37af4..fbddc6683 100644 --- a/docs/selkies/developer-guide/development.md +++ b/docs/selkies/developer-guide/development.md @@ -13,7 +13,6 @@ Clone the [selkies repository](https://github.com/selkies-project/selkies), moun ```bash git clone https://github.com/selkies-project/selkies.git cd selkies -git checkout -f lsio docker run --rm -it \ --shm-size=1gb \ -e DEV_MODE=selkies-dashboard \ @@ -64,11 +63,12 @@ The container builds the mounted source (all Rust build dependencies are present | [linuxserver/sealskin-apps](https://github.com/linuxserver/sealskin-apps) | App manifests and autostart scripts | | [linuxserver/pelorus](https://github.com/linuxserver/pelorus) | Agent API and chat UI | | [selkies-project/selkies-desktop](https://github.com/selkies-project/selkies-desktop) | The minimal desktop shell | -| [linuxserver/waylandtyper](https://github.com/linuxserver/waylandtyper) | Our maintained fork of wtype for unicode text injection | +| [linuxserver/selkies-layers](https://github.com/linuxserver/selkies-layers) | The prebuilt, patched Xvfb, labwc, KWin, selkies-desktop, and waylandtyper layers the baseimages copy in, one Dockerfile per distro and layer | +| [linuxserver/waylandtyper](https://github.com/linuxserver/waylandtyper) | Our maintained fork of wtype, shipped in the images as a command line tool | Both Rust libraries publish generated API documentation: [pixelflux rustdocs](https://pixelflux.selkies.io/pixelflux/index.html) and [pcmflux rustdocs](https://pcmflux.selkies.io/pcmflux/index.html). Reach for those when you need the exact settings fields, frame types, and callbacks rather than the overview on the [Pixelflux page](../components/pixelflux.md). -The baseimage Dockerfile is the authoritative recipe for everything compiled into the images (labwc with the IPC patch, the patched wlroots, selkies-desktop, waylandtyper), so when you need to know exactly how a piece is built, read the corresponding Dockerfile stage. +The baseimage Dockerfile builds the web frontend and the interposers itself and copies the display server layers in from [selkies-layers](https://github.com/linuxserver/selkies-layers), whose per distro Dockerfiles and `patches/` directory are the authoritative recipe for the patched Xvfb, labwc, wlroots, and KWin. When you need to know exactly how a piece is built, read the corresponding Dockerfile stage in whichever repository owns it. ## Contributing diff --git a/docs/selkies/developer-guide/protocol.md b/docs/selkies/developer-guide/protocol.md index 2d2831ddf..e2c58df63 100644 --- a/docs/selkies/developer-guide/protocol.md +++ b/docs/selkies/developer-guide/protocol.md @@ -6,8 +6,8 @@ The wire protocol between the Selkies server and the web client, for anyone impl The client connects to `wss://host/websocket`. Role assignment happens one of two ways: -- **Fragment mode** (default): the URL fragment the page was opened with decides the role, `#shared` (view only), `#collab` (full control), `#player2` through `#player4` (gamepad slot only), `#display2-right` and friends (second monitor surface). No fragment means primary. -- **Token mode** (when the server was started with `SELKIES_MASTER_TOKEN`): the client must present `?token=` in the WebSocket URL. Tokens and their roles are registered by the orchestrator via `POST /tokens` on the internal control port with the master token as a bearer credential. Close codes: `4001` invalid token, `4002` revoked, `4029` reconnecting too fast. +- **Fragment mode** (default): the URL fragment the page was opened with decides the role, `#shared` (view only), `#player2` through `#player4` (gamepad slot only), `#display2-right` and friends (second monitor surface). No fragment means a controller: the first page owns the display's size, and later pages on the plain address join beside it with full control. +- **Token mode** (when the server was started with `SELKIES_MASTER_TOKEN`): the page is opened with `#token=` in the fragment, where no request line or proxy log sees it, or `?token=` in the query, and the client offers the token to the WebSocket as the `selkies.token.` subprotocol or as `?token=`. Tokens and their roles are registered by the orchestrator via `POST /api/tokens` on the normal port with the master token as a bearer credential, or read from `SELKIES_TOKENS_FILE`. Close codes: `4000` server shutting down, `4001` invalid token, `4002` revoked, `4029` reconnecting too fast. On success the server sends `MODE websockets`, an auth confirmation with the assigned role, and a `server_settings` JSON message containing every tunable setting with its value, allowed range or enum, and locked flag, this single message is what renders the sidebar UI, which is why locking a setting server side removes the control everywhere. @@ -20,7 +20,7 @@ The first byte of every binary frame is a type tag: | `0x00` | Full frame video | `[1]` keyframe flag, `[2:4]` uint16 frame id, payload follows | | `0x01` | Opus audio packet | 2 byte header, then the Opus packet | | `0x03` | JPEG stripe | `[2:4]` frame id, `[4:6]` stripe Y offset, then the JPEG | -| `0x04` | Video stripe | `[1]` low nibble frame type (1 IDR, 2 I, 0 other), high nibble codec id (1 H.264, 2 VP8, 3 VP9, 4 AV1, 5 H.265), `[2:4]` frame id, `[4:6]` stripe Y offset, `[6:8]` width, `[8:10]` height, then the codec's own bitstream units | +| `0x04` | Video stripe | `[1]` low nibble frame kind (1 key, 2 intra, 0 delta, with `0x08` ORed in on a delta the encoder keeps as a long term anchor), high nibble codec id (1 H.264, 2 VP8, 3 VP9, 4 AV1, 5 H.265), `[2:4]` frame id, `[4:6]` stripe Y offset, `[6:8]` width, `[8:10]` height, `[10:12]` id of the frame it predicts from (its own id where it names none), then the codec's own bitstream units | These headers are produced by pixelflux itself (see the [wire format details](../components/pixelflux.md#api-sketch)); the Python server broadcasts them untouched. A full frame is just a stripe at Y offset 0 with full height. The client configures a WebCodecs `VideoDecoder` per stream from the codec id and the key frame's own parameter sets, decodes JPEG stripes with `createImageBitmap`, and composites stripes onto the canvas at their Y offsets. The frame type nibble reflects what the encoder actually emitted, clients use it to recover decoder state after drops. diff --git a/docs/selkies/index.md b/docs/selkies/index.md index 2b5e578fd..3442a8b8b 100644 --- a/docs/selkies/index.md +++ b/docs/selkies/index.md @@ -38,7 +38,7 @@ Selkies is a ground up, web native remote desktop protocol designed to replace l 1. **Hybrid protocol.** Damage tracking like VNC, video codecs like a streaming service. The screen is divided into horizontal stripes, only changed stripes are captured and encoded, and each stripe can be processed on a separate CPU core in parallel. 2. **Paint over quality.** A video codec (H.264 by default, with H.265, VP8, VP9, and AV1 available) handles fluid motion, and once motion stops the server repaints the static screen at high quality so text stays crisp. With FullColor 4:4:4 H.264 the painted over result is visually indistinguishable from a lossless image. A JPEG encoder remains available for older browsers that cannot decode video frames at all. -3. **WebSockets by default, WebRTC when you need it.** Frames are delivered over a WebSocket connection and decoded in the browser with WebCodecs. This avoids WebRTC negotiation complexity, works cleanly through reverse proxies, and gives the server precise control over pacing and backpressure. For lossy or high latency links an opt in [WebRTC transport](user-guide/webrtc.md) carries the same stream over UDP with congestion control, and users can switch between the two at runtime. +3. **WebSockets by default, WebRTC when you need it.** Frames are delivered over a WebSocket connection and decoded in the browser with WebCodecs. This avoids WebRTC negotiation complexity, works cleanly through reverse proxies, and gives the server precise control over pacing and backpressure. Congestion control steers a constant bitrate stream to what the link carries on either transport, and for lossy or high latency links an opt in [WebRTC transport](user-guide/webrtc.md) carries the same stream over UDP with loss repair, with users able to switch between the two at runtime. 4. **Zero copy on both display stacks.** The frame stays on the GPU from render to encode. On Wayland the display server is a virtual compositor built on [Smithay](https://github.com/Smithay/smithay) whose framebuffer lives directly on the GPU. On X11 a patched XLibre Xvfb keeps the screen pixmap on the GPU and pixelflux pulls each frame out with a DRI3 blit. Either way the frame is passed as a DMA-BUF handle straight to the hardware encoder (VAAPI or NVENC) without a round trip through system RAM. 5. **Everything in one container.** Compositor, application, streaming server, audio, and web server all run inside a single OCI container built on `docker-baseimage-selkies`, managed by the s6 init system. diff --git a/docs/selkies/user-guide/configuration.md b/docs/selkies/user-guide/configuration.md index 9a303ad42..dc67a8e56 100644 --- a/docs/selkies/user-guide/configuration.md +++ b/docs/selkies/user-guide/configuration.md @@ -32,20 +32,22 @@ These work in every LinuxServer.io container: | `DRI_NODE` | Encoding GPU, enables VAAPI/NVENC stream encoding on the given device, e.g. `/dev/dri/renderD128` | | `DRINODE` | Rendering GPU for EGL and 3D acceleration, e.g. `/dev/dri/renderD128` | | `AUTO_GPU` | Automatic GPU configuration when one is detected, first available GPU used for encoding and rendering. Set `false` to disable, or a vendor or driver name such as `nvidia`, `amdgpu`, or `intel` to pick a specific GPU on multi GPU hosts | -| `PIXELFLUX_CU` | Port to enable the Computer Use API server for AI agent control of the desktop, Wayland mode only | -| `SUBFOLDER` | Subfolder when running behind a subfolder reverse proxy, needs both slashes, e.g. `/subfolder/` | -| `TITLE` | Page title shown in the browser, default `Selkies` | +| `PIXELFLUX_CU` | Port of pixelflux's own Computer Use API server, the standalone fallback for `SELKIES_COMPUTER_USE_BIND`. `PELORUS=true` sets it to `5000`. The server takes a bearer token from `PIXELFLUX_CU_TOKEN`, see [Display backend](#display-backend) | +| `SUBFOLDER` | Subfolder when running behind a subfolder reverse proxy, needs both slashes, e.g. `/subfolder/`. Nginx and the Selkies server both read it, Selkies as the fallback for `SELKIES_SUBFOLDER` | +| `TITLE` | Browser tab title and the name of the page when it is installed as a web app, default `Selkies`. The dashboard reads it from the PWA manifest Nginx writes. An explicit `SELKIES_UI_TITLE` names the sidebar header and takes over the tab | | `DASHBOARD` | Select the web client dashboard: `selkies-dashboard` or `selkies-dashboard-wish` | -| `FILE_MANAGER_PATH` | Change the default upload and download path, must be writable by the `abc` user | +| `FILE_MANAGER_PATH` | Change the default upload and download path, must be writable by the `abc` user. Fallback for `SELKIES_FILE_MANAGER_PATH` | | `START_DOCKER` | If `false`, a privileged container will not automatically start the Docker in Docker setup | | `DISABLE_IPV6` | Set to `true` or any value to disable IPv6 | | `LC_ALL` | Session language, e.g. `fr_FR.UTF-8`, see [Internationalization](#internationalization) | | `NO_DECOR` | Run the application without window borders, for PWA style use. Toggle at runtime with `ctrl+shift+d` | | `NO_FULL` | Do not automatically fullscreen applications when using the single app window manager | -| `NO_GAMEPAD` | Disable the userspace gamepad interposer injection. Also turns off `SELKIES_GAMEPAD_ENABLED`, the player 2 to 4 sharing links, and hides the gamepad section of the sidebar | +| `NO_GAMEPAD` | Disable the userspace gamepad interposer injection. Also locks `SELKIES_GAMEPAD_ENABLED` and the player 2 to 4 sharing links off, and hides the gamepad section of the sidebar | | `NO_STEAM` | Remove the built in Steam installer, see [Installing Applications](installing-apps.md#steam-built-in-reinstalls-itself) | | `NO_WEBCAM` | Disable the virtual webcam. Without it the container creates `/dev/video0`, preloads the V4L2 interposer, and turns on `SELKIES_WEBCAM_ENABLED` so the browser can forward a camera into the session | | `DISABLE_DRI3` | X11 mode only, start Xvfb without the GPU. Applications render on the CPU and capture falls back to shared memory | +| `CUPS_SERVER` | Set by the container to the session's own print queue socket unless you set it yourself or turn printing off. Point it at a network CUPS server to print there instead of through the browser | +| `PAPERSIZE` | Paper size the Selkies print queue prints on, default `A4`, e.g. `letter` | | `WATERMARK_PNG` | Full path inside the container to a watermark PNG, e.g. `/usr/share/selkies/www/icon.png` | | `WATERMARK_LOCATION` | Where to paint the watermark, integer 1 to 6 | @@ -59,7 +61,8 @@ The Selkies server reads its own `SELKIES_*` variables, and the baseimage's init | --- | --- | --- | --- | | `SELKIES_ENCODER` | `h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg` | `h264enc,h265enc,vp8enc,vp9enc,av1enc,h264enc-striped,jpeg` | The striped H.264 encoder is left out of the sidebar menu. Selkies then drops any encoder this host cannot serve, see [Video encoders](#video-encoders) | | `SELKIES_VIDEO_STREAMING_MODE` | `false` | `true` | Desktop use favors damage tracking and paint over. Turn it on for gaming and video | -| `SELKIES_ENABLE_BASIC_AUTH` | `false` | `true` | Nginx handles the login using `CUSTOM_USER` and `PASSWORD`, so the Selkies server's own basic auth stays off | +| `SELKIES_RATE_CONTROL_MODE` | `crf,cbr` | `cbr,crf` | Desktop use favors constant quality, so CRF stays first and CBR is selectable. Upstream defaults to CBR on both transports | +| `SELKIES_ENABLE_BASIC_AUTH` | `false` | `true` | Nginx handles the login using `CUSTOM_USER` and `PASSWORD`, so the Selkies server's own basic auth stays off. Upstream refuses to start with it on and no password set | | `SELKIES_ALLOWED_ORIGINS` | `*` | same origin only | Nginx fronts the server, so the cross origin guard is relaxed inside the container | | `SELKIES_COMMAND_ENABLED` | `true` | `false` | The sidebar apps and launcher section depends on command messages. `HARDEN_DESKTOP=true` flips it back to `false` | | `SELKIES_ENABLE_DUAL_MODE` | `false` | `true` | The WebSocket / WebRTC switch is hidden until you configure WebRTC, see [WebRTC Transport](webrtc.md) | @@ -67,21 +70,24 @@ The Selkies server reads its own `SELKIES_*` variables, and the baseimage's init | `SELKIES_WEBCAM_ENABLED` | `true` | `false` | Turned on when the virtual webcam device can be created, `NO_WEBCAM` prevents it | | `CUSTOM_WS_PORT` | `8082` | `8080` | Port the Selkies server listens on behind Nginx | +Everything the container does not translate keeps its upstream default. Two of those are worth knowing: `SELKIES_CONGESTION_CONTROL` is on, and only acts once a session runs under CBR, and `SELKIES_USE_PAINT_OVER_QUALITY` is on because Turbo mode is off. + `HARDEN_DESKTOP=true` also sets `SELKIES_FILE_TRANSFERS` to empty, turns `SELKIES_PRINTING_ENABLED` off, and hides the files and apps sidebar sections unless you set those variables yourself. Anything you pass explicitly always wins over these defaults. ## Selkies application settings Every facet of the streaming application can be configured with `SELKIES_*` variables. These also drive what the user can change in the sidebar UI: the server sends the resolved settings schema to the client, so a locked or single valued setting simply has no control to change. -Each setting is also a CLI flag with the same name, `SELKIES_VIDEO_CRF` is `--video-crf`. Precedence is CLI flag, then the `SELKIES_*` variable, then the legacy container variable where one exists (`PASSWORD`, `DRI_NODE`, and so on), then the built in default. +Each setting is also a CLI flag with the same name, `SELKIES_VIDEO_CRF` is `--video-crf`. Precedence is CLI flag, then the `SELKIES_*` variable, then the legacy container variable where one exists (`PASSWORD`, `DRI_NODE`, and so on), then the built in default. The upstream [Settings Reference](https://docs.selkies.io/settings) is generated from the server's own definitions and is the authoritative list; this page describes the same settings as the containers ship them. ### Value syntax - **Booleans and locking.** `true` or `1` (case insensitive) is on, anything else is off. Append `|locked` to prevent the user changing the setting in the UI: `-e SELKIES_USE_CPU="true|locked"` - **Enums and lists.** Comma separated values, the first item is the default and the full list is what the user may pick from. A single item hides the UI dropdown entirely: `-e SELKIES_ENCODER="jpeg"`. Matching is case insensitive and invalid items are dropped; if nothing valid is left the built in menu is kept. -- **Ranges.** Three forms. `min-max` restricts the allowed span and keeps the built in initial value: `SELKIES_FRAMERATE="8-120"`. A bare value sets the initial value and keeps the built in span: `SELKIES_FRAMERATE="60"`. Both at once: `SELKIES_FRAMERATE="60,8-120"`. A degenerate span such as `"60-60"` locks the setting. +- **Ranges.** Three forms. `min-max` restricts the allowed span and keeps the built in initial value: `SELKIES_FRAMERATE="8-120"`. A bare value sets the initial value and keeps the built in span, widening it when the value falls outside: `SELKIES_FRAMERATE="60"`. Both at once: `SELKIES_FRAMERATE="60,8-120"`. A degenerate span such as `"60-60"` locks the setting. - **Empty means default.** Setting a variable to `""` means "use the built in default", except for list type settings where `""` or `none` means disabled (for example `SELKIES_FILE_TRANSFERS`). - **Manual resolution.** Setting `SELKIES_MANUAL_WIDTH` or `SELKIES_MANUAL_HEIGHT` to a positive value forces manual resolution mode. +- **Per user overrides.** Some settings say "users may override unless locked": the browser keeps the user's choice for that setting and applies it on later visits, so the variable sets the starting position rather than a fixed value. Append `|locked` to pin it. ### Legacy variables @@ -104,62 +110,65 @@ These names are no longer Selkies settings, but the containers still accept them | Variable | Default | Description | | --- | --- | --- | | `SELKIES_MODE` | `websockets` | Streaming transport, `websockets` or `webrtc`. The containers switch this to `webrtc` automatically when any WebRTC variable is set, see [WebRTC Transport](webrtc.md) | -| `SELKIES_ENABLE_DUAL_MODE` | `false` (upstream `true`) | Show the transport switch in the UI so users can move between WebSockets and WebRTC at runtime | +| `SELKIES_ENABLE_DUAL_MODE` | `false` (upstream `true`) | Show the transport switch in the UI so users can move between WebSockets and WebRTC at runtime. With it on, a page whose WebRTC media path keeps failing is offered WebSockets | | `SELKIES_BACKPRESSURE_QUEUE_SIZE` | `120` | WebSockets mode only. Max frames or audio chunks buffered per stream before dropping under backpressure, `1` to `100000`. Higher tolerates larger client hiccups at the cost of latency | | `SELKIES_WEBRTC_PACER` | `true` | WebRTC mode only. Pace outgoing packets with strict priorities (audio and RTCP, then data channel, then video) so audio and input stay responsive when video bursts on a congested link. `SELKIES_WEBRTC_PACER_STALE_MS` sets the stale GOP purge deadline in milliseconds, `0` disables | -| `SELKIES_CONGESTION_CONTROL` | `false` | WebRTC mode only. Adapt the video bitrate to the bandwidth estimate from receiver feedback. Effective in CBR rate control mode, may trade quality for responsiveness | +| `SELKIES_CONGESTION_CONTROL` | `true` | Under CBR on either transport, adapt a display's bitrate to what its path carries, never above the bitrate the client chose: over WebRTC from the receiver's transport wide feedback (loss and a rise in one way delay), over WebSockets from the queue the frame round trip shows. A page keeps the rate its display settled on for ten seconds, and a restarted server starts that page's stream there for a day or until the user sets a bitrate. `false` holds the chosen bitrate whatever the path carries, so a slower path lags or drops frames. No effect under CRF, the container default | ### Video encoders `SELKIES_ENCODER` is a comma separated menu of the values below. The first item is the default and the whole list is what the sidebar offers. Every full frame codec is encoded on the GPU when the encoding device has an engine for it, and by the software encoder in the pixelflux build otherwise, so the same menu works on a CPU only host, an Intel or AMD card, and an Nvidia card. The container default is `h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg`. -| Value | Codec | Hardware | Software | Shape | FullColor 4:4:4 | WebRTC | Browser decode | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `h264enc` | H.264 | NVENC, VA-API | x264 | Full frame | NVENC and x264. VA-API has no 4:4:4 H.264 profile on current drivers, so the request is honored on the CPU instead | Yes | Every browser | -| `h265enc` | H.265 (HEVC) | NVENC, VA-API | x265 | Full frame | NVENC and x265, VA-API negotiates per device | Yes | Safari, and Chromium where the operating system supplies an HEVC decoder. Chrome and Firefox on Linux decode no HEVC | -| `vp8enc` | VP8 | VA-API, on the few GPUs that still carry a VP8 engine | libvpx | Full frame | No | Yes | Every browser | -| `vp9enc` | VP9 | VA-API | libvpx | Full frame | Profile 1 on VA-API and libvpx | Yes | Chromium, Firefox | -| `av1enc` | AV1 | NVENC on Ada and newer, VA-API on GPUs with an AV1 engine | SVT-AV1 | Full frame | No | Yes | Chromium, Firefox. WebKit refuses it at decode time | -| `h264enc-striped` | H.264 | None, always CPU | x264 | Striped, one stripe per core | x264 | No | Every browser | -| `jpeg` | JPEG | None, always CPU | libjpeg-turbo | Striped, one stripe per core | Always full color | No | Every browser, including ones without WebCodecs | +| Value | Codec | Hardware | Software | Shape | FullColor 4:4:4 | 10-bit | WebRTC | Browser decode | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `h264enc` | H.264 | NVENC, VA-API | x264 | Full frame | NVENC and x264. VA-API has no 4:4:4 H.264 profile on current drivers, so the request is honored on the CPU instead | x264 only | Yes | Every browser | +| `h265enc` | H.265 (HEVC) | NVENC, VA-API | x265 | Full frame | NVENC and x265, VA-API negotiates per device | NVENC, VA-API, x265 | Yes | Safari, and Chromium where the operating system supplies an HEVC decoder. Chrome and Firefox on Linux decode no HEVC | +| `vp8enc` | VP8 | VA-API, on the few GPUs that still carry a VP8 engine | libvpx | Full frame | No | No | Yes | Every browser | +| `vp9enc` | VP9 | VA-API | libvpx | Full frame | Profile 1 on VA-API and libvpx | VA-API and libvpx, profiles 2 and 3 | Yes | Chromium, Firefox | +| `av1enc` | AV1 | NVENC on Ada and newer, VA-API on GPUs with an AV1 engine | SVT-AV1 | Full frame | No | NVENC, VA-API, SVT-AV1 | Yes | Chromium, Firefox. WebKit refuses it at decode time | +| `h264enc-striped` | H.264 | None, always CPU | x264 | Striped, one stripe per core | x264 | No | No | Every browser | +| `jpeg` | JPEG | None, always CPU | libjpeg-turbo | Striped, one stripe per core | Always full color | No | No | Every browser, including ones without WebCodecs | -Nvidia has no VP8 or VP9 encode engine, so those two codecs are software on every Nvidia card. AMD has no VP8 or VP9 engine either, and AV1 starts with RDNA 3. Intel carries VP9 and, on Arc and newer integrated graphics, AV1. Run `vainfo` inside the container to see the encode entry points your card exposes. +Nvidia has no VP8 or VP9 encode engine, so those two codecs are software on every Nvidia card. AMD has no VP8 or VP9 engine either, and AV1 starts with RDNA 3. Intel carries VP9 and, on Arc and newer integrated graphics, AV1. Run `vainfo` inside the container to see the encode entry points your card exposes. The same pixelflux wheels also carry a Jetson's Tegra encoder and V4L2 memory to memory engines; this guide covers the three desktop GPU vendors. How the menu behaves at runtime: -- **The host trims the menu.** At startup Selkies probes the encoding device once and drops every encoder that neither the GPU nor the pixelflux software build can serve, logging `Encoders not served on this host are left off the menu`. If the default itself is not served the session falls back to `h264enc`. The wheels in the containers carry a software encoder for all five codecs, so on a CPU only host the full menu still appears, all of it in software. -- **The browser greys out what it cannot decode.** Encoders the browser has no decoder for stay in the menu disabled and labelled `(Unsupported Browser)`. A browser that cannot decode the server default steps through the allowed list in the order `h264enc`, `h264enc-striped`, `vp9enc`, `vp8enc`, `av1enc`, `h265enc`, `jpeg` until one plays. A shared viewer, or a browser facing a single valued locked encoder, gets an on page error instead. +- **The host trims the menu.** At startup Selkies probes the encoding device once and drops every encoder that neither the GPU nor the pixelflux software build can serve, logging `Encoders not served on this host are left off the menu`. If the default itself is not served the session starts on the next encoder down the ladder below. The wheels in the containers carry a software encoder for all five codecs, so on a CPU only host the full menu still appears, all of it in software. +- **The browser greys out what it cannot decode.** Encoders the browser has no decoder for stay in the menu disabled and labelled `(Unsupported Browser)`. A browser that cannot decode the server default steps down one ladder: the codecs this host encodes in hardware, most efficient first (AV1, H.265, VP9, H.264, VP8), then the ones it encodes in software in order of encode time (H.264, AV1, VP8, H.265, VP9), then `h264enc-striped`, and `jpeg` last. Only encoders in the allowed menu are stepped to, so a browser facing a single valued locked encoder it cannot play gets an on page error instead. - **Only H.264 and JPEG are striped.** H.265, VP8, VP9, and AV1 always encode the whole frame, so they never get the per core parallelism or the dirty stripe savings of the H.264 and JPEG paths. In software they also cost several times the CPU of x264. On a machine whose GPU lacks the engine, keep `h264enc` first and treat the other codecs as options for GPU hosts. -- **Software encoding toggle.** The sidebar's CPU encoding switch only appears for a codec the host serves both ways, since it moves the session between the GPU and the software encoder and would be a no-op otherwise. `SELKIES_USE_CPU` still applies to every full frame codec. -- **Demotion.** If pixelflux still cannot serve the codec once capture starts it demotes the display to H.264 and Selkies rewrites the encoder setting to match, logging `streams H.264 as 'h264enc': no encoder served`. Trust the encoder shown in the sidebar over the one you configured. -- **Colors.** VP8 cannot signal BT.709, so it is encoded and declared BT.601 and looks very slightly different from the other codecs. Firefox paints AV1 received over WebRTC as BT.601 for the same reason. +- **Software encoding toggle.** The sidebar's CPU encoding switch only appears for a codec the host serves both ways, since it moves the session between the GPU and the software encoder and would be a no-op otherwise. `SELKIES_USE_CPU` still applies to every full frame codec. While a session encodes in software the sidebar also shows the multithreading switch behind `SELKIES_CPU_ENCODE_THREADING`. +- **Demotion.** If pixelflux still cannot serve the codec once capture starts it walks the same ladder itself, the other codecs the host serves in hardware first, then software, then striped H.264, then JPEG, and Selkies rewrites the encoder setting to match, logging `no encoder served`. Trust the encoder shown in the sidebar over the one you configured. +- **10-bit.** `SELKIES_VIDEO_10BIT` asks for 10 bits per sample where the codec and encoder carry it. The desktop is an 8-bit source, so the extra bits add precision to the color conversion rather than new shades, and a software encoder pays for them in CPU. It is only worth pairing with FullColor 4:4:4, and the sidebar offers it only where the browser's decoder returns a 10-bit picture for that codec. +- **Colors.** VP8 cannot signal BT.709, so it is encoded and declared BT.601 and looks very slightly different from the other codecs. Firefox before 157 paints AV1 received over WebRTC as BT.601 for the same reason. ### Video encoding | Variable | Default | Description | | --- | --- | --- | | `SELKIES_ENCODER` | `h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg` (upstream adds `h264enc-striped`) | Menu of video encoders, first is default, see [Video encoders](#video-encoders). Encoders the host cannot serve are dropped at startup, and the five full frame codecs are the ones that stream over WebRTC | -| `SELKIES_FRAMERATE` | `8-240`, initial `60` | Framerate range, initial value, or both | -| `SELKIES_RATE_CONTROL_MODE` | `crf` | Rate control for every video codec, `crf` (constant quality) or `cbr` (constant bitrate). WebRTC mode defaults to `cbr` unless you pin this, and so does a session on OpenH264 in a GPL free pixelflux build | -| `SELKIES_ENABLE_RATE_CONTROL` | `true` | Let the client pick the rate control mode. Set `false` to lock the encoder to CRF | +| `SELKIES_FRAMERATE` | `8-240`, initial `60` | Framerate range, initial value, or both. The sidebar also offers the client display's own refresh rate | +| `SELKIES_RATE_CONTROL_MODE` | `crf,cbr` (upstream `cbr,crf`) | Menu of rate control modes for every video codec, first is default: `crf` constant quality, `cbr` constant bitrate. A single value locks it. The container keeps CRF first on both transports, upstream defaults to CBR. CBR is what [congestion control](#transport) steers, so pick it for lossy links | +| `SELKIES_ENABLE_RATE_CONTROL` | `true` | Let the client pick the rate control mode. Set `false` to lock the encoder to the first mode in the menu | | `SELKIES_VIDEO_CRF` | `5-50`, initial `25` | CRF range, initial value, or both. Lower is higher quality. The value is on the H.264 QP scale and pixelflux maps it onto each codec's own quantizer range, so one number means the same quality on every encoder | | `SELKIES_VIDEO_BITRATE` | `100-1000000`, initial `8000` | CBR bitrate in kbps: range, initial value, or both. `8000` is 8 Mbps | | `SELKIES_VIDEO_MIN_QP` | `0` | CBR mode minimum quantizer on the H.264 QP scale, `0` to `51`, mapped onto each codec's own range. `0` is the encoder default. Raising it caps bit spend on easy content | | `SELKIES_VIDEO_MAX_QP` | `0` | CBR mode maximum quantizer on the same scale, `0` is the encoder default. Lowering it keeps text legible under motion at the cost of overshooting the bitrate target | | `SELKIES_KEYFRAME_INTERVAL` | `0` | Seconds between scheduled recovery keyframes, `0` to `300`. `0` keeps the GOP infinite and sends keyframes only on demand, which keeps bitrate and quality steady | -| `SELKIES_VIDEO_FULLCOLOR` | `false` | Encode 4:4:4 chroma instead of 4:2:0 where the codec and encoder carry it: H.264 and H.265 on NVENC, x264, and x265, VP9 profile 1 on VA-API and libvpx. VP8, AV1, and OpenH264 stay 4:2:0. A client whose decoder has no 4:4:4 profile turns it off for itself and keeps the codec; where it is locked, such a client steps to the next allowed encoder whose 4:4:4 it decodes, JPEG last. See the [GPU caveats](gpu.md#fullcolor-444-and-hardware-encoders) | +| `SELKIES_VIDEO_FULLCOLOR` | `false` | Encode 4:4:4 chroma instead of 4:2:0 where the codec and encoder carry it: H.264 and H.265 on NVENC, x264, and x265, H.265 on VA-API where the driver negotiates a surface, VP9 profile 1 on VA-API and libvpx. VP8, AV1, and OpenH264 stay 4:2:0. The server knows which of its encoders carry it on this host and the browser reports which 4:4:4 profiles it decodes, so a client without the profile streams 4:2:0 on the same codec; where it is locked, such a client steps to the next allowed encoder whose 4:4:4 it decodes, JPEG last. See the [GPU caveats](gpu.md#fullcolor-444-and-hardware-encoders) | +| `SELKIES_VIDEO_10BIT` | `false` | Encode 10 bits per sample where the codec has a 10-bit profile an encoder on this host codes: H.264 on x264, and H.265, VP9, and AV1 on VA-API, x265, libvpx, and SVT-AV1, and H.265 and AV1 on NVENC. A GPU that does not encode the format hands the session to the software encoder, as it does a 4:4:4 it lacks. Costs CPU on software encoders, so only recommended together with FullColor. A client whose decoder shows no 10-bit picture turns it off for itself; where it is locked, such a client steps to the next allowed encoder it decodes. Chromium decodes AV1 over WebRTC at 8 bits only | | `SELKIES_VIDEO_STREAMING_MODE` | `false` (upstream `true`) | Turbo mode: encode every frame like a traditional video stream instead of damage tracking. Useful for gaming and full motion video | | `SELKIES_USE_CPU` | `false` | Force software encoding for the full frame codecs even when the encoding GPU serves them. The striped encoders are CPU regardless | +| `SELKIES_CPU_ENCODE_THREADING` | `true` | Spread each frame a software encoder codes across threads: x264 codes a frame in slices on several threads, libvpx, x265, and SVT-AV1 run their worker threads, and the striped encoders code their stripes in parallel. Set `false` on hosts that pack many sessions onto their cores so each session's encode keeps to one thread, at the cost of a slower frame that a high frame rate notices. Hardware encoders are unaffected, so the sidebar shows the switch only while the session encodes in software. Users may override unless locked | | `SELKIES_JPEG_QUALITY` | `1-100`, initial `40` | JPEG encoder quality range, initial value, or both | -| `SELKIES_USE_PAINT_OVER_QUALITY` | `true` | High quality paint over for static scenes | +| `SELKIES_USE_PAINT_OVER_QUALITY` | `true` | Clean up a still screen at the paint over quality. Left unset it is on except while Turbo mode drives a video encoder, so the container default is on. Under CRF, and for JPEG, a still screen is only cleaned up with it on. Under CBR a video stream is cleaned up to the paint over CRF whatever this says, since a rate control alone leaves a low bitrate short of a clean picture, so the sidebar hides the switch there | | `SELKIES_PAINT_OVER_JPEG_QUALITY` | `1-100`, initial `90` | JPEG paint over quality range, initial value, or both | -| `SELKIES_VIDEO_PAINTOVER_CRF` | `5-50`, initial `18` | Paint over CRF range, initial value, or both, for every video codec. Must be lower than the CRF to trigger, and paint over is off by default under CBR | -| `SELKIES_VIDEO_PAINTOVER_BURST_FRAMES` | `1-30`, initial `5` | Paint over burst frames range, initial value, or both, for every video codec | +| `SELKIES_VIDEO_PAINTOVER_CRF` | `5-50`, initial `18` | Paint over quality for every video codec, on the CRF scale: range, initial value, or both. Under CRF it must be lower than the CRF to trigger. Under CBR it is the finest a still screen is refined to, reached as fast as the path carries | +| `SELKIES_VIDEO_PAINTOVER_BURST_FRAMES` | `1-30`, initial `5` | Frames a video encoder keeps sending after a cleanup or a key frame on a still screen so rate control settles: range, initial value, or both. Under CBR most encoders keep sending until the screen is clean instead | | `SELKIES_GPU_ID` | `''` | Hardware encoder GPU index, selects `/dev/dri/renderD{128 + n}` and the GPU stats index. Empty encodes on the first GPU or the one `AUTO_GPU` chose, `-1` disables hardware encoding. Ignored when `DRI_NODE` gives a device path | | `SELKIES_ENCODE_DRI` (or `DRI_NODE`) | `''` | DRI render node the encoder uses for VA-API or NVENC | | `SELKIES_RENDER_DRI` (or `DRINODE`) | `''` | DRI render node the display server renders on, the Wayland compositor or Xvfb through glamor. Defaults to the `AUTO_GPU` pick, else software rendering | | `SELKIES_AUTO_GPU` (or `AUTO_GPU`) | `true` | GPU auto selection for rendering: `true` picks the first GPU, `false` disables, or a vendor name, kernel driver name, devicetree prefix, or PCI vendor ID picks the first GPU it matches | -| `SELKIES_RECORDING_SOCKET` (or `PIXELFLUX_RECORDING_SOCKET`) | `''` | Unix socket path for an out of band recording tap, pixelflux multiplexes the elementary stream of the full frame codec to connected clients: Annex B for H.264 and H.265, OBU for AV1, IVF for VP8 and VP9. Not available on striped H.264 or JPEG. Empty is off | +| `SELKIES_RECORDING_SOCKET` (or `PIXELFLUX_RECORDING_SOCKET`) | `''` | Unix socket path for an out of band recording tap, pixelflux multiplexes the elementary stream of the full frame codec to connected clients: Annex B for H.264 and H.265, OBU for AV1, IVF for VP8 and VP9. Not available on striped H.264 or JPEG. Empty is off. The operator API's own recorder, a fragmented MP4 with the session's audio, needs no socket, see the upstream [Operator API](https://docs.selkies.io/usage#operator-api) | ### Audio @@ -169,7 +178,7 @@ How the menu behaves at runtime: | `SELKIES_AUDIO_FRAME_DURATION_MS` | `10` | Opus frame duration: `2.5`, `5`, `10`, `20`, `40`, or `60`. Lower cuts audio latency at a small bandwidth and packet rate cost | | `SELKIES_AUDIO_REDUNDANCY` | `true` | Opus RED (RFC 2198) redundancy to cut dropouts under packet loss. On WebSockets it only engages when every connected client supports it | | `SELKIES_AUDIO_REDUNDANCY_DISTANCE` | `2` | Number of prior Opus frames carried as redundancy, `0` to `4`. Higher survives longer loss bursts at proportionally more bandwidth | -| `SELKIES_AUDIO_CHANNELS` | `2` | Number of audio channels | +| `SELKIES_AUDIO_CHANNELS` | `2` | Number of audio channels. `6` or `8` creates a surround sink, which a WebSockets page plays as is and a WebRTC page receives where its browser decodes multichannel Opus, folded to stereo otherwise | | `SELKIES_AUDIO_DEVICE_NAME` | `output.monitor` | PulseAudio source pcmflux captures | ### Display and input @@ -180,13 +189,13 @@ How the menu behaves at runtime: | `SELKIES_MANUAL_WIDTH` | `0` | Fixed width up to `16384`, a positive value forces manual resolution mode | | `SELKIES_MANUAL_HEIGHT` | `0` | Fixed height up to `16384`, a positive value forces manual resolution mode | | `SELKIES_ENABLE_RESIZE` | `true` | Dynamically resize the display to match the browser window | -| `SELKIES_SCALING_DPI` | `96` | Default DPI for UI scaling, `96` to `288` in steps of `24` | +| `SELKIES_SCALING_DPI` | `96` | Default DPI for UI scaling, `96` to `288` in steps of `24`. Set explicitly it pins the desktop's DPI against the pages' own picks | | `SELKIES_FORCE_ALIGNED_RESOLUTION` | `false` | Forces the display resolution to be a multiple of 16 pixels | -| `SELKIES_USE_CSS_SCALING` | `false` | HiDPI when false. When true a lower resolution is sent and the canvas is stretched | -| `SELKIES_SECOND_SCREEN` | `true` | Offer the Add Screen button for a second monitor. Set `false` to hide it. On Wayland the button only appears when Selkies detects compositor support (labwc or KWin) regardless of this value | +| `SELKIES_USE_CSS_SCALING` | `false` | HiDPI when false. When true a lower resolution is sent and the canvas is stretched; the UI scaling pick then divides that resolution and the desktop stays at 96 DPI | +| `SELKIES_SECOND_SCREEN` | `true` | Offer the Add Screen button for a second monitor. Set `false` to hide it. On X11 each display is a RandR output of its own, since the containers' Xvfb starts with spare outputs. On Wayland the button only appears when Selkies detects compositor support (labwc or KWin) regardless of this value | | `SELKIES_ENABLE_CURSORS` | `true` | Send the remote application cursor to the client | | `SELKIES_USE_BROWSER_CURSORS` | `true` | Use browser CSS cursors instead of rendering the cursor onto the canvas | -| `SELKIES_CURSOR_SIZE` (or `XCURSOR_SIZE`) | `-1` | Cursor size in points at 96 DPI, scaled with the session DPI. `-1` is the platform default, 32 on X11 and 24 on Wayland | +| `SELKIES_CURSOR_SIZE` (or `XCURSOR_SIZE`) | `-1` | Cursor size in points at 96 DPI, scaled with the session DPI. `-1` is the platform default, 32 on X11 and 24 on Wayland. An application's own cursor is delivered whole up to 128 points, the most a browser shows, and a larger size set here raises that cap with it | | `SELKIES_MAC_CMD_AS_CTRL` | `true` | macOS clients send Command chords as Control, so Cmd+C copies remotely. Set `false` when the session's window manager binds Super itself, Command then arrives as Super. Users may override unless locked | | `SELKIES_KEYBOARD_SHORTCUTS` | `true` | Keep the client's own chords (Ctrl+Shift with F, M, X, or G, and Ctrl+Shift+click) for the sidebar instead of passing them to the session. Set `false` to pass them through; the sidebar buttons still work and three presses of Escape still leave gaming mode. Users may override unless locked | | `SELKIES_PUBLISH_INPUT_DEVICES` | `false` | Mirror the session keyboard and pointer onto evdev input devices for applications that enumerate evdev directly, such as fullscreen games and remappers. Uses a kernel uinput device where `/dev/uinput` is writable, else the input interposer | @@ -195,32 +204,35 @@ How the menu behaves at runtime: ### Feature toggles and session start state -The `*_ENABLED` variables decide what the server offers at all. The `*_ON_START` variables decide whether a feature is already running when a client connects, or waits for the user to press its sidebar button. +The `*_ENABLED` variables decide what the server offers at all. The `*_ON_START` variables decide whether a feature is already running when a client connects, or waits for the user to press its sidebar button. Whatever starts off is not captured at all until it is turned on. | Variable | Default | Description | | --- | --- | --- | | `SELKIES_AUDIO_ENABLED` | `true` | Server to client audio streaming. Disabling it also disables the microphone | | `SELKIES_MICROPHONE_ENABLED` | `false` | Client to server microphone forwarding | | `SELKIES_WEBCAM_ENABLED` | `true` (upstream `false`) | Client to server webcam forwarding into the virtual V4L2 device. Set by the baseimage unless `NO_WEBCAM` is present | -| `SELKIES_GAMEPAD_ENABLED` | `true` | Gamepad support. Forced off by `NO_GAMEPAD` | +| `SELKIES_GAMEPAD_ENABLED` | `true` | Gamepad support. Locked off by `NO_GAMEPAD` | | `SELKIES_VIDEO_ON_START` | `true` | Start with video on. Off, nothing is captured for the primary display until the user turns video on; shared viewers and second screens always start their stream | | `SELKIES_AUDIO_ON_START` | `true` | Start with audio on. Off, capture stays stopped until the user turns audio on. Unlike `SELKIES_AUDIO_ENABLED=false` nothing is torn down and the microphone keeps working | -| `SELKIES_MICROPHONE_ON_START` | `false` | Start with the microphone on, so the browser asks for the device as soon as the session connects | -| `SELKIES_WEBCAM_ON_START` | `false` | Start with the webcam on, so the browser asks for the camera as soon as the session connects | +| `SELKIES_MICROPHONE_ON_START` | `false` | Microphone policy: `true` asks the browser for the device as soon as the session connects, `false` waits for the sidebar toggle, `demand` asks only while an application in the session records from the virtual source and releases it ten seconds after the last one stops. Under `demand` any process on the desktop can raise a permission prompt, so every ask and release is logged and audited | +| `SELKIES_WEBCAM_ON_START` | `false` | Webcam policy: `true` asks the browser for the camera as soon as the session connects, `false` waits for the sidebar toggle, `demand` asks only while an application holds the virtual device open and releases it two seconds after the last one closes it. Under `demand` the device exists before any uplink, so a pixel format of `auto` resolves to I420 | | `SELKIES_GAMEPAD_ON_START` | `true` | Start with gamepad input on. The user's choice is remembered by the browser and takes precedence on later visits | | `SELKIES_COMMAND_ENABLED` | `true` (upstream `false`) | Parsing of command messages from the client, which the sidebar apps section needs. `HARDEN_DESKTOP` turns it off | | `SELKIES_DEBUG` | `false` | Debug logging | -### Clipboard and files +### Clipboard, files, and apps | Variable | Default | Description | | --- | --- | --- | | `SELKIES_ENABLE_CLIPBOARD` | `true` | Clipboard policy: `true` both directions, `in` client to server only, `out` server to client only, `false` disabled. `out` is what stops the page reading the local clipboard at all, which is the read Firefox and Safari raise a paste prompt for | | `SELKIES_ENABLE_BINARY_CLIPBOARD` | `true` | Allow binary data such as images on the clipboard. Rich text pasted from the browser into the session follows the same switch | | `SELKIES_CLIPBOARD_SEAMLESS` | `true` | Sync the clipboard automatically on every copy on either side. Set `false` and the clipboard only moves through the sidebar's clipboard box. The direction policy above still bounds it. Users may override unless locked | -| `SELKIES_FILE_TRANSFERS` | `upload,download` | Allowed transfer directions, comma separated. Empty or `none` disables | +| `SELKIES_FILE_TRANSFERS` | `upload,download` | Allowed transfer directions, comma separated. Empty or `none` disables. A view only viewer is refused both regardless | | `SELKIES_FILE_TRANSFER_LIMIT_MBPS` | `0` | Static throttle in Mbit/s shared by all uploads and downloads, for links whose rate you know. `0` disables. Transfers are already paced to protect the video stream without it | | `SELKIES_FILE_MANAGER_PATH` (or `FILE_MANAGER_PATH`) | `~/Desktop` | Directory uploads land in and the file browser serves, created at startup if missing | +| `SELKIES_APPS_REPO_FOLDER` (or `PA_REPO_FOLDER`) | `''` | Directory of a proot-apps local repository, the folder `proot-apps localrepo` fills, mounted into the session read only. The apps panel then lists that folder's catalog instead of the remote one and installs from it. Set `PA_REPO_FOLDER` on the container so `proot-apps` in a terminal reads the same folder, see [Installing Applications](installing-apps.md) | + +A copy a password manager marks as sensitive is kept out of the sidebar's clipboard view and off the local clipboard, and formatted content travels with its plain text so a terminal paste gets text rather than markup. ### Webcam @@ -231,51 +243,59 @@ The containers create a virtual `/dev/video0` and preload a V4L2 interposer so o | `SELKIES_WEBCAM_WIDTH` | `1280` | Width of the virtual webcam device, client frames are scaled and letterboxed to fit | | `SELKIES_WEBCAM_HEIGHT` | `720` | Height of the virtual webcam device | | `SELKIES_WEBCAM_PIXEL_FORMAT` | `auto` | Pixel format of the virtual device. `auto` follows the uplink (MJPEG for a browser sending JPEG, otherwise I420). Or pin `I420`, `NV12`, `YUYV`, or `MJPEG` | -| `SELKIES_WEBCAM_ENCODER` | `auto` | Codec the browser uses for the camera uplink: `auto`, `h264`, `h265`, `vp8`, `vp9`, `av1`, or `mjpeg`. Over WebSockets `auto` tries H.264, then VP8, then VP9, AV1, or H.265 where the browser encodes them, and JPEG where nothing keeps up; a codec name pins it, still dropping to JPEG when it cannot keep up. Over WebRTC the browser sends the named codec when the session negotiated it. Users may override unless locked | +| `SELKIES_WEBCAM_ENCODER` | `auto` | Codec the browser uses for the camera uplink: `auto`, `h264`, `h265`, `vp8`, `vp9`, `av1`, or `mjpeg`. Over WebSockets `auto` tries H.264, then VP8, then VP9, AV1, or H.265 where the browser encodes them, and JPEG where nothing keeps up; Firefox sends JPEG. A codec name pins it, still dropping to JPEG when it cannot keep up. Over WebRTC the browser sends the named codec when the session negotiated it. Users may override unless locked | | `SELKIES_WEBCAM_DEVICE` | `auto` | Also mirror the webcam into a v4l2loopback kernel device: `auto` uses the first one found (usually only on a host or privileged container), a path such as `/dev/video10` uses that device, `false` never does | -| `SELKIES_WEBCAM_SOCKET_PATH` | `/tmp` | Directory for the V4L2 interposer socket, `selkies_webcam0.sock` | +| `SELKIES_WEBCAM_SOCKET_PATH` | `''` | Directory for the V4L2 interposer socket, `selkies_webcam0.sock`. Empty uses `XDG_RUNTIME_DIR`, which the containers set to `/config/.XDG` | ### Gamepads +Inside the containers the interposer serves four slots as `js0` to `js3` and `event1000` to `event1003`. A game's rumble is mixed per slot and played on the controller of the browser driving it; each browser can turn that off in its gamepad section. + | Variable | Default | Description | | --- | --- | --- | -| `SELKIES_JS_SOCKET_PATH` | `/tmp` | Directory for the input interposer sockets, `selkies_js{0-3}.sock` | +| `SELKIES_JS_SOCKET_PATH` | `''` | Directory for the input interposer sockets, `selkies_js{0-3}.sock` and `selkies_event{1000-1003}.sock`. Empty uses `XDG_RUNTIME_DIR`, which the containers set to `/config/.XDG`, and the interposer looks in the same place | | `SELKIES_UINPUT_GAMEPAD` | `auto` | Register gamepads as kernel devices through `/dev/uinput`, which Steam, Proton, and browsers inside the session find without the interposer. `auto` only does so where the interposer is not configured and `/dev/uinput` is writable, `true` always attempts it, `false` never does | | `SELKIES_UINPUT_MOUSE_SOCKET` | `''` | Path to a uinput mouse socket, if not provided uinput is used directly | ### Printing and audit -The session has a printer named `Selkies`. Selkies runs the CUPS queue itself as the session user, the containers ship `cups-daemon` and `cups-filters` and point `CUPS_SERVER` at it, and every printed document is turned into a PDF and handed to the browser, which opens its own print dialog. Documents also collect in the sidebar's printing section for reprinting or saving. Selkies can also post an audit event for each clipboard transfer, file transfer, print, connect, disconnect, and recording. +The session has a printer named `Selkies`. Selkies runs the CUPS queue itself as the session user, the containers ship `cups-daemon` and `cups-filters` and point `CUPS_SERVER` at it, and every printed document is turned into a PDF and handed to the browser, which opens its own print dialog. Documents also collect in the sidebar's printing section for reprinting or saving. The queue prints on A4 unless `PAPERSIZE` names another size such as `letter`. + +Selkies can also post an audit event for each clipboard transfer, file transfer, print, connect, disconnect, recording, and `demand` camera or microphone request, plus an `audit.dropped` count once an overflowed queue drains. | Variable | Default | Description | | --- | --- | --- | | `SELKIES_PRINTING_ENABLED` | `true` | Offer the `Selkies` printer to the session and hand each document to the browser. `false` runs no queue and hides the printing section. `HARDEN_DESKTOP` turns it off unless you set it yourself. Documents go to the page holding the session, shared viewers receive none | -| `SELKIES_PRINT_SPOOL_PATH` | `~/.local/state/selkies/print` | Directory finished print jobs land in as PDFs until a page takes them | +| `SELKIES_PRINT_SPOOL_PATH` | `''` | Directory finished print jobs land in as PDFs until a page takes them. Empty uses `selkies/print` under `XDG_STATE_HOME`, `/config/.local/state/selkies/print` in the containers | | `SELKIES_AUDIT_WEBHOOK_URL` | `''` | URL that receives one JSON POST per audit event. Metadata only, never content. Empty is off, and a queue of 1024 pending events drops on overflow with no retry | | `SELKIES_AUDIT_WEBHOOK_TOKEN` | `''` | Bearer token sent in the `Authorization` header of every audit POST | | `SELKIES_AUDIT_WEBHOOK_TIMEOUT` | `2.0` | Seconds one audit POST may take before it counts as failed and the next one is sent | ### Sharing +Without a master token the sharing section hands out view only and player links, and a second page opened on the session's own address joins as a controller beside the first rather than taking it over. With a master token the session is in secure mode: every page presents a provisioned token that carries its role, and the fragments are ignored. See [Security](security.md#sharing-links). + | Variable | Default | Description | | --- | --- | --- | -| `SELKIES_ENABLE_SHARING` | `true` | Master toggle for all sharing features | +| `SELKIES_ENABLE_SHARING` | `true` | Master toggle for all sharing features. Off, one page holds the session and a second one takes it over instead of joining | | `SELKIES_ENABLE_COLLAB` | `true` | Let a viewer holding the session's master key token act as a read write collaborator. Secure mode only, this is not a sharing link of its own | | `SELKIES_ENABLE_SHARED` | `true` | View only sharing links | -| `SELKIES_ENABLE_PLAYER2` | `true` | Sharing link for gamepad player 2. Forced off by `NO_GAMEPAD` | -| `SELKIES_ENABLE_PLAYER3` | `true` | Sharing link for gamepad player 3. Forced off by `NO_GAMEPAD` | -| `SELKIES_ENABLE_PLAYER4` | `true` | Sharing link for gamepad player 4. Forced off by `NO_GAMEPAD` | -| `SELKIES_MASTER_TOKEN` | `''` | Master token that enables secure mode and protects the token control plane API, used by [SealSkin](../components/sealskin.md) | +| `SELKIES_ENABLE_PLAYER2` | `true` | Sharing link for gamepad player 2. Locked off by `NO_GAMEPAD` | +| `SELKIES_ENABLE_PLAYER3` | `true` | Sharing link for gamepad player 3. Locked off by `NO_GAMEPAD` | +| `SELKIES_ENABLE_PLAYER4` | `true` | Sharing link for gamepad player 4. Locked off by `NO_GAMEPAD` | +| `SELKIES_MASTER_TOKEN` | `''` | Master token that enables secure mode and authorizes the token control plane (`POST /api/tokens`), used by [SealSkin](../components/sealskin.md) | +| `SELKIES_TOKENS_FILE` | `''` | JSON file holding the session token table in the body format of `POST /api/tokens`, provisioned at start and again whenever it changes, so a restart needs no POST. Root or the server's user must own it and no one else may write to it | | `SELKIES_BASIC_AUTH_VIEWONLY_PASSWORD` (or `VIEWONLY_PASSWORD`) | `''` | Optional second basic auth password that grants view only access when the Selkies server's own basic auth is on. Ignored in secure mode | ### Client UI visibility | Variable | Default | Description | | --- | --- | --- | -| `SELKIES_UI_TITLE` | `Selkies` | Title in the top left corner of the sidebar | +| `SELKIES_UI_TITLE` | `Selkies` | Title in the top left corner of the sidebar, and of the browser tab when set | | `SELKIES_UI_SHOW_LOGO` | `true` | Show the Selkies logo in the sidebar | | `SELKIES_UI_SHOW_SIDEBAR` | `true` | Show the main sidebar UI | | `SELKIES_UI_SHOW_CORE_BUTTONS` | `true` | Show the display, audio, microphone, webcam, and gamepad core buttons | +| `SELKIES_UI_SHOW_CONNECTION_INDICATOR` | `true` | Show the poor connection mark over the stream while the server finds the client's connection losing frames, with or without the sidebar | | `SELKIES_UI_SIDEBAR_SHOW_VIDEO_SETTINGS` | `true` | Video settings section | | `SELKIES_UI_SIDEBAR_SHOW_SCREEN_SETTINGS` | `true` | Screen settings section | | `SELKIES_UI_SIDEBAR_SHOW_AUDIO_SETTINGS` | `true` | Audio settings section | @@ -283,7 +303,7 @@ The session has a printer named `Selkies`. Selkies runs the CUPS queue itself as | `SELKIES_UI_SIDEBAR_SHOW_SHORTCUTS` | `true` | Keyboard shortcuts section | | `SELKIES_UI_SIDEBAR_SHOW_CLIPBOARD` | `true` | Clipboard section | | `SELKIES_UI_SIDEBAR_SHOW_FILES` | `true` | File transfer section. `HARDEN_DESKTOP` hides it | -| `SELKIES_UI_SIDEBAR_SHOW_APPS` | `true` | Applications section. `HARDEN_DESKTOP` hides it | +| `SELKIES_UI_SIDEBAR_SHOW_APPS` | `true` | Applications section. `HARDEN_DESKTOP` hides it, and the session hides it on its own where the host denies `ptrace`, which proot needs | | `SELKIES_UI_SIDEBAR_SHOW_SHARING` | `true` | Sharing section | | `SELKIES_UI_SIDEBAR_SHOW_GAMEPADS` | `true` | Gamepads section. `NO_GAMEPAD` hides it | | `SELKIES_UI_SIDEBAR_SHOW_WEBCAM` | `true` | Webcam toggle among the core buttons. Hides the control only, `SELKIES_WEBCAM_ENABLED` governs whether the server accepts frames | @@ -295,13 +315,15 @@ The session has a printer named `Selkies`. Selkies runs the CUPS queue itself as ### Server and diagnostics -The container's built in Nginx owns the listening ports, TLS, basic auth, and the `SUBFOLDER` prefix, and the init scripts hand the matching values to Selkies. Use the container variables (`CUSTOM_PORT`, `CUSTOM_HTTPS_PORT`, `CUSTOM_WS_PORT`, `CUSTOM_USER`, `PASSWORD`, `SUBFOLDER`) rather than the Selkies server's own listener, HTTPS, and basic auth settings, which exist for running the `selkies` binary outside these containers. +The container's built in Nginx owns the listening ports, TLS, basic auth, and the `SUBFOLDER` prefix, and the init scripts hand the matching values to Selkies. Use the container variables (`CUSTOM_PORT`, `CUSTOM_HTTPS_PORT`, `CUSTOM_WS_PORT`, `CUSTOM_USER`, `PASSWORD`, `SUBFOLDER`) rather than the Selkies server's own listener (`SELKIES_ADDR`, `SELKIES_PORT`, `SELKIES_UNIX_SOCKET`), HTTPS, and basic auth settings, which exist for running the `selkies` binary outside these containers. | Variable | Default | Description | | --- | --- | --- | | `SELKIES_ALLOWED_ORIGINS` | `*` (upstream same origin) | Comma separated browser Origins allowed to open the streaming WebSocket, a cross site WebSocket hijacking guard. Relaxed in the containers because Nginx fronts the server | +| `SELKIES_FRAME_ANCESTORS` | `'self'` | Comma separated pages allowed to show the client in a frame, sent as the Content-Security-Policy `frame-ancestors` directive. `'self'` admits the container's own origin only, an origin such as `https://portal.example.com` admits a portal embedding the desktop from elsewhere, `*` any page, `'none'` no frame at all. A page the list does not name gets an empty frame | +| `SELKIES_SUBFOLDER` (or `SUBFOLDER`) | `''` | URL path prefix the server is proxied under, prepended to every route. The container's Nginx reads the same `SUBFOLDER`, so set that one | | `SELKIES_PUBLIC` | `false` | Listen on every interface instead of loopback. Leave it alone in the containers, Nginx is the public face and the server refuses to start when both this and `SELKIES_ADDR` are set | -| `SELKIES_ENABLE_METRICS_HTTP` | `false` | Prometheus metrics endpoint on the Selkies server | +| `SELKIES_ENABLE_METRICS_HTTP` | `false` | Prometheus metrics endpoint at `/api/metrics` on the Selkies server, behind the session's login | | `SELKIES_ENABLE_WEBRTC_STATISTICS` | `false` | Dump WebRTC statistics CSVs from the client | | `SELKIES_WEBRTC_STATISTICS_DIR` | `/tmp` | Directory for those CSVs, `selkies-stats-video-[timestamp].csv` and `selkies-stats-audio-[timestamp].csv` | @@ -313,7 +335,8 @@ The container's built in Nginx owns the listening ports, TLS, basic auth, and th | `SELKIES_APP_WAYLAND_DISPLAY` | `''` | Wayland socket applications run on when it differs from the capture compositor, for a nested session. Empty auto detects | | `SELKIES_WAYLAND_HOST_DISPLAY` | `''` | Socket of an external compositor (labwc started headless, for example) that pixelflux captures and injects into as a client instead of compositing itself. Empty keeps the built in compositor | | `SELKIES_WAYLAND_SOCKET_INDEX` | `0` | Index for the Wayland command socket, `0` is `wayland-0` | -| `SELKIES_COMPUTER_USE_BIND` | `''` | Start pixelflux's Computer Use HTTP server on comma separated entries, a bare port listens on loopback only, `host:port` names the address. `PIXELFLUX_CU` remains the standalone fallback | +| `SELKIES_COMPUTER_USE_BIND` | `''` | Start pixelflux's Computer Use HTTP server on comma separated entries, a bare port listens on loopback only, which every account in the container can reach, `host:port` names the address. A caller drives the desktop with the session user's full authority, so the server needs `SELKIES_COMPUTER_USE_TOKEN` and does not start without it. `PIXELFLUX_CU` remains the standalone fallback | +| `SELKIES_COMPUTER_USE_TOKEN` (or `PIXELFLUX_CU_TOKEN`) | `''` | Bearer token every Computer Use request has to carry as `Authorization: Bearer `. Never sent to clients | ### Lifecycle hooks @@ -328,7 +351,7 @@ The container's built in Nginx owns the listening ports, TLS, basic auth, and th | Variable | Default | Description | | --- | --- | --- | -| `SELKIES_WATERMARK_PATH` (or `WATERMARK_PNG`) | `''` | Absolute path to a watermark PNG | +| `SELKIES_WATERMARK_PATH` (or `WATERMARK_PNG`) | `''` | Absolute path to a watermark PNG. On the X11 DRI3 path the X server composites it, so it costs no readback | | `SELKIES_WATERMARK_LOCATION` (or `WATERMARK_LOCATION`) | `-1` | Watermark location enum 0 to 6 | ### WebRTC networking, STUN, and TURN diff --git a/docs/selkies/user-guide/gpu.md b/docs/selkies/user-guide/gpu.md index d0e5ceb40..c5daf1b01 100644 --- a/docs/selkies/user-guide/gpu.md +++ b/docs/selkies/user-guide/gpu.md @@ -138,7 +138,11 @@ If you notice blurry text, especially light text on dark backgrounds, enable **F FullColor is carried by H.264 and H.265 on NVENC, x264, and x265, and by VP9 profile 1 on VA-API and libvpx. VP8, AV1, and the OpenH264 software encoder stay 4:2:0 whatever you set, and the sidebar only shows the switch on a codec that carries it. -The caveat is H.264 on Intel and AMD. Current VA-API drivers expose no 4:4:4 H.264 profile, so pixelflux honors the request on the CPU with x264 rather than silently downgrading, which forces a pixel readback from the GPU and costs significant performance. On those cards prefer the default 4:2:0 for motion and let paint over handle static clarity, or try H.265 or VP9, where the driver may negotiate a 4:4:4 surface and stay on the card. The log line `Colorspace:` reports what the session settled on. +The caveat is H.264 on Intel and AMD. Current VA-API drivers expose no 4:4:4 H.264 profile, so pixelflux honors the request on the CPU with x264 rather than silently downgrading, which forces a pixel readback from the GPU and costs significant performance. On those cards prefer the default 4:2:0 for motion and let paint over handle static clarity, or try H.265 or VP9, where the driver may negotiate a 4:4:4 surface and stay on the card. The log line `Colorspace:` reports what the session settled on. The server knows before a frame is sent which of its encoders carry 4:4:4 on this host, and the browser reports which profiles it decodes, so a full color stream only goes where both ends carry it and the session says so when it streams 4:2:0 instead. + +### 10-bit + +`SELKIES_VIDEO_10BIT` asks for 10 bits per sample. NVENC carries it for H.265 and AV1, VA-API for H.265, VP9, and AV1, and the software encoders for everything but VP8 and JPEG, x264 included. A GPU without the format hands the session to the software encoder, as it does for a 4:4:4 it lacks, and a software encoder pays for the conversion and the high bit depth coding in CPU. The desktop is an 8 bit source, so the gain is precision in the color conversion rather than new shades: pair it with FullColor 4:4:4 on a card that encodes the format, or leave it off. ## Wayland and X11 @@ -146,6 +150,8 @@ Zero copy works on both display stacks. On Wayland the pixelflux compositor rend The same rule applies on either stack: rendering and encoding must happen on the same device for zero copy. A session the DRI3 path cannot serve, for example software encoding or a codec the GPU has no engine for, streams through shared memory capture instead, and the log line says why. +Two more things the containers' Xvfb does for GPU sessions: it fakes its vblank at the rate the session is captured at, so an application that waits for vsync presents one frame per streamed frame instead of being held to 60, and on Nvidia it signals a GPU semaphore after each capture blit that the NVENC encode waits on, so a GPU bound game is encoded without the CPU learning of the blit first. + ## Related environment variables | Variable | Description | diff --git a/docs/selkies/user-guide/installing-apps.md b/docs/selkies/user-guide/installing-apps.md index 07d353c00..cc0aead4b 100644 --- a/docs/selkies/user-guide/installing-apps.md +++ b/docs/selkies/user-guide/installing-apps.md @@ -23,10 +23,13 @@ proot-apps remove filezilla The list of supported applications is maintained in the [proot-apps README](https://github.com/linuxserver/proot-apps?tab=readme-ov-file#supported-apps). -Two things to know: +Three things to know: - Applications are mostly ingested from the Alpine repositories for maintainability, so what you get is the Alpine build of an app regardless of the flavor of the container you run it in. - **Nvidia GPUs are not supported inside proot-apps.** If an app needs Nvidia acceleration, run it as its own dedicated image or bake it into a custom one. +- proot traces every process it starts, so on a host that denies `ptrace` nothing can be installed or launched. Selkies checks at startup and hides the sidebar's apps section rather than showing it failing. + +For a fleet without internet access, or one that should only offer a curated set, point the session at a local repository: fill a folder with `proot-apps localrepo`, mount it read only, and set `PA_REPO_FOLDER` on the container. The sidebar's apps section then lists that folder's catalog instead of the remote one and installs from it, and `proot-apps` in a terminal reads the same folder. The [proot-apps administrator guide](https://github.com/linuxserver/proot-apps#for-administrators) covers building the folder. ## Native packages via Docker mods (non persistent) diff --git a/docs/selkies/user-guide/reverse-proxy.md b/docs/selkies/user-guide/reverse-proxy.md index 0ac7673db..223efa23f 100644 --- a/docs/selkies/user-guide/reverse-proxy.md +++ b/docs/selkies/user-guide/reverse-proxy.md @@ -50,7 +50,7 @@ To serve a container under a path like `https://example.org/desktop/`, set the ` - SUBFOLDER=/desktop/ ``` -Both slashes are required. Then proxy the location through: +Both slashes are required. The container's Nginx and the Selkies server both read it, so every route lands under the prefix. Then proxy the location through: ```nginx location /desktop/ { @@ -75,3 +75,4 @@ If you are heading toward many users and many apps, that is exactly what [SealSk - Server side events and the streaming WebSocket dislike buffering proxies, always disable response buffering (`proxy_buffering off` or your proxy's equivalent). - If the client loads but you get a black screen or no video, it is almost always a blocked WebSocket upgrade or an HTTP (not HTTPS) page context. - Enabling the [WebRTC transport](webrtc.md) changes nothing at the proxy. Its signaling goes through the same proxied web port, and its media goes around the proxy entirely over UDP. +- A portal that embeds the desktop in a frame on another origin is refused by default: Selkies sends `frame-ancestors 'self'`. List the portal's origin in `SELKIES_FRAME_ANCESTORS`, see [Security](security.md#embedding-and-origins). diff --git a/docs/selkies/user-guide/security.md b/docs/selkies/user-guide/security.md index 84667db62..86ac46b32 100644 --- a/docs/selkies/user-guide/security.md +++ b/docs/selkies/user-guide/security.md @@ -30,6 +30,10 @@ Modern browser features the client depends on, WebCodecs for video and audio in - Mounting `/var/run/docker.sock` hands the container control of the host Docker daemon, which is root on the host in practical terms. Only do this deliberately. - `--security-opt seccomp=unconfined` disables a key Docker security layer. Use it only when a legacy host kernel or old libseccomp genuinely requires it, and treat it as a temporary workaround. +## Embedding and origins + +Two settings decide which web pages may reach a session at all. `SELKIES_ALLOWED_ORIGINS` is the cross site WebSocket hijacking guard on the streaming socket; the containers relax it to `*` because Nginx fronts the server, so tighten it to your own origin when the container is reachable from the open internet without a proxy. `SELKIES_FRAME_ANCESTORS` is the Content-Security-Policy `frame-ancestors` directive, `'self'` by default, so another site cannot lay the session under clicks meant for itself. A portal that embeds the desktop in a frame from another origin lists that origin there. Both are in the [Configuration Reference](configuration.md#server-and-diagnostics). + ## Hardening variables For kiosk deployments, single app terminals, classrooms, or any situation where the person at the keyboard is not the administrator, the baseimage ships lockdown variables. @@ -93,13 +97,17 @@ Locking the sidebar off entirely (`SELKIES_UI_SHOW_SIDEBAR=false|locked`) is the ## Sharing links -The sharing feature generates links that grant access to the running session: full collaboration, view only, or gamepad only for players 2 through 4. The built in links are a **demo of the multi user protocol**, they carry no authentication of their own. Proper shared sessions need the token based access control an orchestrator like [SealSkin](../components/sealskin.md) wires up. Remember that anyone with a collaboration link has the same power as the primary user, including that passwordless sudo terminal. Disable what you do not need: +The sharing feature generates links that grant access to the running session: view only, or gamepad only for players 2 through 4. A second page opened on the session's plain address joins as a full controller beside the first. The built in links are a **demo of the multi user protocol**, they carry no authentication of their own beyond whatever guards the page, so anyone with the address has the same power as the primary user, including that passwordless sudo terminal. Disable what you do not need: ```bash --e SELKIES_ENABLE_SHARING=false # master switch --e SELKIES_ENABLE_COLLAB=false # or individually +-e SELKIES_ENABLE_SHARING=false # one page holds the session, a second takes it over +-e SELKIES_ENABLE_SHARED=false # or drop individual links ``` +Proper shared sessions need **secure mode**: set `SELKIES_MASTER_TOKEN` and the server ignores the fragments and admits only pages presenting a provisioned token that carries its own role and gamepad slot. The token table is posted to `/api/tokens` with the master token, or read from a file named by `SELKIES_TOKENS_FILE`. [SealSkin](../components/sealskin.md) is the reference orchestrator for it; the upstream [Secure Mode](https://docs.selkies.io/secure-mode) page has the wire details. + ## Pelorus and the computer use API Enabling `PELORUS=true` starts an agent control API inside the container and forces the accessibility bridge on for all applications, which means any process in the session can read the full UI text of every app. The Pelorus API itself has no authentication and relies on the container's Nginx and network isolation. Treat a Pelorus enabled container as fully readable and controllable by anything that can reach it on its internal port, and never publish its ports directly. Details on the [Pelorus](../components/pelorus.md) page. + +The pixelflux Computer Use server underneath drives the desktop with the session user's full authority. It requires a bearer token (`SELKIES_COMPUTER_USE_TOKEN`, or `PIXELFLUX_CU_TOKEN` for the standalone `PIXELFLUX_CU` port) and refuses to start without one, and a bare port binds loopback only, which every account inside the container can still reach. diff --git a/docs/selkies/user-guide/troubleshooting.md b/docs/selkies/user-guide/troubleshooting.md index 096fbda61..373e305c5 100644 --- a/docs/selkies/user-guide/troubleshooting.md +++ b/docs/selkies/user-guide/troubleshooting.md @@ -29,11 +29,14 @@ This loop resolves the majority of reports, particularly GPU and reverse proxy i | Choppy video during motion | CPU limited or bandwidth limited | Check the Stats section, lower FPS or raise CRF, try Turbo mode off, consider a [GPU](gpu.md) | | CPU pegged after picking H.265, VP9, or AV1 | The GPU has no engine for that codec, so it encodes full frame in software | Go back to H.264, or check `vainfo` and `nvidia-smi` for what the card encodes, see [which codecs a GPU encodes](gpu.md#which-codecs-a-gpu-encodes) | | An encoder you configured is missing from the sidebar | Neither the encoding GPU nor the software build serves it, or the browser cannot decode it | The log line `Encoders not served on this host` lists the first case, a greyed out `(Unsupported Browser)` entry is the second | -| Sidebar shows H.264 though the default is another codec | The browser cannot decode the default, or pixelflux demoted the codec at capture start | Expected. The log line `no encoder served` marks a demotion, otherwise the browser stepped to a codec it decodes | +| Sidebar shows a different codec than the default | The browser cannot decode the default, or pixelflux demoted the codec at capture start | Expected. Both step down the same ladder, hardware codecs first, then software, then JPEG. The log line `no encoder served` marks a demotion, otherwise the browser stepped to a codec it decodes | +| A poor connection mark sits over the stream | Frames are lost or queued on the link to that browser | Lower the bitrate or frame rate, and run the session on CBR so congestion control has a target to steer. The mark judges a moving screen only | +| CPU jumps after turning on 10-bit | A software encoder converts every frame to 10-bit samples and codes at high bit depth | Pair 10-bit with FullColor on a GPU that encodes the format, or leave it off | | Cursor feels laggy in games | Absolute pointer mode | Use Gaming mode (pointer lock) | | Gamepad not detected in app | App does not use the joystick API path | Some apps are incompatible with the userspace interposer; try another input mode in app, or file an issue | | Files will not upload | Proxy body size limit | Raise `client_max_body_size` at your proxy | -| Auth prompt not appearing | `PASSWORD` unset | Both `CUSTOM_USER` and `PASSWORD` behavior: no password means no auth | +| Auth prompt not appearing | `PASSWORD` unset | No password means no auth, `CUSTOM_USER` alone does nothing | +| Apps section missing from the sidebar | `HARDEN_DESKTOP`, or the host denies `ptrace`, which proot needs | Expected. Selkies checks at startup and hides the panel rather than showing it failing | | Volume permission errors in `/config` | PUID and PGID mismatch | Set them to your host user's `id` values | ## GPU problems @@ -52,7 +55,7 @@ Work through the ladder in [GPU Acceleration](gpu.md#debugging-gpu-problems). Su docker logs -f ``` -The startup sequence prints the s6 service initialization, GPU detection results, and the mode selection (Wayland vs X11). The pixelflux layer logs its encoding decision, look for lines telling you whether the zero copy path or a readback path was chosen, and which encoder is active. On X11 it also says whether it captures through DRI3 or shared memory, and why when it declined DRI3. +The startup sequence prints the s6 service initialization, GPU detection results, and the mode selection (Wayland vs X11). The pixelflux layer logs its encoding decision, look for lines telling you whether the zero copy path or a readback path was chosen, and which encoder is active. On X11 it also says whether it captures through DRI3 or shared memory, and why when it declined DRI3. The sidebar's stats section shows the same facts to the user, so a report can name the capture path and encoder without the logs. For deeper debugging set `-e SELKIES_DEBUG=true`. diff --git a/docs/selkies/user-guide/web-client.md b/docs/selkies/user-guide/web-client.md index 618ee5d1f..8f2b81c00 100644 --- a/docs/selkies/user-guide/web-client.md +++ b/docs/selkies/user-guide/web-client.md @@ -8,12 +8,13 @@ Toggle the sidebar with its handle on screen. At the top you get the core toggle ## Video settings -- **Encoder**: H.264, H.265, VP8, VP9, AV1, or JPEG. Each video codec is encoded on the GPU when the card has an engine for it and in software otherwise, so the menu only lists what this host can serve. H.264 is the default and right for everything modern, with FullColor 4:4:4 plus paint over it is visually equal to lossless stills. Entries your browser cannot decode are greyed out as `(Unsupported Browser)`, H.265 outside Safari being the usual one. JPEG exists for browsers that cannot decode video frames at all. See the [Video encoders](configuration.md#video-encoders) table for what each codec needs. -- **Frames per second** and **CRF** (quality, lower is better) sliders, plus separate paint over quality controls. "Paint over" is the platform's signature feature: after the screen goes still, it is repainted at high quality so text stays crisp. The CRF slider means the same quality on every codec. -- **FullColor 4:4:4**: true 8 bit color with no chroma subsampling. Fixes blurry colored text. Offered on H.264, H.265, and VP9 only. Note the [GPU caveats](gpu.md#fullcolor-444-and-hardware-encoders). +- **Encoder**: H.264, H.265, VP8, VP9, AV1, or JPEG. Each video codec is encoded on the GPU when the card has an engine for it and in software otherwise, so the menu only lists what this host can serve. H.264 is the default and right for everything modern, with FullColor 4:4:4 plus paint over it is visually equal to lossless stills. Entries your browser cannot decode are greyed out as `(Unsupported Browser)`, H.265 outside Safari being the usual one, and a browser that cannot play the default steps down to the next codec it decodes. JPEG exists for browsers that cannot decode video frames at all. See the [Video encoders](configuration.md#video-encoders) table for what each codec needs. +- **Frames per second** and **CRF** (quality, lower is better) sliders, plus separate paint over quality controls. The frame rate slider also offers your own display's refresh rate. "Paint over" is the platform's signature feature: after the screen goes still, it is repainted at high quality so text stays crisp. The CRF slider means the same quality on every codec. +- **FullColor 4:4:4**: true 8 bit color with no chroma subsampling. Fixes blurry colored text. Offered on H.264, H.265, and VP9 only, and only where your browser decodes that codec's 4:4:4 profile. Note the [GPU caveats](gpu.md#fullcolor-444-and-hardware-encoders). +- **10-bit**: 10 bits per sample where the codec, the encoder, and your decoder all carry it. The desktop is an 8 bit source, so it adds precision to the color conversion rather than new shades, costs CPU on a software encoder, and is only worth pairing with FullColor. - **Turbo mode** (streaming mode): disables all the damage tracking logic and encodes every frame like a traditional video stream. Can be useful for gaming and full motion video. -- **CPU encoding** toggle to force software encoding. It only appears when the current codec is available both on the GPU and in software on this host. -- **Rate control**: CRF (constant quality, the default on WebSockets) or CBR with a bitrate slider (the default on WebRTC). +- **CPU encoding** toggle to force software encoding. It only appears when the current codec is available both on the GPU and in software on this host. While the session encodes in software a **multithreading** switch appears beside it, which an administrator turns off on hosts that pack many sessions onto their cores. +- **Rate control**: CRF (constant quality, the default in the containers on both transports) or CBR with a bitrate slider. Under CBR, congestion control steers the bitrate to what your link carries, and the paint over switch disappears because a still screen is cleaned up regardless. - **Transport**: when the administrator has enabled WebRTC, a switch lets you move between WebSockets and WebRTC at runtime, see [WebRTC Transport](webrtc.md). !!! tip "On a slow machine or a slow link, turn the stream down" @@ -24,7 +25,7 @@ Toggle the sidebar with its handle on screen. At the top you get the core toggle - **Resolution**: by default the remote resolution follows your browser window exactly. You can instead pick a preset (720p through 4K) or type a manual width and height. - **UI scaling (DPI)** for HiDPI displays, with a choice between pixel perfect HiDPI rendering and CSS scaling (lighter on bandwidth, softer image). - **Scale locally** stretches a fixed remote resolution to fit your window. -- **Add Screen +**: opens a second browser window that becomes a second monitor for the session, positioned left, right, above, or below the primary. Arrange each window on the matching physical monitor and you have a real dual screen remote desktop. Always available on X11. On Wayland, Selkies detects at startup whether the running compositor supports it, which today means labwc (the single app containers and the default Webtop shell) and KWin (the KDE Webtops), and only offers the button when it does. +- **Add Screen +**: opens a second browser window that becomes a second monitor for the session, positioned left, right, above, or below the primary. Arrange each window on the matching physical monitor and you have a real dual screen remote desktop. Always available on X11, where the extra display is plugged in as a monitor of its own. On Wayland, Selkies detects at startup whether the running compositor supports it, which today means labwc (the single app containers and the default Webtop shell) and KWin (the KDE Webtops), and only offers the button when it does. ## Audio and microphone @@ -32,11 +33,11 @@ Opus audio streams from the session to your browser, and output device selection ## Webcam -The webcam button forwards your local camera into the session as a virtual V4L2 device, so video conferencing inside a remote browser or any camera aware application works. The administrator can size the virtual device and disable the feature entirely with `NO_WEBCAM`. +The webcam button forwards your local camera into the session as a virtual V4L2 device, so video conferencing inside a remote browser or any camera aware application works. The administrator can size the virtual device, have the browser ask for the camera only while an application in the session actually opens it (`SELKIES_WEBCAM_ON_START=demand`), and disable the feature entirely with `NO_WEBCAM`. ## Clipboard -Bidirectional clipboard sync between your machine and the session, automatic in both directions for text. The sidebar shows an editable view of the server clipboard. **Image support** (binary clipboard, `SELKIES_ENABLE_BINARY_CLIPBOARD`) adds images and other binary formats. Clipboard direction and availability can be locked down by the admin with the `SELKIES_ENABLE_CLIPBOARD` policy. +Bidirectional clipboard sync between your machine and the session, automatic in both directions for text. Formatted content keeps its formatting, and travels with its plain text so a terminal paste gets text rather than markup. The sidebar shows an editable view of the server clipboard, an **Upload Image** button that puts a picture file on the session's clipboard, and three switches: send to session, receive from session, and **Seamless**, which is what makes every copy follow automatically. Off, content only moves through the clipboard box when you ask. **Image support** (binary clipboard, `SELKIES_ENABLE_BINARY_CLIPBOARD`) adds images and other binary formats, and rich text from the browser follows the same switch. A copy a password manager marks as sensitive stays out of the sidebar view and off your local clipboard. Clipboard direction and availability can be locked down by the admin with the `SELKIES_ENABLE_CLIPBOARD` policy. ## Files @@ -47,7 +48,7 @@ Transfers can be restricted per direction or disabled entirely with `SELKIES_FIL ## Gamepads -Plug and play controller support for PlayStation, Xbox, and Switch pads, with mappings derived from the SDL game controller database. Inside the session, apps see a standard Xbox 360 style device provided by a userspace interposer, no kernel modules or privileged devices involved. Up to four controllers are supported, and the sharing feature can hand player 2 through 4 slots to remote friends. A configurable **touch gamepad** overlay provides on screen controls for phones and tablets. +Plug and play controller support for PlayStation, Xbox, and Switch pads, with mappings derived from the SDL game controller database. Inside the session, apps see a standard Xbox 360 style device provided by a userspace interposer, no kernel modules or privileged devices involved; where `/dev/uinput` is writable, on a privileged container for example, Selkies registers real kernel devices instead. Up to four controllers are supported, and the sharing feature can hand player 2 through 4 slots to remote friends. A game's rumble plays back on your controller through the browser, with a **Rumble** toggle in the gamepad section to turn it off. A configurable **touch gamepad** overlay provides on screen controls for phones and tablets, and vibrates on phones that allow it. ## Sharing and collaboration @@ -55,11 +56,10 @@ The Sharing section generates links for other people to join your running sessio | Link | Capability | | --- | --- | -| Collaboration | Full interactive control alongside you | | View only | Watch the session, no input | | Player 2, 3, 4 | Gamepad input only, for couch co-op over the internet | -Anyone with a collaboration link has full control, treat these links like credentials. Admins can disable any of them, see [Security](security.md#sharing-links). +A second page opened on the session's own address, another tab or another browser, joins as a controller beside the first rather than taking it over: both stream and both drive input, and a stream setting picked in either applies to both. Each link is the page's own address with a fragment, so whatever guards the page guards the link. Anyone with the plain address has full control, treat it like a credential. Admins can disable any of this, see [Security](security.md#sharing-links). !!! warning "The built in sharing links are a demo" Out of the box these links carry no real authentication of their own, they are a demonstration of the underlying multi user protocol. For proper shared sessions, the token based access control that [SealSkin](../components/sealskin.md) wires up is the intended path: named participants, revocable access, and per user permissions. Use the raw links only on networks you trust. @@ -78,9 +78,9 @@ If these menus seem missing, the administrator has likely [hardened](security.md On phones and tablets the client offers: -- **Trackpad mode**: the screen becomes a laptop style trackpad with relative cursor movement, tap to click, tap and hold to drag, and two finger scrolling. Ideal for desktop apps on a phone. -- **Direct touch mode**: taps map straight to absolute clicks, best for touch friendly apps. -- The **on screen keyboard** button raises your device keyboard, with IME input (for example Chinese Pinyin) supported end to end. +- **Trackpad mode**: the screen becomes a laptop style trackpad with relative cursor movement, tap to click, tap and hold to drag, two finger scrolling, two finger tap to right click, and three finger tap to middle click. The pointer accelerates with the finger's speed, with a speed picker beside the touch controls. Ideal for desktop apps on a phone. +- **Direct touch mode**: taps map straight to absolute clicks, a long press right clicks, and a pinch reaches the session as Ctrl+wheel, best for touch friendly apps. +- The **on screen keyboard** button raises your device keyboard, with IME input (for example Chinese Pinyin) supported end to end. Soft modifier keys sit beside it, and **More keys** opens a palette of what a phone keyboard lacks: function keys, arrows, Home and End, and chords such as Alt+Tab or Ctrl+Alt+Del, plus any chord you type into it. ## macOS keyboards @@ -88,7 +88,7 @@ Command chords are sent as Control by default, so Cmd+C and Cmd+V work as they d ## Printing -Every session has a printer named **Selkies**. Print to it from any application and the document arrives in your browser as a PDF, where the browser's own print dialog offers whatever printers your local machine can reach. The sidebar's printing section lists each document so it can be printed again or saved as a PDF, its **Print automatically** switch is what opens the dialog on arrival. Documents go to the page holding the session, shared viewers do not receive them. Administrators turn the feature off with `SELKIES_PRINTING_ENABLED=false`, and `HARDEN_DESKTOP` does so by default. +Every session has a printer named **Selkies**. Print to it from any application and the document arrives in your browser as a PDF, where the browser's own print dialog offers whatever printers your local machine can reach. The sidebar's printing section lists each document so it can be printed again or saved as a PDF, its **Print automatically** switch is what opens the dialog on arrival. On a phone or tablet, where no browser prints a PDF from a frame, each document raises a notice with an **Open** link into a new tab instead. The queue prints on A4 unless the container sets `PAPERSIZE`. Documents go to the page holding the session, shared viewers do not receive them. Administrators turn the feature off with `SELKIES_PRINTING_ENABLED=false`, and `HARDEN_DESKTOP` does so by default. ## Gaming mode @@ -96,7 +96,7 @@ Gaming mode grabs the pointer (pointer lock) and sends relative mouse movement f ## Stats -Live overlay of client FPS, round trip latency, bandwidth, video bitrate, CPU, memory, and GPU utilization of the server. Your first stop when something feels off, before diving into [Troubleshooting](troubleshooting.md). +Live overlay of client FPS, round trip latency, bandwidth, video bitrate against its target, frame timing from capture to the wire, and the CPU, memory, and GPU utilization of the server's own container. It also names the capture path and the encoder the session runs on, and why a faster path was declined. Independent of the sidebar, a **poor connection** mark appears in the corner of the stream while the server is losing frames on the way to your browser. Your first stop when something feels off, before diving into [Troubleshooting](troubleshooting.md). ## Apps section diff --git a/docs/selkies/user-guide/webrtc.md b/docs/selkies/user-guide/webrtc.md index 9f1406cbe..7b499f6bd 100644 --- a/docs/selkies/user-guide/webrtc.md +++ b/docs/selkies/user-guide/webrtc.md @@ -7,13 +7,13 @@ Selkies streams over WebSockets by default, and for most people that is the righ ## When to use it -WebRTC helps when the path between the browser and the container is lossy or has variable latency, typically a mobile network, a long haul link, or a wireless client on a congested network. UDP does not stall on a lost packet the way TCP does, the audio pacer keeps sound and input responsive while video is squeezed, and congestion control can adapt the bitrate to what the link actually delivers. +WebRTC helps when the path between the browser and the container is lossy or has variable latency, typically a mobile network, a long haul link, or a wireless client on a congested network. UDP does not stall on a lost packet the way TCP does, the audio pacer keeps sound and input responsive while video is squeezed, and congestion control reads the receiver's own loss and delay feedback. A lost frame is repaired before it costs a keyframe: the server adds FlexFEC repair packets as a peer's loss rises, and the encoder predicts past a frame a peer lost. The trade offs: - **Only the full frame codecs stream over WebRTC.** H.264, H.265, VP8, VP9, and AV1 all ride the RTP track. The striped H.264 and JPEG encoders are WebSocket only, and the encoder menu is narrowed to what WebRTC can carry while it is the active transport. The previous choice is restored when switching back. -- **The browser's RTP receiver decides the codec.** H.264 and VP8 are taken by every major browser, VP9 and AV1 by Chromium and Firefox, H.265 by Safari and by Chromium where the operating system decodes it. A browser that declines the codec is answered with H.264 when `h264enc` is in the menu, otherwise that browser gets no video. -- **Rate control defaults to CBR.** A congestion controlled transport needs the encoder holding a bandwidth target, so `SELKIES_VIDEO_BITRATE` becomes the setting you tune rather than CRF. Pin `SELKIES_RATE_CONTROL_MODE` if you want otherwise. +- **The browser's RTP receiver decides the codec.** H.264 and VP8 are taken by every major browser, VP9 and AV1 by Chromium and Firefox, H.265 by Safari and by Chromium where the operating system decodes it. A browser that declines the codec answers with the next encoder down the [ladder](configuration.md#video-encoders) it decodes, and the display moves to that encoder for every viewer. A single valued locked encoder the browser cannot play leaves it without video. +- **Pick CBR for a lossy link.** Congestion control is on by default and steers a CBR stream on both transports, but the containers keep CRF as the default rate control. Switch to CBR in the sidebar, or set `SELKIES_RATE_CONTROL_MODE=cbr,crf`, and `SELKIES_VIDEO_BITRATE` becomes the setting you tune. Upstream defaults to CBR. - **UDP has to get through.** A reverse proxy alone is not enough. Media takes a direct path from the container to the browser, and something has to make that path reachable: forwarded ports, a public address, or a TURN relay. - **Turbo mode and paint over still apply.** Damage tracking and paint over work on both transports. @@ -22,7 +22,7 @@ The trade offs: The baseimage keeps WebRTC hidden until you configure it. Set `SELKIES_MODE=webrtc`, or any WebRTC, STUN, TURN, or Cloudflare variable from the [configuration reference](configuration.md#webrtc-networking-stun-and-turn), and the container: 1. Sets `SELKIES_MODE=webrtc` if you have not set a mode yourself, so new clients start on WebRTC. -2. Sets `SELKIES_ENABLE_DUAL_MODE=true` so the transport switch appears in the sidebar and users can fall back to WebSockets at will. +2. Sets `SELKIES_ENABLE_DUAL_MODE=true` so the transport switch appears in the sidebar and users can fall back to WebSockets at will. With the switch on, a page whose media path keeps failing is offered WebSockets by the dashboard itself. Without any of those variables the switch is hidden and the container is WebSocket only, exactly as before. Anything you set explicitly wins, so `SELKIES_MODE=websockets` plus a TURN host gives you a WebSocket default with WebRTC available in the menu, and `SELKIES_ENABLE_DUAL_MODE=false` plus `SELKIES_MODE=webrtc` gives you WebRTC with no way back. @@ -178,6 +178,7 @@ Nothing changes at your reverse proxy. Signaling goes through the container's we - **Client loads, transport switch shows WebRTC, no picture.** ICE failed. Switch to WebSockets in the sidebar to confirm everything else works, then check the media path: is the UDP mux port forwarded, is `SELKIES_WEBRTC_PUBLIC_IP` set on a NAT host, can the browser reach the TURN server? - **Works on the LAN, fails from outside.** Host candidates are private addresses. Set `SELKIES_WEBRTC_PUBLIC_IP` or configure TURN. +- **A poor connection mark sits over the stream.** The server is losing frames on the way to that browser, lost past repair or more than the link carries. Lower the bitrate or the frame rate, and keep the session on CBR so congestion control has a target to steer. `SELKIES_UI_SHOW_CONNECTION_INDICATOR=false` hides the mark, not the loss. - **Works but stutters, stats say `relay`.** You are on the default Open Relay TURN or a distant relay. Fix the direct path so the browser never needs the relay, or move the TURN server closer. - **Fails on one specific network only.** That network blocks UDP. Add a TCP mux port on `443` or set `SELKIES_TURN_PROTOCOL=tcp`. - **Container will not start after adding a mux port.** The port is taken on the host, pick another.