What Selkies 2.1 brings
Selkies 2.1.0 is the first feature release after 2.0. It ships with pixelflux 2.2.1 and pcmflux 2.2.0, so no part of the capture stack links FFmpeg any more.
- The stream follows the path by default. Both transports now start in CBR with congestion control on, and back the bitrate off to what the link carries.
- A session rides over a cut connection. A proxy or ingress that drops long-lived sockets no longer ends the session.
- Several pages can control one display. A second tab joins beside the first instead of evicting it.
- A page on a slow link no longer costs the others key frames. It is resynced from a long-term reference instead.
- New in the session: 10-bit video, gamepad rumble, a touch trackpad that accelerates with the finger, and gaming mode that holds the keyboard in Firefox and Safari too.
- Hardened throughout. Every interface the server exposes went through a security review, and the code that parses what a remote peer sends is now fuzzed.
Upgrading from 2.0.0
- Only this server's own origin may frame the client by default.
--frame-ancestors(SELKIES_FRAME_ANCESTORS) now defaults to'self', and an empty value means the same. Before, any site could frame a live session and lay its controls under clicks meant for its own page. A platform that embeds the desktop in a page of another origin names that origin, such ashttps://portal.example.com.*admits any page. - CBR and congestion control are the defaults on both transports. CRF ran every encoder uncapped. On a band of fresh noise at 60 fps under CRF 25, a WebSockets stream reached 94.7 Mbit/s, where CBR holds 6.3.
--rate-control-mode=crfstill selects constant quality.--congestion-control=falseholds the chosen bitrate whatever the path carries. - The paint-over defaults off under Turbo and on without it, JPEG included.
- Under CBR,
pixelfluxnow cleans up a still screen to the paint-over quality whether the switch is on or off, so both dashboards hide the switch there. - An operator's value or a user's pick still wins.
- Under CBR,
- The Computer Use server needs a token.
--computer-use-bindno longer startspixelflux's Computer Use server without--computer-use-token(SELKIES_COMPUTER_USE_TOKEN). Every request then carriesAuthorization: Bearer <token>.- A loopback port is reachable from every account on the host and from any page open in the session's own browser.
- Whatever calls it drives the desktop with the session owner's authority.
- Screenshot and automation clients set the token.
- The tools are
selkies session,selkies resize, andselkies gpu-probe. The AppImage takessession,resize, orgpu-probeas its first argument. selkies sessionstarts its Xvfb screen at 1920x1080, or at the manual resolution where one is set, as the container images do. The 8192x4096 framebuffer remains the largest size a resize can reach. Before, a desktop laid out, or a capture started, before the first resize took all of it (#483).- VP8 runs on one thread unless set. libvpx's VP8 row threads cost screen content up to four times the bits at the same quality, while spinning on the rows they wait for.
--cpu-encode-threading=truethreads it again. - View-only credentials can no longer list or download files. This covers the viewer token and the view-only Basic password, and applies to recordings too. Uploads and printed documents already refused them.
- Every TLS context requires TLS 1.2 or later. On older Python builds the system's OpenSSL could still admit 1.0 and 1.1.
- A connection that carries no request is closed after 75 s (#473, by @rforced).
- The interposer sockets are served from the session's runtime directory.
- Selkies refuses a socket path that another account owns.
- The input and camera interposers accept a socket only when it is served by the session's user or root.
- The
coturnadd-on refuses to start without aTURN_SHARED_SECRETof its own. Before, it fell back to a public literal. - The Debian packages are named
debianbookwormanddebiantrixie, as the images name their flavors. Theel9package is built on, and depends on, EL9's Python 3.12.
Running this build
Container: the desktop, a browser, and an audio stack, with nothing to install:
docker run --name selkies -it -d --rm --shm-size=2g -p 8080:8080 \
ghcr.io/selkies-project/selkies/desktop:2.1.0-ubuntu26.04Open https://localhost:8080, accept the container's self-signed certificate, and log in as ubuntu with the password mypasswd. Set another with -e PASSWD= before anyone else can reach the session. debiantrixie is the other flavor, and latest now points at this release. The flags for NVIDIA, Intel, and AMD GPUs are in Getting Started.
PyPI: the wheel and the sdist, with pixelflux 2.2.1 and pcmflux 2.2.0 resolved from PyPI as well, on Python 3.9 through 3.15:
pip install selkiesAppImage: runs from wherever you put it, on any distribution with glibc 2.28 or newer. Given session as its first argument, it starts a whole session with its own display and sound server:
APP="selkies-2.1.0-$(uname -m).AppImage"
curl -O -fsSL "https://github.com/selkies-project/selkies/releases/download/2.1.0/${APP}"
chmod +x "./${APP}"
"./${APP}" --public --port=8080 --basic-auth-user=user --basic-auth-password=mypasswdNative packages: install a private environment at /opt/selkies and put selkies on PATH:
. /etc/os-release
DISTRO="${ID}$([ "${ID}" = "ubuntu" ] && echo "${VERSION_ID}" || echo "${VERSION_CODENAME}")"
PKG="selkies-2.1.0-${DISTRO}-$(dpkg --print-architecture).deb"
curl -O -fsSL "https://github.com/selkies-project/selkies/releases/download/2.1.0/${PKG}"
sudo apt-get install -y "./${PKG}"Native Install has the .rpm, .apk, and .pkg.tar.zst lines. Jupyter, Coder, and Open OnDemand covers selkies session and the three integrations.
New in this release: every file here is listed with its SHA-256 sum in SHA256SUMS, and GitHub attests the build provenance of each. You can check a download with either:
sha256sum --check --ignore-missing SHA256SUMSgh attestation verify "./${PKG}" --repo selkies-project/selkies
Changes since 2.0.0
Streaming over a real network
- Congestion control on WebSockets, and on by default. A CBR display is steered to the rate its path carries, read from the queue its frame round trip shows beyond the path's own delay and jitter.
- Over WebRTC it now also backs off on a rise in one-way delay, before the buffer overflows into loss.
- On a clean 5 Mbit/s link over WebSockets, glass-to-glass latency fell from 142/341 ms (median/p90) to 35/119 ms.
- On the same link, the time spent above 250 ms fell from 21 s a minute to 1 s.
- A restarted server starts each page's stream at the rate its path last carried, not at the configured rate.
- A session rides over a cut of its socket. Some ingresses cut every long-lived socket, with no close frame, about ten minutes after each reload.
- Over WebSockets, the page reopens at once and keeps its playback, its microphone and webcam, and the keys held.
- Over WebRTC, the server keeps the session for 3 s while the signaling socket reconnects, and the media path carries on beside it.
- Elsewhere, both cores now reconnect in place instead of reloading the page, so fullscreen and the keyboard lock survive.
- A page on a narrow link is served as a selective forwarding unit serves a receiver.
- It gets the frames its link carries, and the encoder predicts past the rest.
- The server tells each display's encoder which frames every page holds. A page further behind is then resynced from a long-term reference, not from a key frame sent to everyone.
- Before, a 1 Mbit/s page beside an 8 Mbit/s stream cost the owner's stream 16 key frames in 30 s over WebSockets and 29 over WebRTC.
- A still screen is cleaned up at the rate the path carries, within four times the session's rate, on both transports.
- WebRTC loss repair:
- A packet's first retransmission is sent twice, so one lost repair no longer freezes the picture.
- A lone lost data-channel message is repaired after 0.4 s instead of 1 s.
- A frame is declared lost only after its repairs had a round trip to land.
- The pacer sends a frame in one burst where the link has room.
- Video RTP is stamped with the capture instant and carries
abs-capture-time. - A receive-only browser measures the round trip through RTCP XR.
- TWCC accounting, FlexFEC, and DTLS fixes (#420, #426, #428, #429, #430, #433, by @danoki-jm):
- late TWCC acknowledgments are counted exactly once, and goodput is measured across report boundaries;
- queued video is drained at its drain rate until the path clears, and loss handling holds across cruise restoration;
- mutable RTP extensions are normalized before FlexFEC encoding;
- complete DTLS application records are drained before the next receive.
- The page's size travels in the WebRTC hello. The primary is resized during the handshake, and its first capture waits up to 2 s for the size. The first picture is at the page's size, not a frame of the old screen scaled down.
- A slow decoder is streamed the frame rate it keeps up with. It no longer builds a backlog and asks for key frames that decode slower still.
- WebSockets is offered when WebRTC's media path keeps failing. This covers a network that drops UDP while the page's own signaling socket stays up.
Video
- 10-bit video (
--video-10bit, by @thelamer).- Where it's available: H.264 on x264, and H.265, VP9, and AV1 on VA-API, NVENC (H.265 and AV1), x265, libvpx, and SVT-AV1.
- When it's offered: only where the host encodes the format and the browser decodes a real 10-bit key frame of that codec.
- Over WebRTC, the page names the formats it decodes in its hello, so the first offer already fits.
- In the dashboards, a toggle shows only where it applies.
--cpu-encode-threadingspreads a software encoder's frame across threads, and is on by default. Turn it off on hosts that pack many sessions onto their cores. Both dashboards show the switch only while the session encodes in software.- The display's own refresh is offered as the frame rate, measured from the page's animation frames. A 60 fps stream on a 59.94 Hz display would otherwise skip a frame every 17 s. Fractional rates are carried whole through both transports.
- A codec the browser refuses is handled in its own tab. The fallback is held for the tab that needed it, never as the user's pick, and the client says why a tab streams a fallback or decodes in software.
- Cursors are scaled to the displayed stream (#440). The video keeps its exact size when local scaling is off (#452), and antialiasing changes apply to static video (#453), all by @danoki-jm.
Several pages, one display
- A page of another tab controls a display beside its owner instead of evicting it, on both transports.
- Each page can change the display's stream settings by its own user's pick.
- Those settings are shared between the display's pages.
- The owner's session is not disrupted.
- A collaboration room's pages share a display, each by its own token. A token can hold several gamepad slots, one for each of its page's pads.
- The token table can be provisioned from a file (
--tokens-file, #467, by @rforced). It is read at start and again whenever the file changes. - The session token can be carried in the URL fragment (
#token=).- A browser never sends the fragment, so the token reaches no request line, proxy log, or
Referer. - The client keeps it out of every URL it requests afterwards.
- The sharing links leave out the sharer's token.
- A browser never sends the fragment, so the token reaches no request line, proxy log, or
Input
- Touch:
- Trackpad mode accelerates the pointer by the finger's speed, with a speed setting in both dashboards.
- Trackpad mode draws its cursor on the page instead of waiting for the video.
- A pinch is sent as Ctrl+wheel on a touchscreen or a touchpad.
- Two-finger scrolling follows the distance the fingers travel.
- A three-finger tap middle-clicks.
- On Wayland, a touchpad's scroll reaches the session as a finger's, with the stop that starts kinetic scrolling.
- A palette offers touch clients the keys an on-screen keyboard lacks: F1 to F12, the arrows and the editing block, and chords.
- Gaming mode:
- It holds Escape and the browser's shortcuts in Firefox and Safari, through a fullscreen asked for with
keyboardLock: "browser". - In Chrome it now locks every key, Ctrl+W and Ctrl+T among them.
- Firefox takes the pointer lock once fullscreen has placed the page.
- It holds Escape and the browser's shortcuts in Firefox and Safari, through a fullscreen asked for with
- Gamepads:
- A game's rumble plays on the controller in the browser, and each client can turn it off.
- Pads are polled at the browser's 4 ms sampling.
- Stick noise is cut by distance from center.
- The pad reader no longer locks out input under load (by @thelamer).
- Pointer motion is sent as it arrives, from
pointerrawupdatewhere the engine has it, spaced 2 ms apart. On X11 it is injected as one packed XTEST request. - macOS clients:
- Command chords hold Control while Command is down.
- Dead keys compose instead of arriving as Alt chords, each mouse-wheel detent is one notch, and Control-click presses the primary button in Firefox (by @thelamer).
- IME and text:
- A CJK IME commit is typed once, and a backspace takes one character of text not yet typed.
- On Wayland, Unicode text is typed on spare keys in runs that fit them.
- A held key whose client stopped confirming it is let go.
- Selkies' kernel devices are named, so a host can keep them off its own seat.
- The stylus no longer registers twice (#456, by @thelamer).
- The input interposer resolves the real libc call in a hook that runs before its constructor (#457, by @ne0YT).
Audio
- Surround over WebRTC. Surround is negotiated, or its stereo fold where the browser decodes no multistream Opus. Before, every engine answered G722 and nothing was audible.
- Sound, microphone, and webcam on browsers without WebCodecs audio, through libopus-wasm. This covers Safari before 26, Firefox before 130, and Firefox for Android.
- A chosen output device is reached through a media element where the browser lacks
AudioContext.setSinkId, as Firefox does. - The images give a PulseAudio client that leaves its buffer to the server 40 ms, not 2 s.
- Silence is marked: the page plays a sound out when the server goes quiet and never takes the silence for a late delivery.
Clipboard, files, and printing
- A password manager's copy stays out of sight, in both dashboards' clipboard panes, in the log, and off the local clipboard. This applies to KeePassXC's and KDE's secret hint.
- Pasting:
- File transfers (#474, #475, #476, by @rforced):
- A directory holding a name that isn't UTF-8 is listed.
- A chunked upload is staged under a name no other account can predict.
- A replaced file keeps its mode only from this user's own file, without set-ID bits.
- Printing:
Dashboards and the stats panel
- A Poor connection mark over the stream shows when the server is losing a page's frames. It has its own setting,
--ui-show-connection-indicator. - The stats panel shows more:
- What reaches the screen, and how long after arrival.
- Time from capture to the wire, and the video rate against its target.
- The GPU gauge counts the encoder on every vendor (Intel and AMD usage reported by @thelamer).
- The Wish dashboard runs on Base UI (#439) and is redesigned with accent styling and accordions (#458), by @vishalkadam47.
- Traditional Chinese in both dashboards, chosen by the browser tag's script and region (#411, by @bigistudio).
- The browser tab is named after
--ui-titlewhere one is set (#466, by @rforced). - The app dashboard lists local proot-apps repositories where the environment has them (by @thelamer).
- Sliders: the frame rate, bitrate, and CRF sliders step through shared stops, sized for a finger. The frame rate runs from 8 through 240.
Displays and the desktop images
- The images' Xvfb:
- It runs its fake vblank at the session's capture rate, to the millihertz, so vsynced applications and compositors are no longer held to 60 fps.
- It answers
XFree86-VidModeExtensionwith that rate, so ANGLE's GL backend stops logging (#482). - It signals a GPU semaphore after the capture's blit, for the encoder to wait on.
- It synchronizes DRI3 presents explicitly, so frames from the NVIDIA driver no longer reach the screen out of order.
- A resize picks its mode by refresh, never below the stream's rate, and makes the mode at exactly the refresh asked for.
- Desktop layouts:
- A moved or resized display is published under a new monitor name, so Qt follows it.
- Windows move with their display when an X11 layout moves it.
- KWin's protocol offers the second display.
- A desktop's density is applied only to the session on this display.
- The container images:
- Chrome keeps its sandbox wherever the container grants its namespaces.
- The X display admits the session's user and root by uid, rather than every local process.
ucfinstalls the configuration files it manages under fakeroot, which LibreOffice needed.
Security
These came out of an audit of every interface the server exposes. Beyond the changes under Upgrading from 2.0.0:
- Signaling:
- Only the streaming server may hold the server role and take sessions.
- A WebRTC hello is bounded in what it may carry and name.
- A view-only page cannot supersede a live controller.
- ICE:
- A connectivity check without the session's credentials is refused.
- A peer's remote candidates are bounded, and mDNS names are resolved off the signaling.
- WebRTC parsing:
- SCTP reassembly is bounded over every stream together.
- Malformed and premature SCTP chunks are dropped instead of ending the DTLS loop.
- A remote SDP is parsed in time linear in its length.
- Who may do what:
- The connect-time clipboard read is held to the input authority.
- Finger scrolls are held to it in secure mode.
- A session token without the controller role is taken as a viewer's.
- The touch gamepad's setup is taken only from the page's own origin.
- Logs and files:
- TURN credentials and the
Referer's query are kept out of the log. - The client's stats are quoted in the CSV where a spreadsheet would run them.
- The statistics files are opened without following links.
- TURN credentials and the
- TURN: the server warns at startup when relay credentials are minted for anyone because no API key is set.
- Fuzzing: the STUN, RTP/RTCP, SCTP, and SDP parsers are fuzzed under ClusterFuzzLite.
- Supply chain:
- Every GitHub Action is pinned to a full commit.
- Every floating download is checked against the hash its source publishes.
Packaging and CI
- The capture stack is built as its projects build it, from the commit built, and handed over as an artifact.
- The AppImage:
- It is checked on a pristine host at its glibc floor.
- It points its bundled libdbus at the host's system bus.
- It gives its libxkbcommon the host's keymaps.
- It carries pixman.
- The Coder module and the Open OnDemand app:
- The Open OnDemand app keeps its session tokens off curl's command line and away from proxies.
- The Coder module resolves the latest release without GitHub's rate-limited API, downloads the Debian package by its current name, and installs PulseAudio where the workspace has no sound server.
- The images no longer install FFmpeg, x264, or opus-tools, since the
pixelfluxandpcmfluxwheels carry their codec libraries. - Dependencies: website and dashboard dependencies are up to date,
markdown-it14.3.3 (GHSA-gfr2-rv44-r23c),sharp0.35.5 (GHSA-wq5f-xc86-pv6w), andsource-map-js1.2.2 (GHSA-68fv-2mgg-jv7q) among them. The controller database the build fetches is checked against its hash (#477, by @rforced).
The capture stack
pixelflux 2.2.1 brings the encoder side of the changes above:
- No FFmpeg.
- Long-term references in every encoder that keeps them.
- Still screens cleaned up from their content.
- 10-bit encoding.
cpu_encode_threading.- An X11 H.264 session that started in software because it was larger than the GPU encoder's maximum returns to the GPU encoder after a resize (#483).
pcmflux 2.2.0 adds:
- The stereo fold beside a surround capture.
opus_complexity.- The silence marker.
- A microphone queue that keeps no standing delay.
Their release notes list the breaking changes for anyone using either directly.
Known limitations
- WebRTC behind NAT may still need a STUN/TURN server; see WebRTC and Firewall Issues. The default WebSocket transport does not.
- AV1 in WebKit: 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.
- WebRTC in Firefox: some Firefox releases paint WebRTC AV1 with the wrong color matrix; the FAQ says which.
- 10-bit in Chromium: Chromium-based browsers decode AV1 over WebRTC at 8 bits only.
- Gaming mode holds the keyboard in Chromium-based browsers, Firefox 156 and later, and Safari 26.4 and later. Brave's Shields block the Keyboard Lock API by default.
- Hosts are Linux only. macOS and Windows support is not implemented.
Reporting
Issues go to https://github.com/selkies-project/selkies/issues, and most of the testing conversation happens on Discord. We can act on a report right away when it names:
- the transport (
websocketsorwebrtc); - the backend (X11 or Wayland);
- the browser;
- the GPU.
We are in need of maintainers and community contributors. Please consider stepping up, as we can never have too much help!
Contributors
There are 651 commits (626 plus 25 merges) between 2.0.0 (2026-09-24) and 6ee68a7a8 (2026-10-21), the commit this release was built from, 16 of them from Dependabot. Commits are counted by the GitHub account that authored each.
| Handle | Name | Commits |
|---|---|---|
| @ehfd | Seungmin Kim | 582 |
| @thelamer | Ryan Kuba | 18 |
| @danoki-jm | 10 | |
| @vishalkadam47 | Vishal Kadam | 10 |
| @rforced | 9 | |
| @bigistudio | big i | 2 |
| @gutschke | 2 | |
| @ne0YT | 1 | |
| @sghael | Sandeep Ghael | 1 |