# LinuxPlay: a remote desktop for Linux built on WebRTC and GStreamer

> LinuxPlay is a remote desktop for Linux written from scratch in Rust, carrying encrypted peer-to-peer media over WebRTC with GStreamer for encoding, a GTK4 and libadwaita client, PIN based pairing and per-connection approval on the host. It runs on any desktop rather than a specific one, and it deliberately ships without a server component.

**Techlm77/LinuxPlay** — An open-source, ultra-low-latency remote desktop for Linux.

- Repository: https://github.com/Techlm77/LinuxPlay
- Website: https://www.youtube.com/watch?v=GVuqSS2yx78&list=PLSBp-ZrhU7QhCTyW4Dlgvg4T6wUWejpXi&index=1
- Stars: 633 · Forks: 16
- Language: Rust
- License: GPL-3.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/techlm77-linuxplay

## A remote desktop with no server in the middle

LinuxPlay is a low-latency remote desktop for Linux, built from scratch in Rust, and the first architectural decision is the transport. It uses WebRTC through the str0m library over mutual-TLS signalling, which the README describes as encrypted and peer-to-peer. There is no relay component in this project, no session broker, no account service. Two machines connect directly to each other, and the media path is encrypted between them. That is the whole appeal and the whole constraint. A peer-to-peer design means there is nothing to scale and nothing to run, and it also means the two ends have to be able to reach each other, either on the same network or through a private network overlay such as Tailscale. The audience is therefore a person with two Linux machines who wants a fast remote session between them, not an organisation that wants to give hundreds of support agents a queue of desktops to open. The repository also carries a video playlist as its homepage link, which tells you how the project expects to be evaluated.

## GStreamer for media, GTK4 for the window

The media stack is GStreamer, and the hardware acceleration story is spelled out in the feature list. It supports hardware H.264, H.265 and AV1, with VA-API on Intel and AMD and NVENC on Nvidia, with a software fallback when the hardware path is unavailable, plus Opus for audio. The fallback matters more than it sounds. On a machine with an unusual GPU, the tool still works, just more slowly, and the README says that Nvidia machines without the nvcodec GStreamer plugin fall back to software, with the plugin shipping in Arch's gst-plugins-bad, in gstreamer1.0-plugins-bad on Debian and Ubuntu, and in RPM Fusion on Fedora. The user interface is GTK4 with libadwaita, and the client additionally needs those libraries while the host needs only GStreamer, which is what allows a headless machine to share its screen. The project states that it runs on any desktop and does not depend on a particular desktop environment or window manager, and the multi-monitor support follows the same principle, with each display offered separately, streamable at once, and the load rebalanced across them.

## Pairing with a PIN, then approval on every connection

The security model has two distinct parts, and separating them is what makes the design coherent. The first is pairing. Both ends use self-signed identities that are pinned by fingerprint, and the first pairing is authenticated by a PIN, using SPAKE2 with key confirmation binding both certificates. The result is that after the first successful pairing, the client recognises the host by its certificate, and the PIN is a one-time bootstrap rather than a recurring password. The second part is authorisation, and it applies to every connection. Each time someone connects, the host is asked to choose view only, allow control or deny, in a notification, in the tray menu or from a terminal, and no answer within a minute means no. There is one refinement for multi-display setups, since while one window of a device is approved, its other display windows join without asking again. The design choice worth noting is that a paired device still has to be approved per session, so a compromised key does not silently grant permanent control, and the view-only option lets a host share a screen without handing over the keyboard.

## Codec, bitrate and framerate adapt to the link

The adaptive layer is what turns the project from a screen share into a usable remote desktop. The codec, the bitrate and the framerate are picked from the link, whether a local area network or the internet, and from the hardware at both ends, and then the session follows the measured bandwidth live. That last part is the important one, since a fixed quality setting that looks fine on the LAN becomes unusable over a variable mobile connection. The choice of codec is therefore constrained by what the hardware can do, which is why the project lists H.264, H.265 and AV1 with their hardware paths rather than just claiming a single pipeline. The repository layout shows how the code is organised, as a Cargo workspace with four members, the protocol crate, the networking crate, the host and the client:

```toml
[workspace]
resolver = "3"
members = ["crates/proto", "crates/net", "crates/host", "crates/client"]
```

The dependencies match the feature list closely. str0m at 0.24 with the aws-lc-rs backend for the WebRTC side, rustls and tokio-rustls for TLS, tokio for the async runtime, spake2 for the PIN protocol, rcgen for generating the self-signed identities, hmac and sha2 for the cryptographic primitives, the GStreamer crates at 0.25, and clap for the command line. The workspace version is 2.0.0-dev on the 2024 edition, with a minimum Rust of 1.85.

## Building and installing the host and the client

Building needs a current Rust, 1.85 or newer, and GStreamer, and the README is emphatic about using rustup rather than the distribution's cargo, because Ubuntu 24.04 ships a 1.75 toolchain that is too old, while Arch's rolling package is fine. The dependency lists for the three main distribution families are in the README, and the shape is the same everywhere, the Rust toolchain, the GStreamer development packages, GTK4 and libadwaita for the client, and the VA-API and libav plugins for hardware encoding. On NixOS a flake.nix is included, with nix build producing the binaries and nix develop giving a shell with every build dependency. The installation itself is a script:

```sh
./packaging/install.sh
```

That builds the project and installs it for the current user under the .local directory, with an icon and application menu entries, so you get two launchers, one to connect to another computer and one to share this computer's screen. On a machine you only want to share, and which has no GTK4, the installer builds and installs only the host. A system-wide install is done with sudo and a PREFIX override, and removal is an uninstall argument to the same script.

## The port plan and the listen modes

Network exposure is handled by an explicit choice rather than a default bind. The listen option decides where the host listens. The default, lan, listens on the private addresses of the machine's network interfaces, and the README is specific that this excludes VPNs, containers and public addresses. The tailscale mode listens only on the machine's Tailscale addresses and refuses to start if Tailscale is down, which is a useful failure mode because it avoids silently listening more widely than intended. Loopback restricts the host to the machine itself, and all listens on every interface, which the documentation marks as not recommended. An exact address can be chosen instead with a repeatable bind option. The port plan is fixed and small, with signalling on TCP 47800 and one UDP port per display from 47801 to 47816, so a firewall can allow exactly those and nothing else. There is also a check for what the machine can encode, with the host encoders command, and one for identity and paired clients, with the host info command.

## Unattended access and the crate workspace

For a computer nobody sits at, the host can run in unattended mode, and the conditions are narrow on purpose. Paired devices can connect without the approval prompt, although each connection still raises a notification, and the Wayland screen-share grant can be remembered. Since nobody is there to read a PIN, you can set a pairing password instead:

```sh
linuxplay-host set-password     # asks twice, without echo; stored 0600
linuxplay-host --unattended
```

The password is only used with unattended mode, must be at least 12 characters, and is never taken from the command line or the environment, which is a deliberate choice against a password that ends up in a shell history or a process listing. Tailscale is treated as a transport rather than as trust. It gives a private network between machines without opening ports, but it does not replace LinuxPlay's own pairing and approval, because being on the tailnet does not make a device trusted. The Tailscale section even includes a policy snippet that grants access to a tagged host for the LinuxPlay ports only. The host also has a management surface for a running instance, with commands for listing sessions, approving or denying a waiting connection, disconnecting a session or stopping sharing, and revoking a paired fingerprint, all of which talk to the running host over a private socket, so they work over SSH and report what the host actually did.

## Conclusion

LinuxPlay suits someone who wants to reach a Linux desktop over a private network or a tailnet without a relay server, and who is prepared to build it from source and read its CLI surface. It is the wrong choice for a support team that needs to connect to arbitrary machines through a broker, since the design is peer-to-peer and pairing is per device, and the build needs a current Rust toolchain and a GStreamer stack on both ends. Before you rely on it, install the host with the unattended password flow and check the host commands for encoders and sessions, and read the network exposure section carefully, because the listening mode decides whether your desktop is reachable at all.

## FAQ

### How do I build and install LinuxPlay?

You need Rust 1.85 or newer installed with rustup rather than the distribution cargo, plus GStreamer and, for the client, GTK4 and libadwaita. Then run ./packaging/install.sh, which builds the project and installs it for the current user under the .local directory with menu entries. A Nix flake is included, with nix build and nix develop.

### How does the first connection work?

The host shows a PIN and the client enters it, and that first pairing is authenticated with SPAKE2 and key confirmation binding both certificates, after which the client recognises the host by its certificate. Every later connection still needs host approval, with a choice of view only, allow control or deny, and no answer within a minute counts as no.

### Which ports does LinuxPlay need?

Signalling uses TCP 47800 and media uses one UDP port per display from 47801 to 47816, so a firewall can allow exactly those. The listen option decides where the host listens, with lan as the default for private interface addresses, plus tailscale, loopback and all, and a repeatable bind option for exact addresses.

### Can I use LinuxPlay on a computer nobody sits at?

Yes, with linuxplay-host --unattended, which lets paired devices connect without the approval prompt while still showing a notification for each connection. You can set a pairing password with linuxplay-host set-password, it must be at least 12 characters, is never taken from the command line or the environment, and is stored with 0600 permissions.

## Sources

- [Issues](https://github.com/Techlm77/LinuxPlay/issues)
- [License: GPL-3.0](https://github.com/Techlm77/LinuxPlay/blob/main/LICENSE)
- [Project website](https://www.youtube.com/watch?v=GVuqSS2yx78&list=PLSBp-ZrhU7QhCTyW4Dlgvg4T6wUWejpXi&index=1)
- [README](https://github.com/Techlm77/LinuxPlay/blob/main/README.md)
- [Techlm77/LinuxPlay on GitHub](https://github.com/Techlm77/LinuxPlay)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/techlm77-linuxplay
