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
});
OptionDefaultDescription
port8060ECP HTTP port
devPassword"rokudev"Developer password for sideload/screenshot
timeout10000Request timeout in ms
keyCooldown0Minimum delay between key presses in ms
webCooldown0Minimum 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

PatternExampleMatches
Tag nameAppButtonAll elements with that tag
ID (name)#play_buttonElement with name="play_button"
Tag + IDAppButton#playAppButton with that name
DescendantHomePage CarouselCarousel anywhere inside HomePage
Direct childLayoutGroup > LabelLabel that is a direct child
Attribute value[focused="true"]Element with that attribute value
Attribute existence[visible]Element with that attribute present
Tag + attrLabel[text="Home"]Label with text="Home"
Tag + name + attrAppButton#play[focused="true"]AppButton named "play" with focused=true
nth-childNavTab:nth-child(2)Second NavTab among siblings
Adjacent siblingModule + ModuleModule 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);
CategoryDetected Patterns
errorsBRIGHTSCRIPT: ERROR, Runtime Error
crashesBacktrace, -- crash, BRIGHTSCRIPT STOP
exceptionsSTOP 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' });
OptionDefaultDescription
timeout10000Max wait in ms (waitForStable defaults to 3000)
interval200Poll 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
}
ErrorWhen
EcpTimeoutErrorDevice unreachable or request timed out
EcpHttpErrorNon-OK HTTP status (includes method, path, status)
EcpAuthErrorDigest authentication failure
EcpSideloadErrorSideload operation failed
EcpScreenshotErrorScreenshot 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