Mutter Desktop
The web client and its bridge in one window. The main process runs web/bridge/server.mjs,
and the window loads the shared web/app. The shell adds desktop capture, global push to
talk, and protected password storage.
Why it exists. A managed browser carries its employer's policies onto every network, and a
common one disables WebRTC over UDP — voice keeps working because the bridge sends that from
Node, but a screen share from the browser process can never connect. Electron ships its own
Chromium and reads no policy from the registry, so the same code connects. It also gives a real
screen picker with system audio on Windows (loopback capture, which getDisplayMedia in a
browser tab does not offer), one taskbar entry with our icon, and an install that asks for no
admin.
Run it
Same three shapes as VS Code. Grab one from the releases:
| File | Like VS Code's… | What happens |
|---|---|---|
Mutter-x.y.z-win-setup.exe |
User Setup | Installs to %LOCALAPPDATA%\Programs\Mutter, Start menu and desktop shortcut, no admin prompt. Settings in %APPDATA%\Mutter. |
Mutter-x.y.z-win.zip |
.zip download | Unzip anywhere and run Mutter.exe. Create a data folder beside it to keep server details and settings with the app. |
Mutter-x.y.z-win-portable.exe |
Portable Mode | One file. Keeps a data folder beside itself, so a USB stick carries the app and its servers together. |
.dmg / .AppImage |
— | macOS and Linux. The data-folder rule works for the AppImage too. |
They are unsigned, so Windows SmartScreen will say "unrecognised app" the first time: More info → Run anyway.
Saved passwords and TURN secrets use the operating system’s credential protection. Moving a portable profile to another computer or OS account may require entering passwords again. If secure storage is unavailable, passwords stay in memory for the current session; Mutter does not fall back to a plaintext file. Existing localStorage passwords migrate on launch.
On a managed work laptop, don't
If the machine runs an EDR such as CrowdStrike, or AppLocker/WDAC, an unsigned executable it has
never seen is likely to be blocked — and may raise a detection with your name on it. Nothing here
is worth that. The browser-and-bridge setup in web/ already
gives you "install and it works" on such a machine without introducing a single new Windows
binary: install Mutter as an app from the browser's address bar, and have the bridge start with
Windows. Use these builds on machines you own, and hand them to friends.
Build it
Releases are built by the workflow on GitHub's runners —
push a tag like v0.1.0 and the three platforms build and attach themselves to a release, so no
toolchain is needed anywhere near the laptop.
To build locally you need Node 22+:
cd desktop
npm install
npm start # run from source, with the bridge on localhost:8789
npm run test:persistence # two Electron launches with an isolated profile (needs a GUI session)
npm run dist:win # portable + per-user installer into desktop/dist (run this on Windows)
Windows targets can only be built on Windows without wine; macOS targets only on macOS.
How it fits together
BrowserWindow ──http/ws──▶ bridge (in the main process) ──TLS + UDP──▶ murmur
│
└── getDisplayMedia() ──▶ setDisplayMediaRequestHandler ──▶ our picker (web/app/picker.html)
└──▶ audio: 'loopback' on Windows
- The desktop bridge uses loopback port 8789. Keeping the origin stable preserves saved
servers, settings, and certificate pins between launches. The browser bridge uses 8788.
PORTcan select another fixed port; changing it selects a different browser storage origin. Port 0 is refused, and an occupied port produces an error instead of silently switching to a fresh settings store. The bridge'sreadypromise resolves with the URL. web/and the fonts are shipped as extra resources next to the app, not inside the archive, so they can be inspected and edited in place. The bridge finds them throughprocess.resourcesPath.- The renderer runs sandboxed with context isolation. Its preload exposes only credential
reads and writes; the main process checks the requesting window, frame, and URL. The picker
has a separate preload exposing its selection controls. The picker loads the shared
web assets and receives the main window’s selected theme and appearance before it is shown. The main process talks to
the page only through
executeJavaScriptagainstwindow.mutter, to read the push-to-talk binding and to press or release it.
Push to talk from any window
The browser only hears your push-to-talk key while its tab is focused. The desktop app keeps hearing it: while the Mutter window is in the background it watches for the one key or mouse button you bound under Settings, and holds the mic open for exactly as long as you hold it. The key still reaches whatever you are doing, so a game bound to the same key keeps working.
This uses a system-wide input hook (uiohook-napi), which is the same mechanism Discord uses. It compares every key event against your one binding and does nothing else with them; nothing is stored or sent anywhere, and it is inactive while the Mutter window is focused because the page handles the key itself there.
Platform notes:
- Windows: works out of the box.
- macOS: the first press prompts for the Input Monitoring permission in System Settings → Privacy & Security. Until it is granted, push to talk only works with Mutter focused.
- Linux: works on X11 and for XWayland apps. Native Wayland apps do not expose key events to other programs, so there push to talk only works with Mutter focused.
If the hook cannot load, the app logs one line and carries on without it.
Signing, later
Unsigned is fine for a handful of friends. If SmartScreen becomes a nuisance, an Authenticode
certificate plus CSC_LINK/CSC_KEY_PASSWORD secrets in the workflow is all electron-builder
needs; the config already leaves room for it.