A headless Wayland compositor whose screen is a browser tab. Clients render on the GPU, the composited frame is hardware-encoded with VA-API or NVIDIA NVENC (AV1, HEVC, VP9 or H.264, whichever the browser decodes best and the GPU encodes) and streamed over a WebSocket, and the browser decodes it with WebCodecs. Mouse, keyboard, audio and the clipboard travel the same way.
Design notes: docs/architecture.md, docs/protocol.md, docs/panels.md, docs/desktop-api.md, docs/mcp.md.
Releases made from vX.Y.Z tags carry separate native packages and tarballs for Debian 13
and Ubuntu 26.04, plus an Arch package. Choose the artifact for your distribution;
FFmpeg shared-library ABIs differ between releases.
Building from source needs Rust stable, Node 24 for the viewer, Clang, pkg-config, glib-compile-schemas and the development
packages for FFmpeg, libva, PipeWire, libgbm, libEGL and libxkbcommon; make builds the viewer and then
target/release/elsewhere. The schema compiler comes from libglib2.0-bin on Debian/Ubuntu
and glib2 on Arch. make version reports the nearest reachable vX.Y.Z release tag,
adding .N for commits since that tag and -dirty for tracked changes or untracked files.
For example, three commits after v0.1.2 produce v0.1.2.3, or v0.1.2.3-dirty with local changes.
Ignored build outputs do not make a checkout dirty. Fetch tags and full history before building;
shallow checkouts and checkouts with no reachable release tag cannot derive a version. Plain Cargo builds report 0.0.0-dev unless
ELSEWHERE_VERSION is set.
make package-deb, make package-tar and make package-arch build into dist. Debian needs
cargo-deb and the Debian packaging tools; Arch needs makepkg and a non-root build user. The release
workflow uses these same targets on their native distributions. Tarball filenames include the build distribution. They and the binary's
--version retain the Git version. Debian and Arch metadata and package filenames omit v, spell
-dirty as .dirty, and add package revision -1, so v0.1.2.3-dirty becomes
0.1.2.3.dirty-1 in both packages and their filenames.
The Arch PKGBUILD in packaging/arch also derives the version when used directly.
Package targets always derive their version from Git. make check-version runs the version fixtures
with Python 3 and Git; run it in the Docker rig.
After installing a native package in a disposable Docker container, run
python3 scripts/check-package-tls.py from a checkout as root, with Python and OpenSSL installed,
to check the broadcast helper's trusted, untrusted and wrong-host TLS connections.
- Linux, with a GPU render node (
/dev/dri/renderD128) and Mesa for Intel/AMD or the NVIDIA driver, or none at all (Mesa's llvmpipe renders and the CPU encodes; see below). - FFmpeg 7.1 or later, with VAAPI or NVENC support for hardware encoding.
--software-encodinguses libvpx, x264 or OpenH264, x265, and libaom, according to the installed FFmpeg build. Software encoding runs the desktop at 30 Hz. Standard distribution FFmpeg packages supply these libraries; codec availability is checked by opening an encoder and producing a keyframe. ca-certificatessupplies the system trust store for verified RTMPS broadcasts. Native packages require it; install it separately when using a tarball.xorg-xwaylandfor X11 clients. Audio requires PipeWire 1.4.2+, its Pulse compatibility service, WirePlumber 0.5.6+ andpactl. See session audio for packages and host-service compatibility.--no-audioneeds no audio services; the native PipeWire client library remains a runtime dependency.- Rust stable and Node 24 to build. The browser needs WebCodecs (Chromium, Firefox 130+, Safari 26+).
make run ARGS="--exec foot" # any Wayland client; WAYLAND_DISPLAY is set for it(make web once, then plain cargo run --release -- --exec foot works too.)
The server prints its certificate fingerprint and plain connection URLs, without tokens. Open a URL in a browser, compare the fingerprint before accepting the self-signed certificate, and paste a token into the connection dialog. Create an admin token on the server with:
elsewhere token create --adminUse the same user, configuration and execution environment as the server. In Docker, run
docker exec <container> elsewhere token create --admin. The command works before startup or while
the server runs. It prints a new secret once, after committing its hash and grants to
$XDG_CONFIG_HOME/elsewhere/state.sqlite3, or ~/.config/elsewhere/state.sqlite3 when
XDG_CONFIG_HOME is unset. Startup creates no tokens. See token permissions and management
for scoped access, recovery and database backups.
The desktop takes the size of the controlling viewer's display area. Fullscreen fills the screen with the display, with keyboard lock so shortcuts like Ctrl+W reach the desktop. Use Ctrl+Alt+Shift+H to show or hide viewer controls while fullscreen.
Hide controls hides the top bar and open panels. Outside fullscreen, it keeps the status bar, statistics and stream controls. Use the highlighted Show Controls button at the left of the status bar or Ctrl+Alt+Shift+H to return. The shortcut also releases mouse capture. The mode belongs to this viewer and resets on reload; hidden terminal sessions stay open. Browser tabs and the address bar stay visible in windowed mode.
Open Search from the toolbar or with Ctrl+Alt+Shift+S. Search Applications, Windows, and Viewer actions by name, application description or window application ID. Arrow keys select, Enter invokes, and Escape returns focus. Window IDs distinguish matching titles. Applications launch a new instance; Windows activate an existing window, including a minimized one. Viewer actions open Files, Terminal, Settings, Windows and Statistics, toggle Fullscreen, or Show/Hide Controls. Results follow live windows and permissions. Read-only users can search windows. Opening panels preserves fullscreen. The Help menu lists keyboard shortcuts. Window popups offer Fullscreen and Show/Hide Controls only. Opening the Search menu releases held desktop input and mouse capture; search and composition stay in the viewer.
Any number of people can watch at once, each with a stream scaled to their own window. The first to
connect with desktop.control drives the pointer and keyboard; other eligible sessions can use
request control for the current controller to approve within five minutes. Sessions with
desktop.take_control and desktop.control can take over immediately. The Participants menu lists live connections,
and observers see the desktop pointer. Each token grants explicit feature access. Desktop viewing, clipboard, files, audio,
camera, microphone and program execution have separate permissions.
Use --screen-size 1920x1080 to set a fixed desktop resolution from startup. Browser resizes and
control handoffs keep that resolution; each browser scales the picture to fit while preserving its
aspect ratio. Both dimensions must be even and between 2 and 8192 pixels. Without this option, the
desktop follows the controlling browser's size. Settings offers the same choice at runtime: select
Auto: fit controlling viewer, a resolution preset, or custom pixel dimensions, then Apply resolution.
Fixed resolutions use scale 1. Returning to Auto immediately uses the controller's last display size
and device scale, or retains the current output until a controller supplies a size.
Each viewer picks its own codec, quality and encoding effort through status-bar chips with radio popovers. The codec list is what both the server
and that browser can do ("Auto (HEVC)" shows the pick). A window narrower than 57 rem folds these controls into the
status bar's Stream Settings popover, with a radio fieldset for each choice. All five quality levels adapt under their
selected ceiling: Very Low (2 Mbit/s), Low (5), Medium (--bitrate, 8 by default), High (12), and
Max (25). Medium is the default. The stream starts at its ceiling; under pressure the server halves the
bitrate and holds it, then climbs back a quarter at a time. Targets below 3 Mbit/s have a 30 fps cap.
A custom Medium ceiling is displayed as configured, even if it exceeds another level.
Effort defaults to Fast. Balanced and High spend more encoding time for possible picture improvements
at the same bitrate. Changes restart the viewer's stream immediately and are remembered on reconnect.
See encoding effort for mappings and measured tradeoffs.
Before accepting connections, the server probes the codecs allowed by --codecs. Hardware encoding
mode advertises only working VA-API or NVENC encoders on the selected render node. The browser sends its
supported codecs in preference order, H.264, HEVC, AV1, VP9, then VP8, with a manual selection first.
The server starts the first shared codec and sends its supported list and current selection to the viewer.
If encoding fails twice, the server moves to the next shared codec and notifies the viewer. Failure
counts clear after an attempt produces at least 120 frames over two seconds. If every candidate fails,
the connection stays open with a Retry video button. Choosing a codec starts selection over.
GET /api/codecs also exposes the startup probe results.
After the picture stops changing one more frame
encodes the settled picture at the current target so text left rough by motion can sharpen.
Frames are painted on a 2D canvas. ?renderer=webgpu in the URL uses a WebGPU external-texture path
instead; it is opt-in because Chromium on Linux occasionally presents a blank frame that way, which looks like flicker.
Windows that don't draw their own title bar (X11 applications, Vulkan and SDL programs, anything that asks for server-side decorations) get one from the compositor: drag it, double-click it to maximize, resize from the edges, close, maximize and minimize with its buttons.
Other clients can join later: WAYLAND_DISPLAY=elsewhere some-app. X11 apps work too: an
Xwayland is started automatically and the log prints its DISPLAY. Super (or Alt) + left drag moves
any window from anywhere in it.
Each desktop owns a private PipeWire server, Pulse compatibility service and WirePlumber policy. Startup commands, menu/API launches and their descendants receive the private audio socket selectors. Applications play into the session output, which is encoded as 48 kHz stereo Opus for the browser. The viewer's microphone button sends the controlling browser's microphone into the session microphone. Stopping capture stops the browser's recording indicator and leaves silence for recording applications.
Open Audio Mixer in the status bar for session devices and individual application streams, with actual peak meters, volume and mute. Read-only viewers can inspect; the controlling viewer changes the shared session. Muting the session microphone does not stop browser capture. With multiple session endpoints, the mixer also offers routing and default selection through WirePlumber.
--no-audio starts no audio services. Audio startup or service failure leaves the desktop running with
audio unavailable. Shutdown stops the audio workers before the private services. For clients launched
separately, use the connection variables printed in the log. See session audio
for the device names, environment and lifecycle.
The browser's webcam works the same way through a v4l2loopback device, the one kind of camera every
application understands: on the host, modprobe v4l2loopback exclusive_caps=1 card_label=elsewhere
(the package is v4l2loopback-dkms on most distributions) and start the server with --webcam /dev/videoN (the device it made; in Docker add --device /dev/videoN --group-add $(stat -c %g /dev/videoN)
to docker run). The camera button in the status bar then sends the webcam as VP8, scaled to 720p, to
that device, which video calls in the desktop pick as a camera. With exclusive_caps=1, the loopback
advertises only video output until it receives frames. Turn the browser camera on before starting a
call so the application can discover it.
In the Docker image, the guvcview menu entry uses the device selected by --webcam. Enable the
browser camera before launching guvcview. If it was opened early, close it and relaunch after enabling
the camera. Applications launched by the compositor receive the configured device in ELSEWHERE_WEBCAM_DEVICE.
Without --webcam (or if the device can't be opened, which the log says) there is no button.
--exec runs at startup with the environment of a Wayland session (XDG_SESSION_TYPE, the toolkits'
backend switches, DISPLAY for X11 programs), and --kiosk fullscreens every window. Together they run a whole nested desktop; with the
mutter-devkit package installed, GNOME:
cargo run --release -- --kiosk --exec 'dbus-run-session -- gnome-shell --devkit'Applications launched by the desktop inherit GSK_RENDERER and QT_QPA_PLATFORM from the
Elsewhere process. Set them in its launcher environment when needed:
GSK_RENDERER=ngl QT_QPA_PLATFORM='wayland;xcb' elsewhereGSK_RENDERER=ngl selects GTK's GL renderer to work around intermittent thin dark triangles
with GTK 4.22's Vulkan renderer, including the nested shell's GTK viewer.
QT_QPA_PLATFORM='wayland;xcb' makes Qt try Wayland, then X11 for builds without a Wayland plugin.
For Docker, pass -e GSK_RENDERER=ngl -e 'QT_QPA_PLATFORM=wayland;xcb' before the image name.
Without these settings, applications choose their toolkit defaults.
Kiosk Mode in Settings applies to existing and newly opened application windows. Turning it off
restores their previous window state and geometry. Windows opened in kiosk become maximized within
the work area, accounting for panels and compositor title bars. Kiosk does not prevent applications
from changing their own fullscreen state. Kiosk and resolution are shared desktop settings requiring
desktop.control; changes last until the server restarts, when startup flags apply again.
The devkit's window follows the browser size. Don't add --virtual-monitor: that adds a second
monitor, and GNOME puts its top bar only on the first one.
Panels work without a nested desktop: the compositor speaks wlr-layer-shell and wlr-foreign-toplevel-management, so waybar and xfce4-panel run as ordinary clients with working taskbars (maximized windows stay clear of the panels, minimize goes through the taskbar).
cargo run --release -- --exec 'waybar & exec foot' # add "wlr/taskbar" to modules-left for a taskbar
cargo run --release -- --exec 'dbus-run-session -- sh -c "xfce4-panel & exec foot"' # first run asks for the default configxfce4-panel needs a D-Bus session bus for xfconfd; the wrapper is only needed where there is none.
Its pager lists the shared workspaces. Waybar's default config draws its
icons with Font Awesome (otf-font-awesome); GTK only shows icons in the Xfce menus with
gtk-menu-images=1 in ~/.config/gtk-3.0/settings.ini, which xfsettingsd normally sets. Windows
that draw their own title bar take its buttons from the GSettings key
org.gnome.desktop.wm.preferences button-layout. Elsewhere supplies
menu:minimize,maximize,close as the session default while preserving explicit user preferences.
The Dockerfile packages all of that on Arch Linux: Elsewhere, the Xfce panel and apps,
Firefox and Chromium, applications for what the desktop can do (guvcview for the webcam, Audacity, GIMP,
mpv with VA-API decode, Ristretto, pavucontrol), nano and a passwordless sudo for the elsewhere user, with PipeWire for audio and
Mesa's OpenGL and Vulkan drivers for Intel and AMD (glxgears, vkcube and the info tools are included
to check them), and GTK menu icons. Programs launched from the desktop start in the home folder.
The desktop starts empty; the viewer's menu launches the applications, and --exec xfce4-panel after
the image name adds the panel. make docker-run builds the image and runs it; the details are
in the Dockerfile's header.
The page says when the server closed its socket with a token dialog ("wrong token" or "token revoked or expired").
In Settings, enable Capture Mouse on Click for games that use edge scrolling. The first click captures the mouse; movement, clicks, and the mouse wheel reach the desktop only while captured. A Mouse Captured warning sits in the top bar while capture lasts and controls are shown. Use the application's fullscreen button to send normal Escape presses to the remote application while keeping the mouse captured in supported browsers. Hold Escape to use the browser's release gesture. Outside supported fullscreen, normal Escape may release capture; browser-only F11 fullscreen does not enable this behavior. The setting is remembered in your browser and starts off. Games that request pointer lock still capture automatically; after the browser releases capture, click to resume.
Use --url-prefix /elsewhere/alice to host an instance beneath a path, including nested paths.
By default the proxy preserves the prefix; add --proxy-strips-prefix when it removes it.
Reverse proxy setup covers both nginx modes, multiple instances under one
HTTPS hostname, TLS termination with --no-tls, and separate UDP ports for optional WebRTC.
The video travels on the WebSocket. A viewer can move it to a WebRTC data channel (UDP, ordered and
reliable) by opening the transport status control in the status bar. The page offers and the server answers with
candidates that the UI addresses using the page's hostname and the server's UDP port, with --rtc-addr overriding the hostname. The frames move to the channel once it opens; input, audio, events and the signalling
stay on the WebSocket either way, so the socket is needed whatever carries the video.
The server paces data-channel fragments according to each viewer's stream target to limit bursts. The channel remains ordered and reliable: missing packets can hold up later video until SCTP retransmits them. Lost frames or arrival gaps over half a second caused by the link in three of ten seconds trigger fallback to the socket. Pending video with no byte acknowledgements for three seconds, or a frame waiting that long at the server queue's front, also triggers fallback. Encoder restarts preserve the acknowledgement deadline. An idle channel stays open. See stream reliability for the controlled-link checks and measurements.
The selected transport stays WebRTC during fallback. Socket video continues while a fresh channel connects. Failed attempts retry with jittered exponential delays of about 1 to 30 seconds; ten seconds without channel loss or stalls resets the backoff. Each attempt has a ten-second total budget. The transport control shows the actual path and recovery state, with Retry Now beside it while waiting. Its popover selects the preferred transport; choosing WebRTC does not claim it is already carrying video. Statistics also shows the reason, retry count and next attempt. Selecting WebSocket cancels recovery. A server or browser without WebRTC support stays on the socket without retrying, preserving the preference. Quality selection and its adaptive ceiling are independent of transport recovery.
The server listens on UDP on the listen port's number. --rtc-port sets both the local and advertised UDP port while keeping the browser's hostname.
The server supplies its UDP port and optional --rtc-addr to the UI. The UI applies that endpoint
to the ICE answer, using the page's hostname when no address is configured. The browser resolves the hostname. Use a fully qualified hostname: browser ICE resolution does not use DNS search suffixes.
For dual-stack hostnames, make UDP reachable over both IPv4 and IPv6.
Browsers that cannot use hostname candidates recover through the existing WebSocket path.
Docker needs both TCP and UDP published, for example -p 8443:8443 -p 8443:8443/udp. Forward the UDP port unchanged; the page can use a different TCP port.
When UDP uses a different port from the page, such as behind an HTTPS-only proxy, set
--rtc-port and forward that port unchanged. Set --rtc-addr <reachable IP> only to override the hostname. This advertises that IP with --rtc-port,
or the listen port's number if omitted; the advertised UDP port must be forwarded unchanged.
For browsers behind a strict NAT give them a STUN server (--stun stun:host:3478) or a TURN server (--turn turn:host:3478 --turn-user … --turn-pass …, its address as the browsers reach it). coturn is one container to self-host:
docker run -d --name turn -p 3478:3478/udp -p 3478:3478 -p 49160-49200:49160-49200/udp coturn/coturn \
-n --lt-cred-mech --user=USER:PASS --realm=elsewhere --listening-port=3478 \
--min-port=49160 --max-port=49200 --relay-ip=<the address the server reaches it at>--no-rtc leaves the option out entirely.
Select the NVIDIA DRM render node with --render-node. On hybrid systems the default
/dev/dri/renderD128 may belong to the integrated GPU. Elsewhere matches the selected node's PCI
address to a visible CUDA device, so the renderer and encoder use the same GPU.
Use the NVIDIA driver with DRM modesetting, GBM/EGL support and its EGL GBM external platform
library, plus an FFmpeg build with NVENC. Elsewhere renders with GLES, reads the composed pixels
back to memory, converts them to limited-range BT.709 YUV420P and submits them to NVENC.
This path copies pixels through CPU memory. --software-encoding keeps NVIDIA rendering and
uses CPU encoders instead.
H.264, HEVC and AV1 are probed independently at startup. The GPU must support the requested codec;
RTX 30-series GPUs encode H.264 and HEVC but do not encode AV1. No CUDA SDK is needed to build
Elsewhere. The NVIDIA driver supplies libcuda.so.1 and libnvidia-encode.so.1 at runtime.
A missing driver library, an incompatible driver or an unsupported codec causes startup to fail
if no requested encoder works. NVENC session limits apply across viewers and other applications.
For Docker, install the NVIDIA Container Toolkit
on the host, then configure Docker with sudo nvidia-ctk runtime configure --runtime=docker
and restart Docker. With Docker BuildKit available, build the standard image and its NVIDIA variant:
docker build -t elsewhere .
docker build -f Dockerfile.nvidia -t elsewhere-nvidia .The NVIDIA image links the host-supplied GBM backend into the Arch library search directory.
It supports Arch and Ubuntu/Debian hosts without packaging driver libraries. Use the standard image
for Intel, AMD or CPU-only operation. --build-arg BASE_IMAGE=<image> selects a different build
of the standard Elsewhere image.
Pass the NVIDIA runtime explicitly, even when --gpus all works with Docker's default runtime.
The NVIDIA runtime
also supplies the graphics driver registrations used by EGL and Vulkan. For example, when NVIDIA
is renderD129:
docker run --rm --runtime=nvidia --gpus all --device /dev/dri --shm-size 1g \
--group-add "$(stat -c %g /dev/dri/renderD129)" \
-e NVIDIA_DRIVER_CAPABILITIES=compute,video,graphics,utility,display \
-p 8443:8443 -p 8443:8443/udp \
-v elsewhere-data:/home/elsewhere/.config/elsewhere \
elsewhere-nvidia --render-node /dev/dri/renderD129The host must initialize /dev/nvidia-modeset before creating a headless container; if needed,
run sudo nvidia-modprobe -m on the host. Passing only /dev/dri does not supply NVENC or CUDA.
Check the startup log for a GL Vendor: line naming NVIDIA and the verified NVENC encoders.
nvidia-smi or successful encoding alone does not establish GPU rendering: a missing EGL/GBM
setup can leave the compositor on llvmpipe. Render-node numbers can change after a reboot;
check which device belongs to NVIDIA before starting the container.
See NVIDIA validation for the tested paths and hardware limits.
On a machine with no /dev/dri at all (a VPS, a CI runner, a container started without devices) the
server renders with Mesa's llvmpipe through the surfaceless EGL platform and encodes on the CPU: pass
--render-node none, or nothing (a default node that isn't there means the same). Frames are read back
into memory for the software encoders, clients draw into shared memory (there is no dmabuf global to
offer them GPU buffers), and X11 clients render in software too. The same binary with the node present
behaves as before. To select CPU rendering explicitly, set
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json and
LIBGL_ALWAYS_SOFTWARE=1 in the environment. These select Mesa and force llvmpipe.
The Files tab browses the remote filesystem with the server process's Unix permissions. It starts in
Downloads (--files-dir selects another transfer folder). Each viewer chooses its own directory.
Upload and page drops outside the desktop use that directory; navigation during a batch cannot change
its destination. Browse folders, download files, create directories, rename entries, and confirm file
deletion. Home, transfer-folder shortcuts, breadcrumbs, sorting, and hidden files are available.
Listings load on navigation, Refresh, and this viewer's own changes to the displayed directory.
Reopening the panel or changes from another viewer do not refresh it. File access requires a control
token, including the API; read-only viewers have no Files tab or file download controls.
Drag a file over the desktop itself and the application under the pointer sees a drag coming; let go and,
once the file is uploaded, it is dropped there as a file:// URI, to copy or to move. Since the
application picks the folder, such a file is not uploaded to Downloads but staged under the cache
directory (~/.cache/elsewhere/drops, a folder with a random name per drop): Thunar moves it out into
the folder shown, Nautilus copies, an editor opens it where it is, and whatever is left there is swept after
a day (an editor still showing the file loses it then).
A drop no application took is copied to the transfer folder. The result shows saved names and offers
Open folder without changing the selected directory. Files copied in a file manager inside the desktop
show up in the status bar with a download button, and files copied on your machine paste into the
desktop's file manager, staged the same way.
The page works on a phone. Fingers reach applications as real touch points (wl_touch; X11
applications get XI2 touch through Xwayland), so a map pans with one finger and zooms with two, and a
drawn title bar drags with a finger. The hand button switches to "touch as mouse" for applications that
handle touch badly: a tap clicks, a finger drags, a hold of half a second right-clicks, two fingers
scroll, and a pinch zooms the picture on the phone (the desktop keeps the phone's size; two fingers pan
while zoomed, pinching back undoes it). The side panel slides over the stage, the top bar
keeps its icons. The keyboard button opens a full on-screen keyboard on any device. It is available to
desktop controllers and to window viewers with control permission, including Picture-in-Picture.
Letters and punctuation follow the desktop's keyboard layout. Shift, Ctrl, Alt and Super stay selected
for the next on-screen key or IME input; Esc, Tab, Return, Backspace, arrows and Del are also available. The keyboard overlays
the picture without resizing the desktop or streamed window. Its device-keyboard field supports phone
keyboards and IME composition. A phone's own keyboard can shrink the browser viewport. In a window
popup this resizes the application and clears maximized or fullscreen state, just like resizing the
popup itself. Closing the phone keyboard resizes it again to match the restored viewport. Fullscreen works where the browser allows it, including Android.
iOS Safari has no fullscreen for pages and needs Safari 26 for WebCodecs.
The ↗ button on a window's row in the panel opens it in a popup of its own, streaming only that window
(/?window=ID): the pointer, keyboard and clipboard work there as in the viewer, resizing the popup
resizes the window, and the popup reports when the window closes. With kiosk off, popup resizing also
resizes maximized or fullscreen applications, including windows maximized after kiosk exit. With kiosk
on, applications stay fullscreen. Popup resizing leaves the desktop resolution unchanged. Each popup
has its own encoder.
Picture-in-picture is also available in supporting browsers on a secure origin. The top-bar action opens the whole desktop; a window row's action opens that application. One PiP window belongs to each opener, with a compact title/status bar and Return to Viewer. Choosing another target replaces its content. Ordinary popups remain available.
The main viewer and desktop PiP share one participant and its control requests. While that participant controls the desktop, PiP supplies input and dimensions. The main desktop stays visible beneath a dimmed overlay; Return to Viewer or closing PiP restores it. Toolbar and sidebar controls remain usable. Another participant's control is preserved when PiP opens, closes or reconnects. Desktop playback moves to PiP; microphone and camera capture stay with the main connection while it survives. Capture stops when the participant loses control and must be restarted explicitly. Window PiP has no audio. PiP cannot use fullscreen keyboard capture. Use the on-screen keyboard or its IME field for desktop and window input, and return to the normal viewer for copied-file downloads or capture controls. Browser checks and known interaction limits are recorded in the desktop UI notes.
The compositor is the window manager, so the viewer and outside scripts can see and drive the desktop.
HTTP calls send the token as Authorization: Bearer <token>; the viewer page takes it from its URL
fragment once (#token=, never sent to the server), keeps it in sessionStorage and drops it from the
address bar, and sends it as the first message on its WebSocket. A tab without a token shows a dialog
asking for one. There are no cookies and a token is never in a URL the server sees. Each operation
requires its documented permissions. Token management uses /api/tokens; revoking a token ends
only its own sessions and live resources. Token values are disclosed only at creation.
T=$(elsewhere token create --admin)
curl -s -H "Authorization: Bearer $T" http://host:8443/api/windows | jq # the window list
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
http://host:8443/api/control -d '{"id":3,"op":"minimize"}' # act on a window
curl -X POST ... /api/control -d '{"op":"spawn","cmd":"firefox"}' # start a program
curl -o w.png -H "Authorization: Bearer $T" 'http://host:8443/api/windows/3/snapshot.png?percentage=50'
curl -o screen.png -H "Authorization: Bearer $T" http://host:8443/api/screenshot.png
curl -s -H "Authorization: Bearer $T" http://host:8443/api/windows/3/elements | jq # with --elements
curl -X POST ... /api/input -d '{"type":"click","window":3,"x":549,"y":47}' # click an element
curl -X POST ... /api/input -d '{"type":"text","text":"hello"}' # type; also key, scroll, move, buttonEach window reports id, title, app_id, icon (with the picture at /api/windows/{id}/icon), content
(video, game or photo when the client says so), x11, pid, its geometry x y w h in logical pixels, where
that geometry sits in the client's surface (geo_x geo_y), its open popups ([x, y, w, h] relative to
the geometry), the height of the title bar the compositor draws above it (decoration, 0 when the
application draws its own), its stacking index z (null while minimized), maximized, fullscreen,
minimized, focused, updated_ms on the compositor clock, and monotonic content_revision for preview invalidation. Ops: activate, close, minimize, unminimize, maximize,
unmaximize, fullscreen, unfullscreen, move (x, y), resize (w, h), spawn (cmd, run
with sh -c in the same environment as --exec), launch (app, an id from GET /api/applications,
the installed .desktop launchers, whose icons are at /api/applications/{id}/icon) and quit, which
ends Elsewhere. Requests are fire-and-forget; unknown ids are ignored.
Snapshots are lossless PNGs of a window's own buffers, so they include covered and minimized windows.
Omitted sizing returns native dimensions. Supply one of width, height, or
percentage; see sizing limits.
With --elements, /api/windows/{id}/elements lists a window's UI elements (buttons, links, text fields,
tabs, menu items, …) with role, name and rectangle relative to the window, so a script or an agent can
target a control instead of interpreting pixels. It reads the toolkits' accessibility trees over the D-Bus
session Elsewhere was started in (dbus-run-session -- elsewhere --elements … if there is
none; the container does this). GTK and Qt applications and Firefox publish their trees when started from
--exec or spawn; Chromium and Electron apps need --force-renderer-accessibility. Without the flag
the route answers 501; 503 means the tree couldn't be read (no bus, or the application went away).
/api/input clicks, types, presses key chords and scrolls as a user would, with coordinates relative to a
window when you pass its id, so element rectangles can be used as they are. /api/clipboard reads what an
application last copied, text or a PNG, and sets what it will paste; the viewer page bridges the same
clipboard to the browser's (copy text or an image in an application, paste locally; Ctrl+V in the page
pastes the browser's clipboard, screenshots included).
The status bar's clipboard button opens a preview of the desktop clipboard. Text can be selected, edited or cleared; images have a bounded preview, and copied files have download buttons. An unfinished text draft survives closing the panel. If another application copies while you edit, the panel lets you load that content or replace it with your draft. Opening the panel does not change the browser's clipboard. Type Text sends the preview or unsaved draft as keystrokes to the focused application. It uses the active keyboard layout, with Enter for newlines and Tab for tabs; unsupported characters are skipped. Save changes the clipboard without typing. View-only sessions can inspect permitted content but cannot edit it or download copied files.
The same operations are MCP tools at /mcp (Streamable HTTP, same bearer token), so a coding agent can
drive the desktop: windows, elements, snapshot, screenshot, window_control, move_window,
resize_window, applications, launch, spawn, click, move_pointer, button, scroll, key,
type, clipboard_read, clipboard_write.
claude mcp add --transport http elsewhere https://host:8443/mcp --header "Authorization: Bearer $T"
ELSEWHERE_TOKEN=$T codex mcp add elsewhere --url https://host:8443/mcp --bearer-token-env-var ELSEWHERE_TOKENThe server hands the agent its manual on connection (skills/elsewhere/SKILL.md) and the
generated reference.md with every route, body and tool schema; both are also served at /skill/
without a token and can be copied into an agent's skills directory. With a self-signed certificate,
point the agent at the fingerprint the server prints, or run --no-tls behind a reverse proxy.
The viewer page uses the same data over its WebSocket: its application menu lists the installed
programs by category, with a search box, and starts them; its side panel lists the windows with thumbnails
and open-in-popup, snapshot, maximize, minimize and close buttons (click a row to bring the window
forward, type in the box at the top to run a command), its Statistics tab shows the stream's numbers
(per-stage timings, drops, audio lead) once a second. Settings → Overlays in the top bar controls
local coloured window outlines and the focused window's accessibility-element outlines. Both apply
immediately and are remembered. UI Elements requires server accessibility support (--elements).
The power menu quits Elsewhere. Desktop notifications appear as toasts with their actions;
Elsewhere is the session's notification daemon when no other runs. A panel is optional. In the browser console,
elsewhere.windows(), elsewhere.activate(id), elsewhere.control({...}), elsewhere.spawn(cmd), elsewhere.snapshot(id) and
elsewhere.elements(id) do the same.
The Broadcasts tab starts and stops independent desktop streams to RTMP/RTMPS services. Presets live in browser storage and share across instance paths on the same origin. The same controls are available through HTTP and MCP. See desktop broadcasts, including YouTube and Twitch setup.
The viewer lives in web/ (React, TypeScript, Tailwind CSS, Vite) and is built into web/dist, which the binary
embeds at compile time; make builds the viewer (Node 24) and then the binary, make web only the
viewer, and a cargo build without web/dist stops with that hint. npm run dev in web/ serves the
page with hot reload, proxying /ws and /api to a server started with --no-tls --listen 127.0.0.1:8080.
Useful flags: --no-tls (localhost or HTTPS proxy), --url-prefix, --proxy-strips-prefix, --listen, --bitrate <kbps> (Medium ceiling),
--codecs h264,hevc,av1,vp9,vp8 (allowlist applied before probing; omitted means all codec families;
its order does not override the browser's preferences; no usable encoder stops startup),
--software-encoding, --exec, --kiosk, --elements, --no-audio, --webcam, --no-rtc, --rtc-port, --rtc-addr, --stun,
--turn, --turn-user, --turn-pass, --socket-name, --render-node (none for no DRM node). --help
lists them all.
Games and other clients that lock the pointer get raw mouse deltas: the page mirrors the lock with the Pointer Lock API. In supported application fullscreen, normal Escape reaches the client and holding Escape releases capture through the browser.
GTK client headers default to minimize, maximize and close buttons in ordinary and Docker sessions. The binary supplies session-scoped GSettings defaults; explicit user settings and application schemas retain their precedence. Fullscreen and fixed-size windows still follow their toolkit's window-control rules.
Certificate and tokens live in $XDG_CONFIG_HOME/elsewhere/; delete them to regenerate.
The session audio visualiser loads when opened. Click the Elsewhere logo to open About, which links to acknowledgements and the source repository.
Source and build instructions are available in the
GitHub repository.
For a release, run elsewhere --version and check out the matching
vX.Y.Z tag. For a development or modified build, use the revision and any local
changes supplied by its distributor; the current default branch may differ.
Install the build dependencies listed under Install and
Requirements, then run
make. This installs the locked npm dependencies, builds the viewer, and builds
the release binary. web/package-lock.json pins viewer dependencies and records
their download URLs and integrity hashes.
The check workflow runs on pushes and pull requests in an
Arch Linux container. It builds the application and examples, runs workspace Rust tests,
checks the browser visualiser and decoder disposal, exercises software RTMP/RTMPS broadcasts,
and checks application startup, screenshots and software video encode/decode. Its dependency
installation and commands can also be run in a disposable archlinux:base-devel Docker
container with the checkout mounted and 1 GiB of shared memory.
The ignored VAAPI import test requires a real render node. The ignored private-audio tests
require the audio Docker rig, with ELSEWHERE_TEST_PIPEWIRE pointing to an isolated graph
for the stream capture test. V4L2 integration checks require a loopback device.
Those checks run separately in a suitably equipped Docker rig.
Routine CI does not run the reliability benchmark suite.