Skip to main content
Roku deep linking lets an external source — another channel, a Roku search result, or a voice command — launch GlobalTV directly to a specific piece of content, bypassing the channel grid. GlobalTV supports the mediaType=live deep link type for live channel playback. A deep link payload on Roku is an associative array with at least two fields:
FieldRequired value
mediaType"live" (case-insensitive)
contentIdNon-empty string identifying the channel
Any other mediaType value, or an empty contentId, is silently ignored. All deep link validation is centralised in source/main.brs. The function GTV_IsLiveDeepLink() is the single gate that both entry points pass their payload through before doing anything with it:
function GTV_IsLiveDeepLink(link as Dynamic) as Boolean
    if link = invalid then return false
    if link.contentId = invalid or link.mediaType = invalid then return false

    mediaType = LCase(link.mediaType.ToStr())
    contentId = link.contentId.ToStr()

    if mediaType <> "live" then return false
    if contentId = "" then return false
    return true
end function
The check is deliberately strict: both fields must be present and non-empty, and mediaType must be exactly "live" after lowercasing.

Two entry points

launchDeepLink

Set at cold start from the args passed to Main(). The channel was not running when the link arrived.

inputDeepLink

Set while the app is already running, via an roInput event. The channel handles the link without restarting.
When Roku launches the channel, it passes an args associative array to Main(). If the args represent a valid live deep link, the payload is written to scene.launchDeepLink:
sub Main(args as Dynamic)
    ' ... screen and scene setup ...

    if args <> invalid
        if GTV_IsLiveDeepLink(args)
            scene.launchDeepLink = args
        end if
    end if

    ' ... message loop ...
end sub
Observing launchDeepLink in MainScene triggers onLaunchDeepLink(), which stores the payload as m.pendingDeepLink. The pending deep link is then resolved once the channel list is available after auth and playlist load:
sub onLaunchDeepLink()
    link = m.top.launchDeepLink
    if link = invalid then return
    if not IsLiveDeepLinkPayload(link)
        GTV_Warn("MainScene", "Cold-start deep link ignored (mediaType/contentId invalid)")
        return
    end if
    GTV_Log("MainScene", "Cold-start deep link: contentId=" + link.contentId.ToStr())
    m.pendingDeepLink = link
end sub
If the channel is already running when a deep link arrives, Roku fires an roInputEvent. The main message loop reads the event info and, if valid, writes it to scene.inputDeepLink:
else if type(msg) = "roInputEvent"
    info = msg.GetInfo()
    if info <> invalid
        if GTV_IsLiveDeepLink(info)
            scene.inputDeepLink = info
        end if
    end if
Observing inputDeepLink triggers onInputDeepLink(), which calls ProcessDeepLink() immediately rather than queuing it as pending:
sub onInputDeepLink()
    link = m.top.inputDeepLink
    if link = invalid then return
    if not IsLiveDeepLinkPayload(link)
        GTV_Warn("MainScene", "Warm-start deep link ignored (mediaType/contentId invalid)")
        return
    end if
    GTV_Log("MainScene", "Warm-start deep link: contentId=" + link.contentId.ToStr())
    ProcessDeepLink(link)
end sub
ProcessDeepLink() in components/MainScene.brs handles both cases after the initial validation:
sub ProcessDeepLink(link as Object)
    if not IsLiveDeepLinkPayload(link)
        GTV_Warn("MainScene", "ProcessDeepLink ignored (mediaType/contentId invalid)")
        return
    end if

    channels = m.global.channelList
    if channels = invalid or channels.Count() = 0
        ' Channel list not loaded yet — park the link and process after playlist loads
        m.pendingDeepLink = link
        return
    end if

    idx = ResolveDeepLinkChannelIndex(link, channels)
    if idx >= 0
        GTV_Log("MainScene", "Deep link resolved to index " + idx.ToStr())
        ShowPlayer(idx)
        return
    end if

    GTV_Warn("MainScene", "Deep link: channel " + link.contentId.ToStr() + " not found - going to MainScreen")
end sub
1

Validate payload

ProcessDeepLink() re-validates the payload with IsLiveDeepLinkPayload().
2

Check channel list availability

If the channel list is empty (playlist has not loaded yet), the link is stored as m.pendingDeepLink and processed after OnPlaylistDone() completes.
3

Resolve channel index

ResolveDeepLinkChannelIndex() looks up the contentId in the loaded channel list.
4

Open the player

If a matching channel is found, ShowPlayer(idx) is called directly. If the contentId is not found in the list, the app falls back to the main screen.

Manifest requirement

For warm-start deep links to work, the channel manifest must declare input launch support:
supports_input_launch=1
Without this field, Roku will not route roInputEvent messages to a running channel, and warm-start deep links will never arrive.
supports_input_launch=1 is already present in the GlobalTV manifest and is one of the fields verified by the make check certification target.
You can send a test deep link to a sideloaded channel using the Roku External Control Protocol (ECP) from any machine on the same network as the Roku device.

Cold start (launch)

curl -d '' 'http://<ROKU_IP>:8060/launch/dev?mediaType=live&contentId=<CHANNEL_ID>'
This terminates any running instance of the channel and re-launches it with the given args.

Warm start (input)

curl -d '' 'http://<ROKU_IP>:8060/input/dev?mediaType=live&contentId=<CHANNEL_ID>'
This sends an roInputEvent to the already-running channel without restarting it.
Replace <ROKU_IP> with your device’s IP address (visible under Settings → Network → About on the Roku) and <CHANNEL_ID> with a contentId value that exists in your playlist.
ECP only works on devices in developer mode. The launch/dev path targets the sideloaded channel specifically.