Skip to main content

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.

Video node

Roku’s native Video SceneGraph node plays HLS streams with streamFormat = "hls" and live = true

Channel banner

Info banner shows channel name, number, logo, and live status — auto-hides after 3 s

Number overlay

Digit-key input accumulates a channel number and tunes to it on commit

Watchdog timer

Freeze detection fires every 10 s; restarts the stream if position has not advanced

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.
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 schemeOutput
hls://http://...http://... (pass-through)
hls://...http://...
hlss://https://...https://... (pass-through)
hlss://...https://...
Any otherUnchanged

Channel index persistence

Every time a channel is tuned, its index is written to the Registry:
GTV_RegSaveLastChannelIndex(idx)
' key: REG_KEY_CHANNEL = "lastChannelIndex"
On the next launch, MainScene reads this value to resume the last channel.

Buffer and startup

ConstantValueDescription
CHANNEL_BUF_MS1200 msInitial 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:
ConstantValueDescription
BANNER_MS3000 msTime banner stays fully visible after appearing
BANNER_FADE_MS300 msFade-out animation duration
The banner updates statusText reactively based on player state:
Player stateBanner 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:
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:
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:
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
ConstantValueDescription
FREEZE_CHECK_MS10000 msWatchdog 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:
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:
if key = "options"
    return false
end if
Do not change the options key handler to return true. Intercepting the OPTIONS key fails Roku certification.

Key input summary

KeyAction
upNext channel (index + 1, wraps)
downPrevious channel (index − 1, wraps)
leftOpen channel list overlay
09Accumulate in number overlay
OK (first press)Show banner, arm settings shortcut
OK (second press)Open settings screen
backExit player
optionsNot intercepted — passed to OS