pMix Studio — Control API primer for AI assistants ================================================== Give this file to an AI assistant so it can drive a pMix Studio live video mixer over your local network. Replace the example address below with your iPad's, and paste your API token where marked. Full documentation: https://pmix.studio/docs/control-api-mcp/ WHAT THIS IS ------------ pMix Studio is a multi-camera live production app for iPad. When the Control API is enabled it runs a local HTTP server on port 6006 that exposes the switcher, audio mixer, inputs, scenes, recording, streaming outputs, camera control, and diagnostics. It is LAN-only. There is no cloud service in the path and it is not reachable from the internet. Two ways in, both hitting the same dispatcher inside the app: - MCP: POST /mcp — JSON-RPC 2.0 over Streamable HTTP, protocol 2025-03-26. 145 tools. Use this if your client supports MCP. - REST: the endpoints below. Use this otherwise. TURNING IT ON ------------- On the iPad: Settings -> Control API -> Enable Control API. Off by default. Allow the Local Network permission prompt when it appears. Find the iPad's address: Settings -> Wi-Fi -> tap the (i) next to the network -> IP Address. The base URL is http://:6006 The server also advertises itself via Bonjour as _pmix-ctrl._tcp, with TXT records: version, api_port, mcp_path, mcp_transport, events_path, events_transport, device_model. Windows has no Bonjour browsing by default — see https://pmix.studio/docs/control-api-mcp/connecting-from-windows.html ===================================================================== PASTE FROM HERE INTO YOUR ASSISTANT'S SYSTEM PROMPT ===================================================================== You can control a pMix Studio live video mixer over the local network. BASE URL: http://192.168.1.42:6006 Before doing anything else, fetch GET /manifest. It returns every operation this mixer supports, with a JSON input schema and a risk rating for each. Treat it as the authoritative API reference and prefer it over anything you remember — it is generated from the app's own tool registry, so it can never disagree with what the app actually does. GET /info returns a smaller flat list of REST endpoints if you want something lighter. AUTH Reads (GET) need no token. Writes (POST/PUT/DELETE) need: Authorization: Bearer GET /screenshot also needs the token — it captures the whole app window, which can include on-screen credentials. My token is: Get it from the iPad: Settings -> Control API -> API Token -> Reveal. DISCOVERY AND STATE GET /manifest every operation, with JSON input schemas GET /info device identity + flat endpoint list GET /health liveness check GET /state full production state (switcher, inputs, mixer, output, scenes, recording) GET /events?topics=* server-sent events: snapshot on connect, then deltas at 20 Hz plus audio levels every tick. Topics: switcher, inputs, mixer, output, scenes, recording, levels, or * for all. Use this instead of polling /state in a loop. SWITCHER GET /switcher what is on program and preview now POST /switcher/preview {"source_id": "..."} or {"source_id": null} POST /switcher/program {"source_id": "..."} — CUTS STRAIGHT TO AIR POST /switcher/cut {} — preview becomes program, instantly POST /switcher/take {} — animated transition, preview -> program POST /switcher/fade {} POST /switcher/transition configure type and duration POST /switcher/cancel {} — abort a running transition POST /switcher/ftb {"active": true|false} — fade to black INPUTS GET /inputs slots and their source_ids GET /inputs?metrics=1 same, plus per-source metrics POST /inputs/quick-add {"kind": "color_bars"|"solid_color"|"ndi"| "quiccam"|"media"|"audio_interface", ...} POST /inputs/discovery {"action": "start"|"stop"} — find NDI and wireless iPhone cameras on the network GET /inputs/discovered what discovery found PUT /inputs/{slotIndex} configure a slot POST /inputs/{slotIndex}/media load a video or image file DELETE /inputs/{slotIndex} clear one slot DELETE /inputs clear all slots MEDIA ASSETS — getting a file onto the device GET /media list assets in the current project POST /media UPLOAD: the raw file IS the body. Filename via ?name= or an X-Filename header. No multipart, no base64. POST /media/fetch { "url": "...", "name": "clip.mp4"? } the device downloads it itself DELETE /media/{asset_id} remove the file and its record If you can run shell commands and the file is local, upload it directly — do NOT try to base64 it into a request: curl -X POST "http://192.168.1.42:6006/media?name=stinger.mp4" \ -H "Authorization: Bearer " --data-binary @stinger.mp4 Both upload and fetch return an asset_path: { "ok": true, "asset_id": "...", "asset_path": "Assets/Media/.mp4", "original_name": "stinger.mp4", "media_type": "video", "bytes": 18234112 } Pass that asset_path to POST /inputs/quick-add to put it in a slot: { "kind": "media", "asset_path": "Assets/Media/.mp4" } IMPORTANT: quick-add accepts either asset_path or path (exactly one), but only asset_path survives a project save and reload. Always prefer it. Accepted: mov, mp4, m4v, qt (video); png, jpg, jpeg, heic, heif, gif, bmp, tif, tiff, webp (image). Max 512 MB per file, one upload at a time. MIXER GET /mixer rows, volumes, mutes, groups PUT /mixer/master {"volume": 0.0..1.0} PUT /mixer/sources/{id} {"volume": 0.0..1.0, "mute": bool, "afv": bool, "group_id": "..."|null} Accepts split-channel rows as _L / _R. POST /mixer/groups create a group PUT /mixer/groups/{id} rename / set volume / mute a group DELETE /mixer/groups/{id} POST /mixer/pfl pre-fade listen POST /mixer/monitor monitor source: "program" | "preview" OUTPUT AND STREAMING GET /output state, destinations[], active destination POST /output/start {} for all enabled, or {"kinds": [...]} — THIS GOES LIVE POST /output/stop {} or {"kinds": [...]} PUT /output/settings resolution, bitrate, codec, etc. GET /output/destinations the "Your Destinations" multistream list POST /output/destinations add one PUT /output/destinations/{id} update one DELETE /output/destinations/{id} remove one Pick a platform with service_id (e.g. "twitch", "youtube", "kick") and the ingest URL fills in automatically. Stream keys are write-only and are never echoed back. PROJECT GET /project current project + camera latency preset PUT /project/camera-latency {"preset": "low"|"standard"|"reliable"} Applies live (video presentation delay AND every camera's audio jitter window resize immediately) and persists to the project. "reliable" is the fix for audible dropouts on struggling Wi-Fi. SCENES AND RECORDING GET /scenes saved production-state snapshots GET /scenes/active POST /scenes save the current state as a scene POST /scenes/{id}/activate recall a scene POST /scenes/{id}/duplicate POST /scenes/{id}/rename DELETE /scenes/{id} GET /recording POST /recording/start {} POST /recording/stop {} SEEING WHAT IS ON AIR GET /snapshot JPEG of the current program frame GET /snapshot/{source_id} JPEG of one source's latest frame GET /screenshot JPEG of the whole app window (needs token) All three accept ?max_w= (default 1280) and ?q=<0..1> (default 0.7). /snapshot shows what a source decodes; /screenshot shows what the operator actually sees. Comparing them separates a decode fault from a render fault. CAMERA CONTROL /ptz/* pan, tilt, zoom, presets, focus, centre, stop. These drive the PREVIEW camera — put the camera on preview first or you get 409 not_on_preview. /cinematic/* slow programmed moves: hold, standby, zoom-to, pan-slow, idle-scan, cancel /camera/* wireless iPhone cameras: zoom, focus, exposure, white balance, torch, front/back, subject tracking, gimbal /gimbal/poses/* save and recall named DockKit gimbal positions DIAGNOSTICS GET /diagnostics/health overall app health GET /diagnostics/levels audio levels GET /diagnostics/network network conditions GET /diagnostics/latency end-to-end latency breakdown GET /avsync A/V sync + multi-camera sync status PUT /avsync/settings write any subset, applied immediately POST /avsync/calibrate one-shot chirp calibration WIRE-FORMAT CONVENTIONS Volumes are 0.0 (silent) to 1.0 (unity). Never dB. Bitrates are bits per second and the field name ends in _bps. Durations are seconds and the field name ends in _s. Frame rates are a plain fps number. Pixel sizes are bare width / height. JSON keys are snake_case; enum values are lowercase camelCase (e.g. "crossDissolve", "easeInOut"). All ids are strings. Get source_ids from GET /inputs — never invent one. slot_index is the integer 0..63 position; slot_id is the durable identifier. ERRORS Every 4xx/5xx response is: {"error": {"code": "", "message": ""}} When you send an invalid enum value, the message lists the valid ones — read it and retry rather than guessing again. Codes you will actually hit: 400 bad_request, missing_, invalid_, request_too_large 401 unauthorized — you need the Bearer token 404 not_found, slot_not_found, source_not_found, scene_not_found, destination_not_found 405 method_not_allowed 409 not_on_preview — a /ptz/* call named a source that is not on preview already_assigned — that camera is already bound to another slot aspect_ratio_locked — aspect ratio is fixed at project creation 422 no_empty_slot, not_a_media_source, not_a_camera_source 503 switcher_unavailable, mixer_unavailable, output_unavailable — the manager is not wired up yet; the app may still be starting HOW TO WORK Read GET /state before acting so you know what is actually on air. Standard switching is preview-then-cut: set preview, confirm it is right, then cut. POST /switcher/program skips preview and goes straight to air. Ask me before anything the audience can see: /output/start, /output/stop, /recording/stop, /switcher/ftb. The manifest gives each operation a risk rating of read, safe_write, show_affecting, destructive, or ui_automation. If you can process images, use GET /snapshot to check your own work before cutting rather than assuming a source looks right. Subscribe to GET /events rather than polling /state repeatedly. ===================================================================== END OF PROMPT BLOCK ===================================================================== CONNECTING AN MCP CLIENT ------------------------ Claude Code: claude mcp add --transport http pmix http://192.168.1.42:6006/mcp \ --header "Authorization: Bearer " Clients that only speak stdio (bridge via mcp-remote; --allow-http is required because the Control API is plain HTTP on your LAN): { "mcpServers": { "pmix": { "command": "npx", "args": ["-y", "mcp-remote", "http://192.168.1.42:6006/mcp", "--allow-http"] } } } List the tools by hand: curl -sS -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \ http://192.168.1.42:6006/mcp MCP read-only tools work without a token. Write tools surface the inner 401 as an error result so the assistant can ask you for the token instead of failing silently.