Roku Integration Skill

Integration conventions for the StreamLayer Roku SDK that keep an AI agent's diff small and correct — copy in the SLSDK module, place one SLView over your Video, initialize with apiKey + sdkUri, drive pause ads, observe visibility, hand over the remote via the focus field, and the minimal-diff rules for a BrightScript / SceneGraph channel.

StreamLayer Roku SDK Integration

How to wire StreamLayer into an existing Roku channel cleanly. Follow the
rules — they prevent the common over-integration mistakes on BrightScript /
SceneGraph.

This page is the human-readable form of the streamlayer-roku-integration agent
skill. Reference it from your AI agent alongside the streamlayer-docs MCP — see
Integrate with an AI Agent.

📘

BrightScript & SceneGraph

You integrate through a small, self-contained SLSDK module (SLView +
SLManager) that you copy into your channel. It hosts the StreamLayer UI
bundle — loaded dynamically at runtime — and exposes one SLView node you
drive with callFunc
and observable fields. You never reach into the SDK's internal components.

Pick the package

  • SLSDK module — the integration surface: a self-contained folder
    (SLView + SLManager) you copy into your channel's components/. SLView
    is the node your app talks to; SLManager loads the SDK and drives it. Copy it
    as-is from the roku-sdk-sample.
  • StreamLayerSDK.pkg — the StreamLayer UI bundle (the StreamLayer Element).
    It is CDN-hosted and loaded dynamically at runtime from the sdkUri you
    pass to initialize (via a SceneGraph ComponentLibrary), not compiled in — so
    sdkUri is a required init parameter, kept in settings/config rather than
    hardcoded. Point it at the StreamLayer CDN URL in production; you can override it
    with a locally served package for debugging.
  • Underlying node: internally SLManager creates a StreamLayerSDK:StreamLayer
    node and calls initSdk on it. You normally stay at the SLView layer; the
    raw node's methods (initSdk, setFocus, showPauseAd, …) are documented in
    the API Reference.
  • Requirements: Roku OS 11.5+, a SceneGraph channel, and a Video node that
    exposes control and state.

Rules (do this)

  1. Copy in the SLSDK module; place one SLView over your Video. Copy the
    self-contained SLSDK folder into components/. In the SceneGraph component
    that owns your <Video>, add <SLView id="SLView" visible="false" /> as a
    sibling rendered over the video. One SLView per player screen — not in the
    main scene / app root, and not one per interactive feature.

    • ❌ Placing SLView in MainScene / a global, away from the player.
    • <SLView id="SLView" visible="false" /> beside <Video> in the player screen.
  2. Initialize once with initialize, passing apiKey, sdkUri and your
    Video.
    Call initialize a single time; it loads the UI bundle from
    sdkUri and starts the SDK. Then bind the event with setEvent(eventId), and
    re-call setEvent whenever playback switches content. Keep apiUrl / sdkUri
    in your settings or globals, not inline literals.

    • ❌ Omitting sdkUri (nothing loads); calling initialize more than once per SLView.
    • initialize({ apiKey, sdkUri, playerRef: m.player, … })setEvent("SL_EVENT_ID_HERE").
    m.slView = m.top.findNode("SLView")
    m.slView.callFunc("initialize", {
        apiKey: "SL_SDK_KEY_HERE"
        sdkUri: getGlobal("sdkUri")
        playerRef: m.player
        apiUrl: getGlobal("apiUrl")
        isLoggingEnabled: true
        isAnalyticsEnabled: true
        isVastModeEnabled: false
    })
    m.slView.callFunc("setEvent", "SL_EVENT_ID_HERE")
  3. Drive ads and playback through SLView; observe state, don't poll.
    Call setPauseState(true/false) when the host player pauses/resumes. Close a
    pause ad with closePauseAd(), and clear all surfaces with closeOverlay()
    (for example on the Back key). Observe isResumeRequested and resume
    your own Video when it flips true — the SDK signals resume, it does not
    control your player. Observe isPromoVisible / isNotificationVisible /
    isPauseAdVisible to coordinate host UI. Use observeField, never a polling
    loop.

    m.slView.observeField("isResumeRequested", "onResumeRequested")
    
    sub onResumeRequested(event as object)
        if event.getData() = true then m.player.control = "resume"
    end sub

    There are two entry points, one per lifecycle, and which one you call
    decides who owns the stream:

    showPauseAd(params)showAd(params)
    Streamalready stopped by your appstill playing
    Who resumes ityour app, when the viewer leavesthe ad card, by itself
    ShapesPauseVastFullBleed, PauseVastAd, PauseAdSidebar11, PauseAdSidebar21SideBar21, LBar21, SideBarImageOnly, LBarImageOnly, SideBySide
    Closed withclosePauseAd()closeOverlay()

    Both take { vastUrl, type, isNotificationEnabled }: the tag to load, the shape
    to draw, and whether to tease the ad with a notification the viewer opens it
    from. Omitting type leaves the choice to the SDK, which picks a shape from the
    creative. In full mode showPauseAd() takes nothing and the SDK serves its own
    pause ad.

    • showPauseAd(vastUrl) — the bare-URL form is gone; pass an object.
    • ❌ Asking for a standard shape through showPauseAd, or a pause shape through showAd.
  4. Wait for isEventReady before asking for an ad. Resolving the event takes a
    network round trip, so setEvent returns long before the SDK can serve
    anything. showAd called earlier is ignored — observe isEventReady and
    ask from there.

    • ❌ Calling showAd on the line after setEvent.
    • m.slView.observeField("isEventReady", "onEventReady"), then ask inside it.
    m.slView.observeField("isEventReady", "onEventReady")
    
    sub onEventReady()
        m.slView.callFunc("showAd", {
            vastUrl: getGlobal("vastUrl")
            type: "LBar21"
        })
    end sub
  5. Hand over the remote through the focus field, gated on visibility. Give
    the overlay the remote with m.slView.focus = true, and take it back with
    m.slView.focus = false — but only give focus while m.slView.visible is
    true, otherwise the remote strands on a hidden node. On Back with an
    overlay up, call closeOverlay() and return focus to your own UI. Route focus
    through SLView; don't call setFocus on the SDK's internal nodes.

    • m.slView.focus = true while it's hidden; trapping the remote while a surface is up.
    • if m.slView.visible then m.slView.focus = m.top.focus.
  6. Auth is automatic; keep credentials and the diff simple. On Roku there is
    no separate login call and no token to forward — pass the apiKey to
    initialize and the SDK authenticates anonymously itself. The SDK API Key is a
    public, client-side value from StreamLayer Studio
    — a manifest or settings value is fine; don't add secret scaffolding or a
    login screen. Keep the diff minimal: drop in SLSDK, add one SLView, and
    wire only the callFunc methods and observers your features use — don't
    restructure the channel or wrap the player screen. Tearing the SDK down with
    disposeSdk() makes it inactive; call initialize() again before returning to
    a StreamLayer-enabled screen.

Gotchas

  • Nothing renders / blank overlay. The UI bundle is loaded from sdkUri at
    runtime — a wrong or missing sdkUri (or apiKey) means nothing loads. Also
    check that setEvent ran and the Video node exposes control and state.
  • A callFunc that does nothing is not a crash, and may not even be logged.
    A method that exists but is unavailable in the current mode (or before
    initialize) is a logged no-op — look for the reason in the console. But a
    callFunc naming a function the component does not declare is silent: Roku
    neither throws nor logs, the call simply does nothing. If a call has no effect
    and no log line, check the spelling and that the function is declared on the
    node's interface before suspecting your logic.
  • Ads asked for too early are dropped. showAd before isEventReady is
    ignored — the SDK has no event to attach the ad to yet.
  • Pause-ad mode matters. In full mode showPauseAd() takes nothing and the SDK
    serves its own pause ad; in VAST mode it takes { vastUrl, type, isNotificationEnabled }. setVastModeEnabled switches modes at runtime but only
    affects the next request.
  • Left-paused stream. You own playback — resume your Video when
    isResumeRequested flips true. The SDK signals; it won't resume for you.
  • Focus stranded on a hidden node. Only hand SLView focus while it's
    visible; guard every m.slView.focus = true with an m.slView.visible check.
  • Stay at the SLView layer. Talk to SLView (or, if you skip the wrapper,
    the StreamLayer node's callFunc); don't reach into PromoPresenter /
    NotificationPresenter / PauseAdPresenter or the SDK's managers — they're
    internals.
  • Authoritative API details: the streamlayer-docs MCP (search / fetch) and
    the API Reference.

Related