> ## Documentation Index
> Fetch the complete documentation index at: https://globaliptv.misidev.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deep linking

> How GlobalTV Roku handles Roku deep links to launch directly into live content, both at cold start and while the app is already running.

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.

## What a valid deep link looks like

A deep link payload on Roku is an associative array with at least two fields:

| Field       | Required value                           |
| ----------- | ---------------------------------------- |
| `mediaType` | `"live"` (case-insensitive)              |
| `contentId` | Non-empty string identifying the channel |

Any other `mediaType` value, or an empty `contentId`, is silently ignored.

## Validation: `GTV_IsLiveDeepLink()`

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:

```brightscript theme={null}
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

<CardGroup cols={2}>
  <Card title="launchDeepLink" icon="power-off">
    Set at cold start from the `args` passed to `Main()`. The channel was not running when the link arrived.
  </Card>

  <Card title="inputDeepLink" icon="bolt">
    Set while the app is already running, via an `roInput` event. The channel handles the link without restarting.
  </Card>
</CardGroup>

### Cold start: `launchDeepLink`

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`:

```brightscript theme={null}
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:

```brightscript theme={null}
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
```

### Warm start: `inputDeepLink`

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`:

```brightscript theme={null}
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:

```brightscript theme={null}
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
```

## How MainScene processes a deep link

`ProcessDeepLink()` in `components/MainScene.brs` handles both cases after the initial validation:

```brightscript theme={null}
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
```

<Steps>
  <Step title="Validate payload">
    `ProcessDeepLink()` re-validates the payload with `IsLiveDeepLinkPayload()`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Resolve channel index">
    `ResolveDeepLinkChannelIndex()` looks up the `contentId` in the loaded channel list.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Manifest requirement

For warm-start deep links to work, the channel manifest must declare input launch support:

```text theme={null}
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.

<Note>
  `supports_input_launch=1` is already present in the GlobalTV manifest and is one of the fields verified by the `make check` certification target.
</Note>

## Testing deep links with the Roku ECP

You can send a test deep link to a sideloaded channel using the [Roku External Control Protocol (ECP)](https://developer.roku.com/docs/developer-program/debugging/external-control-api.md) from any machine on the same network as the Roku device.

### Cold start (launch)

```bash theme={null}
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)

```bash theme={null}
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.

<Tip>
  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.
</Tip>

<Warning>
  ECP only works on devices in developer mode. The `launch/dev` path targets the sideloaded channel specifically.
</Warning>
