Research date: 2026-06-30.
This summarizes the current Bitfocus Companion VDO.Ninja module so the native Stream Deck plugin can at least mirror it, then add Stream Deck specific controls and customization.
verified-api-command-and-callback-reference.mdBryce Seifert is the author on the recent module releases and most recent feature work in the GitHub release history:
| Release | Relevant VDO.Ninja change |
|---|---|
| v1.1.0 | Stops polling on module disable. |
| v2.2.0 | Adds custom WebSocket server config. |
| v2.2.1 | Improves feedback responsiveness and director presets. |
| v2.2.2 | Allows stream variables on all streams, including rooms. |
| v2.3.0 | Adds guest-in-scene feedback / variable. |
| v2.4.0 | Upgrades to Node 22 and adds PTZ actions. |
| v2.5.0 | Adds stream ID variables and variables in actions. |
The native plugin should match the v2.5.0 surface as the parity baseline.
The Companion module is intentionally lean:
@companion-module/base and ws.apiIDwsCustomwsServer, default api.vdo.ninja, only shown when wsCustom is enabled.wss://{serverUrl}:443{ "join": apiID }, then immediately sends { "action": "getDetails" }.this.states is keyed by stream ID.this.streams is an array of { id, label } choices for feedback/action dropdowns.getDetails snapshots seed state; later update messages mutate it.sendRequest(action, target, value, local) helper.{ action, target, value, local }, with missing values serialized as "null".Native plugin translation:
"null" internally; normalize to JSON null and only adapt if the API requires the string form.Incoming WebSocket messages are handled in two families:
| Message form | Companion behavior | Native plugin implication |
|---|---|---|
callback.action == "getDetails" |
Iterate result and call processGetDetails(data) for every stream. |
Use this as the full snapshot path. |
callback.local |
Finds the local stream and updates it if the action was not a toggle. | Native plugin should use callback result to update local button state quickly. |
update |
Calls processUpdate(data). |
Use this as the incremental event path. |
Important update actions:
| Update action | Companion behavior |
|---|---|
hangup |
Deletes stream state and removes it from dropdown choices. |
newViewConnection |
Requests fresh getDetails. |
director |
Updates director state and rebuilds actions/variables/feedbacks. |
endViewConnection |
Deletes stream by data.value. |
positionChange |
Requests fresh getDetails. |
directorMuted |
Stores director-enforced guest audio mute under others["mute-guest"]. |
directorVideoHide |
Stores director-enforced guest video hide under others["hide-guest"]. |
remoteMuted / mic |
Updates muted. |
remoteVideoMuted / camera |
Updates videoMuted. |
details |
Requests fresh getDetails. |
seeding / tracksAdded |
Requests fresh getDetails. |
Native plugin enhancement:
| Companion action ID | VDO.Ninja action |
|---|---|
speaker |
speaker with toggle / true / false |
mic |
mic with toggle / true / false |
camera |
camera with toggle / true / false |
volume |
volume percent |
sendChat |
sendChat text |
record |
record true / false |
reload |
reload |
hangup |
hangup |
bitrate |
bitrate reset / pause / custom kbps |
panning |
panning 0 to 180, center 90 |
togglehand |
togglehand |
togglescreenshare |
togglescreenshare |
forceKeyframe |
forceKeyframe |
nextSlide |
nextSlide |
previousSlide |
Companion action ID; current local VDO runtime confirms prevSlide as the actual VDO API action name |
ptzPan |
local pan, value divided by 100 |
ptzTilt |
local tilt, value divided by 100 |
ptzZoom |
local zoom, value divided by 100 |
ptzFocus |
local focus, value divided by 100 |
ptzExposure |
local exposure, value divided by 100 |
| Companion action ID | VDO.Ninja action |
|---|---|
group |
self group |
joinGroup |
self joinGroup |
leaveGroup |
self leaveGroup |
viewGroup |
self viewGroup |
joinViewGroup |
self joinViewGroup |
leaveViewGroup |
self leaveViewGroup |
soloVideo |
soloVideo |
| Companion action ID | VDO.Ninja action |
|---|---|
guestForward |
forward |
guestAddScene |
addScene |
guestMuteScene |
muteScene |
guestGroup |
group with target |
guestMic |
mic with target |
guestHangup |
hangup with target |
guestSoloChat |
soloChat |
guestAltSoloChat |
soloChatBidirectional |
guestSpeaker |
speaker with target |
guestDisplay |
display |
guestOverlay |
sendDirectorChat |
guestForceKeyframe |
forceKeyframe with target |
guestSoloVideo |
soloVideo with target |
guestVolume |
volume with target |
Parity note: current Companion actions include local PTZ but not full guest PTZ actions in the action table despite v2.4.0 release notes. The VDO.Ninja API and local ptz.html support guest PTZ via ptzZoom, ptzPan, ptzTilt, ptzFocus, and ptzAutofocus; native Stream Deck should expose those directly.
| Feedback | Inputs | Logic |
|---|---|---|
| Mic status | stream, muted/unmuted state | Guest streams use others["mute-guest"] as director-enforced override; otherwise compare muted. |
| Camera status | stream, muted/unmuted state | Guest streams use others["hide-guest"] as director-enforced override; otherwise compare videoMuted. |
| Speaker status | stream, muted/unmuted state | Compare speakerMuted. |
| Guest in scene | stream, scene ID/name | Compare stream.scenes[scene] == true. |
Native plugin should add:
others.Variables:
director_micdirector_cameradirector_speakerguest_{position}_streamIDguest_{position}_micguest_{position}_cameraguest_{position}_labelguest_{position}_scenes{streamID}_mic{streamID}_camera{streamID}_label{streamID}_scenesPresets:
Native plugin should go further:
There are no physical faders/sliders in the Stream Deck family reviewed, but Stream Deck + and + XL have dials/encoders and an LCD touch strip/infobar. Use those for:
panning).ptzZoom, ptzPan, ptzTilt, ptzFocus, ptzAutofocus.{ action, target, value, value2 }.Mirror Companion’s states and streams, but add:
connectionsByApiKey.streamsById.slotsByPosition.selectedTarget.lastCommandByAction.statsByStream.errorsByConnection.derivedFlags such as guestAudioMutedEffective, guestVideoHiddenEffective, inScene, stale, held, handRaised.getDetails on connect and selected update events, but does not appear to poll continuously. Native plugin should have configurable polling while visible feedback actions exist.target / value as "null". Native code should isolate this as a transport adapter detail.previousSlide is a Companion naming quirk. Send prevSlide to VDO.Ninja and, if useful, keep previousSlide only as a native plugin compatibility label.