@putdotio/rokit 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,4 @@
1
1
  <div align="center">
2
- <p>
3
- <img src="https://static.put.io/images/putio-boncuk.png" width="72">
4
- </p>
5
-
6
2
  <h1>rokit</h1>
7
3
 
8
4
  <p>A tiny CLI companion for Roku device harness work.</p>
@@ -85,7 +81,9 @@ await waitForSceneGraphAssertion(context, "expected player", (xml) => {
85
81
  rokit check
86
82
  rokit device-info
87
83
  rokit active-app
84
+ rokit media-player
88
85
  rokit wait-active <app-id> [--timeout-ms <ms>]
86
+ rokit wait-media-player <state> [--timeout-ms <ms>]
89
87
  rokit launch <app-id> [--param key=value]
90
88
  rokit press [--delay-ms <ms>] <key> [key...]
91
89
  rokit query <ecp-path>
@@ -111,8 +109,12 @@ and reports failures as `{ "status": "failed", "error": { "message": "..." } }`.
111
109
  is reachable.
112
110
  - `device-info` prints enhanced Roku device metadata as JSON.
113
111
  - `active-app` prints the foreground app.
112
+ - `media-player` prints parsed `/query/media-player` playback state, including
113
+ state, container, position, duration, and format metadata.
114
114
  - `wait-active` waits until the requested app is foregrounded and tolerates
115
115
  transient ECP read failures while polling.
116
+ - `wait-media-player` waits until `/query/media-player` reports a target state
117
+ such as `play`, `pause`, or `buffer`.
116
118
  - `launch` opens an app and waits until it is active. Use repeated `--param`
117
119
  values for deeplink parameters. Roku launch responses can race app startup, so
118
120
  launch accepts transient timeout/fetch failures and then verifies foreground
@@ -152,6 +154,7 @@ tokens, and app-specific media identifiers do not belong in git.
152
154
  - launch and deeplink parameters
153
155
  - remote keypresses
154
156
  - raw ECP queries
157
+ - parsed media-player state from `/query/media-player`
155
158
  - SceneGraph state queries and named-node assertions
156
159
  - SceneGraph attribute, numeric geometry, bounds, and translation readers
157
160
  - SceneGraph geometry assertions, status/failure readers, and custom assertion
package/dist/index.d.mts CHANGED
@@ -57,6 +57,24 @@ type DeviceSummary = {
57
57
  readonly model: string;
58
58
  readonly name: string;
59
59
  };
60
+ type MediaPlayerState = "buffer" | "close" | "error" | "none" | "open" | "pause" | "play" | "stop" | string;
61
+ type MediaPlayerInfo = {
62
+ readonly audio?: string;
63
+ readonly buffering?: {
64
+ readonly current?: number;
65
+ readonly max?: number;
66
+ readonly target?: number;
67
+ };
68
+ readonly captions?: string;
69
+ readonly container?: string;
70
+ readonly durationMs?: number;
71
+ readonly error: boolean;
72
+ readonly isLive?: boolean;
73
+ readonly positionMs?: number;
74
+ readonly state?: MediaPlayerState;
75
+ readonly video?: string;
76
+ readonly videoResolution?: string;
77
+ };
60
78
  type RetryOptions = {
61
79
  readonly attempts?: number;
62
80
  readonly retryDelayMs?: number;
@@ -73,6 +91,14 @@ declare const waitForActiveApp: (context: RokuContext, appId: string, timeoutMs?
73
91
  declare const launchApp: (context: RokuContext, appId: string, params?: ReadonlyMap<string, string>) => Promise<ActiveApp>;
74
92
  declare const pressKey: (context: RokuContext, key: string) => Promise<void>;
75
93
  declare const queryEcp: (context: RokuContext, path: string) => Promise<string>;
94
+ declare const queryMediaPlayerXml: (context: RokuContext) => Promise<string>;
95
+ declare const queryMediaPlayer: (context: RokuContext) => Promise<MediaPlayerInfo>;
96
+ declare const readMediaPlayerInfo: (xml: string) => MediaPlayerInfo;
97
+ declare const readMediaPlayerState: (xml: string) => MediaPlayerState | undefined;
98
+ declare const readMediaPlayerPositionMs: (xml: string) => number | undefined;
99
+ declare const readMediaPlayerContainer: (xml: string) => string | undefined;
100
+ declare const isActiveMediaPlayerState: (state: string | undefined) => boolean;
101
+ declare const waitForMediaPlayerState: (context: RokuContext, expectedState: MediaPlayerState, timeoutMs?: number) => Promise<MediaPlayerInfo>;
76
102
  declare const querySceneGraph: (context: RokuContext, options?: RetryOptions) => Promise<string>;
77
103
  declare const assertSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation) => Promise<void>;
78
104
  declare const waitForSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation, timeoutMs?: number) => Promise<void>;
@@ -85,4 +111,4 @@ declare const takeScreenshot: (context: RokuContext & {
85
111
  }, outputPath: string) => Promise<string>;
86
112
  declare const validateRemoteKey: (key: string) => void;
87
113
  //#endregion
88
- export { type ActiveApp, type DeviceSummary, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, type SceneGraphAssertion, type SceneGraphBounds, type SceneGraphPoint, type SceneGraphStatus, type WaitForSceneGraphAssertionOptions, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphAssertion, waitForSceneGraphNode };
114
+ export { type ActiveApp, type DeviceSummary, type MediaPlayerInfo, type MediaPlayerState, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, type SceneGraphAssertion, type SceneGraphBounds, type SceneGraphPoint, type SceneGraphStatus, type WaitForSceneGraphAssertionOptions, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isActiveMediaPlayerState, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerXml, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as readActiveApp, C as readNamedNodeAttribute, D as readNamedNodeTranslation, E as readNamedNodeNumber, M as readXmlTag, O as readSceneGraphFailure, S as parseSceneGraphNumberList, T as readNamedNodeBounds, _ as assertNamedNodeState, a as launchApp, b as assertSceneGraphNumberNear, c as queryEcp, d as validateRemoteKey, f as waitForActiveApp, g as assertNamedNodeSize, h as assertNamedNode, i as installPackage, j as readXmlAttribute, k as readSceneGraphStatus, l as querySceneGraph, m as waitForSceneGraphNode, n as checkDevice, o as pressKey, p as waitForSceneGraphAssertion, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot, v as assertNamedNodeText, w as readNamedNodeAttributes, x as isNamedNodeVisible, y as assertNamedNodeTranslation } from "./roku-CNLr4tbj.mjs";
2
- export { assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphAssertion, waitForSceneGraphNode };
1
+ import { A as parseSceneGraphNumberList, B as readXmlTag, C as assertNamedNode, D as assertNamedNodeTranslation, E as assertNamedNodeText, F as readNamedNodeTranslation, I as readSceneGraphFailure, L as readSceneGraphStatus, M as readNamedNodeAttributes, N as readNamedNodeBounds, O as assertSceneGraphNumberNear, P as readNamedNodeNumber, R as readActiveApp, S as waitForSceneGraphNode, T as assertNamedNodeState, _ as takeScreenshot, a as isActiveMediaPlayerState, b as waitForMediaPlayerState, c as queryActiveApp, d as queryMediaPlayerXml, f as querySceneGraph, g as readMediaPlayerState, h as readMediaPlayerPositionMs, i as installPackage, j as readNamedNodeAttribute, k as isNamedNodeVisible, l as queryEcp, m as readMediaPlayerInfo, n as checkDevice, o as launchApp, p as readMediaPlayerContainer, r as getDeviceInfo, s as pressKey, t as assertSceneGraphNode, u as queryMediaPlayer, v as validateRemoteKey, w as assertNamedNodeSize, x as waitForSceneGraphAssertion, y as waitForActiveApp, z as readXmlAttribute } from "./roku-CXHAzYR-.mjs";
2
+ export { assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isActiveMediaPlayerState, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerXml, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
package/dist/rokit.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as launchApp, c as queryEcp, f as waitForActiveApp, i as installPackage, l as querySceneGraph, m as waitForSceneGraphNode, n as checkDevice, o as pressKey, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot } from "./roku-CNLr4tbj.mjs";
2
+ import { S as waitForSceneGraphNode, _ as takeScreenshot, b as waitForMediaPlayerState, c as queryActiveApp, f as querySceneGraph, i as installPackage, l as queryEcp, n as checkDevice, o as launchApp, r as getDeviceInfo, s as pressKey, t as assertSceneGraphNode, u as queryMediaPlayer, y as waitForActiveApp } from "./roku-CXHAzYR-.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync } from "node:fs";
@@ -98,6 +98,24 @@ const runCommand = async (context, command) => {
98
98
  status: "ok"
99
99
  };
100
100
  }
101
+ if (command.name === "media-player") {
102
+ const mediaPlayer = await queryMediaPlayer(context);
103
+ return {
104
+ command: command.name,
105
+ data: mediaPlayer,
106
+ message: formatMediaPlayerMessage(mediaPlayer),
107
+ status: "ok"
108
+ };
109
+ }
110
+ if (command.name === "wait-media-player") {
111
+ const mediaPlayer = await waitForMediaPlayerState(context, command.state, command.timeoutMs);
112
+ return {
113
+ command: command.name,
114
+ data: mediaPlayer,
115
+ message: formatMediaPlayerMessage(mediaPlayer),
116
+ status: "ok"
117
+ };
118
+ }
101
119
  if (command.name === "wait-active") {
102
120
  const app = await waitForActiveApp(context, command.appId, command.timeoutMs);
103
121
  return {
@@ -254,6 +272,16 @@ const parseCommand = (argv) => {
254
272
  if (name === "check") return { name };
255
273
  if (name === "device-info") return { name };
256
274
  if (name === "active-app") return { name };
275
+ if (name === "media-player") return { name };
276
+ if (name === "wait-media-player") {
277
+ const state = args[0];
278
+ if (!state) fail("usage: rokit wait-media-player <state> [--timeout-ms <ms>]");
279
+ return {
280
+ name,
281
+ state,
282
+ timeoutMs: parseTimeoutOption(args.slice(1), "rokit wait-media-player <state>")
283
+ };
284
+ }
257
285
  if (name === "wait-active") {
258
286
  const appId = args[0];
259
287
  if (!appId) fail("usage: rokit wait-active <app-id> [--timeout-ms <ms>]");
@@ -382,6 +410,15 @@ const formatNodeData = ({ expectation, nodeName, timeoutMs }) => ({
382
410
  nodeName,
383
411
  timeoutMs
384
412
  });
413
+ const formatMediaPlayerMessage = (mediaPlayer) => {
414
+ return `media-player: ${[
415
+ `state=${mediaPlayer.state ?? "unknown"}`,
416
+ `container=${mediaPlayer.container ?? "unknown"}`,
417
+ `position=${formatMaybeMs(mediaPlayer.positionMs)}`,
418
+ `duration=${formatMaybeMs(mediaPlayer.durationMs)}`
419
+ ].join(" ")}`;
420
+ };
421
+ const formatMaybeMs = (value) => value === void 0 ? "unknown" : `${value}ms`;
385
422
  const sleep = (ms) => new Promise((resolve) => {
386
423
  setTimeout(resolve, ms);
387
424
  });
@@ -411,7 +448,9 @@ usage:
411
448
  rokit check
412
449
  rokit device-info
413
450
  rokit active-app
451
+ rokit media-player
414
452
  rokit wait-active <app-id> [--timeout-ms <ms>]
453
+ rokit wait-media-player <state> [--timeout-ms <ms>]
415
454
  rokit launch <app-id> [--param key=value]
416
455
  rokit press [--delay-ms <ms>] <key> [key...]
417
456
  rokit query <ecp-path>
@@ -189,6 +189,52 @@ const pressKey = async (context, key) => {
189
189
  await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
190
190
  };
191
191
  const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
192
+ const queryMediaPlayerXml = async (context) => await queryEcp(context, "/query/media-player");
193
+ const queryMediaPlayer = async (context) => readMediaPlayerInfo(await queryMediaPlayerXml(context));
194
+ const readMediaPlayerInfo = (xml) => {
195
+ const playerAttributes = /<player(?:\s+([^>]*))?>/.exec(xml)?.[1] ?? "";
196
+ const formatAttributes = /<format(?:\s+([^>]*))?\/>/.exec(xml)?.[1] ?? "";
197
+ const bufferingAttributes = /<buffering(?:\s+([^>]*))?\/>/.exec(xml)?.[1];
198
+ return {
199
+ audio: readXmlAttribute(formatAttributes, "audio"),
200
+ buffering: bufferingAttributes === void 0 ? void 0 : {
201
+ current: readXmlNumberAttribute(bufferingAttributes, "current"),
202
+ max: readXmlNumberAttribute(bufferingAttributes, "max"),
203
+ target: readXmlNumberAttribute(bufferingAttributes, "target")
204
+ },
205
+ captions: readXmlAttribute(formatAttributes, "captions"),
206
+ container: readXmlAttribute(formatAttributes, "container"),
207
+ durationMs: readXmlNumberTag(xml, "duration"),
208
+ error: readXmlAttribute(playerAttributes, "error") === "true",
209
+ isLive: readXmlBooleanTag(xml, "is_live"),
210
+ positionMs: readXmlNumberTag(xml, "position"),
211
+ state: readXmlAttribute(playerAttributes, "state"),
212
+ video: readXmlAttribute(formatAttributes, "video"),
213
+ videoResolution: readXmlAttribute(formatAttributes, "video_res")
214
+ };
215
+ };
216
+ const readMediaPlayerState = (xml) => readMediaPlayerInfo(xml).state;
217
+ const readMediaPlayerPositionMs = (xml) => readMediaPlayerInfo(xml).positionMs;
218
+ const readMediaPlayerContainer = (xml) => readMediaPlayerInfo(xml).container;
219
+ const isActiveMediaPlayerState = (state) => state === "buffer" || state === "pause" || state === "play";
220
+ const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4) => {
221
+ const start = Date.now();
222
+ let lastState;
223
+ let lastError;
224
+ while (Date.now() - start < timeoutMs) {
225
+ try {
226
+ const mediaPlayer = await queryMediaPlayer(context);
227
+ lastState = mediaPlayer.state;
228
+ lastError = void 0;
229
+ if (mediaPlayer.state === expectedState) return mediaPlayer;
230
+ } catch (error) {
231
+ lastError = formatErrorMessage(error);
232
+ }
233
+ await sleep(500);
234
+ }
235
+ const suffix = lastError ? `; last ECP error: ${lastError}` : "";
236
+ throw new Error(`expected media-player state ${expectedState}, got ${lastState ?? "unknown"}${suffix}`);
237
+ };
192
238
  const querySceneGraph = async (context, options = {}) => {
193
239
  const attempts = options.attempts ?? 1;
194
240
  const retryDelayMs = options.retryDelayMs ?? 500;
@@ -291,9 +337,27 @@ const postLaunchMaybeAccepted = async (context, url) => {
291
337
  }
292
338
  };
293
339
  const ecpUrl = (context, path) => new URL(path, `http://${context.target}:${ecpPort}`);
340
+ const readXmlNumberTag = (xml, tag) => {
341
+ const rawValue = readXmlTag(xml, tag);
342
+ if (rawValue === void 0) return;
343
+ const match = /-?\d+(?:\.\d+)?/.exec(rawValue);
344
+ if (!match) return;
345
+ return Number(match[0]);
346
+ };
347
+ const readXmlBooleanTag = (xml, tag) => {
348
+ const value = readXmlTag(xml, tag);
349
+ if (value === "true") return true;
350
+ if (value === "false") return false;
351
+ };
352
+ const readXmlNumberAttribute = (attributes, name) => {
353
+ const value = readXmlAttribute(attributes, name);
354
+ if (value === void 0) return;
355
+ const parsed = Number(value);
356
+ return Number.isFinite(parsed) ? parsed : void 0;
357
+ };
294
358
  const sleep = (ms) => new Promise((resolve) => {
295
359
  setTimeout(resolve, ms);
296
360
  });
297
361
  const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
298
362
  //#endregion
299
- export { readActiveApp as A, readNamedNodeAttribute as C, readNamedNodeTranslation as D, readNamedNodeNumber as E, readXmlTag as M, readSceneGraphFailure as O, parseSceneGraphNumberList as S, readNamedNodeBounds as T, assertNamedNodeState as _, launchApp as a, assertSceneGraphNumberNear as b, queryEcp as c, validateRemoteKey as d, waitForActiveApp as f, assertNamedNodeSize as g, assertNamedNode as h, installPackage as i, readXmlAttribute as j, readSceneGraphStatus as k, querySceneGraph as l, waitForSceneGraphNode as m, checkDevice as n, pressKey as o, waitForSceneGraphAssertion as p, getDeviceInfo as r, queryActiveApp as s, assertSceneGraphNode as t, takeScreenshot as u, assertNamedNodeText as v, readNamedNodeAttributes as w, isNamedNodeVisible as x, assertNamedNodeTranslation as y };
363
+ export { parseSceneGraphNumberList as A, readXmlTag as B, assertNamedNode as C, assertNamedNodeTranslation as D, assertNamedNodeText as E, readNamedNodeTranslation as F, readSceneGraphFailure as I, readSceneGraphStatus as L, readNamedNodeAttributes as M, readNamedNodeBounds as N, assertSceneGraphNumberNear as O, readNamedNodeNumber as P, readActiveApp as R, waitForSceneGraphNode as S, assertNamedNodeState as T, takeScreenshot as _, isActiveMediaPlayerState as a, waitForMediaPlayerState as b, queryActiveApp as c, queryMediaPlayerXml as d, querySceneGraph as f, readMediaPlayerState as g, readMediaPlayerPositionMs as h, installPackage as i, readNamedNodeAttribute as j, isNamedNodeVisible as k, queryEcp as l, readMediaPlayerInfo as m, checkDevice as n, launchApp as o, readMediaPlayerContainer as p, getDeviceInfo as r, pressKey as s, assertSceneGraphNode as t, queryMediaPlayer as u, validateRemoteKey as v, assertNamedNodeSize as w, waitForSceneGraphAssertion as x, waitForActiveApp as y, readXmlAttribute as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@putdotio/rokit",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "A tiny CLI companion for Roku device harness work.",
5
5
  "keywords": [
6
6
  "cli",