# mirror **Device-agnostic screen mirroring that lives entirely in the browser.** `mirror` runs a small HTTP server on your machine. Open it from any device on your network: | Page | URL | What it does | | ------------ | ------------------ | ----------------------------------------------------------------------- | | **Share** | `/mirror` | Picks a window, screen, or tab via the OS-native picker and mirrors it. | | **Watch** | `/watch` | Shows every screen currently being mirrored; click a tile to take the stage. | | **Index** | `/` | Signpost + live count of mirrors. | Any number of people can mirror at the same time and any number of people can watch. There are **no accounts and no passwords** — this is built for a trusted local network, and that assumption is deliberate and documented in [SECURITY.md](SECURITY.md). What *does* decide who gets in is the source address: by default any address is welcome, and `--allow-cidr` / `MIRROR_ALLOW_CIDRS` narrows that to the networks you name. ## Quickstart ```bash uv sync --extra dev uv run mirror serve # http://0.0.0.0:8055 ``` Then, from another machine on the LAN: ``` http://:8055/mirror # to share http://:8055/watch # to watch ``` Linux is the primary target. On Linux the native picker is X11/Wayland's own (Chromium uses `xdg-desktop-portal` on Wayland/GNOME/KDE, and its built-in X11 picker on Xorg), so nothing has to be installed on the client side. The target *client* setup for an always-on display is Fedora Silverblue running GNOME Kiosk — see [`docs/kiosk-silverblue.md`](docs/kiosk-silverblue.md). ## How it works Media never touches the server. Each mirror is a WebRTC peer connection established directly between the sharer's browser and each watcher's browser (a "mesh" fan-out). The server is a **signaling relay and roster only**: it tracks who is publishing what, forwards SDP/ICE between peers, and pushes roster deltas to viewers. ``` sharer browser ──ws──► ┌──────────────────────┐ ◄──ws── watcher browser │ │ FastAPI │ ▲ │ offer/ICE ───►│ /ws (signalling) │◄─── offer/ICE │ │ │ registry (in-mem) │ │ └────── WebRTC (SRTP, LAN, p2p) ─────────┴─────────────────┘ ``` ## Configuration Environment variables (all optional; a `.env` file is loaded if present): | Variable | Default | Meaning | | ---------------------- | ------- | ---------------------------------------------------- | | `MIRROR_HOST` | `0.0.0.0` | Bind address for `mirror serve`. | | `MIRROR_PORT` | `8055` | Bind port. | | `MIRROR_ICE_SERVERS` | `[]` | JSON array of RTCIceServer objects, if peers cannot reach each other directly. | | `MIRROR_MAX_MIRRORS` | `0` | Cap on concurrent mirrors (`0` = unlimited). | | `MIRROR_ALLOW_CIDRS` | *(unset)* | Comma-separated source networks allowed to connect, e.g. `10.2.0.0/22,fd00::/64`. Unset means unrestricted. | | `MIRROR_TRUSTED_PROXIES` | `127.0.0.0/8, ::1/128` | Which peers may speak for a client via `X-Forwarded-For`. Only a proxy that overwrites the header belongs here. | | `DEBUGPY` | `0` | `1` opens a debugpy listener on `DEBUGPY_PORT`. | | `DEBUGPY_PORT` | `5678` | debugpy port. | ## Platform notes **Screen capture needs a secure context.** `http://localhost` is one; a plain `http://192.168.x.x` opened from another device is not, and the browser hides `getDisplayMedia()` completely. The `/mirror` page detects this and prints the three ways forward (share from localhost, serve HTTPS, or mark that one origin as secure on the sharing device). Watching never needs permission. This is why the fleet deployment terminates TLS in nginx rather than exposing `mirror serve` directly — over `https://`, every device on the network can share and none of this applies. A bare `mirror serve` is for the machine it runs on and for testing. **Linux.** On X11/Xorg Chromium uses its native capturer — the picker appears and that is the end of it. On Wayland (GNOME/KDE) Chromium goes through `xdg-desktop-portal`, so the compositor shows its **own permission dialog for each share**, which mirror neither controls nor tries to bypass. Chrome's picker offers whole screens and individual windows; on Wayland what the portal will agree to depends on the compositor. ## Deployment The internal deployment is `https://mirror.reeseapps.com`, described and automated in the deployments repo under `mirror/` (`app/` quadlet, `nginx/` edge, `certbot/`, `route53/`, and a README with the VM SOP). Three things about it are worth knowing from this side: - **TLS ends at nginx on the service VM**, not in this process, so there is no `--https` flag here. That is what makes `getDisplayMedia()` available on every device, and it means an `Upgrade` header and a long idle timeout on `/ws` are load-bearing. - **The address list is enforced twice** — nginx `allow`/`deny`, and `MIRROR_ALLOW_CIDRS` in the container for the path that bypasses nginx. Change both in one commit. - **The container has no internet egress.** Media goes browser-to-browser and the server only relays JSON, so it dials nothing; `Internal=true` is what makes that a fact rather than an intention. ### Kiosk client A wall display is a Fedora Silverblue box running the locked-down GNOME Kiosk session, auto-logged in as `kiosk`, with Firefox in `--kiosk` mode pointed at `/watch`. It is set up and repaired entirely over SSH — no keyboard, no console, no monitor: ```bash scp -r docs/kiosk-silverblue client:/tmp/ && ssh client \ 'cd /tmp/kiosk-silverblue && sudo sh install-kiosk.sh && sudo systemctl restart gdm' ssh client 'cd /tmp/kiosk-silverblue && sudo sh check-kiosk.sh' ``` Because nobody is standing at the machine, every claim about it is verified from the outside: `loginctl` for the session, the server's `/api/streams` for the viewer, the sharer's own peer state for the media, and a probe page for the rendered resolution. The runbook records why each file lives where it does on an otherwise read-only OS. ## Development ```bash uv run pytest # unit + API + browser E2E (~20s) uv run pytest -m "not e2e" # everything except the browser (~0.5s) uv run ruff check . && uv run ruff format --check . && uv run pyright ``` The browser suite drives **real capture**: Chromium's fake media device yields a moving 1280×720 monitor stream, so the assertions are on decoded, changing pixels at the watcher — not on log lines. There is a second, opt-in rung that exercises Chromium's **real X11 capture backend** on a private Xvfb display, isolated from your own session so it can never prompt you or mirror your desktop: ```bash MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay ``` See [AGENTS.md](AGENTS.md) for the operating rules, [.agents/PLAN.md](.agents/PLAN.md) for the locked architectural decisions, and [SECURITY.md](SECURITY.md) for why there is no login.