Architecture
Three things move through the system: your events, Maestro's configuration for each one, and the panels and overlays the SDK renders from it.
- Event ingestion. Maestro polls the schedule feed you already publish. Each event is matched to a template, which decides which features are on and how they are configured. Maestro sets this up with you; see Ingestion rules and templates.
- Config files on the CDN. For every event, Maestro publishes a JSON document at a path built from your Site ID and your own event identifier. It is rebuilt whenever your feed changes or your team edits settings in the Admin.
- The SDK. Your app instantiates it once with the Site ID, then configures it for content. The SDK fetches the configuration, builds the panels and overlays it describes, and renders them where your app says.
Core concepts
Site. The top-level container for your content and configuration. Every SDK integration points at one site, identified by a Site ID.
Page. A layout within a site. Channels are the page type this SDK uses: interactive video pages with panels beside the player and overlays over it. A page created in the Admin is identified by a Page ID.
Event. One piece of scheduled content in your feed, identified by your own event ID. Once ingestion is set up, the SDK can be configured with that ID directly and fetches the configuration Maestro published for it.
Panel. An interactive component that lives outside the video player: chat, stats, shop, quests, fantasy, betting, schedule, and custom panels Maestro builds for you.
Overlay. An interactive graphic that appears over the video to drive immediate action: polls, predictions, trivia, lower thirds, highlights, calls to action.
Automation rules. Trigger overlays from real-time data, show or hide panels by user context, and configure whole experiences from templates.
Page ID and Event ID
The SDK can be pointed at content two ways, and they make different calls.
- A Page ID identifies a page you created in the Maestro Admin. The SDK loads that page's configuration directly. Hello World uses a Page ID so nothing depends on a feed.
- An Event ID is your identifier from your own schedule feed. After ingestion is set up, the SDK fetches the configuration document Maestro published under it from the CDN. The eventID will be any unique ID passed around your application, directly mapping to a maestro page.
In practice you will never need to know or use a Page ID. Once ingestion is set up you use your own identifier exclusively, and Maestro resolves it to the page behind the scenes. Hello World uses a Page ID only because it runs before any feed exists.
Parameter names differ by platform and are stated in each Hello World section.
How it works: Stats panel example
Your app talks to the SDK. The SDK talks to Maestro. The steps in the shaded box are the ones Hello World implements.
Steps 7 to 9 happen on their own once a Stats panel is configured for the event. They need your app to feed playerTimecode to the SDK; each platform exposes a call for that, and Hello World wires it in the overlay section.
Where the SDK lives in your app
Create the Maestro SDK when your app starts, as a sibling of your video player, never inside it. The SDK is designed to be reconfigured for new content without being destroyed, which is what lets it keep its panel on screen while the video underneath changes. If instead the video experience owns the SDK, the SDK is torn down and redrawn every time content changes. Maestro expects you will implement against this lifecycle.
The panel's host view, whether that is a SwiftUI column, a Compose slot, a SceneGraph node, or a DOM element, is owned by your app shell for the same reason.
As a sequence, one SDK instance spans the whole session. Content selection repeats; instantiation and teardown happen once each.
The same sequence in each SDK
Each step of the diagram maps to one call or one observable per platform. Values shown are the Hello World defaults against Maestro's demo page. "Fetch config" and "Build supported panels" happen inside the SDK; the row shows what your app can observe.
- Swift (tvOS)
- Kotlin (Android TV / Fire TV)
- BrightScript (Roku)
- JavaScript (Web, Samsung, LG)
| Diagram step | Interface | Hello World value |
|---|---|---|
| Open application, instantiate SDK | MaestroManager.shared.configure(siteID:jwt:maestroManagerDelegate:maestroWorkingEnvironment:defaultPanel:) | siteID: "69b2f133e8117ec6536d59a1", jwt: "", .prod, defaultPanel: .stats |
| Select content, pass pageID or eventID | await MaestroManager.shared.userDidStartWatchingEvent(eventID:delegate:hideBetsPanel:hideBetsWagers:disableBetsOverlays:disableFantasyOverlays:) returns MaestroEventInterface | eventID: "6aa966d669342216ada5a4b8" (the Page ID goes in this parameter) |
| Fetch config, build supported panels | Internal. The await returns once the session exists; MaestroPanel() renders the built panels. defaultPanel selects the first tab. | Stats tab visible |
| Select other content | await MaestroManager.shared.userDidStopWatchingEvent(currentID) then userDidStartWatchingEvent(eventID: nextID, ...) | next Page ID |
| Application closed, destroy | await MaestroManager.shared.userDidStopWatchingEvent(currentID). There is no separate teardown call; the shared manager lives with the process. | current Page ID |
// Open application
MaestroManager.shared.configure(
siteID: "69b2f133e8117ec6536d59a1", jwt: "",
maestroManagerDelegate: AppDelegate(),
maestroWorkingEnvironment: .prod, defaultPanel: .stats)
// Select content
let event = await MaestroManager.shared.userDidStartWatchingEvent(
eventID: "6aa966d669342216ada5a4b8", delegate: self,
hideBetsPanel: false, hideBetsWagers: false,
disableBetsOverlays: false, disableFantasyOverlays: false)
// Select other content
await MaestroManager.shared.userDidStopWatchingEvent("6aa966d669342216ada5a4b8")
let next = await MaestroManager.shared.userDidStartWatchingEvent(eventID: nextPageID, delegate: self, /* ... */)
// Application closed
await MaestroManager.shared.userDidStopWatchingEvent(nextPageID)
| Diagram step | Interface | Hello World value |
|---|---|---|
| Open application, instantiate SDK | MaestroSDK.configure(context, maestroEventDelegate, params: MaestroSDKParameters) returns MaestroSDK.Instance | siteId = "69b2f133e8117ec6536d59a1", pageId = "6aa966d669342216ada5a4b8" |
| Select content, pass pageID or eventID | Page ID is passed at configure. For an Event ID: sdk.onEventDataUpdated(MaestroEventData(metadataValue = eventId)) | pageId as above; metadataValue is your Event ID once ingestion is live |
| Fetch config, build supported panels | Internal. MaestroPanel(sdk = sdk) renders the built panels; the SDK calls delegate.shouldShowPanel() when it wants the panel visible. | panel composable populates |
| Select other content | sdk.onEventDataUpdated(MaestroEventData(metadataValue = nextEventId)). Do not call configure again; it detaches the instance. | next Event ID |
| Application closed, destroy | sdk.onEventDataUpdated(null) then sdk.detach() |
// Open application
val sdk = MaestroSDK.configure(
context = applicationContext,
maestroEventDelegate = MaestroDelegate(),
params = MaestroSDKParameters(
siteId = "69b2f133e8117ec6536d59a1",
pageId = "6aa966d669342216ada5a4b8",
),
)
// Select content (Event ID path, once ingestion is live)
sdk.onEventDataUpdated(MaestroEventData(metadataValue = eventId))
// Select other content
sdk.onEventDataUpdated(MaestroEventData(metadataValue = nextEventId))
// Application closed
sdk.onEventDataUpdated(null)
sdk.detach()
| Diagram step | Interface | Hello World value |
|---|---|---|
| Open application, instantiate SDK | createObject("roSGNode", "ComponentLibrary"), set uri, wait for loadStatus = "ready", then createObject("roSGNode", "MaestroPanelLib:MaestroPanel") and m.top.appendChild(m.lib) | uri = "https://roku-sdk.us-central1-master.gcp.maestro.io/4.1.16/maestrokit.pkg" |
| Select content, pass pageID or eventID | m.lib.config = { siteID, pageID or eventID, useProdEnv } | siteID: "69b2f133e8117ec6536d59a1", pageID: "6aa966d669342216ada5a4b8", useProdEnv: false |
| Fetch config, build supported panels | Observe isReady (SDK ready) and panelsBuilt (panel configuration built). errorInformation reports a rejected config. | panelsBuilt = true |
| Select other content | Set m.lib.config again. The field notifies on every set and the node stays in the scene. | next Page ID |
| Application closed, destroy | m.lib.callFunc("destroySDK"), m.top.removeChild(m.lib), m.lib = invalid |
' Open application
m.componentLibrary = createObject("roSGNode", "ComponentLibrary")
m.componentLibrary.observeField("loadStatus", "onLibraryLoadStatusChanged")
m.componentLibrary.uri = "https://roku-sdk.us-central1-master.gcp.maestro.io/4.1.16/maestrokit.pkg"
sub onLibraryLoadStatusChanged()
if m.componentLibrary.loadStatus = "ready"
m.lib = createObject("roSGNode", "MaestroPanelLib:MaestroPanel")
m.lib.observeField("panelsBuilt", "onPanelsBuilt")
m.lib.observeField("errorInformation", "onErrorInformation")
m.top.appendChild(m.lib)
' Select content
m.lib.config = { siteID: "69b2f133e8117ec6536d59a1", pageID: "6aa966d669342216ada5a4b8", useProdEnv: false }
end if
end sub
' Select other content
m.lib.config = { siteID: "69b2f133e8117ec6536d59a1", pageID: nextPageID, useProdEnv: false }
' Application closed
m.lib.callFunc("destroySDK")
m.top.removeChild(m.lib)
m.lib = invalid
| Diagram step | Interface | Hello World value |
|---|---|---|
| Open application, instantiate SDK | SDK.configure({ siteID, userSettings?, platform? }) | siteID: "69b2f133e8117ec6536d59a1" |
| Select content, pass pageID or eventID | await SDK.userDidStartWatchingEvent({ pageId or eventId, delegate, useProdEnv }) returns IMaestroEvent | pageId: "6aa966d669342216ada5a4b8", useProdEnv: false |
| Fetch config, build supported panels | Internal. The promise resolves once the page configuration is loaded; SDK.renderPanel("panel-section") renders the built panels into your element. | panel renders in #panel-section |
| Select other content | await SDK.userDidStartWatchingEvent({ pageId: next, delegate, useProdEnv: false }) again. This is the refresh path: the session stays open and a mounted panel carries on untouched. Do not stop first. See the Web SDK lifecycle. | next Page ID |
| Application closed, destroy | await SDK.userDidStopWatchingEvent({ pageId }). Unwinds every open scope innermost-first, running the panel cleanup for you if one is still mounted, and removes the overlay container. | current Page ID |
// Open application
SDK.configure({ siteID: "69b2f133e8117ec6536d59a1" });
// Select content
const event = await SDK.userDidStartWatchingEvent({
pageId: "6aa966d669342216ada5a4b8",
delegate,
useProdEnv: false,
});
// Select other content: call start again, the session refreshes in place
const next = await SDK.userDidStartWatchingEvent({ pageId: nextPageId, delegate, useProdEnv: false });
// Application closed: stop unwinds the session, including a mounted panel
await SDK.userDidStopWatchingEvent({ pageId: nextPageId });
Tech stack
One SDK per platform, each built with that platform's native UI toolkit. Versions below are the minimums the current SDKs are built against; the sample apps use the same or newer.
- Swift (tvOS)
- Kotlin (Android TV / Fire TV)
- BrightScript (Roku)
- JavaScript (Web, Samsung, LG)
| Language | Swift 5 |
| UI framework | SwiftUI. MaestroPanel and MaestroOverlay are SwiftUI views. UIKit apps host them in a UIHostingController. |
| Minimum OS | tvOS 18.0 |
| Toolchain | Xcode 16 |
| SDK package | MaestroKit, distributed as a binary XCFramework through Swift Package Manager from github.com/lessthan3/MaestroKit.swift. No CocoaPods. Current release 10.8.x. |
| Dependencies | rive-ios 6.12 or later, resolved automatically by SPM. Manual framework installs must add it by hand. |
| App requirements | None. No Info.plist keys, entitlements, privacy manifest, or font registration. |
| Language | Kotlin 2.1 or later |
| UI framework | Jetpack Compose. MaestroPanel and MaestroOverlays are composables. There is no View-based API. Compose BOM 2025.10 or later. |
| Minimum OS | Android 5.0, API 21. Compile and target SDK 36. |
| Toolchain | Android Gradle Plugin 8.13, Gradle 8.13, Java 17 source and target, jvmTarget 17. |
| SDK package | com.lessthan3:maestropanel, an AAR on GitHub Packages at maven.pkg.github.com/lessthan3/MaestroKit.android. Requires a GitHub personal access token with the read:packages scope. Current release 4.0.x. |
| Dependencies | Core library desugaring is mandatory: com.android.tools:desugar_jdk_libs 2.1.2 or later with isCoreLibraryDesugaringEnabled = true. Transitive dependencies resolve from Maven Central and JitPack, so both repositories must be declared. |
| App requirements | None for the SDK. INTERNET permission and ProGuard consumer rules ship inside the AAR. Android TV store listing still needs the usual leanback launcher and banner entries. |
| Language | BrightScript with SceneGraph XML |
| UI framework | SceneGraph. The SDK is a ComponentLibrary loaded at runtime; the panel is the MaestroPanelLib:MaestroPanel node. |
| Minimum OS | Not pinned by the SDK. The SDK and sample apps declare rsg_version=1.2. |
| Toolchain | Nothing is compiled into your channel; no BrighterScript is required. The sample apps use the VS Code BrightScript extension and brighterscript for sideloading. A device in developer mode on the same network. |
| SDK package | Remote package at https://roku-sdk.us-central1-master.gcp.maestro.io/<version>/maestrokit.pkg, fetched by URL when the channel runs. Nothing is vendored into your repo. Current release 4.5.x; the sample pins 4.1.16. |
| Dependencies | None on your side. |
| App requirements | Nothing in the manifest. siteID plus pageID or eventID in the config object. |
| Language | JavaScript or TypeScript. Type definitions ship in the package. |
| UI framework | React 16, 17, 18, or 19 as peer dependencies; the SDK does not bundle React. Preact works through preact/compat aliases and is how the Tizen and webOS sample builds are produced. |
| Minimum runtime | Node.js 18 for the build. Browser targets follow the TV platforms: Tizen and webOS remote key codes are handled natively. |
| Toolchain | Any bundler. ESM and CommonJS builds ship in the package, plus a UMD build that expects window.React and window.ReactDOM. Next.js needs reactStrictMode: false. |
| SDK package | @maestro_io/maestro-web-sdk on npm. Current release 6.x. |
| Dependencies | Bundled inside the package: Rive WebGL2 runtime, Firebase for overlays, Axios. No CSS import is needed; styles are injected by the bundle. |
| App requirements | A host element for the panel. Overlays create their own container on document.body. Tizen packaging rejects hyphens in the application id; use the tv-samsung profile. |
Start with Hello World.