> ## 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.

# Player

> Live stream playback, overlays, freeze detection, error handling, and ad viewport integration in PlayerScreen.

## Overview

`PlayerScreen` manages live HLS stream playback using Roku's built-in `Video` node. It layers an info banner, a channel number badge, and ad renderers on top of the video, and handles errors, reconnection, and network loss gracefully.

<CardGroup cols={2}>
  <Card title="Video node" icon="play">
    Roku's native `Video` SceneGraph node plays HLS streams with `streamFormat = "hls"` and `live = true`
  </Card>

  <Card title="Channel banner" icon="rectangle-horizontal">
    Info banner shows channel name, number, logo, and live status — auto-hides after 3 s
  </Card>

  <Card title="Number overlay" icon="hashtag">
    Digit-key input accumulates a channel number and tunes to it on commit
  </Card>

  <Card title="Watchdog timer" icon="eye">
    Freeze detection fires every 10 s; restarts the stream if position has not advanced
  </Card>
</CardGroup>

***

## Starting playback

Playback is initiated by `PlayChannel(idx)`, called either from `onChannelIndexChanged()` (when `MainScene` sets `m.top.channelIndex`) or from user navigation keys inside the player.

```brightscript theme={null}
sub PlayChannel(idx as Integer)
    channels = m.global.channelList
    ch = channels[idx]

    m.currentIndex = idx
    m.retryCount   = 0

    ' Save resume position to Registry
    GTV_RegSaveLastChannelIndex(idx)
    m.global.currentChannelIndex = idx
    m.global.currentChannelId    = ch.contentId

    playUrl = GTV_NormalizePlayerUrl(ch.url)
    content              = CreateObject("roSGNode", "ContentNode")
    content.url          = playUrl
    content.streamFormat = "hls"
    content.live         = true

    m.video.content = content
    m.video.control = "play"
end sub
```

### URL normalization

`GTV_NormalizePlayerUrl` rewrites non-standard URL schemes:

| Input scheme         | Output                       |
| -------------------- | ---------------------------- |
| `hls://http://...`   | `http://...` (pass-through)  |
| `hls://...`          | `http://...`                 |
| `hlss://https://...` | `https://...` (pass-through) |
| `hlss://...`         | `https://...`                |
| Any other            | Unchanged                    |

### Channel index persistence

Every time a channel is tuned, its index is written to the Registry:

```brightscript theme={null}
GTV_RegSaveLastChannelIndex(idx)
' key: REG_KEY_CHANNEL = "lastChannelIndex"
```

On the next launch, `MainScene` reads this value to resume the last channel.

***

## Buffer and startup

| Constant         | Value     | Description                                  |
| ---------------- | --------- | -------------------------------------------- |
| `CHANNEL_BUF_MS` | `1200` ms | Initial buffer target before playback begins |

During the buffering state, the spinner overlay is shown and the banner displays `"Reconectando la transmisión"`.

***

## Overlay system

### Channel banner

The `channelBanner` node (`m.banner`) displays channel metadata and a live/status label. Timing constants:

| Constant         | Value     | Description                                     |
| ---------------- | --------- | ----------------------------------------------- |
| `BANNER_MS`      | `3000` ms | Time banner stays fully visible after appearing |
| `BANNER_FADE_MS` | `300` ms  | Fade-out animation duration                     |

The banner updates `statusText` reactively based on player state:

| Player state               | Banner text                     |
| -------------------------- | ------------------------------- |
| `buffering`                | `"Reconectando la transmisión"` |
| `playing`                  | `"En vivo"`                     |
| Channel switch in progress | `"Cambiando canal..."`          |
| Network lost               | `"Sin conexión"`                |
| Stream error               | `"Error de transmisión"`        |

### Number overlay

The number overlay (`m.numberOverlay`) accumulates digit key presses. When the user stops pressing digits, `numberCommitted` fires and `OnNumberCommitted()` looks up the channel by number and calls `PlayChannel`:

```brightscript theme={null}
sub OnNumberCommitted()
    committed = m.numberOverlay.numberCommitted
    target = Val(committed)
    idx = FindChannelIndexByNumber(target)
    if idx >= 0
        PlayChannel(idx)
    else
        m.banner.statusText = "Canal " + committed + " no disponible"
    end if
end sub
```

***

## Error handling and reconnection

### Automatic retry

When `OnVideoError()` fires and the error is not an auth/inactive signal, `PlayerScreen` retries up to `RETRY_MAX = 3` times with a 2-second delay between attempts:

```brightscript theme={null}
if m.retryCount < AppConstants().RETRY_MAX
    m.retryCount = m.retryCount + 1
    retryTimer.duration = 2
    retryTimer.control  = "start"
else
    ShowError(GTV_BuildStreamErrorMessage(errCode, errMsg))
end if
```

After `RETRY_MAX` failures, an error dialog is shown with **Retry** and **Back** options.

### Stream restart

`RestartStream()` clears the `Video` node's content, reassigns it, and calls `play` again — it does not change the channel.

### Auto-recoverable errors

`GTV_IsAutoRecoverableStreamError` classifies timeout, network, and HTTP response errors as auto-recoverable. When network connectivity is restored (via `ConnectivityTask`), any open error dialog of this type is automatically dismissed and the stream restarts.

### Auth/inactive errors from stream

`GTV_IsInactiveStreamError` inspects the error code and message for inactive-account signals (HTTP 401/403 with `inactivo`, `inactive`, `subscriberDisabledReason`, etc.). If detected:

1. Playback is stopped.
2. `m.global.authReasonCode` is set to `AUTH_REASON_INACTIVE` or `AUTH_REASON_PASSWORD_CHANGED`.
3. `m.top.userInactive = true` is set — `MainScene` handles the re-login flow.

***

## Freeze detection

The watchdog timer fires every `FREEZE_CHECK_MS = 10000` ms while the video state is `"playing"`. It compares the current playback position against the last recorded position. If they are equal (and the position is valid), the stream is considered frozen and `RestartStream()` is called:

```brightscript theme={null}
sub OnWatchdog()
    state = m.video.state
    if state <> "playing" then return

    currentPos = m.video.position
    if currentPos = m.lastPosition and m.lastPosition >= 0
        GTV_Warn("PlayerScreen", "Stream frozen at position " + currentPos.ToStr() + " - restarting")
        RestartStream()
    end if
    m.lastPosition = currentPos
end sub
```

| Constant          | Value      | Description                                  |
| ----------------- | ---------- | -------------------------------------------- |
| `FREEZE_CHECK_MS` | `10000` ms | Watchdog timer interval for freeze detection |

***

## Network handling

A `netCheckTimer` fires periodically during playback. It reads both `GTV_IsOnline()` (network interface up) and `m.global.hasInternet` (real internet reachability). Either failure puts the player into the offline state:

1. Video is stopped.
2. An offline dialog is shown with a **Retry** button.
3. `ConnectivityTask` is triggered for an immediate reachability probe.

When connectivity is restored, `TryRecoverDialogsAfterNetworkRestore()` hides the offline dialog and restarts the stream.

***

## Interaction with ads (viewport reduction)

When `AdManager` activates a Format C ad, it emits `videoHeightReduction` and `videoOffsetY` fields. `PlayerScreen` observes these and adjusts the `Video` node's geometry accordingly:

```brightscript theme={null}
sub OnVideoHeightReduction()
    ApplyVideoViewportState()
    SyncAdViewport()
end sub

sub ApplyVideoViewportState()
    reduction = m.adManager.videoHeightReduction
    offsetY   = m.adManager.videoOffsetY
    newH = m.videoBaseHeight - reduction
    if newH < 480 then newH = 480  ' minimum video height
    m.video.translation = [m.videoBaseX, m.videoBaseY + offsetY]
    m.video.height = newH
end sub
```

After adjusting the video, `SyncAdViewport()` pushes the updated viewport rect back to `AdManager` so ad renderers can reposition themselves relative to the live video area.

If all ads are hidden (`allAdsHidden` event from `AdManager`), the video is restored to its base dimensions.

***

## OPTIONS key handling

The `options` key is explicitly **not intercepted** — it returns `false` to let the Roku OS handle it. This is a certification requirement:

```brightscript theme={null}
if key = "options"
    return false
end if
```

<Warning>
  Do not change the `options` key handler to `return true`. Intercepting the OPTIONS key fails Roku certification.
</Warning>

***

## Key input summary

| Key                 | Action                              |
| ------------------- | ----------------------------------- |
| `up`                | Next channel (index + 1, wraps)     |
| `down`              | Previous channel (index − 1, wraps) |
| `left`              | Open channel list overlay           |
| `0`–`9`             | Accumulate in number overlay        |
| `OK` (first press)  | Show banner, arm settings shortcut  |
| `OK` (second press) | Open settings screen                |
| `back`              | Exit player                         |
| `options`           | **Not intercepted** — passed to OS  |
