One image (gitea.reeseapps.com/experimental/mirror), tagged :main on main and a version tag on release, exactly like brain-of-reese — plus a `test` job the publish job `needs`. A red pipeline therefore leaves :main on the last good build, which is what podman auto-update keeps serving: red means nothing shipped, not that production broke. The test job runs unit + API only and --ignores tests/e2e rather than deselecting it: -m filtering happens after collection, and tests/e2e/conftest.py imports playwright, so a marker alone would not keep the browser suite out of a container that has no browser. The e2e and realdisplay rungs stay local gates (AGENTS.md), and pyright is not in CI either, since the python container has no node to run it with — that asymmetry is written down rather than pretended away. The image sheds curl and its HEALTHCHECK: health belongs to the deployment (HealthCmd in the quadlet, which probes with the python already in the image), so the runtime image no longer ships a package that exists only to answer /healthz — and no longer contains a way to make outbound requests, which is consistent with a server that dials nothing. 171 MB to 157 MB. The image source label points at the real repo now instead of example.invalid. Verified locally with podman 5.8: the image builds, `mirror --help` exits 0, and it serves on 8080 when MIRROR_PORT says 8080. LLM-Generated: true
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 is no authentication — this is built for a trusted local network, and that assumption is deliberate and documented in SECURITY.md.
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). |
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.
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.
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.