Documentation
Complete API reference for @danecodes/roku-ecp -- the TypeScript client for the Roku External Control Protocol.
Installation
npm install @danecodes/roku-ecp Requires Node.js 22+ and a Roku device in developer mode on your network.
Quick Start
import { EcpClient, Key, parseUiXml, findElement, findFocused } from '@danecodes/roku-ecp';
// Connect by IP
const roku = new EcpClient('192.168.0.30');
// Or discover on the network
// const roku = await EcpClient.discover();
// const allDevices = await EcpClient.discoverAll();
// Send remote control input
await roku.press(Key.Down, { times: 3 });
await roku.press(Key.Select);
// Type into a search field
await roku.type('breaking bad');
// Inspect the SceneGraph UI tree
const xml = await roku.queryAppUi();
const tree = parseUiXml(xml);
const button = findElement(tree, 'AppButton#play_button');
console.log(button?.attrs.text); // "Play"
// Check what's focused
const focused = findFocused(tree);
console.log(focused?.tag, focused?.attrs.name);
// Query device state
const info = await roku.queryDeviceInfo();
const app = await roku.queryActiveApp();
const player = await roku.queryMediaPlayer();
const apps = await roku.queryInstalledApps(); Discovery (SSDP)
Find Roku devices on the local network via SSDP M-SEARCH. No hardcoded IPs required.
// Find the first Roku on the network (5s default timeout)
const roku = await EcpClient.discover();
// Custom timeout
const roku = await EcpClient.discover({ timeout: 10000 });
// Find ALL Roku devices on the network
const all = await EcpClient.discoverAll();
console.log(all.map(d => d.ip)); // ['192.168.0.30', '192.168.0.42'] EcpClient
Create a client by passing the device IP and optional configuration.
const roku = new EcpClient('192.168.0.30');
// With options
const roku = new EcpClient('192.168.0.30', {
port: 8060, // ECP HTTP port (default: 8060)
devPassword: 'rokudev', // developer password for sideload/screenshot
timeout: 10000, // request timeout in ms
}); | Option | Default | Description |
|---|---|---|
port | 8060 | ECP HTTP port |
devPassword | "rokudev" | Developer password for sideload/screenshot |
timeout | 10000 | Request timeout in ms |
keyCooldown | 0 | Minimum delay between key presses in ms |
webCooldown | 0 | Minimum delay between web server requests in ms |
Key Input
Send remote control key presses to the device.
await roku.keypress(Key.Select); // single press
await roku.keydown(Key.Right); // key down
await roku.keyup(Key.Right); // key up
await roku.press(Key.Down, { times: 5, delay: 100 }); // repeated press
await roku.type('search text', { delay: 50 }); // character-by-character Available Keys
All standard Roku keys are available on the Key object:
Home Back Select Up Down Left Right Play Rev Fwd Info Search Enter Backspace InstantReplay VolumeUp VolumeDown VolumeMute PowerOn PowerOff InputHDMI1 InputHDMI2 InputHDMI3 InputHDMI4 InputAV1 InputTuner Touch Input
Send touch events to the device screen. Roku's coordinate origin is bottom-left.
await roku.touch({ x: 100, y: 200 }); // tap at coordinates
await roku.touch({ x: 100, y: 200, op: 'down' }); // touch down
await roku.touch({ x: 150, y: 250, op: 'move' }); // drag
await roku.touch({ x: 150, y: 250, op: 'up' }); // release Operations: 'press' (default), 'down', 'up', 'move'.
App Lifecycle
Launch, install, deep link, and manage apps on the device.
await roku.launch('12345'); // launch by channel ID
await roku.launch('dev', { contentId: 'abc', mediaType: 'episode' }); // with params
await roku.deepLink('dev', 'abc', 'episode'); // shorthand
await roku.install('12345'); // install from store
await roku.input({ key: 'value' }); // send input params
await roku.closeApp(); // press Home Device Queries
Query device state, active app, installed apps, media player, and performance.
const info = await roku.queryDeviceInfo(); // DeviceInfo
const app = await roku.queryActiveApp(); // ActiveApp
const apps = await roku.queryInstalledApps(); // InstalledApp[]
const player = await roku.queryMediaPlayer(); // MediaPlayerState
const xml = await roku.queryAppUi(); // raw XML string
const perf = await roku.queryChanperf(); // ChanperfSample DeviceInfo
Returns model, firmware version, serial number, network info, display capabilities, and more. Parsed from the ECP /query/device-info endpoint.
ActiveApp
Returns the currently running app's ID, name, and version.
MediaPlayerState
Returns playback state, position, duration, buffering status, and stream info from /query/media-player.
Sideload & Screenshot
Deploy dev channel builds and capture screenshots. Requires developer mode enabled on the device.
// Deploy a dev channel build (zip file)
await roku.sideload('./build.zip');
// Deploy from a directory
await roku.sideload('./my-roku-app');
// Capture a screenshot (returns Buffer)
const png = await roku.takeScreenshot(); Both operations use digest authentication with the configured devPassword.
Debug Console (Port 8085)
Read BrightScript console output and send debug commands via TCP on port 8085.
// Read console output for 3 seconds, filtering for 'error'
const output = await roku.readConsole({ duration: 3000, filter: 'error' });
// Send a debug command (e.g., backtrace)
const response = await roku.sendConsoleCommand('bt'); UI Tree: Parsing & Querying
Parse the SceneGraph XML response and query it with CSS-like selectors.
import { parseUiXml, findElement, findElements, findFocused, formatTree } from '@danecodes/roku-ecp';
const tree = parseUiXml(await roku.queryAppUi());
// Find a single element
findElement(tree, 'AppButton#play'); // by tag#name
findElement(tree, '#titleLabel'); // by name only
findElement(tree, 'HomePage HomeHeroCarousel'); // descendant
findElement(tree, 'LayoutGroup > AppLabel'); // direct child
findElement(tree, 'AppButton:nth-child(1)'); // nth-child
findElement(tree, 'CollectionModule + CollectionModule'); // adjacent sibling
findElement(tree, '[focused="true"]'); // attribute value
findElement(tree, '[visible]'); // attribute existence
findElement(tree, 'AppButton[focused="true"]'); // tag + attribute
findElement(tree, 'AppButton#play[focused="true"]'); // tag + name + attribute
// Find all matching elements
findElements(tree, 'AppButton');
// Find the currently focused node
findFocused(tree);
// Pretty-print the tree
console.log(formatTree(tree, { maxDepth: 3 })); parseUiXml is synchronous -- no async/await needed for parsing.
Selector Syntax
| Pattern | Example | Matches |
|---|---|---|
| Tag name | AppButton | All elements with that tag |
| ID (name) | #play_button | Element with name="play_button" |
| Tag + ID | AppButton#play | AppButton with that name |
| Descendant | HomePage Carousel | Carousel anywhere inside HomePage |
| Direct child | LayoutGroup > Label | Label that is a direct child |
| Attribute value | [focused="true"] | Element with that attribute value |
| Attribute existence | [visible] | Element with that attribute present |
| Tag + attr | Label[text="Home"] | Label with text="Home" |
| Tag + name + attr | AppButton#play[focused="true"] | AppButton named "play" with focused=true |
| nth-child | NavTab:nth-child(2) | Second NavTab among siblings |
| Adjacent sibling | Module + Module | Module preceded by another Module |
UiNode Interface
The parsed tree is made up of UiNode objects:
interface UiNode {
tag: string; // SceneGraph component name
name?: string; // name or id attribute
attrs: Record<string, string>; // all XML attributes
children: UiNode[];
parent?: UiNode;
} Console Parser
Scan BrightScript debug output for structured issues.
import { parseConsoleForIssues } from '@danecodes/roku-ecp';
const output = await roku.readConsole({ duration: 5000 });
const { errors, crashes, exceptions } = parseConsoleForIssues(output); | Category | Detected Patterns |
|---|---|
| errors | BRIGHTSCRIPT: ERROR, Runtime Error |
| crashes | Backtrace, -- crash, BRIGHTSCRIPT STOP |
| exceptions | STOP in file, PAUSE in file |
Wait Helpers
Poll the device until a condition is met, with configurable timeout and interval.
import {
waitFor, waitForElement, waitForFocus,
waitForApp, waitForText, waitForStable,
} from '@danecodes/roku-ecp';
const getTree = async () => parseUiXml(await roku.queryAppUi());
// Wait for an element to appear
const el = await waitForElement(getTree, '#loginBtn');
// Wait for a specific element to gain focus
await waitForFocus(getTree, 'AppButton#play');
// Wait for any element to be focused (no selector)
const focused = await waitForFocus(getTree);
// Wait for an app to become active
await waitForApp(roku, '12345');
// Wait for text content to appear
await waitForText(getTree, '#title', 'Now Playing');
// Wait for UI to stabilize after animation
await roku.keypress(Key.Down);
await waitForStable(getTree, { interval: 150, timeout: 3000 });
// Generic: poll any custom condition
const state = await waitFor(async () => {
const p = await roku.queryMediaPlayer();
return p.state === 'play' ? p : undefined;
}, { timeout: 5000, label: 'waitForPlayback' }); | Option | Default | Description |
|---|---|---|
timeout | 10000 | Max wait in ms (waitForStable defaults to 3000) |
interval | 200 | Poll interval in ms (waitForStable defaults to 150) |
Transient EcpTimeoutError and EcpHttpError during polling are caught and retried until the deadline. Non-transient errors throw immediately.
Typed Errors
All errors are typed for clean catch handling.
import {
EcpHttpError, EcpTimeoutError, EcpAuthError,
EcpSideloadError, EcpScreenshotError,
} from '@danecodes/roku-ecp';
try {
await roku.queryDeviceInfo();
} catch (err) {
if (err instanceof EcpTimeoutError) // device unreachable
if (err instanceof EcpHttpError) // non-ok HTTP status
if (err instanceof EcpAuthError) // digest auth failure
} | Error | When |
|---|---|
EcpTimeoutError | Device unreachable or request timed out |
EcpHttpError | Non-OK HTTP status (includes method, path, status) |
EcpAuthError | Digest authentication failure |
EcpSideloadError | Sideload operation failed |
EcpScreenshotError | Screenshot capture failed |
Structured Log Parsing
Powered by @danecodes/roku-log. Full structured parsing with file/line/function extraction, real-time streaming, and aggregation.
import { LogParser, LogStream, LogSession, LogFormatter } from '@danecodes/roku-ecp';
// Parse raw text into structured entries
const parser = new LogParser();
const entries = parser.parse(output); // LogEntry[] with type, source, message
// Stream logs in real time
const stream = new LogStream('192.168.0.30');
stream.on('error', (err) => console.log(err.errorClass, err.source));
stream.on('crash', (bt) => console.log(bt.frames));
stream.on('beacon', (b) => console.log(b.event, b.duration));
await stream.connect();
// Aggregate and analyze
const session = new LogSession();
session.addAll(entries);
console.log(session.summary()); // { errorCount, crashCount, ... }
// Color-coded terminal output
const fmt = new LogFormatter({ color: true });
entries.forEach(e => console.log(fmt.format(e))); Requirements
- Node.js 22+
- Roku device in developer mode on the same network
- No native dependencies -- pure TypeScript/JavaScript