vdo-streamdeck

Runtime Comparison Audit

Audit date: 2026-06-30. Compatibility recheck: 2026-07-10.

This compares the native Stream Deck plugin implementation in plugin/ against the current local VDO.Ninja runtime. The plugin still stays inside vdo-streamdeck/; no VDO.Ninja source files were changed for this audit.

Recheck Summary

Repeated source rechecks on 2026-06-30 confirmed the plugin should continue treating VDO.Ninja state as page-derived live state, not as a durable room model:

Source Paths Checked

Confirmed API Transport Behavior

The native plugin is correct to use the &api WebSocket as the primary surface:

  1. A controlled VDO.Ninja page opened with &api=KEY connects to session.apiserver.
  2. It sends { "join": "KEY" }.
  3. On socket open, it pushes an async update:
{
  "update": {
    "action": "details",
    "streamID": "localStreamId",
    "value": {
      "localStreamId": {}
    }
  }
}

The value comes from getDetailedState(session.streamID), so it is often a partial page-local snapshot, not a full room snapshot.

Incoming plugin commands use:

{
  "action": "mic",
  "target": null,
  "value": "toggle",
  "get": "request-id"
}

processMessage routes commands with a non-null target through targetGuest(...); otherwise it calls local Commands[action]. When a response is non-null, the page returns:

{
  "callback": {
    "action": "mic",
    "value": "toggle",
    "get": "request-id",
    "result": false
  }
}

Because the original command object is reused, callback.get, callback.target, callback.value, and callback.value2 echo the normalized request where present. callback.result is the function return value, not a guaranteed durable state snapshot.

The reference HTTP relay resolves successful HTTP requests with only callback.result. It returns plain failed when no VDO.Ninja page is joined to the API key and plain timeout when a page does not return a callback in time. The plugin treats those relay strings as errors, not successful command results.

HTTP fallback note: VDO.Ninja’s relay POST path always adds a get ID and waits for a callback internally. Even plugin actions configured as “do not wait” may still wait when they fall back to HTTP, because HTTP has no fire-and-forget route in the reference relay.

Implementation rule: HTTP fallback responses are normalized into the same callback path as WebSocket callbacks so getDetails, getGuestList, and command feedback update the Stream Deck state store consistently.

Custom command parsing rule: the plugin preserves numeric-looking text values such as 01 as strings. VDO.Ninja itself parses numeric command values where needed, while stream IDs, push/view IDs, and custom scene keys can be numeric-looking strings that must not be coerced.

API host rule: default secure WebSocket remains wss://api.vdo.ninja:443. Custom hosts with explicit ports are preserved instead of blindly appending :443, and non-TLS WebSocket mode defaults to port 80.

Important API-key scope: the relay broadcasts commands to every WebSocket client joined to the same API key. The callback does not include a stable page identity. The MVP plugin therefore treats one API key as one controlled VDO.Ninja page/session for deterministic state feedback. Reusing the same API key across a director page plus scene/view/push pages can produce multiple callbacks and ambiguous local state; use separate API keys until a multi-page state model exists.

Connection-state rule: opening the WebSocket to the relay is not enough to show “connected” in Stream Deck. The plugin should only mark a VDO page as connected after a VDO.Ninja page sends an update or returns a callback. A relay connection with no answering page becomes no-page after the getDetails timeout.

Implementation Corrections Made

The initial plugin implementation treated every details payload as a complete replacement. That is wrong for current VDO.Ninja because several pokeAPI("details", ...) calls are partial:

The plugin now does this:

Regression coverage was added in plugin/src/state/session-store.test.ts for these cases.

State Source Truths

getDetailedState is the main state source for the native plugin. It is built on demand from live session objects and the current DOM. It is not a persistent room or scene database.

Remote stream entries can include:

Local stream entries can include:

Room, Push/View, Scene, and Transfer Implications

The plugin cannot assume that an API key controls a director room.

Director Room

Expected state:

Best plugin behavior:

Expected state:

Best plugin behavior:

Expected state:

Best plugin behavior:

Expected state:

Best plugin behavior:

Transfer Rooms

Expected state:

Best plugin behavior:

Scene Handling

Current VDO.Ninja scene state is dynamic:

Plugin rule:

Callback and Info Calls

There is no current iframe call named getStreamDetails in the local runtime.

Closest calls:

For a native Marketplace plugin, getDetails is the best general state call over &api. getStreamInfo, device APIs, loudness, video frames, and some P2P data helpers remain iframe-only unless a future VDO.Ninja &api bridge is added. Queue activation is now covered by activateQueuedGuest.

Naming note: there is no getDetailsState call in the checked source. Use getDetails for &api and getDetailedState for iframe/source references.

Current Plugin Coverage

Implemented now:

Still planned:

Remaining Gaps Against VDO.Ninja