teamsiso/docs/CONTROL-SURFACE.md

276 lines
8.5 KiB
Markdown

# TeamsISO Control Surface — REST API
TeamsISO 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
1. Open TeamsISO → Settings → DISPLAY tab.
2. Tick "Control surface (Stream Deck / Companion)".
3. Default port is **9755**; change it via the port textbox if needed.
4. The server binds to `127.0.0.1` only — it is NOT reachable from the LAN.
If you need LAN access (e.g. a Stream Deck on a separate control PC),
front it with `ssh -L 9755:127.0.0.1:9755` or a localhost TCP bridge.
When enabled, the toast confirms `Control surface listening on
http://127.0.0.1:9755/`.
## Authentication
None. The localhost-only bind is the security model. Any process on the
operator's machine can hit these endpoints, which is the same threat model
as a Stream Deck's USB connection.
## 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
TeamsISO 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.
```json
{
"product": "TeamsISO",
"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.
```json
{
"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:
```json
{ "enabled": true, "customName": "Host" }
```
`enabled` is optional omitting it toggles the current state. `customName`
is optional and overrides the auto-generated NDI output name.
```sh
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.
```json
{ "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`).
```json
{
"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.
```sh
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.
```sh
curl -X POST http://127.0.0.1:9755/teams/mute
```
### `POST /recording`
Toggle per-output recording on or off. Body or query string:
```json
{ "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" }`.
```sh
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%\TeamsISO\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.
```sh
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.
```sh
curl -X POST http://127.0.0.1:9755/recording/roll
```
Response:
```json
{ "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:
```json
{
"type": "participants",
"participants": [
{ "id": "...", "displayName": "Jane", "isOnline": true,
"isEnabled": true, "customName": "Host", "stateLabel": "LIVE" }
]
}
```
Clientserver 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` only.
Address vocabulary:
```
/teamsiso/iso "DisplayName" {0|1} — toggle/set ISO by display name
/teamsiso/iso/by-id "guid" {0|1} — toggle/set by Id
/teamsiso/preset "Name" — apply preset
/teamsiso/teams/mute — UIA toggle mute
/teamsiso/teams/camera — UIA toggle camera
/teamsiso/teams/leave — UIA leave
/teamsiso/teams/share — UIA share tray
/teamsiso/teams/raise-hand — UIA raise hand
/teamsiso/refresh-discovery — rebuild NDI finder
/teamsiso/stop-all — disable every ISO
/teamsiso/recording {0|1} — recording on/off (default dir)
/teamsiso/recording/marker "Label" — drop a marker on every active recording
/teamsiso/recording/roll — roll every active recording into a new chunk
/teamsiso/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. `/teamsiso/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
- **OSC bridge** over UDP 9000 with `/teamsiso/iso {id} {0|1}` etc. same
command surface, different transport. Adapter sits in front of the REST
handlers.
- **Bidirectional state** via WebSocket push `participants` updates so
controllers can light a button when an ISO is live without polling.
- **REST apply-preset** duplicate the dialog's apply logic into
`IIsoController.ApplyPreset(name)` so the `/presets/{name}/apply`
endpoint becomes a real action.