Skip to content

danecodes/roku-ecp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

roku-ecp

npm version CI License: MIT

Lightweight TypeScript client for the Roku External Control Protocol (ECP). Companion library to @danecodes/roku-mcp.

No WebDriver. No Appium. No Selenium. No Java. No native dependencies. Just HTTP to port 8060.

Install

npm install @danecodes/roku-ecp

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('one piece');

// 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();

API

EcpClient.discover(options?)

Find a Roku on the local network via SSDP. Returns the first device found.

const roku = await EcpClient.discover();                    // 5s timeout
const roku = await EcpClient.discover({ timeout: 10000 });  // custom timeout
const all = await EcpClient.discoverAll();                   // find all devices

new EcpClient(ip, options?)

Option Default Description
port 8060 ECP HTTP port
devPassword "rokudev" Developer password for sideload/screenshot
timeout 10000 Request timeout in ms

Key input

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

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, InputHDMI14, InputAV1, InputTuner.

App lifecycle

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

Queries

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

Sideload & screenshot

await roku.sideload('./build.zip');              // deploy dev channel (zip file)
await roku.sideload('./my-roku-app');            // deploy dev channel (directory)
const png = await roku.takeScreenshot();         // returns Buffer

Requires developer mode. Uses digest auth with the configured devPassword.

Debug console (port 8085)

const output = await roku.readConsole({ duration: 3000, filter: 'error' });
const response = await roku.sendConsoleCommand('bt'); // backtrace

UI tree

Parse the SceneGraph XML and query it with CSS-like selectors:

import { parseUiXml, findElement, findElements, findFocused, formatTree } from '@danecodes/roku-ecp';

const tree = parseUiXml(await roku.queryAppUi());

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

findElements(tree, 'AppButton');  // all matches
findFocused(tree);                // currently focused node

console.log(formatTree(tree, { maxDepth: 3 }));

UiNode

interface UiNode {
  tag: string;                        // SceneGraph component name
  name?: string;                      // name or id attribute
  attrs: Record<string, string>;      // all XML attributes
  children: UiNode[];
  parent?: UiNode;
}

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 (e.g. after a key press)
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' });

All helpers accept WaitOptions:

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

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 { method, path, status, statusText }
  if (err instanceof EcpAuthError)    // digest auth failure { status }
}

Console & log parsing

Powered by @danecodes/roku-log. Quick issue scan:

import { parseConsoleForIssues } from '@danecodes/roku-ecp';

const output = await roku.readConsole({ duration: 5000 });
const { errors, crashes, exceptions } = parseConsoleForIssues(output);

For structured parsing with file/line/function extraction:

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

  • Roku device in developer mode on the same network
  • Node.js 22+

License

MIT

About

TypeScript client for the Roku External Control Protocol (ECP)

Topics

Resources

License

Stars

Watchers

Forks

Contributors