- Rename solution files: TeamsISO.sln/slnf -> Dragon-ISO.sln/slnf - Rename all src/TeamsISO.* directories and project files to src/Dragon-ISO.* equivalents - Update .gitignore to exclude build/test output logs - Update ci.yml, CHANGELOG.md, build-and-test.ps1, docs references Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.9 KiB
Dragon-ISO Control Surface — REST API
Dragon-ISO can expose a localhost HTTP server so external controllers (Bitfocus Companion, Stream Deck plugins, Bome MIDI Translator, custom node-RED flows, command-line scripts) can drive it without a UI binding.
Enabling
- Open Dragon-ISO → Settings → DISPLAY tab.
- Tick "Control surface (Stream Deck / Companion)".
- Default port is 9755; change it via the port textbox if needed.
- By default the server binds to
127.0.0.1only — it is NOT reachable from the LAN. - To allow other machines on the same network to drive Dragon-ISO (the
"headless host PC + thin client" scenario), tick the nested
"LAN-reachable" checkbox underneath. The settings panel will display
the LAN URL (e.g.
http://192.168.1.42:9755/ui) with a Copy button.
When enabled, the toast confirms Control surface listening on http://127.0.0.1:9755/ (or the all-interfaces equivalent in LAN mode).
One-time setup for LAN-reachable mode
Windows requires elevated permission to bind a non-loopback HTTP listener.
If you turn on LAN-reachable mode and don't see a connection from another
machine, run this once in an Administrator PowerShell (replace 9755
if you've changed the port):
netsh http add urlacl url=http://+:9755/ user=Everyone
Also confirm the Windows Firewall is letting inbound traffic to that port
through — New-NetFirewallRule -DisplayName "Dragon-ISO Control Surface" -Direction Inbound -Protocol TCP -LocalPort 9755 -Action Allow
in an elevated PowerShell, or add it through Windows Defender Firewall →
Advanced Settings → Inbound Rules.
Authentication
None — by design. In localhost-only mode, the loopback bind is the security model: any process on the operator's machine can hit these endpoints, the same threat model as a Stream Deck's USB connection.
In LAN-reachable mode, the assumption is a closed/trusted network (a production-control LAN, a dedicated show subnet, a private vlan). Any machine that can route to the host on the listener port can drive Dragon-ISO. Do not enable LAN-reachable mode on an untrusted network.
Response shape
All responses are application/json with Access-Control-Allow-Origin: *
so a browser-based control panel served from another origin can call the
endpoints. Most successful responses include "ok": true plus operation-
specific fields. Errors return HTTP 4xx/5xx with {"error": "..."}.
Endpoints
GET /ui
Self-contained HTML control panel. Open this in a browser to drive
Dragon-ISO from a phone, tablet, or second monitor. Lists participants live
via the same /ws WebSocket the rest of the doc describes, and posts to
the REST endpoints when you click. Single page, no external dependencies,
loads in <50KB.
GET /
Returns server info and an endpoint summary. Useful for "is the surface alive?" probes.
{
"product": "Dragon-ISO",
"version": "1.0.0.0",
"endpoints": ["GET /participants", "POST /participants/{id}/iso", ...]
}
GET /participants
Snapshot of the current participant list as the UI sees it.
{
"participants": [
{
"id": "1c3e2a8b-...-...",
"displayName": "Jane",
"isOnline": true,
"isEnabled": false,
"customName": null,
"stateLabel": "—"
}
]
}
POST /participants/{id}/iso
Enable or disable an ISO by participant Id. Body or query string:
{ "enabled": true, "customName": "Host" }
enabled is optional — omitting it toggles the current state. customName
is optional and overrides the auto-generated NDI output name.
curl -X POST 'http://127.0.0.1:9755/participants/1c3e2a8b-.../iso?enabled=true&customName=Host'
POST /participants/iso
Same as above but resolves by display name instead of Id. The Id varies across meetings; the display name is the operator-stable identifier.
{ "displayName": "Jane", "enabled": true }
POST /presets/{name}/apply
Apply a saved preset to the live participant list. Walks every participant
in the meeting, matches by display name, sets the custom output name, and
reconciles each enable/disable via the engine. Same code path as the
Presets dialog and the auto-apply-on-launch flow (PresetApplier.ApplyAsync).
{
"ok": true,
"name": "Friday Show",
"matched": 4,
"changed": 2,
"skipped": 1
}
matched is how many participants in the preset were live in the meeting;
changed is how many actually flipped state; skipped is preset entries
with no live counterpart.
POST /presets/refresh-discovery
Force NDI discovery to rebuild its finder. Useful after Apply Transcoder Topology or when Teams restarts mid-show. Returns immediately; the rebuild happens on the next poll tick.
curl -X POST http://127.0.0.1:9755/presets/refresh-discovery
POST /presets/stop-all
Disable every running ISO. Equivalent to clicking "Stop all ISOs" in the header. Returns the count that were running.
POST /teams/mute / /camera / /share / /leave / /raise-hand
Drive the corresponding Microsoft Teams in-call control via UIAutomation.
Returns one of Invoked / TeamsNotRunning / ControlNotFound /
InvokeFailed in the result field.
curl -X POST http://127.0.0.1:9755/teams/mute
POST /recording
Toggle per-output recording on or off. Body or query string:
{ "enabled": true, "directory": "D:/recordings/show-2026-05-09" }
directory is optional when enabled=false. Already-running ISOs are not
retroactively recorded — the operator should disable + re-enable a
participant to start recording it.
POST /recording/marker
Drop a timestamped marker into every active recording. Body or query string
optionally carries a label; if omitted, the label defaults to
Marker @ HH:mm:ss. Markers land in each recording's manifest.json under
the markers[] array as { "offsetMs": 12345.6, "label": "Guest answer" }.
curl -X POST 'http://127.0.0.1:9755/recording/marker?label=Guest+answer'
POST /notes
Append a timestamped line to today's show-notes file at
%LOCALAPPDATA%\Dragon-ISO\Notes\<YYYY-MM-DD>.md. Body or query string carries
text. Each line is prefixed with **HH:mm:ss** —; the file is markdown so
it renders nicely in any editor.
curl -X POST 'http://127.0.0.1:9755/notes?text=guest+segment+starts'
POST /recording/roll
Roll every active recording into a new chunk. Each running pipeline is
disabled (recorder finalizes its manifest.json), waits ~150ms, then re-
enabled (recorder opens a fresh subdirectory keyed by display name +
timestamp). Useful for chaptering between show segments — a Stream Deck
button mapped to this gives operators "next segment" without losing the
already-recorded footage.
curl -X POST http://127.0.0.1:9755/recording/roll
Response:
{ "ok": true, "action": "roll-recording", "rolled": 4 }
WebSocket — live state push
For controllers that want to light a button when an ISO goes LIVE without polling, connect to:
ws://127.0.0.1:9755/ws
On connect, the server sends a participants snapshot. Whenever the snapshot changes (participant joins/leaves, ISO toggled, custom name edited), a fresh snapshot is pushed within 250ms. Format:
{
"type": "participants",
"participants": [
{ "id": "...", "displayName": "Jane", "isOnline": true,
"isEnabled": true, "customName": "Host", "stateLabel": "LIVE" }
]
}
Client→server messages are ignored for v1 — all commands go through REST.
OSC over UDP
Same command surface, different transport. Enable the OSC bridge in the
DISPLAY tab (default port 9000 — TouchOSC's default). Bound to
127.0.0.1 by default; honors the same LAN-reachable toggle as the REST
surface — when LAN mode is on, OSC binds to 0.0.0.0 so a TouchOSC tablet
on the same network can talk to the host directly.
Address vocabulary:
/Dragon-ISO/iso "DisplayName" {0|1} — toggle/set ISO by display name
/Dragon-ISO/iso/by-id "guid" {0|1} — toggle/set by Id
/Dragon-ISO/preset "Name" — apply preset
/Dragon-ISO/teams/mute — UIA toggle mute
/Dragon-ISO/teams/camera — UIA toggle camera
/Dragon-ISO/teams/leave — UIA leave
/Dragon-ISO/teams/share — UIA share tray
/Dragon-ISO/teams/raise-hand — UIA raise hand
/Dragon-ISO/refresh-discovery — rebuild NDI finder
/Dragon-ISO/stop-all — disable every ISO
/Dragon-ISO/recording {0|1} — recording on/off (default dir)
/Dragon-ISO/recording/marker "Label" — drop a marker on every active recording
/Dragon-ISO/recording/roll — roll every active recording into a new chunk
/Dragon-ISO/notes "Free-form note" — append a timestamped line to today's notes
Companion → Surfaces → "Generic OSC" supports outbound OSC; bind a button
press to e.g. /Dragon-ISO/iso "Jane" 1. TouchOSC layouts can use the same
addresses on the same UDP port.
Bitfocus Companion recipe
Companion ships a generic HTTP module. Configure a button:
- Action:
HTTP: HTTP POST request - URL:
http://127.0.0.1:9755/teams/mute - Body type: None
Or for a participant-specific toggle:
- URL:
http://127.0.0.1:9755/participants/iso?displayName=Jane&enabled=true
Stream Deck XL recipe (without Companion)
Use the "Web Requests" plugin (or any equivalent). Set the action to a POST on the appropriate endpoint above.
Future work
- HTTPS / token auth — for deployments that don't have a closed network, layer TLS termination + a shared bearer token in front of the HttpListener. Out of scope for v1; the LAN-reachable mode is a trusted-network feature only.