@roku-sdk/ai 0.0.0-stage → 2.18.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 +181 -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 +205 -0
- package/skills/focus-management/references/focus-recipes.md +282 -0
- package/skills/fonts-and-glyphs/SKILL.md +176 -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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roku, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @roku-sdk/ai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI skills library for the Roku RSG TypeScript SDK. Ships SDK-specific "skill"
|
|
4
|
+
documents plus the pure logic that validates and routes them, so AI coding
|
|
5
|
+
assistants give correct, SDK-specific guidance to external Roku SceneGraph
|
|
6
|
+
TypeScript developers.
|
|
7
|
+
|
|
8
|
+
Delivery is one tool-neutral tree: `rk update-ai-context` writes
|
|
9
|
+
`.agents/skills/roku-sdk-<name>/`, the [Agent Skills][spec] location, rather than
|
|
10
|
+
a directory per editor. One artifact, committed to the consuming repo, read by
|
|
11
|
+
every runtime that follows the convention.
|
|
12
|
+
|
|
13
|
+
The package's public API is **content + pure logic only** — no filesystem or
|
|
14
|
+
network access. The effectful delivery pipeline (discovery, generator, writer,
|
|
15
|
+
and the `rk update-ai-context` command) lives in `@roku-sdk/tooling`, which
|
|
16
|
+
depends on this package. The dependency is strictly one-way: `@roku-sdk/ai`
|
|
17
|
+
never imports `@roku-sdk/tooling`.
|
|
18
|
+
|
|
19
|
+
[spec]: https://cursor.com/docs/skills
|
|
20
|
+
|
|
21
|
+
### Upgrading from 2.14.0
|
|
22
|
+
|
|
23
|
+
2.14.0 had a `--target` flag that wrote a directory per editor. If you ran the
|
|
24
|
+
command on that version, delete whichever of these it left behind — nothing
|
|
25
|
+
generates or removes them now, and an editor that reads more than one location
|
|
26
|
+
will keep loading the stale copy alongside the current one:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
rm -f .cursor/rules/roku-sdk-skills.mdc .github/instructions/roku-sdk.instructions.md
|
|
30
|
+
rm -rf .claude/skills/roku-sdk-*
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then run `rk update-ai-context` to generate the tool-neutral tree. Files you
|
|
34
|
+
wrote yourself are untouched: the command only ever owns `.agents/skills/` paths
|
|
35
|
+
under the `roku-sdk-` prefix.
|
|
36
|
+
|
|
37
|
+
## What ships here
|
|
38
|
+
|
|
39
|
+
| Path | Purpose |
|
|
40
|
+
|------|---------|
|
|
41
|
+
| `skills/<name>/SKILL.md` | Skill documents (directory-per-skill) |
|
|
42
|
+
| `skills-manifest.json` | Standalone manifest mapping triggers to skill paths |
|
|
43
|
+
| `skills-manifest.schema.json` | Published JSON Schema for the manifest |
|
|
44
|
+
| `src/manifest.ts` | TS types, zod schema, and `loadManifest()` |
|
|
45
|
+
| `src/router.ts` | `buildRouter` / `route` / `findCollisions` / `findCollisionsAcross` |
|
|
46
|
+
| `src/snippets.ts` | `extractSnippets()` — the TypeScript code fences in a `SKILL.md` |
|
|
47
|
+
|
|
48
|
+
## Public API
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import {
|
|
52
|
+
loadManifest,
|
|
53
|
+
buildRouter,
|
|
54
|
+
route,
|
|
55
|
+
findCollisions,
|
|
56
|
+
findCollisionsAcross,
|
|
57
|
+
extractSnippets,
|
|
58
|
+
type SkillManifest,
|
|
59
|
+
type SkillEntry,
|
|
60
|
+
type SkillSnippet,
|
|
61
|
+
} from "@roku-sdk/ai";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`loadManifest(json)` validates raw JSON against the zod schema and returns a
|
|
65
|
+
typed `SkillManifest` (throwing a descriptive `ManifestValidationError` on
|
|
66
|
+
invalid input). The router functions enforce unique triggers, map a keyword to
|
|
67
|
+
its owning skill, and detect collisions across manifests (also reused by tooling
|
|
68
|
+
discovery).
|
|
69
|
+
|
|
70
|
+
`extractSnippets(markdown)` returns the `ts` / `tsx` code fences of a
|
|
71
|
+
`SKILL.md`, each with the line it starts on in the file and the check mode its
|
|
72
|
+
info string asks for. Line numbers are absolute against the original file so a
|
|
73
|
+
consumer can report a problem at the line an author edits. Only fences starting
|
|
74
|
+
at column 0 are scanned; an indented or blockquoted fence is prose about code,
|
|
75
|
+
not a sample.
|
|
76
|
+
|
|
77
|
+
A fence's info string selects its `mode`. A bare `ts` / `tsx` fence is
|
|
78
|
+
`"default"` — a fragment, which may refer to names its surrounding context
|
|
79
|
+
would provide. `rsg-skill=complete` marks a sample that stands on its own as a
|
|
80
|
+
module, and `rsg-skill=no-check` marks one that is deliberately wrong, such as
|
|
81
|
+
the "don't do this" half of a contrast. An unrecognised `rsg-skill=` value
|
|
82
|
+
throws `SnippetMarkerError` instead of falling back to `"default"`, so a fence
|
|
83
|
+
asking to be exempt is never silently checked.
|
|
84
|
+
|
|
85
|
+
A fence with no closing delimiter throws `UnclosedFenceError`, checked for every
|
|
86
|
+
language rather than only the two extracted here: an open fence consumes the
|
|
87
|
+
fences below it, so tolerating one would return fewer snippets than the file
|
|
88
|
+
appears to hold. Both errors extend `SnippetExtractionError`, which carries the
|
|
89
|
+
`code` and the 1-based `line` of the fence at fault.
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roku-sdk/ai",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.18.0",
|
|
4
|
+
"description": "AI skills library and manifest for the Roku RSG TypeScript SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/**",
|
|
12
|
+
"skills/**",
|
|
13
|
+
"skills-manifest.json",
|
|
14
|
+
"skills-manifest.schema.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"npm": ">=8.5.0",
|
|
19
|
+
"node": ">=24.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"zod": "^3.23.8"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@jest/globals": "^30.4.1",
|
|
26
|
+
"@types/jest": "^30.0.0",
|
|
27
|
+
"jest": "^30.4.1",
|
|
28
|
+
"@roku-sdk/dev-tools": "2.18.0"
|
|
29
|
+
},
|
|
30
|
+
"author": "Roku Inc",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://gitlab.com/rokudev/rsg-sdk.git",
|
|
35
|
+
"directory": "packages/ai"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://gitlab.com/rokudev/rsg-sdk/-/tree/main/packages/ai#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://gitlab.com/rokudev/rsg-sdk/-/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "jest --passWithNoTests",
|
|
46
|
+
"coverage": "jest --coverage --runInBand --passWithNoTests --testTimeout=60000 --forceExit",
|
|
47
|
+
"clean": "rm -rf dist",
|
|
48
|
+
"lint": "eslint --no-error-on-unmatched-pattern \"src/**/*.{ts,tsx,mjs,cjs,js}\""
|
|
49
|
+
}
|
|
6
50
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Ads and the Roku Advertising Framework
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@roku-sdk/ads` exports one component, `<Ads>`, a headless SceneGraph Task that wraps the Roku
|
|
6
|
+
Advertising Framework. You do not call RAF; you write fields on the task and read results off its
|
|
7
|
+
callbacks, in a fixed order. Every trap in the package comes from that being a protocol rather
|
|
8
|
+
than an API.
|
|
9
|
+
|
|
10
|
+
The package's interface may still change between releases: pin the version and re-read the types
|
|
11
|
+
after an upgrade.
|
|
12
|
+
|
|
13
|
+
## Read the reference first for these tasks
|
|
14
|
+
|
|
15
|
+
Wiring a CSAI, SSAI or CSAS module end to end: read
|
|
16
|
+
[`references/integration-flows.md`](references/integration-flows.md) first — it carries one
|
|
17
|
+
complete wired module per mode. The tables below name the commands and say what each mode needs,
|
|
18
|
+
but no row here is a complete answer to "how do I wire this mode".
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- Playing ads at all through `@roku-sdk/ads`: pre-roll, mid-roll or post-roll.
|
|
23
|
+
- A command that appears to do nothing, or a result callback firing with an unexpected value.
|
|
24
|
+
- Choosing between CSAI, SSAI and CSAS, or finding a mode is not the one you asked for.
|
|
25
|
+
- RAF never reporting ready in a new app.
|
|
26
|
+
- Audience measurement fields required for certification.
|
|
27
|
+
|
|
28
|
+
### When not to use
|
|
29
|
+
|
|
30
|
+
| Situation | Use instead | Because |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Which node plays the content video, or draws anything on screen | `sdk-components` | `<Ads>` renders nothing; it only needs refs to nodes something else drew |
|
|
33
|
+
| Loading your own catalog or metadata over the network | `fetch-and-data-loading` | RAF makes the ad request itself — the app never fetches the tag |
|
|
34
|
+
| `ChannelStore` sign-in or purchases | `user-accounts-and-purchases` | Same set-data-then-set-command shape, different subsystem and different fields |
|
|
35
|
+
| Where D-pad focus lives generally | `focus-management` | Only the focus an ad break takes is here |
|
|
36
|
+
| Calling RAF directly from BrightScript | — | This skill is the TypeScript wrapper; the task already owns the RAF calls |
|
|
37
|
+
|
|
38
|
+
## Quick Reference
|
|
39
|
+
|
|
40
|
+
### The protocol, in order
|
|
41
|
+
|
|
42
|
+
1. **Wait for `"ready"`.** `onAdsState` reports it once, before the task's command loop starts.
|
|
43
|
+
A command written earlier is never seen — no error, no result.
|
|
44
|
+
2. **Set the data fields first, `command` last.** The task reacts to the `command` write and reads
|
|
45
|
+
the other fields as they stand at that moment.
|
|
46
|
+
3. **Read the result off the matching callback**, not off the next thing that happens.
|
|
47
|
+
|
|
48
|
+
Two consequences are invisible in the types and are the most common cause of "it worked once":
|
|
49
|
+
|
|
50
|
+
- **A repeated `command` value writes nothing.** The field is signal-driven and compares by value,
|
|
51
|
+
so setting `"showAds"` twice in a row is one command. Reset through `"none"` between two
|
|
52
|
+
identical commands.
|
|
53
|
+
- **A string result field is cleared to `"none"` before the real value is written**, so
|
|
54
|
+
`onShowAdsResult`, `onStitchedAdsInitResult`, `onConstructStitchedStreamResult` and
|
|
55
|
+
`onRenderStitchedStreamResult` all fire with `"none"` before anything you can act on. Guard it
|
|
56
|
+
and return. The catch is that the *first* command hides it — the field already holds `"none"`,
|
|
57
|
+
so that write changes nothing and notifies nobody. An unguarded handler therefore works
|
|
58
|
+
perfectly until the second command, which is where it starts treating the sentinel as a result.
|
|
59
|
+
`onGetAdsResult` is the exception and needs no guard: it carries an array, and RAF returns a
|
|
60
|
+
fresh one each call.
|
|
61
|
+
|
|
62
|
+
### Commands
|
|
63
|
+
|
|
64
|
+
| `command` | Needs | Reports on |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `getAds` | `adUrl` | `onGetAdsResult` — always an array; an empty or unset `adUrl` yields `[]` without a network call |
|
|
67
|
+
| `showAds` | `adView` | `onShowAdsResult` — `"error"` immediately if `adView` is missing; a viewer leaving the break is `"user_exited"` (2.15.0 and newer), not a failure |
|
|
68
|
+
| `stitchedAdsInit` | non-empty pods | `onStitchedAdsInitResult` — `"error"` on empty pods, which is also the only case that leaves the mode alone |
|
|
69
|
+
| `constructStitchedStream` | `contentMetadata` | `onConstructStitchedStreamResult` |
|
|
70
|
+
| `renderStitchedStream` | a built stream and `adView` | `onRenderStitchedStreamResult` — a viewer back-out is `"user_exited"`, a distinct result from `"error"` |
|
|
71
|
+
| `stop` | — | `onAdsState` reports `"stopped"`; the task's loop exits |
|
|
72
|
+
|
|
73
|
+
### Choosing a mode
|
|
74
|
+
|
|
75
|
+
| Your situation | Mode | Needs |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Pause the content, play ads over it, resume where you left off | CSAI | `adView` and `contentView` |
|
|
78
|
+
| The ads are stitched into the content stream server-side and you react to ad events as it plays | SSAI | `contentView` |
|
|
79
|
+
| Build one stitched stream up front and render it yourself | CSAS | `adView` and `contentMetadata` |
|
|
80
|
+
|
|
81
|
+
Command sequences, after `"ready"`: CSAI is `getAds` → `showAds`, repeated per break. SSAI is
|
|
82
|
+
`getAds` → `stitchedAdsInit`, then content playback drives the ad events. CSAS is
|
|
83
|
+
`getAds` → `constructStitchedStream` → `renderStitchedStream`.
|
|
84
|
+
|
|
85
|
+
## Instructions
|
|
86
|
+
|
|
87
|
+
1. **Add `bs_libs_required: "roku_ads_lib"`** to `manifestEntries` in `roku-config.ts`. Without it
|
|
88
|
+
RAF is not linked, `onAdsState` never reports `"ready"`, and the failure names a library path
|
|
89
|
+
rather than the manifest.
|
|
90
|
+
2. **Give `<Ads>` an `adView`** for any mode that renders ads — a ref to any renderable node whose
|
|
91
|
+
lifetime covers the whole break. A full-screen `Rectangle` behind the ad is enough.
|
|
92
|
+
3. **Give it `contentView`** for CSAI and SSAI: a ref to the content `Video`. The task watches
|
|
93
|
+
playback position on it, and that is what drives mid-roll — so set the `Video`'s
|
|
94
|
+
`notificationInterval` fine enough that a cue point is reported.
|
|
95
|
+
4. **Set measurement before the first `getAds`.** `enableAdMeasurements`, `contentGenre` and
|
|
96
|
+
`contentId` are applied as they change, and RAF needs them in place when the pods are fetched.
|
|
97
|
+
Empty strings are skipped. RAF fires the beacons itself — do not intercept or suppress them.
|
|
98
|
+
5. **Do not write a cue-point loop.** The task calls RAF's `getAds` itself at each cue point it
|
|
99
|
+
sees on `contentView` and hands back only the pods due now, so mid-roll arrives as another
|
|
100
|
+
`onGetAdsResult`. There is nothing to schedule and no pod field to filter on.
|
|
101
|
+
6. **Re-assert focus when content resumes.** An ad break does not hand focus back; it is left on
|
|
102
|
+
the screen root, and an app that does not take it returns to receiving no keys.
|
|
103
|
+
7. **Stop with `command = "stop"` while still mounted.** Doing it only from a cleanup is
|
|
104
|
+
best-effort: the callback that would confirm it is torn down by the same unmount.
|
|
105
|
+
|
|
106
|
+
Two more facts a reader cannot derive from the surface. `contentView` binds **once** — the task
|
|
107
|
+
observes position on the first non-invalid value and then stops watching its own field, so
|
|
108
|
+
swapping the `Video` node means unmounting and remounting `<Ads>`. And mid-roll reuses the last
|
|
109
|
+
`adUrl`; there is no per-break tag.
|
|
110
|
+
|
|
111
|
+
### The mode transitions that do not happen
|
|
112
|
+
|
|
113
|
+
Both leave the task in CSAI while the app believes otherwise, so position events then offer
|
|
114
|
+
mid-roll pods for a stream that was never set up for them. Each reports, but only as an `"error"`
|
|
115
|
+
that is easy to log and walk past:
|
|
116
|
+
|
|
117
|
+
- `stitchedAdsInit` on empty pods changes no mode and reports `"error"`. Check `pods.length > 0`
|
|
118
|
+
before issuing it rather than reacting afterwards.
|
|
119
|
+
- A failed `constructStitchedStream` reverts to CSAI, signalled only by its `"error"` result.
|
|
120
|
+
|
|
121
|
+
Treat either as "this is not the mode I asked for" and stop, rather than starting playback.
|
|
122
|
+
|
|
123
|
+
## Example
|
|
124
|
+
|
|
125
|
+
Minimal CSAI pre-roll. It demonstrates both consequences above: `onShowAdsResult` guards the
|
|
126
|
+
`"none"` sentinel, and it resets `command` to `"none"` so the next break's `showAds` is a change.
|
|
127
|
+
|
|
128
|
+
It resumes content on the two results that mean the break is over — `"completed"`, and `"error"`
|
|
129
|
+
for a break that never played — and stops on anything else, which is a viewer who walked out.
|
|
130
|
+
That way round on purpose: `"user_exited"` joined `ShowAdsResult` in 2.15.0, so comparing against
|
|
131
|
+
that literal does not compile on 2.14.0, while an allow-list of the results that permit resuming
|
|
132
|
+
reads the same on both and keeps its meaning as the union grows.
|
|
133
|
+
|
|
134
|
+
```tsx rsg-skill=complete
|
|
135
|
+
import type { AdsCommand, AdsState, EventHandler, EventHandlerAdPods, ShowAdsResult } from "@roku-sdk/ads";
|
|
136
|
+
import { Ads } from "@roku-sdk/ads";
|
|
137
|
+
import { Component, ContentNode, Group, MediaControl, Rectangle, Video } from "@roku-sdk/components";
|
|
138
|
+
import { DomNode } from "@roku-sdk/interfaces";
|
|
139
|
+
import { createSignal } from "@roku-sdk/solid";
|
|
140
|
+
|
|
141
|
+
export const PreRoll: Component = () => {
|
|
142
|
+
const [command, setCommand] = createSignal<AdsCommand>("none");
|
|
143
|
+
const [adUrl, setAdUrl] = createSignal("");
|
|
144
|
+
const [adView, setAdView] = createSignal<DomNode | undefined>();
|
|
145
|
+
const [contentView, setContentView] = createSignal<DomNode | undefined>();
|
|
146
|
+
const [control, setControl] = createSignal<MediaControl>("none");
|
|
147
|
+
|
|
148
|
+
const handleAdsState = (state: AdsState) => {
|
|
149
|
+
if (state !== "ready") return;
|
|
150
|
+
setAdUrl("https://example.com/tag.vmap");
|
|
151
|
+
setCommand("getAds");
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleGetAdsResult: EventHandlerAdPods = pods => {
|
|
155
|
+
if (pods.length === 0) {
|
|
156
|
+
setControl("play");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setControl("stop");
|
|
160
|
+
setCommand("showAds");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleShowAdsResult: EventHandler<ShowAdsResult> = result => {
|
|
164
|
+
if (result === "none") return;
|
|
165
|
+
if (result === "completed" || result === "error") {
|
|
166
|
+
// The break played out, or never played at all: resume content either way.
|
|
167
|
+
setControl("play");
|
|
168
|
+
setCommand("none");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Anything else is the viewer leaving the break, so do not resume.
|
|
172
|
+
setCommand("stop");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<group>
|
|
177
|
+
<Video
|
|
178
|
+
width={1920} height={1080} control={control()} notificationInterval={1.0}
|
|
179
|
+
content={<contentNode url="https://example.com/content.m3u8" />}
|
|
180
|
+
gainFocus={control() === "play"}
|
|
181
|
+
ref={element => setContentView(element)}
|
|
182
|
+
/>
|
|
183
|
+
<rectangle
|
|
184
|
+
width={1920} height={1080} color="0x000000FF"
|
|
185
|
+
visible={control() !== "play"}
|
|
186
|
+
ref={element => setAdView(element)}
|
|
187
|
+
/>
|
|
188
|
+
<Ads
|
|
189
|
+
adUrl={adUrl()} adView={adView()} contentView={contentView()} command={command()}
|
|
190
|
+
onAdsState={handleAdsState}
|
|
191
|
+
onGetAdsResult={handleGetAdsResult}
|
|
192
|
+
onShowAdsResult={handleShowAdsResult}
|
|
193
|
+
/>
|
|
194
|
+
</group>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Common Mistakes
|
|
200
|
+
|
|
201
|
+
| Symptom | Cause | Fix |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `onAdsState` never reports `"ready"`; log shows a `roku_ads` library path | RAF was not linked into the app | `bs_libs_required: "roku_ads_lib"` in `manifestEntries` |
|
|
204
|
+
| First command does nothing, no error | It was written before `"ready"` | Issue the first command from `onAdsState` |
|
|
205
|
+
| Second ad break never plays; the first did | `command` was already that value, so nothing was written | Reset to `"none"` between two identical commands |
|
|
206
|
+
| A handler that worked in testing mishandles the *second* command | It received the `"none"` sentinel, which the first command hides because the field already held it | `if (result === "none") return;` in every string result handler |
|
|
207
|
+
| `showAds` reports `"error"` at once | No `adView` | Pass a ref to a renderable node that outlives the break |
|
|
208
|
+
| Content restarts when the viewer backs out of an ad | `"user_exited"` was handled as if it were `"completed"` | Resume only on the results that mean the break is over; leaving one early usually means leaving the content. `showAds` reports it from 2.15.0, `renderStitchedStream` from earlier |
|
|
209
|
+
| Mid-roll never fires | No `contentView`, so nothing watches playback position | Pass a ref to the content `Video` |
|
|
210
|
+
| The app stops responding to the remote after an ad | The break took focus and did not return it | Re-assert focus when content resumes |
|
|
211
|
+
| A pod field read fine in testing and is `undefined` in production | Pod fields are optional: which ones arrive depends on the VAST tag that answered | Treat pods as opaque; `pods.length > 0` is the whole decision |
|
|
212
|
+
|
|
213
|
+
### Ad pods carry ad-server data, so almost every field is optional
|
|
214
|
+
|
|
215
|
+
The names are RAF's own and lowercase — `rendersequence`, `mimetype`, `adtitle` — not camelCase.
|
|
216
|
+
Only `AdPod.ads` is required; everything else is optional, because RAF passes the ad server's
|
|
217
|
+
response through and a different tag carries different fields. There is no ad count — it is
|
|
218
|
+
`ads.length` — and the break is described by two fields rather than one: `rendersequence`, a string
|
|
219
|
+
(`"preroll"`), and `rendertime`, the offset in seconds RAF has already scheduled it at.
|
|
220
|
+
`tracking` is a flat array of event objects, not a record of URLs, so finding one means searching
|
|
221
|
+
on `event`.
|
|
222
|
+
|
|
223
|
+
None of which a correct integration needs: the task fetches cue-point pods itself, so check
|
|
224
|
+
`pods.length` and read nothing else.
|
|
225
|
+
|
|
226
|
+
## Source of truth
|
|
227
|
+
|
|
228
|
+
`@roku-sdk/ads` ships its `src/`, so the typed surface is in `node_modules/@roku-sdk/ads`: one
|
|
229
|
+
component and 16 types in `src/index.ts`, with TSDoc on every prop of `IAdsProps`. The command loop
|
|
230
|
+
those props drive is `src/xml-components/ads/ads.brs`, and where the two disagree the BrightScript
|
|
231
|
+
is what runs.
|