github selkies-project/selkies 2.0.0

3 hours ago

What Selkies 2.0 is

Selkies is a single Python application serving one port, with the HTML5 client bundled into it. GStreamer is gone from the runtime: screen capture and video encoding are done by pixelflux, audio capture and encoding by pcmflux, two Rust extensions that install with the wheel.

  • WebSockets is the default transport. One TCP port (8080 by default) carries video, audio, input, clipboard, and file transfers, decoded in the browser through WebCodecs, with a striped JPEG path for browsers that have none. Nothing about it needs STUN or TURN.
  • WebRTC is opt-in (--mode=webrtc) on a vendored aiortc fork, and can be confined to a port range, to one shared UDP and/or TCP port, or run ICE-lite for a restrictive firewall. --enable-dual-mode=true lets the page switch transports while the session runs.
  • X11 and Wayland. The X11 backend is the default; --wayland=true runs the same session on a headless Wayland backend, with input injected through pixelflux rather than through any subprocess.
  • Five video codecs, encoded in hardware where the GPU has the engine. --encoder= takes h264enc, h265enc, vp8enc, vp9enc, and av1enc on NVENC and VA-API, each falling back to the software encoder pixelflux was built with (x264 or OpenH264, x265 or kvazaar, libvpx, SVT-AV1), plus CPU-striped H.264 and JPEG. A client is offered only what the host serves and what its own decoder takes, and a codec the browser declines steps down one ladder on both transports: hardware codecs first, then software by encode time, striped H.264, and JPEG last. 4:4:4 chroma follows the codec and the encoder, and the server announces which of its encoders carry it.
  • Zero-copy capture on both backends. On Wayland the compositor renders into GPU buffers the encoder reads in place. On X11 the same is true through NvFBC on the NVIDIA driver and through DRI3 on any X server whose screen lives on the GPU, which the images' patched Xvfb does, so the pixels never leave the card between the screen and the bitstream, and the session log says which path each display took.
  • Input built for games as well as desktops: pointer lock, a gaming mode that fullscreens and holds the pointer and keyboard, raw pointer motion where the browser offers it, an on-screen touch gamepad, and gamepads delivered to applications through the joystick interposer or uinput. The session's own keyboard and pointer are published as evdev devices too, so a fullscreen game or an input remapper reading /dev/input directly sees them.
  • Two-way clipboard carrying text, images, and formatted content in one envelope, so a rich copy pastes with its styling into an editor and as plain text into a terminal; file transfers in both directions, paced against the video stream so a large one does not stall the session; an apps panel that launches desktop entries in the session. The dashboards switch each clipboard direction, seamless copy, and whether the client's own chords are handed to the session.
  • Session sharing and multi-user access. Viewer and player-2-to-4 links share a running session, and input authority is enforced on the server, not in the page: a viewer's keyboard and mouse are refused whatever its client sends, and a gamepad message is refused unless it drives the slot that link carries. Secure mode replaces the links with provisioned session tokens that carry a role and a slot.
  • Microphone and webcam uplinks publish the browser's devices into the session as an ordinary PulseAudio source and a V4L2 capture device, in any codec the browser encodes, AV1 included. --webcam-on-start=demand and --microphone-on-start=demand ask the browser for the device only while something in the session reads it, so the recording indicator is not lit for the whole session.
  • A second display extends the desktop onto another browser page, each at its own resolution and density. On X11 each display is a real RandR output with a CRTC of its own, plugged in and unplugged as its page comes and goes, so KDE, XFCE, MATE, and LXQt lay windows out on it as they would on hardware.
  • A stats panel that says what the session runs on, reported by the capture rather than inferred from settings: the backend, the GPU, the encoder and whether the frame was copied, the host's load, the encode's cost, and the link, sampled only while a controller's dashboard has them on screen.
  • A UI the deployment decides. The sidebar's title, logo, and every section are server settings, and each capability has its own setting behind them, so hiding a control and disabling a feature are separate choices. An optional audit webhook (--audit-webhook-url) posts the metadata of every clipboard and file event, never the payload, to a collector the operator names.
  • A desktop session behind one port. selkies-session brings up what a host lacks for a session, a sound server, an Xvfb of its own on X11, and the desktop the host has installed, then Selkies on one port or Unix socket. Jupyter (pip install 'selkies[jupyter]'), a Coder module, and an Open OnDemand batch connect app are built on it, each behind the platform's own login and proxy.
  • Every medium built from one source: the wheel on PyPI, .deb (Ubuntu 24.04/26.04, Debian bookworm/trixie), .rpm (Fedora, Enterprise Linux 9), Alpine .apk, Arch .pkg.tar.zst, a self-contained AppImage, and multi-architecture container images, all for x86_64 and aarch64 (the Arch package for x86_64 alone).

Running this build

Container — the desktop, a browser, and an audio stack, nothing to install:

docker run --name selkies -it -d --rm --shm-size=2g -p 8080:8080 \
    ghcr.io/selkies-project/selkies/desktop:2.0.0-ubuntu26.04

Open https://localhost:8080 and log in with ubuntu / mypasswd, changing it with -e PASSWD= before the session is reachable by anyone else. debiantrixie is the other flavor, latest now points at this release, and the GPU flags for NVIDIA, Intel, and AMD are in Getting Started. NVIDIA needs the NVIDIA Container Toolkit v1.20.1 or higher on the host, which passes the modeset node in by itself; the --device /dev/nvidia-modeset workaround is gone from the documentation.

PyPI — the wheel and the sdist, with pixelflux and pcmflux resolved from PyPI as well:

pip install selkies

AppImage — runs from wherever you put it, on any distribution with glibc 2.28 or newer (Enterprise Linux 8, Debian 10, Ubuntu 18.10, and later); as <AppImage> selkies-session it also starts the session itself, on a host where nothing can be installed:

APP="selkies-2.0.0-$(uname -m).AppImage"
curl -O -fsSL "https://github.com/selkies-project/selkies/releases/download/2.0.0/${APP}"
chmod +x "./${APP}"
"./${APP}" --addr=0.0.0.0,:: --port=8080 --basic-auth-user=user --basic-auth-password=mypasswd

Native packages — install a private environment at /opt/selkies and put selkies, selkies-session, selkies-resize, and selkies-gpu-probe on PATH:

. /etc/os-release
DISTRO="$([ "${ID}" = "ubuntu" ] && echo "ubuntu${VERSION_ID}" || echo "${VERSION_CODENAME}")"
PKG="selkies-2.0.0-${DISTRO}-$(dpkg --print-architecture).deb"
curl -O -fsSL "https://github.com/selkies-project/selkies/releases/download/2.0.0/${PKG}"
sudo apt-get install -y "./${PKG}"

Every file on this page is selkies-<version>-<distribution>-<architecture>.<format>, the version as the tag spells it; the package inside carries its packager's own version ordering, so this release upgrades over a release candidate. Native Install has the .rpm, .apk, and .pkg.tar.zst lines, how to attach Selkies to a display and audio server you already run, and a full session script; Jupyter, Coder, and Open OnDemand covers selkies-session and the three integrations.

Changes since 2.0.0rc1

  • A desktop session behind one port for Jupyter, Coder, and Open OnDemand (#409). selkies-session starts a sound server unless one answers (PipeWire, else PulseAudio), an Xvfb of the session's own on the X11 backend, and the desktop a display manager would list, chosen by --session or by the host's own default, then Selkies with every argument it was given. The selkies[jupyter] extra registers the desktop with jupyter-server-proxy; addons/coder is a Coder module with the KasmVNC module's variables; addons/ondemand is an Open OnDemand batch connect app that provisions a session token and runs in secure mode. SIGTERM, SIGINT, or SIGHUP stops Selkies first and then everything the session started. The desktop image's LXQt is now also a Wayland session, on labwc.
  • The codec ladder is one order on both transports. A client that cannot decode the display's codec walks the encoders it does decode with the codecs the host encodes in hardware first, most efficient first, then those it encodes in software by their measured encode time, then striped H.264, and JPEG last; over WebRTC the offer lists the same order behind the display's codec, and the display follows the codec the answer took. The server carries which of its encoders take 4:4:4 from pixelflux's probe to the offer and to the client, so a client turns full color off only where the stream would carry it, and a held full color over WebRTC steps to the answer's first codec the peer decodes at 4:4:4, as the WebSocket ladder does. The negotiated codec is logged as the answer spells it.
  • WebRTC loss recovery. Goodput is measured over matched TWCC arrivals, reserved status symbols are rejected, and a GOP that outlived its usefulness is abandoned whole: the queued video is purged, video is refused until the requested key frame, and a key frame asked for and not yet arrived is not asked for again within the interval.
  • The DPI is settled before the server listens, so the first page's density is compared against what the desktop has rather than against unity, and on XFCE it is read from xfconf, where XFCE persists it.
  • A decoder probe's silence is taken as its answer once, so a session waiting on it starts within one timeout, and the stats panel's hardware verdict reads the decoder's own frames on the striped rows too.
  • AppImage. The bundled environment is solved for the AppImage's glibc 2.28 floor rather than the runner's, and every ELF file in it is checked against that floor before the image is built.
  • Images and packaging. The graphics-stack archives the images build are pinned by checksum, and each image records what it was built from. The wheel and the sdist go to PyPI at publication through a trusted publisher. Deployments, releases, image pushes, and tags are confined to main and its tags, every other ref builds self-contained, and a branch's capture stack is built from the sibling branch its pairing file names, main when there is none.
  • Documentation. Each component has a page of its own, the settings are grouped by what they govern, and the images are described from their entrypoints. The site serves every page as Markdown for language models, with llms.txt and copy and open-in actions. NVIDIA Container Toolkit v1.20.1 or higher is the documented requirement, and the AppImage's glibc floor is stated.

Changes since 2.0.0rc0

Everything 2.0.0rc1 carried, described here rather than by pull request title:

  • H.265, VP8, VP9, and AV1 over WebSockets and WebRTC (#326), on NVENC, VA-API, and the software encoders pixelflux 2.1.0 carries, with the wire header naming each frame's codec, the client probing its decoder per codec, and the vendored RTP stack packetizing every one of them. The webcam uplink takes the same codecs, so a browser may send its camera as AV1. The same pull request brought X11 zero-copy capture through NvFBC and let a session run on an X server whose driver reports no display device, such as a Tesla P100.
  • X11 zero-copy through DRI3 on the images' patched Xvfb (#366): the X server blits the root into GPU buffers pixelflux allocated, and NVENC or VA-API encodes them in place, on any GPU and shared between tenants. The interposer deadlock was fixed and the X11 surface area is no longer pre-defined.
  • Displays are real RandR outputs on X11 (#398): a display of an extended desktop is plugged in as an output with a CRTC of its own, given its exact mode and position, and unplugged when its page leaves, so every X11 desktop that supports several monitors treats it as one. The primary is moved back before a display left of it is removed, so nothing renders off screen. The xrandr-only methods remain as the fallback for an X server without pluggable outputs.
  • The session keyboard and pointer are published as evdev devices (#360), through the kernel where /dev/uinput is writable and through the input interposer otherwise, so applications reading evdev directly see the session's input.
  • Formatted clipboard content, and clipboard and chord switches in the panel (#353): markup travels with the plain text its source wrote, in both directions on X11 and Wayland; send, receive, and seamless are switches in both dashboards (--clipboard-seamless sets the start and can be locked); and the client's own Control+Shift chords can be handed to the session (--keyboard-shortcuts).
  • The camera and microphone are asked for only while the session uses them (#397, by @gutschke): --webcam-on-start=demand and --microphone-on-start=demand watch what reads the virtual device and ask one page to capture only while something does.
  • An optional audit webhook (#256, by @DL6ER): --audit-webhook-url, with a bearer token and a timeout, posts JSON metadata of every clipboard and file-upload event, fire-and-forget so a slow collector never stalls a transfer.
  • A stats panel in both dashboards (#394) showing the backend, the GPU, whether the capture is zero-copy, and the figures that explain a choppy stream, generated by pixelflux.
  • Shift with the navigation keys selects text again (#359): the shift neutralization that fixed earlier translation bugs no longer swallows Shift+Home, Shift+End, and the shifted arrows, with a unit test holding it.
  • JPEG stripe decoders are raced at startup (#367) rather than fixed in one order, since Firefox is fastest through ImageDecoder and Chromium through createImageBitmap, the opposite of what the fixed order chose there.
  • One log format, and INFO tells the story of a session (#369): what the server came up as, what each client connected as, the capture and encoder path each display took, and every later change or failure; everything else is DEBUG, and the per-packet WebRTC and per-cursor lines are paced.
  • Three dependencies dropped and the npm lockfiles committed (#368): vite-plugin-env-compatible, @types/js-yaml, and ifaddr go, the client bundle is byte-identical, and every JavaScript install reads its lockfile.

Coming from 1.6.x

The runtime no longer uses GStreamer, and the default WebSocket transport needs no TURN server at all: a single exposed port is the whole deployment. The wheel and every package pin the pixelflux and pcmflux build they were tested with, so nothing about the capture stack is installed separately. Every setting is listed in the Settings Reference; each component, image, and interposer has a page under Components.

Known limitations

  • Gaming mode holds the keyboard only where the browser offers the Keyboard Lock API: Chromium-based browsers do, Firefox and Safari do not, and Brave's Shields block it by default. The client says so when it enters gaming mode without one.
  • WebRTC behind NAT may still need a STUN/TURN server; see WebRTC and Firewall Issues. The default WebSocket transport does not.
  • WebKit answers that it decodes AV1 and then refuses every AV1 key frame; the client treats that as a refusal and steps to H.264 without a reload.
  • Hosts are Linux only; macOS and Windows support is not implemented.

Reporting

Issues go to https://github.com/selkies-project/selkies/issues, and the Discord is where most of the testing conversation happens. A report that names the transport (websockets or webrtc), the backend (X11 or Wayland), the browser, and the GPU is one we can act on immediately.

We are in need of maintainers and community contributors. Please consider stepping up, as we can never have too much help!

Contributors

1268 commits (1233 plus 35 merges) between v1.6.2 (2024-08-15) and 3ec56fb15 (2026-09-24), the commit this
release was built from. Commits counted by the GitHub account each was authored by.

Handle Name Commits
@ehfd Seungmin Kim 802
@thelamer Ryan Kuba 411
@PMohanJ Mohan J 17
@junkerderprovinz Junker der Provinz 7
@vishalkadam47 Vishal Kadam 7
@T2theV 3
@rmounce Ryan Mounce 3
@Rid Grant Millar 2
@dionorgua Dmytro Nezhevenko 2
@victorpahuus Victor Pahuus Petersen 1
@sujeito-operator Sujeito Operator 1
@JohannVR Johann 1
@gutschke 1
@felipecrs Felipe Santos 1
@DL6ER Dominik 1
@danisla Dan Isla 1
@athene20 athene20 1
@aliefe04 1
@Pooyahmti Pooya Hemmati 1
@h012193213 Martin Ng 1

Don't miss a new selkies release

NewReleases is sending notifications on new releases.