Skip to main content

Hello World with default values

By the end of this page the Maestro SDK is running inside your own app on your target device, rendering the panel for Maestro's demo page, showing an overlay, and moving focus between your UI and the SDK. Overlays, specific panels, authentication, analytics, error handling all come after this.

Everything here uses default values against Maestro's public demo site. You do not need an account, a page of your own, or an event feed.

  • Site ID: 69b2f133e8117ec6536d59a1
  • Page ID: 6aa966d669342216ada5a4b8

When you move to your own site, the Site ID is window.INIT.SITE._id in the browser console on your Maestro site, and the Page ID is shown under the page name in its Settings. The end-to-end sequence these sections implement is drawn in Architecture, and the rule about where the SDK lives in your app is here.

Pick your platform once. The choice follows you through every section on this page.

Start with the sample pull request​

Each platform has an example repo with the app on one branch and the same app with the SDK on another. The pull request between them is the integration, and it is the fastest way to see every change in one diff.

maestro-tvos-sdk-example, pull request 2: main is the app without the SDK, feat/basic-sdk-integration is the app with it.

git clone https://github.com/lessthan3/maestro-tvos-sdk-example.git
cd maestro-tvos-sdk-example && git checkout feat/basic-sdk-integration
open "Hello World.xcodeproj"

Run the scheme on the tvOS Simulator or an Apple TV. Xcode 16, tvOS 18. MaestroKit and its Rive dependency resolve through Swift Package Manager.

Maestro Panel​

Instantiate the SDK once at the app level with the Site ID, configure it for the demo page with the Page ID, and give the panel a place next to your player. The panel renders nothing until the page configuration has loaded.

Add the package https://github.com/lessthan3/MaestroKit.swift in Xcode. Configure in your App initialiser; MaestroManagerDelegate has one method, for analytics events.

Hello_WorldApp.swift
import SwiftUI
import MaestroKit

@main
struct Hello_WorldApp: App {
@State private var viewModel = ContentViewModel()

init() {
MaestroManager.shared.configure(
siteID: "69b2f133e8117ec6536d59a1",
jwt: "",
maestroManagerDelegate: AppDelegate(),
maestroWorkingEnvironment: .prod,
defaultPanel: .stats
)
}

var body: some Scene {
WindowGroup {
ContentView().environment(viewModel).environment(\.colorScheme, .dark)
}
}
}

final class AppDelegate: MaestroManagerDelegate {
func trackAnalyticsEvent(name: String, attributes: [String: String]) {}
}

Start the event when the user picks content. On tvOS the eventID parameter is where the Page ID goes. MaestroEventDelegate is how the SDK talks back; every method is required, and for Hello World only the panel and overlay ones do anything.

ContentViewModel.swift
@MainActor
@Observable
class ContentViewModel: MaestroEventDelegate {
var isShowingPanel = false
private var eventInterface: MaestroEventInterface?

func start() async {
eventInterface = await MaestroManager.shared.userDidStartWatchingEvent(
eventID: "6aa966d669342216ada5a4b8", // the Page ID
delegate: self,
hideBetsPanel: false, hideBetsWagers: false,
disableBetsOverlays: false, disableFantasyOverlays: false
)
isShowingPanel = true
}

func shouldShowPanel() { isShowingPanel = true }
func shouldHidePanel() { isShowingPanel = false }

// Stubs for Hello World
func userRequestedNewKeyPlaysData() {}
func playClip(atIndex index: Int) {}
func shouldShowOverlay(buttonSize: CGSize, overlayType: OverlayType, payload: MaestroOverlayEvent?) async {}
func shouldHideOverlay() async {}
func userViewedPanel(panel: MaestroPanelType) {}
func trackAction(analytics: [String: String]) {}
func trackImpression(analytics: [String: String]) {}
func userRequestedLogin() async {}
func playPauseButtonPressed() {}
func shouldShowPanelType(panel: MaestroPanelType) async {}
}

MaestroPanel() is a SwiftUI view with a fixed width of 676 points. Put it in a trailing column beside your player.

ContentView.swift
HStack(spacing: 0) {
VideoPlayerView().focusSection()
if viewModel.isShowingPanel {
MaestroPanel()
.frame(width: 676)
.transition(.move(edge: .trailing))
}
}
.task { await viewModel.start() }

Maestro Overlay​

Overlays appear over the video. The SDK schedules them against playback timecode, so the host has two jobs: give the overlay a place to render, and feed the current timecode. Without timecode, timecoded overlays never fire.

The SDK asks your delegate to show and hide the overlay. Keep the type and payload it gives you and render MaestroOverlay over the player.

// in ContentViewModel
var overlay: (type: OverlayType, payload: MaestroOverlayEvent?)?

func shouldShowOverlay(buttonSize: CGSize, overlayType: OverlayType, payload: MaestroOverlayEvent?) async {
overlay = (overlayType, payload)
await eventInterface?.didShowOverlay()
}
func shouldHideOverlay() async {
overlay = nil
await eventInterface?.didHideOverlay()
}
// in ContentView, over the player
ZStack {
VideoPlayerView()
if let overlay = viewModel.overlay {
MaestroOverlay(
buttonPosition: CGPoint(x: 1100, y: 100),
overlayType: overlay.type,
payload: overlay.payload
)
}
}

Feed timecode from your player's periodic time observer:

eventInterface?.updatePlayerTimeCode(timeCode: currentTimeMilliseconds)

Focus management​

Focus moves in two directions: your app hands it to the SDK when the user opens the panel or an overlay, and the SDK hands it back when they leave. Your app never forwards key events; each SDK owns input while it has focus.

tvOS focus does the work. The panel applies its own .focusSection(); give your player column one too and the Siri Remote moves between them. When the user backs out of the panel, the SDK calls shouldHidePanel() and your flag hides the column. Tell the SDK when you show or hide the panel yourself:

await eventInterface?.didShowPanel()
await eventInterface?.didHidePanel()

Click overlay, show panel​

An overlay's call to action often opens the panel on the tab it advertises. The SDK drives the transition; your app's job is to make sure the panel is visible and focused when it does.

When the viewer engages an overlay whose action opens a panel, the SDK calls shouldShowPanel() and then shouldShowPanelType(panel:) on your delegate with the target tab. Your shouldShowPanel already shows the column; nothing else is required for the panel to switch.

func shouldShowPanelType(panel: MaestroPanelType) async {
isShowingPanel = true
}

You are done when​

  • The panel renders with the tabs configured for the demo page.
  • An overlay appears over the video and dismisses itself.
  • Focus moves from your UI into the panel and back using the remote or keyboard, and Back closes the panel without leaving the app.
  • Engaging an overlay opens the panel on the advertised tab.
  • The device log or browser console shows no SDK errors. On Roku, Session config rejected on errorInformation means a missing Site ID or Page ID; the authToken and SWID notices are informational for the demo page.

Next: Maestro builds customized panels from your SDK.