npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@danecodes/roku-odc

v0.4.2

Published

TypeScript client for Roku On-Device Components (ODC) — registry access, app UI inspection, and file management for sideloaded channels

Downloads

1,197

Readme

roku-odc

License: MIT

Lightweight TypeScript client and on-device component for Roku ODC — registry access, SceneGraph introspection, and file management for sideloaded channels. Companion library to @danecodes/roku-ecp.

ODC is an HTTP API on port 8061 that provides deep runtime access to a dev-sideloaded Roku channel. This package includes both the TypeScript client and the BrightScript component that runs on the device, with automatic injection into your channel at sideload time.

Install

npm install @danecodes/roku-odc

Quick start

import { inject, OdcClient } from '@danecodes/roku-odc';
import { readFile } from 'node:fs/promises';

// Inject the ODC component into your channel zip before sideloading
const channelZip = await readFile('my-channel.zip');
const injectedZip = await inject(channelZip);
// Now sideload injectedZip to your Roku (e.g. via roku-ecp's sideload())

// Connect to the running channel's ODC server
const odc = new OdcClient('192.168.0.30');

// Read/write SceneGraph node fields
const isLoggedIn = await odc.getField('authManager', 'isLoggedIn');
await odc.setField('featureFlags', 'darkMode', true);

// Call interface functions on nodes
await odc.callFunc('authManager', 'login', ['testuser', 'password']);

// Wait for a field to reach a value
const result = await odc.observeField('authManager', 'isLoggedIn', { match: true, timeout: 5000 });

// Find nodes by properties
const buttons = await odc.findNodes({ subtype: 'Button', text: 'Play' });

// Get the focused node
const focused = await odc.getFocusedNode();

// Registry, files, and app-ui also available
const registry = await odc.getRegistry();

Capability matrix

| Feature | Method | Description | | ------- | ------ | ----------- | | getField | odc.getField(nodeId, field) | Read any field on any node by ID | | setField | odc.setField(nodeId, field, value) | Write any field on any node | | callFunc | odc.callFunc(nodeId, func, params?) | Call interface functions on nodes | | observeField | odc.observeField(nodeId, field, opts?) | Wait for a field to change or match a value | | findNodes | odc.findNodes(filters) | Search the SceneGraph tree by subtype/field values | | getFocusedNode | odc.getFocusedNode() | Get the currently focused node with all fields | | getRegistry | odc.getRegistry() | Read all registry sections and keys | | setRegistry | odc.setRegistry(data) | Write registry values (PATCH merge) | | clearRegistry | odc.clearRegistry(sections?) | Clear specific or all registry sections | | getAppUi | odc.getAppUi(fields?) | Full SceneGraph tree as XML | | pullFile | odc.pullFile(source) | Download a file from the device | | pushFile | odc.pushFile(dest, data) | Upload a file to the device | | listFiles | odc.listFiles(path?) | List directory contents |

Injection

The ODC component must be running inside your channel for the client to connect.

CLI

The fastest way — no code needed:

# Inject into a zip
npx @danecodes/roku-odc inject build.zip

# Inject into a directory
npx @danecodes/roku-odc inject ./my-channel

inject(zip): Promise<Buffer>

Inject into a channel zip buffer. Returns a new zip with the ODC component added.

import { inject } from '@danecodes/roku-odc';

const original = await readFile('my-channel.zip');
const injected = await inject(original);
await writeFile('my-channel-odc.zip', injected);

This:

  • Adds the BrightScript ODC server component to components/roku-odc/
  • Adds the launch hook to source/roku-odc/
  • Patches your entry point (Main or RunUserInterface) to initialize ODC at launch
  • Creates the ODC task node after screen.show()

injectDir(dir): Promise<Buffer>

Read a channel directory, inject ODC, and return a ready-to-sideload zip buffer. Does not modify the source directory.

import { injectDir } from '@danecodes/roku-odc';

const zip = await injectDir('./my-channel');
await writeFile('my-channel.zip', zip);
// or pass directly to roku-ecp's sideload()

Squashfs builds

Both functions return zip buffers. If your pipeline needs squashfs, inject first, then convert:

const zip = await injectDir('./build');
await writeFile('build.zip', zip);
// then convert to squashfs

Launch configuration

Once injected, the ODC component supports launch-time configuration via ECP launch params:

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

const ecp = new EcpClient('192.168.0.30');

// Launch with pre-loaded registry state
await ecp.launch('dev', {
  odc_registry: JSON.stringify({ auth: { token: 'test' } }),
});

// Clear registry on launch
await ecp.launch('dev', { odc_clear_registry: 'true' });

// Pass channel data (deeplink-like params)
await ecp.launch('dev', {
  odc_channel_data: JSON.stringify({ contentId: 'abc' }),
});

// Launch to a specific entry point
await ecp.launch('dev', { odc_entry_point: 'screensaver' });
// Options: 'channel', 'screensaver', 'screensaver-settings'

Client API

new OdcClient(ip, options?)

| Option | Type | Default | Description | | --------- | -------- | ------- | ------------------------------ | | port | number | 8061 | ODC server port | | timeout | number | 10000 | Request timeout in milliseconds |

Node primitives

getField(nodeId, field): Promise<unknown>

Read a field value from a node found by ID (recursive search from scene root).

const text = await odc.getField('title', 'text');
const visible = await odc.getField('overlay', 'visible');

setField(nodeId, field, value): Promise<void>

Set a field value on a node.

await odc.setField('title', 'text', 'Welcome');
await odc.setField('featureFlags', 'darkMode', true);

callFunc(nodeId, func, params?): Promise<unknown>

Call an interface function on a node. Supports up to 5 parameters.

// Call with no params
await odc.callFunc('player', 'pause');

// Call with params
const result = await odc.callFunc('authManager', 'login', ['user', 'pass']);

findNodes(filters): Promise<NodeInfo[]>

Search the SceneGraph tree for nodes matching field values.

// Find all Buttons
const buttons = await odc.findNodes({ subtype: 'Button' });

// Find a specific label
const labels = await odc.findNodes({ subtype: 'Label', text: 'Hello' });

Returns an array of NodeInfo objects with id, subtype, and fields.

getFocusedNode(): Promise<NodeInfo | null>

Get the currently focused node with all its fields.

const focused = await odc.getFocusedNode();
if (focused) {
  console.log(focused.subtype, focused.id, focused.fields.text);
}

observeField(nodeId, field, options?): Promise<ObserveResult>

Wait for a field to change or match a specific value. Blocks the ODC server for up to timeout ms.

// Wait for any change
const result = await odc.observeField('player', 'state');

// Wait for a specific value
const result = await odc.observeField('auth', 'isLoggedIn', {
  match: true,
  timeout: 5000,
});

if (result.matched) {
  console.log('Field matched:', result.value);
}

| Option | Type | Default | Description | | --------- | --------- | --------------- | ------------------------------------ | | match | unknown | (any change) | Value to wait for | | timeout | number | client timeout | Max wait in milliseconds |

Registry

getRegistry(): Promise<Record<string, Record<string, string>>>

Read all registry sections and keys for the running channel.

const registry = await odc.getRegistry();

setRegistry(data): Promise<void>

Write registry values. Merges with existing data (PATCH semantics).

await odc.setRegistry({
  auth: { token: 'new-token' },
  prefs: { language: 'en' },
});

clearRegistry(sections?): Promise<void>

Clear specific registry sections, or all sections if none specified.

await odc.clearRegistry(['cache', 'temp']);
await odc.clearRegistry();

App UI

getAppUi(fields?): Promise<string>

Get the current app UI tree as XML. Optionally filter to specific fields per component type.

const ui = await odc.getAppUi();
const ui = await odc.getAppUi({ Label: ['text', 'color'] });

File operations

pullFile(source): Promise<ArrayBuffer>

Download a file from the device.

const data = await odc.pullFile('tmp:/data.json');

pushFile(destination, data): Promise<void>

Upload a file to the device.

const data = new TextEncoder().encode('{"mock": true}');
await odc.pushFile('tmp:/config.json', data);

listFiles(path?): Promise<string>

List files on the device.

const files = await odc.listFiles('tmp:/');

Error handling

All errors are typed for easy catch filtering:

import { OdcHttpError, OdcTimeoutError } from '@danecodes/roku-odc';

try {
  await odc.getField('missing', 'text');
} catch (err) {
  if (err instanceof OdcTimeoutError) {
    console.log(`Timed out after ${err.timeoutMs}ms`);
  } else if (err instanceof OdcHttpError) {
    console.log(`${err.method} ${err.path} → ${err.status} ${err.statusText}`);
  }
}

Requirements

  • Node.js >= 22
  • A Roku device on the same network
  • The channel must be dev-sideloaded with the ODC component injected (via inject() or injectDir())
  • Network access to the device on port 8061

License

MIT