ducoterra 269b64bace
Build and Push Containers / test (push) Successful in 1m20s
Build and Push Containers / build-and-push-app (push) Successful in 9s
fix(cli): mirror --version printed an error instead of a version
`main()` rewrites a bare invocation into `serve`, which is what makes
`mirror --port 8055` work at all — but it did that for every argument that
starts with a dash, so `mirror --version` became `mirror serve --version`, the
subparser has no such flag, and the command exited with "unrecognized
arguments". Found by pulling the image CI published and asking it what it was.

The rewrite rule is now `with_serve()`, extracted so the eight interesting
inputs are asserted directly — including `--port 9999` still becoming a serve
and `serve --version` staying as typed. `serve` also grew its own --version,
because typing it there is just as reasonable and argparse does not inherit
flags from a parent parser.

The extraction is not cosmetic: the first version of this test monkeypatched
`cmd_serve` to prove the rewrite, which did nothing, because argparse binds
`func` at parse time — the test would have started a real uvicorn on 9999 and
hung the suite. Asserting the pure function is both correct and safe.

187 tests pass, 2 skipped; ruff and pyright clean.

LLM-Generated: true
2026-11-01 18:14:45 -05:00

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. 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

uv sync --extra dev
uv run mirror serve            # http://0.0.0.0:8055

Then, from another machine on the LAN:

http://<this-machine>:8055/mirror    # to share
http://<this-machine>: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.

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.

Development

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:

MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay

See AGENTS.md for the operating rules, .agents/PLAN.md for the locked architectural decisions, and SECURITY.md for why there is no login.

S
Description
No description provided
Readme MIT
707 KiB
Languages
Python 70.8%
JavaScript 20.8%
CSS 4.5%
HTML 3%
Shell 0.6%
Other 0.3%