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

kopytko-roku-device

v1.1.0

Published

Roku device communication toolkit: SSDP discovery, ECP client, SceneGraph debug console, BrightScript debug protocol, diagnostics collectors, and Perfetto tracing.

Readme

kopytko-roku-device

Roku device communication toolkit. Everything needed to discover, control, profile, and debug a Roku device — in one standalone, VS Code-free npm package. Used by the vscode-kopytko extension for all of its device features.

The package is deliberately Kopytko-ecosystem-unaware — it speaks only Roku device protocols (no CLI spawning, no .kopytkorc knowledge), so Kopytko packages themselves can depend on it without circularity.

npm install kopytko-roku-device

Also ships a kopytko-roku terminal CLI covering ECP + web-admin operations — see CLI below.

Requires Node.js >= 24. No runtime dependencies beyond ws (Perfetto streaming).

What's inside

| Subsystem | Port | Exports | |---|---|---| | SSDP discovery | UDP 1900 | SsdpClient — M-SEARCH scans + NOTIFY monitoring, per-IP debounce | | ECP (External Control Protocol) | HTTP 8060, 80 | EcpClient, parseRegistryXml, enablePerfettoTracing, triggerHeapSnapshot | | Device discovery orchestration | — | DeviceManager + DeviceStorage / NetworkWatcher injection interfaces | | SceneGraph debug console | TCP 8080 | DebugConsoleClient — idle-framed request/response, auto-reconnect | | Diagnostics parsers + collectors | 8060/8080 | parseChanperf, parseSgNodesCounts, parseR2d2Bitmaps, …, PollingCollector and 10 concrete collectors | | BrightScript remote debug protocol | TCP 8081 | ProtocolClient, IOClient, DebugCommands, BinaryReader/BinaryWriter, protocol constants/types | | Perfetto trace streaming | WS 8060 | PerfettoWebSocketClient — quiet-window framing over ws://…/perfetto-session | | Developer web-admin automation | HTTP 80 | InstallerClient — install/delete/rekey/package/screenshot/profiling-data/update/reboot |

Quick examples

Discover devices

import { SsdpClient, EcpClient, DeviceManager, DeviceStorage } from 'kopytko-roku-device';

const manager = new DeviceManager(new SsdpClient(), new EcpClient(), myStorage, myNetworkWatcher);
manager.on('devices-changed', () => console.log(manager.getDevices()));
await manager.initialize();

DeviceStorage (persistence) and NetworkWatcher (network-change/wake signals) are host-provided interfaces — the VS Code extension backs them with Memento global state and a window-focus-aware poller; a CLI could use a JSON file and a no-op watcher.

Query a device over ECP

import { EcpClient } from 'kopytko-roku-device';

const ecp = new EcpClient();
const info = await ecp.queryDeviceInfo('192.168.1.20');
const apps = await ecp.queryApps('192.168.1.20');
const perf = await ecp.queryChanperf('192.168.1.20'); // per-channel CPU/memory XML

Deep-link into a channel

import { EcpClient } from 'kopytko-roku-device';

const ecp = new EcpClient();

// Relaunch the channel with deep-link params (POST /launch/{appId}?…)
await ecp.launchApp('192.168.1.20', '12', { contentId: 'movie-123', mediaType: 'movie' });

// Send params to the channel already running in the foreground as an
// roInput event, without relaunching (POST /input?…)
await ecp.sendInput('192.168.1.20', { contentId: 'movie-123', mediaType: 'movie' });

// Fetch a channel's icon (GET /query/icon/{appId}) — raw bytes + content type
const { data, contentType } = await ecp.queryAppIcon('192.168.1.20', '12');

Keys and values are encodeURIComponent-encoded (helper: buildEcpQueryString). Non-2xx responses throw with the device's status and response body — a 403 usually means ECP is restricted on-device ("Control by mobile apps"), a 404 that the channel is not installed. sendInput always targets the foreground channel; there is no app id parameter in the ECP /input endpoint.

Simulate the remote control

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: keydown … keyup (always pair them — the device keeps the key
// held until the matching keyup arrives)
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, encoded as Lit_ keys ('€' → 'Lit_%E2%82%AC')
await ecp.sendText('192.168.1.20', 'my search term');

// Lower-level: encode text yourself
textToLitKeys('r€'); // → ['Lit_r', 'Lit_%E2%82%AC']

EcpKeys lists every named key (Home, Select, D-pad, media keys, and the Roku-TV-only volume/power/channel/input keys). isValidEcpKey() accepts named keys and Lit_-prefixed literals.

ECP method reference

Every EcpClient method, mapped to its endpoint (default port 8060 unless noted):

| Method | Endpoint | Purpose | |---|---|---| | queryDeviceInfo | GET /query/device-info | Device model/serial/software-version/etc. | | checkDeviceAlive | GET /query/device-info | Reachability check (swallows errors) | | queryApps | GET /query/apps | Installed channel list | | queryActiveApp | GET /query/active-app | Foreground channel | | queryMediaPlayer | GET /query/media-player | Playback state/position/duration | | queryAppIcon | GET /query/icon/{appId} | Channel icon bytes | | queryRegistry | GET /query/registry/{channelId} | Channel registry dump (dev mode) | | queryChanperf | GET /query/chanperf | Per-channel CPU/memory (raw XML) | | querySgNodes | GET /query/sgnodes/{all\|roots} | SceneGraph node tree, all nodes or only un-parented roots (raw XML) | | querySgNodesById | GET /query/sgnodes/nodes?node-id= | SceneGraph nodes matching one node id (raw XML) | | queryR2d2Bitmaps | GET /query/r2d2-bitmaps | GPU texture/bitmap memory (raw XML, dev mode) | | queryGraphicsFrameRate | GET /query/graphics-frame-rate | Recent rendered FPS, Roku OS 12.0+ (raw XML, dev mode) | | queryAppObjectCounts | GET /query/app-object-counts/{appId} | BrightScript object counts/memory (raw XML) | | queryAppState | GET /query/app-state/{appId} | 'active'\|'background'\|'inactive'\|'unknown' | | queryTvChannels | GET /query/tv-channels | TV tuner channel list — Roku TV only | | queryTvActiveChannel | GET /query/tv-active-channel | Current TV channel/signal — Roku TV only | | queryRendezvousEvents | GET /query/sgrendezvous | Drains queued rendezvous events | | enableRendezvousTracking / disableRendezvousTracking | POST /sgrendezvous/track / /untrack | Toggle rendezvous logging | | queryFwBeacons | GET /query/fwbeacons | Drains queued framework-beacon events | | enableFwBeaconTracking / disableFwBeaconTracking | POST /fwbeacons/track/{appId} / /untrack/{appId} | Toggle beacon tracking for a channel | | launchApp | POST /launch/{appId} | Launch/relaunch with optional deep-link params | | sendInput | POST /input | Deep-link params to the foreground channel (no relaunch) | | keypress / keydown / keyup | POST /keypress\|keydown\|keyup/{key} | Remote-control key simulation | | sendText | POST /keypress/Lit_… (sequential) | Types a string via Lit_ keys | | exitApp | POST /exit-app/{appId}[/true] | Suspend/terminate a running channel | | validatePassword | Digest auth on port 80 | Validates the developer password | | enablePerfettoTracing (free fn) | POST /perfetto/enable/{channelId} | Starts Perfetto tracing, Roku OS 15.2+ | | triggerHeapSnapshot (free fn) | POST /perfetto/heapgraph/trigger/{channelId} | Captures a heap snapshot |

Deliberately not implemented — confirmed absent from or removed from the official ECP docs: /search/* (removed Roku OS 12.0+), /query/screensaver, /query/textedit-state, /query/audio-device (not documented anywhere on the current official page), and ECP-2 WebSocket session/subscription endpoints. See findings/roku-device-api.md in the extension repo for the full rationale.

// New in this release: TV queries, exit-app, sgnodes scope/by-id, r2d2-bitmaps, graphics-frame-rate
await ecp.exitApp('192.168.1.20', 'dev');            // suspend (or terminate if unsupported)
await ecp.exitApp('192.168.1.20', 'dev', true);       // force-terminate, bypassing Instant Resume

const roots = await ecp.querySgNodes('192.168.1.20', 8060, 'roots'); // un-parented nodes only
const byId = await ecp.querySgNodesById('192.168.1.20', '42');

const textures = await ecp.queryR2d2Bitmaps('192.168.1.20');    // GPU texture memory, raw XML
const fps = await ecp.queryGraphicsFrameRate('192.168.1.20');   // raw XML, Roku OS 12.0+

// Roku TV models only
const channels = await ecp.queryTvChannels('192.168.1.20');
const tuned = await ecp.queryTvActiveChannel('192.168.1.20');

Query playback state

// Foreground channel (GET /query/active-app)
const active = await ecp.queryActiveApp('192.168.1.20'); // { id, name, … } | undefined

// Media player state (GET /query/media-player): state ('none'|'play'|'pause'|…),
// owning channel, stream format (codecs/DRM), position/duration
const player = await ecp.queryMediaPlayer('192.168.1.20');
if (player.state === 'play') console.log(player.format?.video, player.positionMs);

Poll runtime metrics

import { DebugConsoleClient, ChanperfCollector } from 'kopytko-roku-device';

const console8080 = new DebugConsoleClient('192.168.1.20');
const collector = new ChanperfCollector(console8080, 1000);
collector.on('sample', (s) => console.log(s)); // { type: 'mem-cpu', memKiB, cpuPct, … }
collector.start();

Collectors never throw into callers — a failed poll skips that interval and the collector self-heals when the device answers again. All 10 concrete collectors:

| Collector | Data source | Metric | |---|---|---| | ChanperfCollector | port 8080 console | Per-channel CPU/memory | | EcpChanperfCollector | ECP /query/chanperf | Per-channel CPU/memory (no telnet console needed) | | NodeCountsCollector | port 8080 console | SceneGraph node counts | | EcpNodeCountsCollector | ECP /query/sgnodes/all | SceneGraph node counts | | EcpObjectCountsCollector | ECP /query/app-object-counts | BrightScript object counts/memory | | TextureCollector | port 8080 console | GPU texture/bitmap memory | | SystemMemCollector | port 8080 console | System-wide free memory | | RendezvousCollector | ECP /query/sgrendezvous | Render-thread rendezvous events | | FwBeaconCollector | ECP /query/fwbeacons | App/media lifecycle beacon events | | AppStateCollector | ECP /query/app-state | Foreground/background/inactive polling |

Drive the BrightScript debugger

import { ProtocolClient, DebugCommands } from 'kopytko-roku-device';

const client = new ProtocolClient('192.168.1.20'); // port 8081
await client.connect();
const commands = new DebugCommands(client);
const threads = await commands.threads();

Web-admin automation (port 80)

import { InstallerClient } from 'kopytko-roku-device';

const installer = new InstallerClient();
const password = 'my-dev-password'; // username is always 'rokudev'

await installer.installChannel('192.168.1.20', password, './build/archive.zip');
await installer.packageChannel(
  '192.168.1.20', password, './build/archive.zip',
  'MyApp/1.0', 'my-signing-password', './out/signed.pkg',
);
await installer.takeScreenshot('192.168.1.20', password, './out/screenshot.jpg');
await installer.checkForUpdate('192.168.1.20', password);

Drives the same Installer/Utilities/Packager/Update tabs a developer would use in a browser at http://<device-ip>/. Digest-auth handshake reuses the same parseDigestChallenge/buildDigestAuthHeader helpers as EcpClient.validatePassword.

CLI

kopytko-roku is a zero-dependency terminal client for everything EcpClient and InstallerClient can do — useful for scripting, CI smoke tests, or quick manual checks without opening the extension.

npm install -g kopytko-roku-device
kopytko-roku --help

# or without installing globally
npx kopytko-roku-device ecp device-info --host 192.168.1.20
kopytko-roku discover [--timeout <ms>] [--json]
kopytko-roku ecp <op> --host <ip> [--port <n>] [--json] [op flags...]
kopytko-roku installer <op> --host <ip> --password <pw> [op flags...]

Every ecp op maps 1:1 to an EcpClient method (device-info, apps, active-app, media-player, icon, launch, input, keypress/keydown/keyup, text, exit-app, tv-channels, tv-active-channel, registry, chanperf, sgnodes, app-object-counts, app-state, rendezvous-track/-untrack/-query, fwbeacons-track/-untrack/-query, graphics-frame-rate, r2d2-bitmaps, perfetto-enable, perfetto-trigger-heap-snapshot); every installer op maps to InstallerClient (screenshot, install, delete, rekey, package, update, reboot). Run kopytko-roku --help for the full flag reference, or see docs/roku-device-cli.md in the extension repo.

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 installer screenshot --host 192.168.1.20 --password secret --out shot.jpg

Config resolution (highest priority first): CLI flags → --config <path> (a JSON file with { "host", "password", "port" }) → ROKU_HOST/ROKU_PASSWORD environment variables. The CLI intentionally does not read .vscode/settings.json or any .kopytkorc — unlike kopytko-format/kopytko-lint, this package stays Kopytko-ecosystem- and editor-unaware even at the CLI layer.

Output: ops returning a device's raw XML body (chanperf, sgnodes, r2d2-bitmaps, graphics-frame-rate, registry, tv-channels, tv-active-channel) always print that XML as-is — --json is a deliberate no-op there, since JSON-escaping an XML document just produces an unreadable quoted blob. Everything else pretty-prints as JSON. See docs/roku-device-cli.md in the extension repo.

Device behaviour notes

  • GET /query/sgrendezvous and GET /query/fwbeacons drain the device-side event queue — never run two pollers against the same endpoint.
  • chanperf/sgnodes/the port-8080 console report nothing while the channel is backgrounded; this is a Roku OS limitation, not an error.
  • Port 8080 responses are idle-framed (> appears inside XML, so it cannot be a terminator).
  • The port-80 web admin's digest-auth POSTs (InstallerClient) send no body on the first, expected-401 request — the multipart payload is only attached on the authenticated retry, so a large file is never uploaded twice.
  • /plugin_install returns HTTP 200 for both success and failure — the real result is a type: "success" | "error" | "info" message embedded as JSON in the page's inline script, which InstallerClient parses and checks. Zip archives must use forward-slash path separators (PowerShell's Compress-Archive produces backslash-separated entries on Windows, which the device rejects as an "Install Failure" despite the 200 status). See findings/roku-device-api.md for the confirmed details, plus which of the 9 actions are live-verified vs. best-effort (rekey/package/update/reboot were not live-tested — see that file).
  • exitApp, queryTvChannels/queryTvActiveChannel, queryGraphicsFrameRate, queryR2d2Bitmaps, and querySgNodes's roots scope / querySgNodesById are all live-verified (Roku Ultra 4850X, 2026-07-06). The tv-* endpoints were only confirmed as an expected 404 on this non-TV hardware — their success response shape on an actual Roku TV remains unverified. exitApp (non-forced) suspends via Instant Resume rather than killing the app, confirmed via queryAppState reporting background afterward. See findings/roku-device-api.md for the exact response shapes captured, including the surprising discovery that sgnodes/all, /roots, and /nodes each wrap results in a different tag name (All_Nodes/Root_Nodes/Nodes_Nodes) — a naive shared parser will not work across all three.

License

MIT