Planning date: 2026-06-30.
This is the concrete plan for a native Elgato Stream Deck plugin dedicated to VDO.Ninja. It assumes no changes outside vdo-streamdeck/ until Steve explicitly approves them.
Build a Marketplace-ready native Stream Deck plugin that controls VDO.Ninja pages opened with &api=KEY.
The plugin should:
getDetails, getGuestList, callbacks, and live API updates.Primary transport is VDO.Ninja’s &api WebSocket. Iframe/P2P controls stay documented as future/advanced mode unless the plugin later owns an embedded controller page.
The current native plugin is an early MVP, not yet a full Companion replacement. Do not position it as Marketplace-ready until it passes the no-regression checklist in professional-parity-and-no-regression-review.md.
Minimum gate:
Local Control via pushToTalk and pushToMute; physical key/pedal release testing is pending.The plugin should not assume users already know VDO.Ninja’s &api parameter. The connection action/property inspector should guide first-time setup:
getDetails connection test and distinguish “relay reachable” from “VDO.Ninja page answering”.See onboarding-and-configuration-review.md for the comparison against other Stream Deck plugin setup patterns.
Use the official Elgato Stream Deck SDK with TypeScript/Node.
Current official docs checked:
create streamdeck scaffolder.*.sdPlugin folder with a manifest, compiled code, assets, UI/property inspector files, and logs.Candidate plugin identity:
| Field | Candidate |
|---|---|
| Display name | VDO.Ninja |
| Category | Video |
| Identifier | ninja.vdo.streamdeck |
Do not lock the identifier until Marketplace submission rules are checked during packaging. Elgato plugin identifiers should be treated as effectively permanent once published.
The plugin should be designed around four practical modes:
| Mode | User | Main needs |
|---|---|---|
| Local / Guest | A performer controlling their own VDO.Ninja page | Mic, camera, speaker, hand, screen share, record, chat, local PTZ. |
| Director | A producer running a room | Guest target selection, guest mic/camera/display, scenes, groups, solo talk, transfer, hangup, stats. |
| Technical Operator | Someone managing stream quality | Bitrate, buffer delay, keyframes, refresh/recover, stats, connection health. |
| PTZ Operator | Camera operator | Guest/local PTZ keys, dials, presets, autofocus, invert axes, selected target. |
Expected package layout after scaffolding:
vdo-streamdeck/
plugin/
package.json
*.sdPlugin/
manifest.json
bin/
imgs/
ui/
logs/
src/
plugin.ts
api/
vdo-client.ts
command-registry.ts
request-tracker.ts
types.ts
state/
session-store.ts
selectors.ts
actions/
connection-status.ts
local-command.ts
local-toggle.ts
guest-command.ts
guest-scene.ts
selected-target.ts
ptz-key.ts
ptz-dial.ts
value-dial.ts
stats-display.ts
custom-command.ts
ui/
property-inspector/
test/
fake-api-server.ts
fixtures/
Keep the exact folder names aligned with the scaffolder output when implementation starts.
Responsibilities:
wss://api.vdo.ninja:443 by default.{ "join": apiKey } on WebSocket open.callback.get or command callbacks to pending requests.update events.value2 payloads.missing-key, connecting, connected, no-page, timeout, disconnected, error.The registry is the source of truth for actions and property inspector choices.
Each command definition should include:
type VdoCommandDefinition = {
id: string;
action: string;
category: "local" | "guest" | "scene" | "group" | "ptz" | "stats" | "utility";
label: string;
target: "none" | "optional" | "required" | "value2";
valueType: "none" | "toggle" | "boolean" | "number" | "text" | "select" | "json";
value2Type?: "none" | "boolean" | "number" | "text" | "select";
min?: number;
max?: number;
step?: number;
feedback?: boolean;
dangerous?: boolean;
dialFriendly?: boolean;
requiresDirector?: boolean;
notes?: string;
};
Important registry rules:
prevSlide, not previousSlide.pauseRoomTimer.setBufferDelay, put milliseconds in value. Use value2 for broad compatibility; current VDO.Ninja also accepts stream ID/UUID/* in target.ptzZoom, ptzPan, ptzTilt, ptzFocus, and ptzAutofocus.muteScene as a guest mute-scene toggle/set action, not as a scene selector.State buckets:
| Bucket | Source |
|---|---|
connection |
WebSocket lifecycle and command timeouts. |
detailsRaw |
getDetails callbacks and update.action == "details". |
guestList |
getGuestList callbacks and positionChange refreshes. |
streamsById |
Normalized from getDetails. |
slotsByPosition |
Normalized from getGuestList. |
selectedTarget |
Local plugin setting per profile/connection. |
statsByStream |
getStats polling for visible stats actions. |
lastCommand |
Per-action command result, timeout, and error. |
Derived flags:
| Flag | How to derive |
|---|---|
localMicMuted |
details[local].muted or update.action == "muted". |
localCameraMuted |
details[local].videoMuted or update.action == "videoMuted". |
localSpeakerMuted |
details[local].speakerMuted or update.action == "speakerMuted". |
guestAudioMutedEffective |
stream.muted plus stream.others["mute-guest"] when present. |
guestVideoMutedEffective |
stream.videoMuted plus director hide/camera controls in others. |
guestInScene(scene) |
stream.scenes[scene] === true. |
guestHeldOrQueued |
stream.others["remove-queue"] === true. |
guestHandRaised |
stream.others["hand-raised"] === true. |
stale |
Last update or snapshot older than a configured threshold. |
Polling defaults:
| Poll | Default |
|---|---|
getDetails |
On connect, after join/leave/position/detail events, and every 2 seconds while feedback actions are visible. |
getGuestList |
On connect, when property inspector opens, after positionChange, and every 5 seconds while target dropdowns are visible. |
getStats |
Only while visible stats actions exist; default 2 to 5 seconds. |
requestStats |
Manual diagnostics only. |
Keep the manifest action list high-level. Use settings and the command registry to avoid one manifest action per VDO command.
Purpose:
getDetails.Settings:
Feedback:
Commands:
miccamera / videospeakerrecordtogglescreensharetogglehandforceKeyframeSettings:
Feedback:
getDetails/update event.Implementation status:
pushToTalk and pushToMute implemented on the Local Control mic action.mic=true or mic=false; key up sends the inverse final state.Commands:
reloadhangupsendChatshowChatOverlaybitratesetBufferDelaytallylightactiveSpeakerlayoutnextSlideprevSlidezoom/focus/pan/tilt/exposure as button nudges.Settings:
Commands:
miccameraspeakerdisplayvolumehangupforwardgroupsoloChatsoloChatBidirectionalsoloVideoforceKeyframesendDirectorChatsendPinnedDirectorChatmirrorrotatechannel / pgmmixorderrefreshVideorefreshConnectionrecoverStreamrequestResolutionsetWidthsetHeightsetAspectRatioTarget modes:
Feedback:
Implementation status:
plugin/src/actions/guest-command.ts.getDetails/getGuestList.{slot}, {label}, {streamID}, {command}, and {state}.Commands:
addSceneaddScene2 through addScene8muteSceneSettings:
Feedback:
details[streamID].scenes.muteScene is weaker; treat it as command-result feedback unless a reliable state field is present.addScene with value set to the scene key. Explicit set-on/set-off is supported in current VDO.Ninja with addScene + value2=true/false, or setScene.Implementation status:
plugin/src/actions/guest-scene.ts.addScene.getDetails[streamID].scenes for button feedback.muteScene, richer stale-target display, and profile presets.Commands:
groupjoinGroupleaveGroupviewGroupjoinViewGroupleaveViewGroupgroupSettings:
Purpose:
Settings:
Feedback:
Implementation status:
plugin/src/actions/select-guest.ts.Guest Command and Guest Scene can target Selected guest.Commands:
zoom, focus, pan, tilt, exposure.ptzZoom, ptzFocus, ptzPan, ptzTilt, ptzAutofocus.Settings:
Implementation status:
plugin/src/actions/ptz-key.ts.zoom, pan, tilt, focus, or exposure.ptzZoom, ptzPan, ptzTilt, ptzFocus, or ptzAutofocus and support slot, stream ID, selected guest, and first-held targeting.value2 to abs; relative mode sends signed numeric deltas.ptzAutofocus: false before the focus command.Dial rotation:
Dial press:
Touch strip:
Implementation status:
plugin/src/actions/ptz-dial.ts.Default dial mappings:
| Dial | Stream Deck + | Stream Deck + XL |
|---|---|---|
| 1 | Selected target volume | Selected target volume |
| 2 | Zoom | Zoom |
| 3 | Pan/Tilt cycle | Pan |
| 4 | Focus/Autofocus | Tilt |
| 5 | N/A | Focus/Autofocus |
| 6 | N/A | Exposure/bitrate/buffer |
Commands:
volumepanningbitratesetBufferDelayvolumeSettings:
Implementation status:
plugin/src/actions/value-dial.ts.volume, panning, bitrate, and setBufferDelay.volume with slot, stream ID, selected guest, and first-held targeting.value2: "*".Purpose:
Data:
getStats for lightweight feedback.requestStats only on press or diagnostics profile.Display ideas:
&api.getDetails.Purpose:
Settings:
actiontargetvaluevalue2Guardrails:
Profiles should be included as importable examples. Users can customize after install.
+------------+------------+------------+
| Connection | Mic | Camera |
+------------+------------+------------+
| Speaker | Record/Hand| Panic Page |
+------------+------------+------------+
Recommended behavior:
Record/Hand defaults to hand for guests and record for directors via setting.Panic Page opens a folder with reload, hangup, and custom command.+------------+------------+------------+------------+------------+
| Connect | Mic | Camera | Speaker | Record |
+------------+------------+------------+------------+------------+
| Share | Hand | Layout 1 | Layout 2 | Stats |
+------------+------------+------------+------------+------------+
| Select G1 | G1 Mic | G1 Cam | G1 Scene | G1 Solo |
+------------+------------+------------+------------+------------+
Alternative director profile:
+------------+------------+------------+------------+
| Connect | Mic | Camera | Speaker |
+------------+------------+------------+------------+
| Record | Share | Select | Scene |
+------------+------------+------------+------------+
Use the infobar for connection status, selected guest label, and last command result when supported by SDK profile assets.
Director-first layout:
+----------+----------+----------+----------+----------+----------+----------+----------+
| Connect | LocalMic | LocalCam | Speaker | Record | Layout1 | Layout2 | Stats |
+----------+----------+----------+----------+----------+----------+----------+----------+
| G1 Label | G1 Mic | G1 Cam | G1 Scene | G1 Talk | G1 Solo | G1 Vol | G1 Hang |
+----------+----------+----------+----------+----------+----------+----------+----------+
| G2 Label | G2 Mic | G2 Cam | G2 Scene | G2 Talk | G2 Solo | G2 Vol | G2 Hang |
+----------+----------+----------+----------+----------+----------+----------+----------+
| Select | Group | ViewGrp | Transfer | Keyframe | Refresh | PTZ Page | Custom |
+----------+----------+----------+----------+----------+----------+----------+----------+
Add folders/pages for:
Keys:
+------------+------------+------------+------------+
| Connect | Mic | Camera | Record |
+------------+------------+------------+------------+
| Select | G Mic | G Cam | G Scene |
+------------+------------+------------+------------+
Dials:
| Dial | Rotate | Press | Touch strip |
|---|---|---|---|
| 1 | Selected target volume | Reset to 100% | target + volume |
| 2 | Selected target zoom | Toggle absolute/relative | target + zoom |
| 3 | Pan/Tilt | Cycle pan/tilt | axis + step |
| 4 | Focus | Toggle autofocus | focus/autofocus |
Use as the premium director/PTZ profile:
+------------+------------+------------+
| PTT/Mic | Cough/Speak| Marker/Rec |
+------------+------------+------------+
Alternative director pedal:
Provide a key-only profile based on the 15-key profile. No dial-only features should be required for core workflows.
Fields:
api.vdo.ninja.value2 commands.Common fields:
Target modes:
Title template tokens:
| Token | Meaning |
|---|---|
{label} |
Stream label. |
{streamID} |
Stream ID. |
{slot} |
Guest slot. |
{state} |
Derived state label. |
{scene} |
Scene ID/name. |
{command} |
Command label. |
{value} |
Configured value. |
{lastResult} |
Last command result. |
Use consistent colors across profiles:
| State | Color intent |
|---|---|
| Connected/on/active | Green. |
| Muted/off/disabled | Red or dark gray depending action. |
| Pending/connecting | Yellow. |
| Error/rejected/timeout | Red pulse or temporary warning icon. |
| Stale/missing target | Gray with warning title. |
| Dangerous armed | Orange/red until confirmed. |
Icon groups:
Use generated or custom bitmap icons for Marketplace polish later. During implementation, start with simple SDK-compatible SVG/PNG assets inside the plugin folder.
State:
getDetails[streamID].others["remove-queue"].Control:
activateQueuedGuest over native &api, with removeQueue and removeQueuedGuest aliases.getDetails, but activation requires a VDO.Ninja build that includes the native action.This is a specific area where the native plugin can go beyond Companion while staying on the standard &api control path.
Deliverables:
vdo-streamdeck/plugin/.VdoClient.Acceptance:
wss://api.vdo.ninja:443.join.getDetails and display stream count.Deliverables:
Commands:
mic, camera, speaker, record, togglescreenshare, togglehand, forceKeyframe, reload, hangup, sendChat, layout, nextSlide, prevSlide.Acceptance:
getDetails.Deliverables:
getGuestList/getDetails.Commands:
mic, camera, speaker, display, volume, addScene, muteScene, group, soloChat, soloChatBidirectional, soloVideo, hangup, forward, forceKeyframe, mirror, rotate, mixorder, refreshVideo, refreshConnection, recoverStream.Acceptance:
Deliverables:
Commands:
zoom, focus, pan, tilt, exposure.ptzZoom, ptzPan, ptzTilt, ptzFocus, ptzAutofocus.volume, panning, bitrate, setBufferDelay.Acceptance:
Deliverables:
Acceptance:
&api=KEY.Automated:
Manual:
&api=KEY.ptz.html reference values.Marketplace smoke:
These are useful but should not block MVP:
| Decision | Default recommendation |
|---|---|
| Multiple API keys in v1? | No. Start with one global connection profile, design data model so multiple profiles can be added. |
| HTTP fallback? | Yes, optional and off by default for command paths that need reliable feedback. |
| Custom command in Marketplace build? | Yes, but with warnings and no unsafe raw WebSocket routing. |
| Queue activation in MVP? | Display queued state only. Add control after a VDO API action exists or embedded mode is approved. |
| Stats polling default? | Disabled unless a stats action is visible. |
| Dangerous command safety? | Double-tap by default for hangup, guest hangup, reload, transfer, recover stream. |
muteScene and group/view-group actions.Completed implementation baseline:
plugin/.VdoClient connects to the &api WebSocket and correlates callbacks with callback.get.getDetails callbacks replace state, partial details updates merge, and join/leave/position events trigger refreshes.