# Open Video Docs This file is generated automatically from the Hugo documentation tree for AI agents and large-context scanning. # Roku SDK --- ## Configuration Source: https://docs.open.video/docs/roku-sdk/configuration/ Both components take one associative array on their `config` field. Pass the same array to each. ## Required | Key | Meaning | |---|---| | `channelSlug` | Your open.video channel slug. | | `adUnit` | The ad unit name you already use for your video inventory. There is no separate connected-TV ad unit to create: the SDK marks every request as CTV (connected-TV device type, your Roku channel id as the app bundle, app identity), and that is what buyers classify on. Keep the value stable; your reporting is grouped by it. | | `appDomain`, `appName` | Your Ezoic domain and app name, sent on every ad request as app identity. | | `sdkLibraryUrl` | The pinned SDK URL, for example `https://sdk.open.video/roku/v0.1.4/OpenVideoRokuSDK.zip`. | ## Ads | Key | Default | Meaning | |---|---|---| | `adsEnabled` | | `true` to serve ads. | | `adLane` | `"app"` | Ad request identity. Leave as `"app"` for a Roku channel. | | `ssaiStitchPlayback` | `false` | `false`: ad breaks render client-side through RAF. `true`: ad breaks are stitched into the stream server-side. Stitched breaks are not eligible for Google demand. | | `adPodDurationSeconds` | `60` | Maximum length of one ad break. | | `adPodMaxAds` | `2` | Maximum ads in one break. | | `adBreakGapSeconds` | | Minimum content time between midrolls. Roku's ad policy floor applies. | | `adBreaksWaitMs` | `750` | On a deep link, how long to wait for the video's ad break plan before freezing it. | ## Consent The SDK does not own consent. Pass through whatever your consent surface collects: | Key | Meaning | |---|---| | `consentTcString`, `consentGdpr` | IAB TCF string and GDPR applicability. | | `consentUsPrivacy` | US Privacy (CCPA) string. | | `consentGpp`, `consentGppSid` | IAB GPP string and section ids. | ## Content and display | Key | Default | Meaning | |---|---|---| | `apiBase`, `pageUrlBase` | open.video | Leave alone unless told otherwise. | | `feedUrl` | derived | Overrides the catalog's channel feed URL. | | `theme` | | `{ surfaceBase }` colors the default ad backdrop (falls back to `#0d0d0f`). | --- ## Ad controller Source: https://docs.open.video/docs/roku-sdk/ad-controller/ `OpenVideo:OpenVideoAdController` is the render-thread playback and ad state machine. It owns your `Video` node's playback fields, stream resolution, ad break routing, bookmarks, the app session and video-start analytics. It does **not** own focus, trick-play chrome, end cards, dialogs, layout, or the screen you return to after playback. Create it only after the library reports `loadStatus = "ready"`: ```brightscript controller = CreateObject("roSGNode", "OpenVideo:OpenVideoAdController") controller.player = m.video controller.config = m.cfg controller.callFunc("startSession") ``` ## Input fields | Field | Type | Description | |---|---|---| | `player` | `node` | Required. Your `Video` node. The controller sets `content`, `control` and `seek`, observes `state` and `position`, and shows or hides the node. You keep its size, position, z-order and focus. | | `adFacade` | `node` | Optional full-screen `Group` RAF renders over. If omitted, the controller creates a hidden 1920x1080 group with a backdrop colored from `config.theme.surfaceBase`. | | `config` | `assocarray` | See [Configuration](../configuration/). | | `bookmarksEnabled` | `bool` | Default `true`. When `false`, resume and completed reads return empty values and writes are skipped. | ## Functions | Function | Arguments | Description | |---|---|---| | `startSession` | none | Starts the once-per-launch app session. Call during boot, before the first playback. | | `play` | `{ videoId, streamUrl?, metadata?, resumePosition?, isAutoplay }` | Starts playback: resolves the stream, loads video context, applies the resume position, runs a preroll when one is due, and starts content. `metadata` can carry `title`, `description`, `posterUrl`, `pageUrl`, `durationSeconds`, `captions` and `adBreaks`. | | `stopPlayback` | `{ reason }` | `user`, `error`, `endcard` or `finished`. `endcard` tears down without emitting `stopped`; `finished` skips saving a bookmark. | | `pause`, `resume`, `togglePause` | none | Content playback control. No-ops during ad breaks. | | `seekRelative` | seconds | Relative seek; repeated presses coalesce. | | `seekTo` | content seconds | Absolute seek in content time (stitched ad breaks are accounted for). | | `streamToContentSeconds` | stream seconds | Converts stream time to content time, for hosts that observe `player.position` during stitched playback. | | `getContentDurationSeconds` | none | Content duration, excluding stitched ad breaks. | | `getResumePosition`, `clearResumePosition`, `isCompleted` | `videoId` | Bookmark helpers for your UI. | | `listContinueWatching` | none | The continue-watching list, most recent first. | | `markInterrupted` | none | Records that an ad break was interrupted, so the next session makes it good with a preroll. | ## Output fields All output fields notify on every write. | Field | Type | Description | |---|---|---| | `state` | `string` | `idle` → `resolving` → zero or more `adBreakStart`/`adBreakEnd` pairs → `contentStart` → `playing` / `paused` / `buffering` → `finished`, `stopped` or `error`. `error` is followed by `stopped`. A client-side midroll resumes as `adBreakEnd` → `buffering` → `playing`, without a second `contentStart`. | | `adBreak` | `assocarray` | `{ type, lane, podIndex, rendered }`; `type` is `preroll` or `midroll`, `lane` is `csai` (client-side) or `ssai` (stitched). | | `error` | `assocarray` | `{ code, message, fatal }`. Non-fatal ad errors fall through to content. | | `controlsHint` | `assocarray` | `{ action, seekTarget? }`, `action` is `show`, `hide` or `hideSeekThumb`. Map it to your own transport chrome, or ignore it. | | `currentVideo` | `assocarray` | The playing video after enrichment, plus `related` when available. | ## Typical host mapping | Controller output | Your channel | |---|---| | `resolving` | Show a loading state. | | `adBreakStart` | Hide content controls. | | `contentStart` / `adBreakEnd` | Restore your preferred focus target. | | `controlsHint` | Show or hide controls, update the seek preview. | | `finished` | Show an end card, or call `stopPlayback({ reason: "finished" })`. | | `error` | Show a playback dialog. | | `stopped` | Return to the screen you came from. | --- ## Catalog Source: https://docs.open.video/docs/roku-sdk/catalog/ `OpenVideo:OpenVideoCatalog` is a headless discovery component. Each function starts an asynchronous request, returns a `requestId` string, and publishes the completion on the `result` field. ## Input fields | Field | Type | Description | |---|---|---| | `config` | `assocarray` | Uses `channelSlug`, and optionally `apiBase`, `pageUrlBase` and `feedUrl`. See [Configuration](../configuration/). | ## Functions | Function | Arguments | Description | |---|---|---| | `listVideos` | `{ category?, page? }` | Your channel's videos. `category` is `featured` (default), `latest`, `most-views` or `most-likes`. Pages of 12. | | `recentUploads` | `{ page? }` | Same as `listVideos` with `latest`; the result `op` is `recentUploads`. | | `listPlaylists` | `{ page?, pageSize? }` | Your channel's playlists. Defaults: page 1, 12 per page. | | `playlistVideos` | `{ playlistId?, playlistSlug?, page?, limit? }` | Videos in one playlist; `playlistSlug` wins when both are passed. Defaults: page 1, 25 per page. | | `search` | `{ query }` | Video `items` plus matching `channels`. | | `getVideo` | `{ videoId }` | One video as `item`, plus `related`. | | `cancel` | `requestId` | Stops a request that is still running. Cancelled requests emit no result. | ## Result ```brightscript { requestId, op, page, items: [], hasMore, error: "" } ``` `search` results also carry `channels`; `getVideo` results carry `item` and `related`. Errors arrive as results with `error <> ""`, an empty `items` and `hasMore = false`; the functions never throw. `hasMore` is true when a full page came back. ```brightscript sub init() m.catalog = CreateObject("roSGNode", "OpenVideo:OpenVideoCatalog") m.catalog.config = m.cfg m.catalog.ObserveField("result", "onCatalogResult") m.catalog.callFunc("listVideos", { category: "featured", page: 1 }) end sub sub onCatalogResult() result = m.catalog.result if result.error <> "" then print "Catalog error: "; result.error if result.op = "listVideos" then m.featuredItems = result.items end sub ``` --- ## App identity and app-ads.txt Source: https://docs.open.video/docs/roku-sdk/app-identity/ ## Your channel id is the app bundle The SDK sends your Channel Store id (`roAppInfo.GetID()`) as the app bundle on every ad request. Buyers and app-ads.txt checks key on it, so register the channel before you test ads. ## Register the channel In the Ezoic dashboard: **Ezoic Ads → Mobile & CTV Apps → Add Mobile App**. - **Platform:** Roku - **Channel ID:** the numeric id from the Roku Developer Dashboard (for example `123456`) - **App name:** your channel's name - **Store URL:** optional for Roku; it's derived from the Channel ID Register it under the same website you pass as `appDomain`. Registration goes to your Ezoic account team for review. AI assistants can do the same through the Ezoic publisher MCP: the `mobile_apps` tool, `apps.add`, with `platform: "Roku"`. ## app-ads.txt The app is yours, so **app-ads.txt is yours**: publish it at the root of the developer website listed on your Channel Store page, with the lines your Ezoic account team gives you. Buyers that can't verify your channel against app-ads.txt will not bid. ## Sideloaded development builds A sideloaded channel reports `dev` as its id. Ezoic maps that to your registered channel for attribution, but answers ad requests from development builds with a test ad (a 16-second sample creative) instead of running a live auction. It goes through the same wrapper and tracking as a real ad, so you can verify playback, ad break behaviour and that impressions reach your reporting. Live demand starts once the channel is installed from the Channel Store and reports its real id. --- ## Development and testing Source: https://docs.open.video/docs/roku-sdk/development/ ## Point a sideload at a different SDK Keep the pinned production URL in your committed config, and override it only in the manifest you sideload. The reference channel's Makefile does this with `SDK_URL`, which appends an `ov_sdk_library_url` line to the staged manifest: ``` # against the latest release make install ROKU_IP= ROKU_PASS= SDK_URL=https://sdk.open.video/roku/latest/OpenVideoRokuSDK.zip ``` Sideloaded channels may load the library over plain HTTP, which is handy for testing an SDK build from your own machine. ## Before you submit - [ ] `sdkLibraryUrl` pins a version (`/roku/vX.Y.Z/`), not `latest`. - [ ] The manifest has `bs_libs_required=roku_ads_lib`. - [ ] A cold launch with no network shows your retry path, not a crash. - [ ] Preroll, a midroll (let content run past the first break) and resume after the break all work on a real device. - [ ] `back` during an ad break and during content returns to your UI. - [ ] The channel is registered in the Ezoic dashboard and app-ads.txt is live. ## Roku static analysis The Channel Store's static analysis only scans your channel package, not the component library loaded at runtime. Because the SDK owns the RAF calls, the report can say RAF beacons, the RAF constructor or the RAF library import are missing even though ads render. If certification asks, explain that RAF runs inside the Open Video component library your channel loads at startup.