← All docs
Ideas

mDNS / network auto-discovery

> ✅ Shipped in v0.5.3. This page kept as the design rationale; what
> actually ships is documented inline below.

Use case

Setting up a multi-machine show: VJ on one laptop running Resolume, controller on another laptop running XOSC. Today the user has to find the Resolume box's IP, type it into the host field, hope they didn't typo. Auto-discovery removes that step.

What's discoverable

  • Resolume Avenue / Arena advertises an mDNS service. If we trust the (unverified) outside-agent claim, the service name is along the lines of _resolumearena._tcp or _resolumeavenue._tcp on port 8080 (Wire = 8081). Verify on a real Resolume install before coding. If they don't advertise mDNS, fall back to network scan.
  • Generic OSC peers — OSC has a registered service type _osc._udp per the spec. Few apps advertise it, but the ones that do (some Lemur templates, custom rigs) become free to find.
  • Companion advertises _companion._tcp on port 16622.
  • Another XOSC instance could advertise itself once we add OSC inbound (osc-inbound.md).

Shape

A passive browser running in the main process. When a peer matching a known service appears, push to renderer:

interface DiscoveredPeer {
  service: "resolume" | "osc" | "companion" | "xosc" | "unknown";
  name: string;
  host: string;
  port: number;
  txt?: Record<string, string>;
  firstSeen: number;
  lastSeen: number;
}

Renderer keeps the list in a small Zustand slice, surfaces it in:

  • Resolume Out / Resolume In inspector — host/port becomes a dropdown ("127.0.0.1:8080" + any discovered Resolumes + manual entry).
  • OSC Out inspector — same dropdown idea for OSC peers.

Implementation

bonjour-service (npm) is the simplest cross-platform mDNS browser. It works on Linux, macOS, and Windows 10+ without extra deps.

import { Bonjour } from "bonjour-service";
const bonjour = new Bonjour();
const browser = bonjour.find({ type: "resolumearena" }, (svc) => emitFoundPeer(svc));

The browser runs continuously while the app is open. Disposed in app.on("window-all-closed").

Implications for air-gap

mDNS is a multicast protocol — it broadcasts on 224.0.0.251:5353. On a hardened venue laptop where outbound multicast is locked down, the browser will simply find nothing, which is the right failure mode. The app still works fine with manual IPs.

We don't advertise XOSC itself by default. Listening is fine; advertising shows up in network scans. Make advertise an opt-in toggle in Settings.

UI pattern

In the Resolume inspector's host field, the existing TextInput becomes a small composite:

┌──────────────────────────────────────┬──┐
│ 127.0.0.1                            │▾ │
└──────────────────────────────────────┴──┘
       │ on click
       ▼
┌──────────────────────────────────────────┐
│ ▶ 127.0.0.1                  loopback    │
│ ▶ 192.168.1.50  Resolume Arena · 8080    │
│ ▶ 192.168.1.51  Resolume Wire · 8081     │
│ ── manual entry ──                       │
│ Type IP…                                 │
└──────────────────────────────────────────┘

When the user picks a discovered peer, host and port are filled in.

What actually shipped (v0.5.3)

  • electron/peer-discovery.ts runs bonjour-service browsers for
_resolumearena._tcp, _resolumeavenue._tcp, _resolumewire._tcp, _resolume._tcp, _osc._udp, and _companion._tcp. Each "up" event is pushed to the renderer on the peer:up channel; "down" mirrors it.
  • Renderer-side src/features/discovery/peerBus.ts keeps a Map keyed by
kind::name::host:port, exposed via a usePeers() hook backed by useSyncExternalStore so re-renders are scoped to subscribers (the picker dropdown only).
  • src/components/ui/HostPicker.tsx is a drop-in for the host TextInput.
Picking a peer fills both host and port. Filters by accept so the Resolume inspectors don't show Companion controllers and vice versa.
  • Wired into Resolume Out, Resolume In, and OSC Out inspectors. The OSC In
inspector keeps a plain bind selector — that's a listen choice, not a connect choice.
  • Advertise is off. Listening only.

Estimate

M. Most work is the renderer-side dropdown component + Zustand slice + IPC plumbing. Bonjour-service does the heavy lifting.

Open questions

  • Should discovered peers persist across app restarts (cached in config, shown as "last seen N hours ago") or always start fresh? Lean toward always-fresh — stale entries are misleading.
  • Scope of advertising. If we advertise XOSC itself, what do we advertise? Probably nothing yet — XOSC is mostly a sender. Once osc-in ships, advertising the inbound listener as _osc._udp is correct.