Skip to content

Roku Cloud Emulator support - #331

Draft
chrisdp wants to merge 26 commits into
v4from
feature/rce
Draft

Roku Cloud Emulator support#331
chrisdp wants to merge 26 commits into
v4from
feature/rce

Conversation

@chrisdp

@chrisdp chrisdp commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Adds Roku Cloud Emulator (RCE) device support to roku-deploy, backing the extension's RCE integration (rokucommunity/vscode-brightscript-language#861).

Stacked on #323 (unified device option); also carries #330's commit until it lands.

  • RceManagementClient: typed client for the RCE management API (devices, lifecycle, snapshots, firmware, logs, user), with DeviceRun matching the full device-history response shape. Every call takes an options object accepting an optional per-call token that overrides the constructor token (one client can serve multi-account callers), and getInstanceUrl resolves a device config addressed by instanceUrl, id, or esn to its live instance URL
  • Device-level RCE additions on RokuDeploy (replacing the earlier RceDevice class): a constructor-level default rceToken for RCE device configs that carry none, memoized instance-url resolution per device+token, resolveDns(device) (resolves a local device config's host to its ip; other device kinds pass through unchanged), sendKeySequence (presses remote keys in order through the same transport as keyPress, so key macros work on local and cloud devices alike), and sendDeveloperSettingsCombo (the RCE instance-api developer-settings action)
  • createRokuDeploySocket({ device, port }): a near-transparent net.Socket replacement for a device's tcp ports (the BrightScript console, the SceneGraph debug server, the debug protocol's control and io ports); a local device connects plain tcp, and an RCE device reaches the same port over the instance api's /api/v0/ports/<port> websocket routes (Instance API v0.3.0; binary frames both ways, byte parity with a raw tcp socket)
  • exports DeviceConfig, RceManagementClient, RceVideoSignalingClient, and createRokuDeploySocket from the package index
  • Remote key input (keyPress/keyUp/keyDown/sendText) prefers the RCE instance-api key route (/api/v0/ecp1/<action>/<key>), which keeps working while the device is in limited ECP mode (the raw ecp1 proxy 403s key presses there) and falls back to the raw proxy otherwise (live-verified in limited mode). Keys are exposed as a RemoteKey enum in canonical device casing, with a RemoteKeyText type enforcing the canonical names on the public key methods; this replaces the old lowercase RokuKey type
  • ecp(device, route): the single public ECP transport, addressing local devices on the HTTP ECP port and RCE devices through the instance's ecp1 proxy, always returning the XML body parsed to JSON. Dedicated wrappers (getDeviceInfo, key presses, launchApp, exitApp, queryApps, queryActiveApp, queryRegistry, queryAppState, queryRendezvous, setRendezvousTracking) all funnel through it, and device-reported failure text (not keyed, limited-mode refusals) rides in the typed errors. ECP requests default to a 10s RokuDeploy.defaults.ecpTimeout (overridable per call), separate from the 150s installer timeout. Live-verified against a cloud device, including in limited ECP mode

chrisdp added 5 commits July 23, 2026 12:55
Route device-info and key input to Roku Cloud Emulator instances, and add a
client for the RCE management API.

- RceDevice: sends ECP2 requests over the instance auth-proxy WebSocket
  (device-info query and key input) and resolves a device addressed by
  instanceUrl, id, or esn to its live instance URL
- RceManagementClient: typed client for the RCE management API (devices,
  lifecycle, snapshots, firmware, logs, user)
- getDeviceInfo and sendKeyEvent use the RCE path when the target is an RCE device
- Add ws and @types/ws
- Make RokuDeploy.defaults public and add stagingDirName, username, and files
- Add getStagingDir, getOutputZipPath, and getOutputPkgPath, and use them inside stage, zip, sideload, and createSignedPackage so consumers resolve the same paths the library does
- Make RekeyDeviceOptions.devId optional to match rekeyDevice behavior
- Replace the unused GetOutputZipFilePathOptions with the new resolver option interfaces
…odules

The device runs endpoint returns creator, ended_at, runtime, snapshot_name,
firmware, and max_runtime fields the old type omitted. Also export
DeviceConfig, RceDevice, and RceManagementClient from the package index.
POSTs the bodyless developer-settings-combo instance-api endpoint, which
enters the dev-settings key combo on the device so the user can complete
the on-screen developer setup wizard.
Negotiates a WebRTC stream from a running RCE instance's Janus gateway
over the standard janus-protocol WebSocket: session create, streaming
plugin attach, watch with the mountpoint id and pin, then the SDP offer
out and the consumer's answer and ICE candidates back in, with keepalives
and a negotiation timeout. The management api token rides the WebSocket
handshake as a bearer header and the instance janus_token is sent as the
apisecret field on every request. Carries no WebRTC dependency; consumers
bring their own peer connection.
Base automatically changed from unified-device-option to v4 July 28, 2026 20:09
@chrisdp chrisdp added this to the v4.0.0 milestone Jul 28, 2026
@chrisdp chrisdp self-assigned this Jul 28, 2026
chrisdp added 12 commits July 28, 2026 18:22
…d wrappers

The instance /ecp1 proxy (now working as intended) lets Cloud Emulator devices share the exact HTTP ECP transport local devices use, so ecp(device, route) owns the base-url swap and always returns the XML body parsed to JSON. Dedicated queryRegistry/queryAppState/queryRendezvous/setRendezvousTracking wrappers refine the common commands; ecp() remains the escape hatch for routes without one. Device-reported failure text (registry not keyed, limited-mode refusals) rides in the typed errors.
Nothing routes ECP over the ECP2 websocket anymore; the instance's /ecp1 proxy carries it as plain HTTP through RokuDeploy.ecp(). RceDevice keeps the instance-url resolution and instance-api actions like the developer-settings combo. Also adds 'guide' to RokuKey (the wake sequence's first key).
Limited ECP mode 403s every keypress on the /ecp1 proxy (with an empty body), but the auth-proxy WebSocket completes the ECP2 authentication challenge and keeps full key input working - the same trust level the mobile app gets. sendKeySequence presses keys in order over that socket (restoring the per-request socket pattern removed with the ECP2 stack, as a private primitive), pausing keyDelayMs between presses so on-screen navigation keeps up, and throws on the first non-2xx response with the failing key and step.
…moteKey vocabulary

An RCE device's key input now tries the instance-api key route (/api/v0/ecp1/<action>/<key>) first, which keeps working while the device is in limited ECP mode - the raw /ecp1 proxy 403s every key press there. Any failure (non-2xx, or a future rename of that path) falls back to the raw proxy, and LAN devices are unaffected.

Keys are exposed as a RemoteKey enum in canonical device casing, with a RemoteKeyText = keyof typeof RemoteKey type enforcing the canonical names on the public keyPress/keyUp/keyDown methods. This replaces the old lowercase RokuKey type (removed).
…pi's ports routes

Instance API v0.3.0 replaced the named telnet WebSocket routes with
/api/v0/ports/<port>, so the channel option (and its per-channel port
defaults) is gone; callers pass the port directly and RCE devices reach
it at that route. The endpoints are binary-frame-only now, which is what
this socket always sent.
Instance API v0.3.0 removed the ecp2 websocket endpoints, so key
sequences now POST /api/v0/ecp1/keypress/<key> per key (the same
route sendKeyEvent uses), which keeps working in limited ECP mode.
Removes the ECP2 auth-proxy socket stack and the createWebSocket
test seam that existed only for it.
ECP commands are small and fast, so they no longer share the 150s
installer timeout; ecp() and the RCE instance-api key route default to
RokuDeploy.defaults.ecpTimeout (10s, still overridable per call), which
also keeps the key route's fall-back-to-proxy behavior from stacking two
long waits against an unreachable device. The fallback now logs at warn,
and EcpResult.status is typed as possibly undefined with
assertEcpStatusOk guarding that case.
waitForConsoleOutput still passed the pre-rename channel option; the
factory takes a plain port number now.
sendRequest now rejects cleanly when the signaling socket is gone
instead of failing with a TypeError that left an orphaned pending
request, and startKeepalive clears any existing timer first.
The ECP wrappers built their error details with
extractHttpDetails(undefined, body), which always returns undefined, so
FailedDeviceResponseError/UnparsableDeviceResponseError/
InvalidDeviceResponseCodeError from those paths never carried
httpDetails. A new buildEcpHttpDetails helper carries what ecp()
retains (status code and body). getDeviceInfo also handles a missing
device-info root explicitly instead of relying on the TypeError from
spreading undefined, so that error carries no bogus cause. Surfaced by
v4's new coverage tests after the merge.
});
}

private buildQueryString(query?: Record<string, string | number | undefined>): string {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely could be a util. Should check and see if we have one already and if not move it there. Also should verify if still needed after the change to using auth headers.

max_runtime: number;
}

export interface DeviceOut {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something about these interface names bother me. Part of it might be using out rather then response or something like that.

I want to revisit these with @TwitchBronBron

Comment on lines +226 to +229
creator_id: string;
created_at: string;
started_at?: string | null;
snapshot_id: number;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TwitchBronBron in general what are your thoughts about the property names? Was almost thinking about making a small utility that runs every prop through a rename to camel case. Maybe overkill be let me know.

Comment thread src/RceDevice.ts Outdated
Comment on lines +28 to +40
/**
* Press a sequence of remote keys, in order, through the instance-api key route
* (`/api/v0/ecp1/keypress/<key>`). Keys must use the canonical `RemoteKey` casing ('Home', 'Up',
* 'Select', ...); the route rejects a wrong-case key with a 422. Each press waits for the
* previous press's response plus a small delay (keyDelayMs) so on-screen navigation keeps up;
* the first failed press throws with the failing key and step.
*
* This deliberately does NOT go through the raw `/ecp1` proxy RokuDeploy's `ecp()` transport
* uses: that proxy forwards unauthenticated ECP, which a device in limited ECP mode rejects with
* an empty 403. The instance-api key route carries the instance's own authentication, so key
* input works regardless of the device's ECP mode - which is what makes key macros like the
* disable-limited-ECP sequence possible in the first place.
*/

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments are a bit verbose and in particular some doc blocks are digging deeper into the underlying implementation then the consumer needs to care about. Want to do a pass on these.

Comment thread src/RokuDeploy.ts
* The default timeout for ECP requests, in milliseconds. ECP commands are small and fast,
* so this is much lower than `timeout` (which must accommodate large installer uploads).
*/
ecpTimeout: 10000,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TwitchBronBron added a separate timeout for ECP as I think the universal default we had is a little nuts for ECP calls. Let me know if you agree that they can be shorter then sideloading for example and if your happy with the 10s I set.

Comment thread src/RokuDeploy.ts
export type RemoteKeyText = keyof typeof RemoteKey;

export interface SendKeyEventOptions extends BaseEcpOptions {
interface SendKeyEventOptions extends BaseEcpOptions {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have removed the export here?

RokuDeploy's constructor options now accept an rceToken used as the
default for any RCE device whose config carries no token of its own.

RceDevice is gone: RceManagementClient.getInstanceUrl() resolves any RCE
device config (instanceUrl directly, id/esn through the management api,
with string config ids converted at the boundary now that DeviceId is a
number), RokuDeploy memoizes resolved instance urls per device+token,
and the two orphaned device actions move onto RokuDeploy as public
methods: sendKeySequence (device-kind-agnostic, each press rides the
keyPress transport) and sendDeveloperSettingsCombo (RCE-only, sends
X-Authorization like the rest of the instance-api calls).

TelnetSocket is renamed to RokuDeploySocket (createRokuDeploySocket,
LocalSocket, RceSocket, SocketOptions) and resolves instance urls
through the management client.
chrisdp added 2 commits July 31, 2026 16:53
PR #399 review: callers pass the whole device config in; a local
device comes back with its host resolved through DNS (some Rokus
reject ECP requests addressed by hostname), any other device kind
passes through unchanged.
… overrides

Every method takes a single options object now, and each accepts an
optional token that overrides the constructor token for that one call,
so one client can serve multi-account flows. Payloads keep their api
shape nested (createDevice({device}), startDevice({deviceId, start}),
updateSnapshot({deviceId, snapshotId, update}), ...) and send()
resolves the per-call token ?? the constructor token.
getInstanceUrl({device, token?}) threads the override through its
nested findDeviceByEsn/getRunningInstanceApiUrl calls.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant