Research date: 2026-06-30.
This captures the VDO.Ninja control surfaces relevant to a native Stream Deck plugin: &api WebSocket/HTTP/SSE, iframe postMessage, and WebRTC/P2P data-channel routing.
../../Companion-Ninja/httpwssapi.md../../Companion-Ninja/iframeapi.md../../Companion-Ninja/dataiframes.md../../Companion-Ninja/p2pdrawing.md../../Companion-Ninja/server/oscninja.js../../ptz.html../../lib.js around commands.*../../lib.js around getDetailedState../../main.js around the iframe message event handler../../webrtc.js around session.sendPeers, session.sendMessage, session.sendRequestverified-api-command-and-callback-reference.mdruntime-comparison-audit.mdPrimary runtime surface:
&api WebSocket for the native Stream Deck plugin.value2 payloads.getDetails, updates, callbacks, and optional polling for state.Secondary/dev surface:
postMessage only for embedded test pages or future custom mini-control surfaces.Advanced/relay surface:
&api action bridge or an iframe it controls.&api WebSocket / HTTP / SSE SurfaceOpen the controlled VDO.Ninja page with an API key:
https://vdo.ninja/?api=YOUR_KEY
https://vdo.ninja/mixer?room=ROOM&api=YOUR_KEY
WebSocket:
{ "join": "YOUR_KEY" }
Then send commands:
{ "action": "mic", "target": null, "value": "toggle" }
{ "action": "ptzZoom", "target": "1", "value": 0.1, "value2": "abs" }
HTTP:
GET https://api.vdo.ninja/{apiKey}/{action}
GET https://api.vdo.ninja/{apiKey}/{action}/{value}
GET https://api.vdo.ninja/{apiKey}/{action}/{target}/{value}
POST https://api.vdo.ninja/{apiKey}
POST body:
{
"action": "ptzZoom",
"target": "1",
"value": 0.5,
"value2": "abs"
}
SSE:
GET https://api.vdo.ninja/sse/{apiKey}
The local Companion-Ninja/server/oscninja.js sample shows the bridge behavior:
join.get ID and wait up to 5 seconds.callback.get resolve the matching HTTP response.in / out routing filters can isolate inbound/outbound relays.Typical command callback:
{
"callback": {
"action": "mic",
"value": "toggle",
"result": false
}
}
State update:
{
"update": {
"streamID": "abc123",
"action": "camera",
"value": true
}
}
pokeAPI events seen in local source include:
detailsdirectorcodirectorseedingnewViewConnectionendViewConnectionpositionChangeremoteMutedremoteVideoMuteddirectorMuteddirectorVideoHidehangupchatstreamAddedtipNative plugin should accept unknown events, store them raw, and only derive UI state from validated fields.
For exact callback payloads, iframe response keys, and field meanings for getDetails, getStats, getGuestList, getStreamIDs, and getStreamInfo, use verified-api-command-and-callback-reference.md.
&api Command Catalog| Action | Value / options | Notes |
|---|---|---|
speaker |
true, false, toggle |
Incoming audio mute state. |
mic |
true, false, toggle |
Local microphone. |
camera / video |
true, false, toggle |
Local camera/video. |
volume |
0 to 200, true, false |
API percent; local command converts to element volume. |
bitrate |
true, false, integer kbps |
true resets/unlocks, false pauses, integer sets rate limit. |
panning |
0 to 180, center 90 |
Requires relevant panning support enabled. |
record |
true, false, or element target |
Local recording. |
hangup |
none | Disconnects local session. |
reload |
none | Reloads page. |
forceKeyframe |
none | Requests PLI/keyframe. |
togglehand / raisehand |
none | Hand status. |
togglescreenshare |
none | Browser will still prompt user. |
sendChat / sendChatMessage |
text | Sends chat. |
showChatOverlay |
text | Local overlay-style message. |
requestStats |
none | Detailed stats snapshot. |
getDetails |
optional stream ID | Full detailed state. |
getStats |
optional stream ID | Quick stats. |
getGuestList |
none | Positional guest list. |
startRoomTimer |
seconds | Room or target timer. |
pauseRoomTimer |
none | Lowercase spelling confirmed in current runtime. |
stopRoomTimer |
none | Timer stop/reset. |
tallylight |
onair, active, standby, off, integer |
Tally override. |
prevSlide |
none | Slide back. Current local runtime does not define previousSlide. |
nextSlide |
none | Slide next. |
soloVideo / highlight |
true, false, toggle, target |
Highlight video. |
activeSpeaker |
toggle, false, 1, 2, 3 |
Enables/configures active speaker. |
setBufferDelay |
ms in value; optional stream ID/UUID/* in target or value2 |
Sets playback delay. value2 remains supported; target now works through the targeted fallback path. |
layout |
0, integer, object/array |
Mixer layout. |
width / height |
integer | Local/device constraints path. |
aspectRatio |
decimal or 16:9 |
Local camera aspect ratio. |
videoConstraint |
constraint name + value2 |
Requires POST/WSS for value2. |
| Action | Value | value2 |
|---|---|---|
zoom |
relative decimal or absolute 0..1 | abs for absolute |
focus |
relative decimal or absolute 0..1 | abs when supported |
pan |
relative decimal or absolute -1..1 | abs when supported |
tilt |
relative decimal or absolute -1..1 | abs when supported |
exposure |
0..1 | local-only in current docs |
| Action | Target | Value |
|---|---|---|
forward |
guest slot or stream ID | destination room |
addScene |
guest slot, stream ID, or UUID | scene ID/name; optional boolean in value2 for explicit state |
setScene |
guest slot, stream ID, or UUID | scene ID/name in value, boolean state in value2 |
activateQueuedGuest |
guest slot, stream ID, or UUID | none |
muteScene |
guest slot or stream ID | optional boolean/toggle; current source does not use value as a scene selector |
group |
none or guest | group ID |
joinGroup / leaveGroup |
none | group ID |
viewGroup / joinViewGroup / leaveViewGroup |
none | group ID |
mic |
guest | true, false, toggle, or none |
speaker |
guest | true, false, toggle, or none |
display |
guest | optional boolean/toggle |
volume |
guest | 0 to 200 |
hangup |
guest | none |
soloChat |
guest | none |
soloChatBidirectional |
guest | none |
sendDirectorChat / sendPinnedDirectorChat |
guest | text |
forceKeyframe |
guest | none |
soloVideo |
guest | optional boolean/toggle |
mirror / mirrorGuest / remoteMirror |
guest | true, false, toggle |
rotate |
guest | true, false, 90, 180, 270 |
channel / pgm |
guest | 0, 1, 2 |
mixorder |
guest | -1 or 1 |
refreshVideo / refreshCamera |
guest | none |
refreshConnection / restartConnection |
guest | none |
recoverStream / refreshAll |
guest | none |
requestResolution |
guest | WIDTHxHEIGHT |
requestAspectRatio |
guest | decimal or 16:9; value2 can carry max dimension |
setWidth / setHeight / setAspectRatio |
guest | capture constraint value |
Use these for director/co-director guest camera control:
| Action | Target | Value | value2 |
|---|---|---|---|
ptzZoom / remoteZoom |
guest | decimal | abs optional |
ptzFocus / remoteFocus |
guest | decimal | abs optional |
ptzPan / remotePan |
guest | decimal | abs optional |
ptzTilt / remoteTilt |
guest | decimal | abs optional |
ptzAutofocus / remoteAutofocus / resetAutofocus |
guest | true, false, manual, off |
none |
Important: plain zoom, focus, pan, tilt, and exposure are local commands, not guest-targeted director commands.
getDetailedState builds state on demand from live session and DOM state. It is not a persistent scene database.
Per remote stream fields observed locally:
| Field | Meaning |
|---|---|
streamID |
Stable stream identifier. |
UUID |
Live internal peer UUID when the entry represents an inbound remote stream. Prefer it for current-session API targeting when available. |
label |
User label when available. |
group |
Current group membership. |
chunkedBufferDefault, chunkedBufferOverride, chunkedBufferRequested, chunkedBufferCeil, chunkedBufferAdaptive |
Chunked/WebCodecs buffer state. |
miscellaneous |
stats.info payload when present. |
layout, slot |
Layout/slot state. |
featured |
Solo/highlight state. |
iframeSrc |
Embedded iframe source if present. |
localStream |
false for remote streams. |
muted |
Remote mute state. |
videoMuted |
Remote video mute state. |
activeSpeaker, defaultSpeaker |
Audio activity/default speaker flags. |
videoVisible, videoVolume, iframeVisible |
DOM/media element state. |
director |
Whether UUID is in director list. |
position |
Director UI position/slot. |
scenes |
Map of scene ID/name to boolean, read from add-to-scene controls. |
others |
Map of other director UI action states, including queue, hand, mute/hide, solo states. |
Local stream fields observed locally:
| Field | Meaning |
|---|---|
label, meta, group, groupView, scene, streamID, iframeSrc |
Local identity/context. |
director |
Current director state. |
localStream / localstream |
Local marker. |
seeding |
Publishing/seeding state. |
muted, videoMuted, speakerMuted |
Local audio/video/speaker states. |
videoVisible |
DOM visibility. |
position, slot |
Director UI position/slot when applicable. |
meshcast, layout |
Session layout/routing state. |
outbound |
Outbound stats when available. |
screenSharing |
Screenshare state when notifications are enabled. |
audioTrack, videoTrack |
Local track presence. |
scenes, featured |
Local director scene/highlight state when applicable. |
getGuestList returns a position-keyed map:
{
"1": { "streamID": "abc123", "label": "Guest name" },
"2": { "streamID": "directorStream:s", "label": "Screen" }
}
Native plugin state normalization:
getDetails snapshots.getGuestList and stream state.others.positionChange, newViewConnection, endViewConnection, details, seeding, and streamAdded.postMessage SurfaceIframe setup:
iframe.allow = "camera;microphone;fullscreen;display-capture;autoplay;";
iframe.src = "https://vdo.ninja/?room=ROOM&cleanoutput";
iframe.contentWindow.postMessage({ mic: "toggle" }, "*");
Always validate e.source === iframe.contentWindow when receiving messages.
Connection/events documented or seen in examples:
guest-connectedview-connectiondirector-connecteddirector-sharescene-connectedslot-updatedpush-connectionjoining-roomrejecteddataReceivedchatcib| Category | Calls |
|---|---|
| Audio | mic, mute, speaker, volume, panning, targetAudioBitrate, audiobitrate, PPT |
| Video | camera, pauseinvisible, keyframe |
| Stream management | requestStream, close, hangup |
| Recording | record |
| Groups | groups, groupView |
| Bitrate/quality | bitrate, targetBitrate, manualBitrate, scale, targetWidth, targetHeight |
| Devices | changeVideoDevice, changeAudioDevice, changeAudioOutputDevice, getDeviceList |
| Layout/display | layout, previewMode, slotmode, advancedMode, toggleSettings, target with add/remove/replace/settings |
| Data/messaging | sendData, sendChat, sendMessage, sendRequest, sendPeers, sendRawMIDI |
| Stats/monitoring | getStats, getFreshStats, getRemoteStats, requestStatsContinuous, getLoudness, getStreamIDs, getStreamInfo, getDetailedState, getGuestList |
| Utility | reload, style, function, saveVideoFrameToDisk, getVideoFrame, copyVideoFrameToClipboard, getSnapshotBySlot, getSnapshotByStreamID |
| Advanced | sceneState, layouts, obsCommand, setBufferDelay, automixer, enableYouTube, nextSlide, prevSlide, getFaces, faceTrack, getEffectsData, action |
The generic iframe action call bridges to Companion-style API actions:
iframe.contentWindow.postMessage({
action: "rotate",
target: "1",
value: 90
}, "*");
For callback-capable iframe calls, include cib:
iframe.contentWindow.postMessage({ getDetailedState: true, cib: "state-1" }, "*");
Then match e.data.cib.
Security notes:
"*" with a specific origin where possible.function: "eval" in any native plugin UI.style, function, routeMessage, and raw data senders as advanced/dev-only.The iframe API exposes WebRTC data-channel routing:
| Iframe call | Runtime path | Direction |
|---|---|---|
sendData |
session.sendGenericData(...) |
Generic P2P payload; received as dataReceived. |
sendMessage |
session.sendMessage(...) |
Message to viewer-side peer connections. |
sendRequest |
session.sendRequest(...) |
Request to publisher-side peer connections. |
sendPeers |
session.sendPeers(...) |
Broadcast to connected peers. |
sendRawMIDI |
sendRawMIDI(...) |
MIDI payload to one/all peers. |
Targeting options:
UUIDstreamIDtype for sendData connection class, with examples using pcs or rpcsPayload best practices from local docs:
{ vdoStreamDeck: { ... } }.type field.Native plugin implication:
&api WebSocket/POST.postMessage to expose full P2P data-channel calls.Use for:
There are no physical sliders on current Stream Deck hardware reviewed. Use dials and touch strips as slider-like controls:
| Control | Suggested VDO action |
|---|---|
| Local volume | volume |
| Guest mic volume | volume with target |
| Audio pan | panning |
| Bitrate | bitrate |
| Buffer delay | setBufferDelay |
| Local zoom/focus/pan/tilt/exposure | zoom, focus, pan, tilt, exposure |
| Guest PTZ | ptzZoom, ptzFocus, ptzPan, ptzTilt, ptzAutofocus |
| Scene/group selector | dial rotates selection, push applies action |
Dial push options: