kopytko-roku-device v1.3.3 Roku Device Toolkit
Everything needed to discover, control, profile, and debug a Roku device — one standalone, editor-agnostic npm package.
Subsystems
One package, every device protocol. All response shapes are verified against real hardware.
SSDP discovery
UDP 1900SsdpClient Active M-SEARCH scans + passive NOTIFY alive/byebye monitoring with per-IP debounce.
ECP client
HTTP 8060 / 80EcpClient Device info, apps, icons, deep-link launch/input, remote-control keys (keypress/keydown/keyup + Lit_ text entry), exit-app, active app, media-player state, registry, app state, chanperf, sgnodes (all/roots/by-id), app-ui (rendered UI tree), object counts, r2d2-bitmaps, graphics-frame-rate, TV channel queries, rendezvous + beacon tracking, dev-password validation (digest auth).
Discovery orchestration
—DeviceManager Merges SSDP, ECP health checks, persistence, and network-change events into one device list. Storage and network signals are injected interfaces.
SceneGraph debug console
TCP 8080DebugConsoleClient Idle-framed request/response console (chanperf, sgnodes, r2d2_bitmaps, free) with exponential-backoff auto-reconnect.
Interactive debug consoles
TCP 8085 / 8080ConsoleStream, completeCommand Raw bidirectional streaming for terminal use — every byte forwarded verbatim, prompts and unsolicited output included — plus a per-port command catalog with completion, subcommands and destructive-command detection.
Diagnostics parsers + collectors
8060 / 8080PollingCollector + 10 collectors Typed event model (mem-cpu, node-counts, textures, rendezvous, beacons, …) with parsers verified against real hardware.
Remote debug protocol
TCP 8081ProtocolClient, DebugCommands, IOClient Binary socket-based debugger: threads, stack traces, variables, breakpoints, step control, app output via the dynamic IO port.
Perfetto streaming
WS 8060PerfettoWebSocketClient, enablePerfettoTracing Streams raw Perfetto TracePacket protobuf from ws://device:8060/perfetto-session with a quiet-window stop.
RALE TrackerTask client
TCP 49152–65535RaleTrackerClient, FrameDecoder Live SceneGraph editing through an injected RALE TrackerTask: ECP-input activation, [start]/[end]-framed JSON commands (selectNode, setField, getNodeTree, …) with uuid correlation.
Web-admin automation
HTTP 80InstallerClient Digest-auth automation of the developer web-admin page: install/delete/rekey/package a channel, screenshot, profiling data, OS update check, reboot.
Installation
npm install kopytko-roku-deviceRequires Node.js ≥ 24. The only runtime dependency is ws (Perfetto streaming).
Device discovery
DeviceManager orchestrates SSDP, ECP health checks, and persistence. Storage and network-change signals are injected interfaces, so it runs anywhere — VS Code, a CLI, or CI.
import { SsdpClient, EcpClient, DeviceManager } from 'kopytko-roku-device';
// DeviceStorage + NetworkWatcher are host-provided interfaces:
// the VS Code extension backs them with Memento global state and a
// focus-aware network poller; a CLI can use a JSON file + no-op watcher.
const manager = new DeviceManager(
new SsdpClient(),
new EcpClient(),
myStorage, // implements DeviceStorage
myNetworkWatcher, // implements NetworkWatcher
);
manager.on('devices-changed', () => {
for (const device of manager.getDevices()) {
console.log(device.friendlyName, device.ip, device.state);
}
});
await manager.initialize(); // starts SSDP + initial M-SEARCH scanECP queries
Typed wrappers over Roku's External Control Protocol on port 8060, plus dev-password validation via digest auth on port 80.
import { EcpClient, parseRegistryXml } from 'kopytko-roku-device';
const ecp = new EcpClient();
const info = await ecp.queryDeviceInfo('192.168.1.20');
console.log(info['model-name'], info['software-version']);
const apps = await ecp.queryApps('192.168.1.20');
const dev = apps.find((a) => a.id === 'dev'); // sideloaded channel
// Registry of the sideloaded channel (all channels sharing the dev key)
const registryXml = await ecp.queryRegistry('192.168.1.20', 'dev');
const registry = parseRegistryXml(registryXml);
console.log(registry.plugins); // e.g. "158987,268970,dev"
// Deep linking — relaunch with params, or roInput to the running channel
await ecp.launchApp('192.168.1.20', '12', { contentId: 'movie-123', mediaType: 'movie' });
await ecp.sendInput('192.168.1.20', { contentId: 'movie-123', mediaType: 'movie' });
// Channel icon — raw bytes + content type (binary-safe)
const { data, contentType } = await ecp.queryAppIcon('192.168.1.20', '12');Remote control & playback state
Simulate the remote (/keypress, /keydown, /keyup), type on the on-screen keyboard with ordered Lit_ keypresses, and query the foreground app and media-player state — the primitives under the extension's Device Manager remote and RASP script runner.
import { EcpClient, EcpKeys, textToLitKeys } from 'kopytko-roku-device';
const ecp = new EcpClient();
// Press-and-release a named key (POST /keypress/{key})
await ecp.keypress('192.168.1.20', EcpKeys.Home);
// Hold a key — the device keeps it pressed until the matching keyup
await ecp.keydown('192.168.1.20', EcpKeys.Right);
await ecp.keyup('192.168.1.20', EcpKeys.Right);
// Type on the on-screen keyboard: one strictly sequential keypress per
// Unicode code point, Lit_-encoded ('€' → 'Lit_%E2%82%AC')
await ecp.sendText('192.168.1.20', 'my search term');
textToLitKeys('r€'); // → ['Lit_r', 'Lit_%E2%82%AC']
// Playback state — foreground app + media player (state, codecs, DRM)
const active = await ecp.queryActiveApp('192.168.1.20');
const player = await ecp.queryMediaPlayer('192.168.1.20');
if (player.state === 'play') console.log(player.format?.video, player.format?.drm);Runtime metrics
Polling collectors emit a typed event stream (mem-cpu, node-counts, textures, rendezvous, fw-beacon, …) — the data source of the extension's Kopytko Diagnostics panel.
import {
DebugConsoleClient,
ChanperfCollector,
EcpObjectCountsCollector,
} from 'kopytko-roku-device';
// SceneGraph debug console (port 8080) — idle-framed, auto-reconnecting
const console8080 = new DebugConsoleClient({ host: '192.168.1.20' });
// Poll per-channel CPU + memory every second
const chanperf = new ChanperfCollector(console8080, 1000);
chanperf.on('sample', (s) => {
// { type: 'mem-cpu', wall, memKiB, anonKiB, cpuPct, ... }
console.log(s);
});
chanperf.start();
// Collectors never throw into callers: a failed poll skips that
// interval and the collector self-heals when the device answers again.BrightScript debugger protocol
A client for Roku's socket-based debug protocol — the transport under the extension's DAP debugger.
import { ProtocolClient, DebugCommands, IOClient } from 'kopytko-roku-device';
// Binary BrightScript remote debug protocol (port 8081)
const client = new ProtocolClient('192.168.1.20');
client.on('update', (update) => console.log(update)); // stops, compile errors, IO port
await client.connect(); // 8-byte magic handshake + version negotiation
const commands = new DebugCommands(client);
const threads = await commands.threads();
const stack = await commands.stacktrace(threads.primary);
await commands.addBreakpoints([{ filePath: 'pkg:/source/main.brs', lineNumber: 42 }]);Web-admin automation
Drives the developer web-admin page (http://<device-ip>/) the same way a browser would — install/delete/rekey/package a channel, capture a screenshot, download profiling data, or check for updates and reboot. Surfaced in the extension through the Device Manager's Device view.
import { InstallerClient } from 'kopytko-roku-device';
const installer = new InstallerClient();
const password = 'my-dev-password'; // username is always 'rokudev'
// Installer tab
await installer.installChannel('192.168.1.20', password, './build/archive.zip');
// Packager tab — installs, signs, and downloads the .pkg
await installer.packageChannel(
'192.168.1.20', password, './build/archive.zip',
'MyApp/1.0', 'my-signing-password', './out/signed.pkg',
);
// Utilities tab
await installer.takeScreenshot('192.168.1.20', password, './out/screenshot.jpg');
await installer.downloadProfilingData('192.168.1.20', password, './out/channel.bsprof');
// Update tab
await installer.checkForUpdate('192.168.1.20', password);RALE TrackerTask client
Talks to an injected Roku Advanced Layout Editor TrackerTask to edit SceneGraph nodes on a running app: an ECP input activates the task, which opens a TCP server on the device; commands are [start]{json}[end]-framed JSON with uuid-correlated, chunked responses. Powers the extension's SceneGraph Tree Edit mode.
import { EcpClient, RaleTrackerClient } from 'kopytko-roku-device';
// Requires Roku's RALE TrackerTask injected and started in the (dev) channel.
const rale = new RaleTrackerClient({ host: '192.168.1.20', ecp: new EcpClient() });
const { raleVersion } = await rale.connect(); // ECP activate -> TCP connect -> init
await rale.hideSelectorView(); // no red selector rectangle on the TV
// setField writes to the *selected* node — always selectNode first.
const { node } = await rale.selectNode([{ child: 0 }, { child: 2 }]);
if (node.item.subtype === 'Label') {
await rale.setField('text', 'Hello from RALE');
await rale.setField('visible', true);
}
rale.close(); // the task returns to awaiting activationTerminal CLI
kopytko-roku exposes every ECP and web-admin operation above from the shell — no VS Code required. Config resolves from flags → --config file → ROKU_HOST/ROKU_PASSWORD env vars; it deliberately never reads .vscode/settings.json or .kopytkorc. See the full CLI reference for every subcommand and flag.
npm install -g kopytko-roku-device
kopytko-roku --help
# ECP
kopytko-roku ecp device-info --host 192.168.1.20
kopytko-roku ecp keypress --host 192.168.1.20 --key Home
kopytko-roku ecp launch --host 192.168.1.20 --app dev --param contentId=42
kopytko-roku ecp app-ui --host 192.168.1.20
# Developer web-admin
kopytko-roku installer screenshot --host 192.168.1.20 --password secret --out shot.jpg
# Discovery
kopytko-roku discover --jsonDevice behaviour notes
- ▸
GET /query/sgrendezvousandGET /query/fwbeaconsdrain the device-side event queue — two simultaneous pollers each miss half the events. - ▸chanperf / sgnodes / the port-8080 console report nothing while the channel is backgrounded — a Roku OS limitation, not an error. Rendezvous, beacons, and app-state keep working.
- ▸Port-8080 responses are idle-framed:
>appears inside XML output, so it can't be used as a terminator. - ▸The port-8085 log stream accepts only one consumer at a time — beacons are collected via ECP instead.
- ▸
/plugin_installreturns HTTP 200 for both success and failure — the real result is asuccess/error/infomessage embedded as JSON in the response page, whichInstallerClientparses and throws on.