@roku-sdk/ai 0.0.0-stage → 2.19.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/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +183 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +210 -0
- package/skills/focus-management/references/focus-recipes.md +290 -0
- package/skills/fonts-and-glyphs/SKILL.md +231 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- package/src/snippets.ts +212 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Ads integration flows: CSAI, SSAI and CSAS
|
|
2
|
+
|
|
3
|
+
One complete wired module per mode. All three obey the same protocol — wait for `"ready"`, set the
|
|
4
|
+
data fields then `command`, read the matching callback — and differ only in which commands they
|
|
5
|
+
issue and which nodes they need.
|
|
6
|
+
|
|
7
|
+
## What differs between the three
|
|
8
|
+
|
|
9
|
+
| | CSAI | SSAI | CSAS |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| Command sequence | `getAds` → `showAds`, per break | `getAds` → `stitchedAdsInit` | `getAds` → `constructStitchedStream` → `renderStitchedStream` |
|
|
12
|
+
| `adView` | required | not used | required |
|
|
13
|
+
| `contentView` | required | required | not used |
|
|
14
|
+
| `contentMetadata` | not used | not used | required |
|
|
15
|
+
| Who owns the `Video` | the app | the app | RAF — the app renders no `Video` at all |
|
|
16
|
+
| Result callbacks | `onGetAdsResult`, `onShowAdsResult` | `onGetAdsResult`, `onStitchedAdResult` | `onGetAdsResult`, `onConstructStitchedStreamResult`, `onRenderStitchedStreamResult` |
|
|
17
|
+
|
|
18
|
+
The `Video`-ownership row is the one that surprises people. CSAS hands RAF a single stream that
|
|
19
|
+
already has the ads spliced in, and RAF plays it; there is no content `Video` in the tree and no
|
|
20
|
+
`contentView` to give, so nothing in a CSAS module reacts to playback position.
|
|
21
|
+
|
|
22
|
+
## CSAI: pause the content, play the break, resume
|
|
23
|
+
|
|
24
|
+
The break's resume point has to be captured **before** the content is stopped, because stopping it
|
|
25
|
+
is what discards the position. `position` is milliseconds and `ContentNode`'s `playStart` is
|
|
26
|
+
seconds. `-1` is the "not started" sentinel: 0 ms is a real cue point, so 0 cannot be it.
|
|
27
|
+
|
|
28
|
+
Mid-roll arrives as another `onGetAdsResult`, from a `getAds` the task fired itself. That is why
|
|
29
|
+
the same handler serves pre-roll and every mid-roll, and why the empty-pods branch is guarded on
|
|
30
|
+
`position() === -1` — an empty mid-roll result means "no fill for this break", and restarting
|
|
31
|
+
content on it would fight the playback already running.
|
|
32
|
+
|
|
33
|
+
```tsx rsg-skill=complete
|
|
34
|
+
import type { AdsCommand, AdsState, EventHandler, EventHandlerAdPods, ShowAdsResult } from "@roku-sdk/ads";
|
|
35
|
+
import { Ads } from "@roku-sdk/ads";
|
|
36
|
+
import { Component, ContentNode, Group, MediaControl, Rectangle, Video } from "@roku-sdk/components";
|
|
37
|
+
import { DomNode } from "@roku-sdk/interfaces";
|
|
38
|
+
import { createSignal } from "@roku-sdk/solid";
|
|
39
|
+
|
|
40
|
+
const AD_TAG = "https://example.com/tag.vmap";
|
|
41
|
+
const CONTENT_URL = "https://example.com/content.m3u8";
|
|
42
|
+
|
|
43
|
+
export const CsaiPlayer: Component = () => {
|
|
44
|
+
const [command, setCommand] = createSignal<AdsCommand>("none");
|
|
45
|
+
const [adUrl, setAdUrl] = createSignal("");
|
|
46
|
+
const [adView, setAdView] = createSignal<DomNode | undefined>();
|
|
47
|
+
const [contentView, setContentView] = createSignal<DomNode | undefined>();
|
|
48
|
+
const [control, setControl] = createSignal<MediaControl>("none");
|
|
49
|
+
const [position, setPosition] = createSignal(-1);
|
|
50
|
+
const [playStart, setPlayStart] = createSignal(0);
|
|
51
|
+
|
|
52
|
+
const handleAdsState = (state: AdsState) => {
|
|
53
|
+
if (state !== "ready") return;
|
|
54
|
+
setAdUrl(AD_TAG);
|
|
55
|
+
setCommand("getAds");
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleGetAdsResult: EventHandlerAdPods = pods => {
|
|
59
|
+
if (pods.length > 0) {
|
|
60
|
+
setPlayStart(position() === -1 ? 0 : position() / 1000);
|
|
61
|
+
setControl("stop");
|
|
62
|
+
setCommand("showAds");
|
|
63
|
+
} else if (position() === -1) {
|
|
64
|
+
setControl("play");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleShowAdsResult: EventHandler<ShowAdsResult> = result => {
|
|
69
|
+
if (result === "none") return;
|
|
70
|
+
if (result === "completed") {
|
|
71
|
+
setControl("play");
|
|
72
|
+
setCommand("none");
|
|
73
|
+
} else {
|
|
74
|
+
setControl("stop");
|
|
75
|
+
setCommand("stop");
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<group>
|
|
81
|
+
<Video
|
|
82
|
+
width={1920}
|
|
83
|
+
height={1080}
|
|
84
|
+
content={<contentNode url={CONTENT_URL} playStart={playStart()} />}
|
|
85
|
+
control={control()}
|
|
86
|
+
notificationInterval={1.0}
|
|
87
|
+
onPosition={value => setPosition(value)}
|
|
88
|
+
ref={element => setContentView(element)}
|
|
89
|
+
gainFocus={control() === "play"}
|
|
90
|
+
/>
|
|
91
|
+
<rectangle
|
|
92
|
+
width={1920}
|
|
93
|
+
height={1080}
|
|
94
|
+
color="0x000000FF"
|
|
95
|
+
visible={control() !== "play"}
|
|
96
|
+
ref={element => setAdView(element)}
|
|
97
|
+
/>
|
|
98
|
+
<Ads
|
|
99
|
+
adUrl={adUrl()}
|
|
100
|
+
adView={adView()}
|
|
101
|
+
contentView={contentView()}
|
|
102
|
+
command={command()}
|
|
103
|
+
onAdsState={handleAdsState}
|
|
104
|
+
onGetAdsResult={handleGetAdsResult}
|
|
105
|
+
onShowAdsResult={handleShowAdsResult}
|
|
106
|
+
/>
|
|
107
|
+
</group>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`gainFocus={control() === "play"}` is doing more than it looks. An ad break takes focus and does
|
|
113
|
+
not give it back, so without a re-assertion on resume the app plays content that answers no remote
|
|
114
|
+
keys. Tying it to the resume condition is the cheapest correct place for it.
|
|
115
|
+
|
|
116
|
+
## SSAI: the stream already contains the ads
|
|
117
|
+
|
|
118
|
+
The app plays one stream and asks RAF what is on screen. `getAds` here exists only to give
|
|
119
|
+
`stitchedAdsInit` its pods — nothing is rendered from the result.
|
|
120
|
+
|
|
121
|
+
There is a **race worth respecting**: `stitchedAdsInit` has to reach the task before position
|
|
122
|
+
events start arriving, because the task is still in CSAI mode until it lands, and a position event
|
|
123
|
+
that arrives first is treated as a CSAI cue point. Start playback from
|
|
124
|
+
`onStitchedAdsInitResult` rather than from `onGetAdsResult`, and the race is answered by the
|
|
125
|
+
protocol instead of by a timing margin. Only `"completed"` may start playback: an `"error"` means
|
|
126
|
+
the mode never changed, so playing anyway puts position events back on the CSAI cue-point path
|
|
127
|
+
against a stream that was never initialised.
|
|
128
|
+
|
|
129
|
+
`onStitchedAdResult` receives RAF's own associative array, so its keys are lowercase:
|
|
130
|
+
`evthandled`, not `evtHandled`. That is the general rule for anything RAF returns.
|
|
131
|
+
|
|
132
|
+
The module below needs **2.15.0 or newer**: that is the release `onStitchedAdsInitResult` and its
|
|
133
|
+
`StitchedAdsInitResult` type arrived in. The `stitchedAdsInit` command itself is older, but nothing
|
|
134
|
+
acknowledged it before then, so on 2.14.0 the only place to start playback from is
|
|
135
|
+
`onGetAdsResult` — which is the race above, answerable there by a timing margin and nothing better.
|
|
136
|
+
|
|
137
|
+
```tsx rsg-skill=complete
|
|
138
|
+
import type {
|
|
139
|
+
AdsCommand,
|
|
140
|
+
AdsState,
|
|
141
|
+
EventHandler,
|
|
142
|
+
EventHandlerAdPods,
|
|
143
|
+
StitchedAdsInitResult
|
|
144
|
+
} from "@roku-sdk/ads";
|
|
145
|
+
import { Ads } from "@roku-sdk/ads";
|
|
146
|
+
import { Component, ContentNode, Group, MediaControl, Video } from "@roku-sdk/components";
|
|
147
|
+
import { DomNode } from "@roku-sdk/interfaces";
|
|
148
|
+
import { createSignal } from "@roku-sdk/solid";
|
|
149
|
+
|
|
150
|
+
const AD_TAG = "https://example.com/tag.vmap";
|
|
151
|
+
const STITCHED_URL = "https://example.com/stitched.m3u8";
|
|
152
|
+
|
|
153
|
+
export const SsaiPlayer: Component = () => {
|
|
154
|
+
const [command, setCommand] = createSignal<AdsCommand>("none");
|
|
155
|
+
const [adUrl, setAdUrl] = createSignal("");
|
|
156
|
+
const [contentView, setContentView] = createSignal<DomNode | undefined>();
|
|
157
|
+
const [control, setControl] = createSignal<MediaControl>("none");
|
|
158
|
+
const [adPlaying, setAdPlaying] = createSignal(false);
|
|
159
|
+
|
|
160
|
+
const handleAdsState = (state: AdsState) => {
|
|
161
|
+
if (state !== "ready") return;
|
|
162
|
+
setAdUrl(AD_TAG);
|
|
163
|
+
setCommand("getAds");
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleGetAdsResult: EventHandlerAdPods = pods => {
|
|
167
|
+
if (pods.length === 0) {
|
|
168
|
+
setControl("play");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
setCommand("stitchedAdsInit");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Playback starts here, once the task confirms it is in SSAI mode.
|
|
175
|
+
const handleStitchedAdsInitResult: EventHandler<StitchedAdsInitResult> = result => {
|
|
176
|
+
if (result === "none") return;
|
|
177
|
+
if (result === "error") {
|
|
178
|
+
// Still in CSAI: position events would drive mid-roll against a stream never
|
|
179
|
+
// set up for it, so this is terminal rather than a warning to play past.
|
|
180
|
+
setCommand("stop");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
setControl("play");
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleStitchedAdResult: EventHandler<Record<string, unknown>> = result => {
|
|
187
|
+
setAdPlaying(result?.evthandled === true);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<group>
|
|
192
|
+
<Video
|
|
193
|
+
width={1920}
|
|
194
|
+
height={1080}
|
|
195
|
+
content={<contentNode url={STITCHED_URL} />}
|
|
196
|
+
control={control()}
|
|
197
|
+
notificationInterval={1.0}
|
|
198
|
+
ref={element => setContentView(element)}
|
|
199
|
+
gainFocus={control() === "play" && !adPlaying()}
|
|
200
|
+
/>
|
|
201
|
+
<Ads
|
|
202
|
+
adUrl={adUrl()}
|
|
203
|
+
contentView={contentView()}
|
|
204
|
+
command={command()}
|
|
205
|
+
onAdsState={handleAdsState}
|
|
206
|
+
onGetAdsResult={handleGetAdsResult}
|
|
207
|
+
onStitchedAdsInitResult={handleStitchedAdsInitResult}
|
|
208
|
+
onStitchedAdResult={handleStitchedAdResult}
|
|
209
|
+
/>
|
|
210
|
+
</group>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`stitchedAdsInit` on empty pods changes no mode and answers `"error"`. The `pods.length === 0`
|
|
216
|
+
branch above is what keeps that case from arising at all; the result is the backstop, and reading
|
|
217
|
+
it as anything other than terminal leaves the app playing a stitched stream through a task still
|
|
218
|
+
in CSAI.
|
|
219
|
+
|
|
220
|
+
## CSAS: build one stream, then render it
|
|
221
|
+
|
|
222
|
+
Two commands in sequence, each gated on the previous one's result. `contentMetadata` describes the
|
|
223
|
+
content to be stitched: a `ContentNode` prop bag, so `url` and `length` in seconds.
|
|
224
|
+
|
|
225
|
+
The task writes `"none"` to both result fields before it writes the real value, so guard on it
|
|
226
|
+
before branching. The shape to avoid is `if (result === "completed") { … } else { fail }`: it puts
|
|
227
|
+
the sentinel down the failure path, which here means abandoning the session before RAF has
|
|
228
|
+
answered.
|
|
229
|
+
|
|
230
|
+
A failed `constructStitchedStream` puts the task back in CSAI mode. Its `"error"` result is the
|
|
231
|
+
only notice of that, so treat it as terminal for the session rather than retrying
|
|
232
|
+
`renderStitchedStream`.
|
|
233
|
+
|
|
234
|
+
```tsx rsg-skill=complete
|
|
235
|
+
import type {
|
|
236
|
+
AdsCommand,
|
|
237
|
+
AdsState,
|
|
238
|
+
ConstructStitchedStreamResult,
|
|
239
|
+
EventHandler,
|
|
240
|
+
EventHandlerAdPods,
|
|
241
|
+
RenderStitchedStreamResult
|
|
242
|
+
} from "@roku-sdk/ads";
|
|
243
|
+
import { Ads } from "@roku-sdk/ads";
|
|
244
|
+
import { Component, Group, IContentNodeProps, Rectangle } from "@roku-sdk/components";
|
|
245
|
+
import { DomNode } from "@roku-sdk/interfaces";
|
|
246
|
+
import { createSignal } from "@roku-sdk/solid";
|
|
247
|
+
|
|
248
|
+
const AD_TAG = "https://example.com/tag.vmap";
|
|
249
|
+
|
|
250
|
+
export const CsasPlayer: Component<{ onFinished: () => void }> = props => {
|
|
251
|
+
const [command, setCommand] = createSignal<AdsCommand>("none");
|
|
252
|
+
const [adUrl, setAdUrl] = createSignal("");
|
|
253
|
+
const [adView, setAdView] = createSignal<DomNode | undefined>();
|
|
254
|
+
const [contentMetadata, setContentMetadata] = createSignal<IContentNodeProps>();
|
|
255
|
+
const [playing, setPlaying] = createSignal(false);
|
|
256
|
+
|
|
257
|
+
const handleAdsState = (state: AdsState) => {
|
|
258
|
+
if (state !== "ready") return;
|
|
259
|
+
setContentMetadata({ url: "https://example.com/content.m3u8", length: 600 });
|
|
260
|
+
// Measurement fields must be in place before the pods are fetched.
|
|
261
|
+
setAdUrl(AD_TAG);
|
|
262
|
+
setPlaying(true);
|
|
263
|
+
setCommand("getAds");
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const handleGetAdsResult: EventHandlerAdPods = () => {
|
|
267
|
+
setCommand("constructStitchedStream");
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const handleConstructResult: EventHandler<ConstructStitchedStreamResult> = result => {
|
|
271
|
+
if (result === "none") return;
|
|
272
|
+
if (result === "completed") {
|
|
273
|
+
setCommand("renderStitchedStream");
|
|
274
|
+
} else {
|
|
275
|
+
setPlaying(false);
|
|
276
|
+
props.onFinished();
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleRenderResult: EventHandler<RenderStitchedStreamResult> = result => {
|
|
281
|
+
if (result === "none") return;
|
|
282
|
+
// "user_exited" is a viewer back-out, not a failure.
|
|
283
|
+
setPlaying(false);
|
|
284
|
+
setCommand("none");
|
|
285
|
+
props.onFinished();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<group>
|
|
290
|
+
<rectangle
|
|
291
|
+
width={1920}
|
|
292
|
+
height={1080}
|
|
293
|
+
color="0x000000FF"
|
|
294
|
+
visible={playing()}
|
|
295
|
+
gainFocus={playing()}
|
|
296
|
+
ref={element => setAdView(element)}
|
|
297
|
+
/>
|
|
298
|
+
<Ads
|
|
299
|
+
adUrl={adUrl()}
|
|
300
|
+
adView={adView()}
|
|
301
|
+
contentMetadata={contentMetadata()}
|
|
302
|
+
command={command()}
|
|
303
|
+
enableAdMeasurements={true}
|
|
304
|
+
contentGenre="Entertainment"
|
|
305
|
+
contentId="csas-sample"
|
|
306
|
+
onAdsState={handleAdsState}
|
|
307
|
+
onGetAdsResult={handleGetAdsResult}
|
|
308
|
+
onConstructStitchedStreamResult={handleConstructResult}
|
|
309
|
+
onRenderStitchedStreamResult={handleRenderResult}
|
|
310
|
+
/>
|
|
311
|
+
</group>
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`onGetAdsResult` ignores its pods here on purpose: `constructStitchedStream` reads the ads the task
|
|
317
|
+
already holds, so there is nothing for the app to pass along and no reason to inspect them.
|
|
318
|
+
|
|
319
|
+
## Debug output
|
|
320
|
+
|
|
321
|
+
`debugOutput={true}` turns on RAF's own logging, which is the only view into what the library did
|
|
322
|
+
with a tag it rejected. It is verbose and belongs in development only — set it from the same
|
|
323
|
+
`"ready"` branch that sets `adUrl`, and leave it off in anything you ship.
|
|
324
|
+
|
|
325
|
+
## Stopping
|
|
326
|
+
|
|
327
|
+
`command = "stop"` ends the task's loop and reports `"stopped"` on `onAdsState`. Issue it while the
|
|
328
|
+
component is still mounted — from a Back key handler, or wherever the app decides to leave the
|
|
329
|
+
player. Doing it from `onCleanup` alone is best-effort: SolidJS may already have disposed the
|
|
330
|
+
effect that writes the field, and `onAdsState` is gone either way, so nothing confirms it. The task
|
|
331
|
+
does self-terminate when SceneGraph destroys the node, so the cleanup is a reasonable belt-and-
|
|
332
|
+
braces addition — just not the primary path.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# App Packaging
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`rk device package` and `rk dcl` talk to a Roku over its developer interface: they sign using the key
|
|
6
|
+
already on that box and manage what it holds, so they need a device. Signing is where the SDK
|
|
7
|
+
stops. Getting a version onto a channel, staged for review or live, is done by uploading the
|
|
8
|
+
signed `.pkg` in the Roku Developer Dashboard; the SDK has no command for it.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- producing a signed `.pkg` from an app that is already installed on a device
|
|
13
|
+
- a signing step in CI, where nothing can be typed at a prompt
|
|
14
|
+
- pointing a box at a different signing key, or checking which developer ID it now reports
|
|
15
|
+
- getting a version onto a channel, staged for review or published outright, and which part of
|
|
16
|
+
that the SDK does
|
|
17
|
+
- listing or clearing the dev channel and the Dynamic Component Libraries a device holds
|
|
18
|
+
- a partner shipping a library separately for the app to load from a URL at runtime, and where
|
|
19
|
+
setting that library up and referencing a node from it is written down
|
|
20
|
+
|
|
21
|
+
### When not to use
|
|
22
|
+
|
|
23
|
+
| Situation | Use instead | Because |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Installing `@roku-sdk` packages from npmjs.com | The `sdk-installation` skill | That is what arrives; this is what leaves |
|
|
26
|
+
| Cutting a release of the SDK's own packages | The repository's release process | This ships an app to a channel, not a library to a registry |
|
|
27
|
+
| Producing the build that then gets signed | The `cli-dev-loop` skill | Signing starts from an app that already exists on the box |
|
|
28
|
+
| Writing the code that builds a Dynamic Component Library, or loads one and pulls a node out of it | `documentation/core-concepts/dcl.md` | No other skill covers it, so the two Quick Reference rows below are the whole routing answer: `rk dcl` manages the ones a device already holds, and building one is an app whose `roku-config.ts` sets `app.type: "dcl"`, side-loaded like any other |
|
|
29
|
+
| What `roku-config.ts` declares about the app | The `project-setup` skill | Its `package` block is read here, but the file belongs there |
|
|
30
|
+
| Reaching the device at all, or which one a command lands on | The `device-management` skill | Everything here takes `--device` or `--host`; what those resolve to is decided there |
|
|
31
|
+
|
|
32
|
+
## Quick Reference
|
|
33
|
+
|
|
34
|
+
| Goal | Do |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Sign what is installed on the device | `rk device package sign` |
|
|
37
|
+
| Sign with nothing to type at | `ROKU_SIGNING_PASSWORD` in the environment |
|
|
38
|
+
| Point a box at another signing key | `rk device package rekey <pkg-path>` |
|
|
39
|
+
| See what is installed | `rk device package ls` |
|
|
40
|
+
| Remove the dev channel | `rk device package delete` |
|
|
41
|
+
| Re-image the installed dev channel as squashfs | `rk device package squashfs` |
|
|
42
|
+
| Get a version onto a channel, staged or live | Upload the signed `.pkg` in the Roku Developer Dashboard; no `rk` command does this |
|
|
43
|
+
| DCLs on a device | `rk dcl ls`, `rk dcl delete <filename>`, `rk dcl delete-all` |
|
|
44
|
+
| A partner's library the app loads at runtime | Build it as its own app with `app.type: "dcl"` in `roku-config.ts`; the host resolves it from the channel store by library name |
|
|
45
|
+
| Referencing a node out of that library | `getComponentLibraryFactory()` from `@roku-sdk/runtime/component-library`, then `factory.createLibrary(<name>)`, `library.load()` and `getDCLComponent(library, <node>)` from `@roku-sdk/rsg-ts` — full walkthrough in `documentation/core-concepts/dcl.md` |
|
|
46
|
+
|
|
47
|
+
## Instructions
|
|
48
|
+
|
|
49
|
+
1. **The signing password resolves from three places, in one order.** `--signing-password`
|
|
50
|
+
first, then the `ROKU_SIGNING_PASSWORD` environment variable, then a masked prompt. The
|
|
51
|
+
prompt needs a TTY on both stdin and stdout; without one the command fails immediately and
|
|
52
|
+
names the first two. A CI job therefore has exactly one tier it can use — the environment
|
|
53
|
+
variable. The flag is the wrong answer there, because the value lands in the process list and
|
|
54
|
+
in any log that echoes the command it was given.
|
|
55
|
+
|
|
56
|
+
2. **`rk device package sign` signs the app on the device, not a file on disk.** It requires a box with
|
|
57
|
+
a dev channel already installed and keyed; a box without one fails saying so, and the fix is
|
|
58
|
+
to sideload first. The signed file lands in `--out`, falling back to `package.outDir` from
|
|
59
|
+
`roku-config.ts`, and then to the working directory. Name `--out` in a job, so the step that
|
|
60
|
+
collects the artifact does not depend on where the command ran.
|
|
61
|
+
|
|
62
|
+
3. **`rekey` finds its package from the project when it is not given one.** The positional wins;
|
|
63
|
+
otherwise `package.signingKeyPackage` is used, and with neither the command explains both.
|
|
64
|
+
`--dev-id` falls back to `package.devId` the same way, and it is an assertion rather than an
|
|
65
|
+
input: the developer ID the box reports afterwards is checked against it.
|
|
66
|
+
|
|
67
|
+
4. **Putting a version on a channel happens in the Developer Dashboard.** The SDK signs; it does
|
|
68
|
+
not upload, stage, wait on review or promote. Whether a version goes live or stays with QA is
|
|
69
|
+
chosen there, against the signed `.pkg` this skill produced. Do not offer an `rk` command or
|
|
70
|
+
flag for any of those steps, and do not read a `publishing` block in `roku-config.ts` as
|
|
71
|
+
evidence of one: the block is accepted, and `rk` does not read it.
|
|
72
|
+
|
|
73
|
+
5. **Only a signed `.pkg` goes onto a channel.** What `rk build` writes to `dist-build` is
|
|
74
|
+
unsigned: side-load it, then sign it on the device with `rk device package sign`. That signed `.pkg`
|
|
75
|
+
is the file to upload.
|
|
76
|
+
|
|
77
|
+
6. **`device package` and `dcl` name a box.** Both take `--device` for a box in the store, or `--host`
|
|
78
|
+
with `--password` for one by address, and both work the same whether the command went through
|
|
79
|
+
a running server or `--local`. The two ways of naming a box are exclusive: `--host` together
|
|
80
|
+
with `--device` is an error, and `--host` alone still needs a password, because an address is
|
|
81
|
+
not in the store to carry one.
|
|
82
|
+
|
|
83
|
+
## Example
|
|
84
|
+
|
|
85
|
+
A CI job that signs without a prompt, keeping the password out of the command line and putting
|
|
86
|
+
the artifact in a fixed directory for the job to collect.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# ROKU_SIGNING_PASSWORD comes from the job's secret store, never from an argument.
|
|
90
|
+
# --out fixes where the .pkg lands, whatever directory the job runs in.
|
|
91
|
+
rk device package sign --device ci-box --out dist-build/package --json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The `.pkg` in `dist-build/package` is then uploaded in the Roku Developer Dashboard, where the
|
|
95
|
+
version is kept for review or published.
|
|
96
|
+
|
|
97
|
+
## Common Mistakes
|
|
98
|
+
|
|
99
|
+
| Symptom | Cause | Fix |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Signing fails in CI asking for a password, and works on a laptop | The prompt is the only tier left, and it needs a TTY | Set `ROKU_SIGNING_PASSWORD` in the job environment |
|
|
102
|
+
| The signed file is not where the next step looks | `--out` was omitted, so it fell back to `package.outDir` or the working directory | Pass `--out`, or set `package.outDir` to the directory that step reads |
|
|
103
|
+
| An unsigned build is offered for upload | The output of `rk build` was taken as the release artifact | Sideload it, then `rk device package sign`; only the signed `.pkg` uploads |
|
|
104
|
+
| An agent proposes an `rk` command to upload, stage or promote a version | The SDK has none | Sign with `rk device package sign`, then upload in the Roku Developer Dashboard |
|
|
105
|
+
| `rekey` reports the wrong developer ID | `--dev-id` or `package.devId` asserts the expected one and it did not match | Confirm which key the box should hold before rekeying it |
|
|
106
|
+
| Signing fails saying no dev channel is installed | Nothing is on the box to sign | Sideload the app first, then sign |
|
|
107
|
+
| `--host` is rejected, or reaches a box you did not mean | It cannot be combined with `--device`, and on its own it still needs `--password` | Give `--host` with `--password`, and drop `--device` — the store is not consulted for an address |
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# App Performance
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
An RSG SDK TypeScript app that stutters, drops frames or grows its memory is diagnosed by
|
|
6
|
+
measuring it on the device, and there are two measurements: a Perfetto trace, which says where
|
|
7
|
+
the time went, and a CDP heap read, which says how much memory is held. They do not coexist —
|
|
8
|
+
the heap read rebuilds the app in debug mode — so decide which question you are asking before
|
|
9
|
+
recording anything. This skill covers taking the measurement and classifying what it shows.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- an animation, scroll or transition that janks or drops frames on real hardware
|
|
14
|
+
- a key press whose effect arrives late, or a screen that takes too long to appear
|
|
15
|
+
- memory that climbs the longer the app is left running
|
|
16
|
+
- a recorded trace that has to be read: which thread, which slice, which interval blocks
|
|
17
|
+
- confirming that a change actually improved something, rather than assuming it did
|
|
18
|
+
|
|
19
|
+
### When not to use
|
|
20
|
+
|
|
21
|
+
| Situation | Use instead | Because |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| The everyday deploy, screenshot, drive and log loop | The `mcp-ai-dev-loop` skill | Those tools answer what the app did, not what it cost |
|
|
24
|
+
| The tracing tools refuse, or no tools are present | The `mcp-setup` skill | The Perfetto tools need a running server; that is setup |
|
|
25
|
+
| A grid's own configuration or API | The `flex-grid` skill | Once a trace names the grid, the fix is that component's subject |
|
|
26
|
+
| A slow build, install or CI job | The `cli-dev-loop` skill | Nothing here measures anything but the running app; how the build and install steps are invoked belongs there |
|
|
27
|
+
| A test that never touches hardware | The `testing` skill | Timings from an in-process tree describe the host machine |
|
|
28
|
+
| A test that passes some runs and fails others, on a device or off it | The `testing` skill for an in-process test; for one that drives real hardware, no skill in this library covers it — take it to the repository's own device-test guidance | Flakiness is a determinism problem; nothing here measures whether a result is repeatable, and a trace of a passing run says nothing about the failing one |
|
|
29
|
+
|
|
30
|
+
## Quick Reference
|
|
31
|
+
|
|
32
|
+
| Question | Call | Gives |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Where did the frame time go | `perfetto_enable`, then `perfetto_start` / `perfetto_stop` | A trace file to open in the Perfetto UI |
|
|
35
|
+
| The same recording with no agent session | `rk perfetto` | The CLI spelling of those three calls; the analysis afterwards is identical and stays here |
|
|
36
|
+
| How much JS memory is held | `cdp_session_create`, then `cdp_heap_usage` | Total and used heap bytes |
|
|
37
|
+
| Which JavaScript function is hot, or what retains memory | The `cpu-and-heap-profiling` skill | A CPU profile or heap snapshot file |
|
|
38
|
+
| Release the debug session | `cdp_session_end` | One session per device; this frees it |
|
|
39
|
+
| Which TypeScript ran inside a slow window | Redeploy with `deploy_app`'s `perfettoPaths` | Named slices for instrumented functions |
|
|
40
|
+
| Which callback fired | The `EventCallback` slice under `observer.callback`, in an instrumented build | Function name and source position |
|
|
41
|
+
|
|
42
|
+
## Instructions
|
|
43
|
+
|
|
44
|
+
1. **Pick the measurement from the question, not the other way round.** "It janks" is a trace.
|
|
45
|
+
"It grows" is a heap read. Recording the wrong one is expensive in a way that is not obvious
|
|
46
|
+
later, because both produce plausible-looking numbers.
|
|
47
|
+
|
|
48
|
+
2. **Enable tracing before the app launches.** `perfetto_enable` configures the device; the app
|
|
49
|
+
reads that configuration at startup. Enabling after the app is already running yields a trace
|
|
50
|
+
missing exactly the early work most worth seeing, and it looks like a successful recording.
|
|
51
|
+
The order is `perfetto_enable`, then deploy, then `perfetto_start` — reproduce the problem —
|
|
52
|
+
then `perfetto_stop`.
|
|
53
|
+
|
|
54
|
+
3. **Read the trace against the render threads first.** `RenderThr:Main` carries `render` and
|
|
55
|
+
`swapBuffers` and is where the frame budget is decided; `AuxRenderThread` carries work that
|
|
56
|
+
can steal from it, and `consumeAllTasks` running on either during an animation is one of the
|
|
57
|
+
three usual causes of jank. TypeScript work arrives under the `rokuos.typescript` category.
|
|
58
|
+
Locate those in the track list before reading a single slice, because every rule below is
|
|
59
|
+
stated relative to where a slice sits.
|
|
60
|
+
|
|
61
|
+
4. **The frame budget is `render` plus `swapBuffers` inside 16.67 ms.** Past that the frame is
|
|
62
|
+
dropped. The usual causes are one long `render`, meaning a heavy scene or too many nodes, or
|
|
63
|
+
an `observer.callback` over 1 ms delaying the first `render`.
|
|
64
|
+
|
|
65
|
+
5. **Only two transaction intervals block, and only in one direction.** Going from TypeScript to
|
|
66
|
+
the render thread, `transaction.start` to `transaction.commit` blocks, and so does
|
|
67
|
+
`transaction.update` to `transaction.end`; over roughly 300 ms during an animation is what
|
|
68
|
+
makes a smooth scroll jank. The trace points carrying `toTS` are the other direction, and a
|
|
69
|
+
wide span there does **not** stall the render thread. Reading one as a stall is the most
|
|
70
|
+
common way to spend a day on the wrong slice.
|
|
71
|
+
|
|
72
|
+
6. **Attribute the inside of a slow window by instrumenting, not by guessing.** Redeploying with
|
|
73
|
+
`deploy_app` and `perfettoPaths: ["packages/solid/src"]` — `--perfetto-paths` is the same
|
|
74
|
+
thing spelled for a terminal — names the reactive work inside the interval, where a
|
|
75
|
+
`createElement` slice means a SceneGraph node was created — during an animation that is
|
|
76
|
+
itself the finding. Widen to `solid-js` only if the narrower path is not enough: each
|
|
77
|
+
tracepoint costs roughly 10–30 microseconds per call, so a broad path distorts what you came
|
|
78
|
+
to measure. The value is matched as a substring of the source file path, so it depends on where
|
|
79
|
+
the SDK sits: `packages/solid/src` inside this workspace, `@roku-sdk/solid/src` in an app that
|
|
80
|
+
installed it. A path that matches nothing instruments nothing and reports no error, which reads
|
|
81
|
+
as an interval with no TypeScript in it. Naming every `createSignal` and `createMemo` makes
|
|
82
|
+
reactive slices attributable, but only in a build with `solidDevMode: true` at the top level
|
|
83
|
+
of `roku-config.ts` built with `rk dev --debug` (or `--solid-dev-mode` on any JS build), which is
|
|
84
|
+
off by default. Plain `rk dev` ignores the config field, and a build that precompiles to Hermes
|
|
85
|
+
bytecode skips it, so reactive work in a production trace stays anonymous no matter how
|
|
86
|
+
the config is set. The switch also adds overhead at component and signal creation, which is
|
|
87
|
+
one more reason the trace it names is not the one to quote timings from.
|
|
88
|
+
|
|
89
|
+
7. **A heap read changes the build, so never compare its timings with a trace.**
|
|
90
|
+
`cdp_session_create` builds and sideloads unminified JavaScript with the debug preset
|
|
91
|
+
(debugger probe stubs, config overrides); plain `rk dev` also ships JavaScript by default. `cdp_heap_usage` reports total and used bytes from that app. Memory numbers
|
|
92
|
+
are meaningful, timings from that build are not. Only one session may exist per device, so
|
|
93
|
+
end it with `cdp_session_end` before starting another.
|
|
94
|
+
|
|
95
|
+
8. **One trace is not evidence.** A TypeScript slice that spikes may have had garbage collection
|
|
96
|
+
run inside it, and one that spikes on a render thread may simply have been scheduled away.
|
|
97
|
+
Trust what repeats across frames, and re-record the same interaction after a fix rather than
|
|
98
|
+
declaring the fix worked.
|
|
99
|
+
|
|
100
|
+
9. **Remove what you instrumented.** Extra tracepoint paths, temporary slices and debug builds
|
|
101
|
+
all outlive the investigation if nothing takes them out. Redeploy normally before handing the
|
|
102
|
+
app back, or the next person measures your instrumentation.
|
|
103
|
+
|
|
104
|
+
## Example
|
|
105
|
+
|
|
106
|
+
Recording a scroll that janks, in the order the calls have to happen:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
perfetto_enable {} -> device configured; must precede launch
|
|
110
|
+
deploy_app { root } -> the app starts with tracing on
|
|
111
|
+
perfetto_start {} -> returns sessionId and filePath
|
|
112
|
+
-- reproduce the jank now
|
|
113
|
+
perfetto_stop { sessionId } -> filePath, bytes, durationMs
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then, in the trace: find the `keyEvent` that starts the scroll, follow the run of `render` and
|
|
117
|
+
`swapBuffers` after it on `RenderThr:Main`, and measure the first frame that exceeds 16.67 ms.
|
|
118
|
+
Everything overlapping that timestamp on the other two threads is the candidate set; a
|
|
119
|
+
`transaction.start` to `transaction.commit` gap spanning it is the answer, and a `toTS` span
|
|
120
|
+
spanning it is not.
|
|
121
|
+
|
|
122
|
+
## Common Mistakes
|
|
123
|
+
|
|
124
|
+
| Symptom | Cause | Fix |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| The trace is missing the startup or first-animation work | Tracing was enabled after the app launched | `perfetto_enable` first, then deploy, then `perfetto_start` |
|
|
127
|
+
| A wide transaction span sits over the dropped frame but changing that code does nothing | It was a `toTS` span, which does not block the render thread | Measure `transaction.start` to `transaction.commit` instead |
|
|
128
|
+
| Timings look far worse than users report | They were read from a build made for a heap session, which has no Hermes bytecode | Trace a normal build; use the debug build only for memory |
|
|
129
|
+
| `cdp_session_create` is refused | A session already exists for that device | `cdp_session_end`, then create the new one |
|
|
130
|
+
| Instrumented slices are everywhere and nothing is legible | The instrumentation path was widened before the narrow one was read | Start at `packages/solid/src` and add more only if it falls short |
|
|
131
|
+
| Reactive work shows up unnamed | Named slices need solid's development build, which a production build skips outright | Set `solidDevMode: true` and redeploy with `rk dev --debug`, or pass `--solid-dev-mode` |
|
|
132
|
+
| A fix looks like an improvement in one recording | A single spike can be garbage collection or a context switch | Re-record and compare what repeats across frames |
|
|
133
|
+
| The tracing tools report that they need a server | Perfetto tools are server-mode only | Start one — see the `mcp-setup` skill |
|