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

# App flow

> Step-by-step walkthrough of GlobalTV Roku execution: from main.brs entry to live channel playback.

This page traces every step the application takes from the moment the Roku OS launches the channel to the point where live video is playing and ads are polling.

## Launch sequence

<Steps>
  <Step title="main.brs entry point">
    The Roku OS calls `Main(args)` in `source/main.brs`. The function:

    1. Creates a `roSGScreen` and a shared `roMessagePort`.
    2. Creates a `roInput` object on the same port to receive warm-start deep links.
    3. Creates a `roDeviceInfo` and `roAppMemoryMonitor` and enables memory warning events.
    4. Calls `screen.CreateScene("MainScene")` — this is the single instantiation of the root scene.
    5. Observes `exitRequested` on the scene so the event loop can close the screen.
    6. Calls `screen.Show()` to start rendering.
    7. Checks the launch `args` for a valid live deep link before entering the event loop.

    ```brightscript theme={null}
    sub Main(args as Dynamic)
        screen = CreateObject("roSGScreen")
        port   = CreateObject("roMessagePort")
        screen.SetMessagePort(port)

        input = CreateObject("roInput")
        input.SetMessagePort(port)

        scene = screen.CreateScene("MainScene")
        scene.ObserveField("exitRequested", port)
        screen.Show()

        if args <> invalid
            if GTV_IsLiveDeepLink(args)
                scene.launchDeepLink = args
            end if
        end if
        ' ... event loop
    end sub
    ```

    Deep link validation requires both `contentId` (non-empty string) and `mediaType = "live"` (case-insensitive). Any other `mediaType` is silently ignored.
  </Step>

  <Step title="MainScene initialisation">
    `MainScene.init()` in `components/MainScene.brs` runs synchronously on the render thread:

    1. Calls `GTV_InitGlobalState()` — adds all `m.global` fields with their default values.
    2. Calls `ApplySceneLayout()` — sizes the background `Rectangle` to fill the screen.
    3. Creates a relayout timer (100 ms, one-shot) and a focus-repair timer (50 ms, one-shot).
    4. Creates a session-auth timer (`SESSION_AUTH_CHECK_MS` = 420 s, repeating) for periodic credential re-validation.
    5. Initialises all screen references to `invalid` and overlay state to `"player_only"`.
    6. Calls `ShowSplash()`.
  </Step>

  <Step title="Splash screen">
    `ShowSplash()` creates a `SplashScreen` child node. When the splash timer fires, the component sets `splashDone = true`, which triggers `OnSplashDone()` in `MainScene`.

    `OnSplashDone()` checks the registry for saved credentials:

    * **Credentials found** → calls `RunAutoLogin(username, password)`.
    * **No credentials** → calls `ShowOnboarding()`.
  </Step>

  <Step title="Auth decision: auto-login vs onboarding">
    **Auto-login path** — `RunAutoLogin()` creates an `AuthTask` with the saved username and password and shows a loading label. The task authenticates against `/auth/{user}/{pass}` with a `TIMEOUT_AUTH` (15 s) timeout. On completion `OnAuthDone()` handles the result.

    **Onboarding path** — `ShowOnboarding()` creates an `OnboardingScreen`. When the user presses the continue button, `OnGoToLogin()` → `ShowLogin()` creates a `LoginScreen`.

    `LoginScreen` first calls `ChannelStore.getUserData` with `requestedUserData = "email"` to obtain the Roku account email (RFI — Roku First-party Identity). Once the RFI resolves (or times out), the user enters credentials via `StandardKeyboardDialog` and `AuthTask` runs.

    Auth failure codes are classified by `GTV_AuthClassifyFailure()`:

    | Code | Meaning           | Action                            |
    | ---- | ----------------- | --------------------------------- |
    | 460  | Network down      | Show offline error                |
    | 401  | Wrong credentials | Re-prompt login                   |
    | 470  | Account inactive  | Show inactivity dialog with retry |
    | 471  | Password changed  | Clear credentials, force re-login |
  </Step>

  <Step title="Handshake">
    Before the first login attempt (when no credentials exist), `RunHandshake()` creates a `HandshakeTask`. The task iterates the server list from `AppConstants().SERVER_LIST`, calling `GET /health` on each with a `TIMEOUT_HEALTH` (2500 ms) timeout and up to `SERVER_LAN_FORCE_RETRIES` (3) retries on LAN addresses.

    The first server that responds successfully is written to `m.global.activeServer`. On `OnHandshakeDone()`, MainScene proceeds to the appropriate next step based on the `handshakeContext` field (`"autologin"` or `"onboarding"`).

    <Note>
      Handshake is skipped when auto-login runs from saved credentials — the active server is either read from the registry (`lastServer`) or resolved lazily by `AuthTask` itself.
    </Note>
  </Step>

  <Step title="Playlist loading">
    `OnAuthSuccess()` starts the session-auth timer and calls `RunPlaylistTask("startup")`. The task:

    1. Downloads `GET /auth/{user}/{pass}/playlist/m3u8/hls`.
    2. Parses the M3U8 file via `M3UParser.brs` into a flat `channels` array and a `categories` map.
    3. Writes results to `m.global.channelList` and `m.global.categories`.
    4. Sets `done = true` on the task node.

    `OnPlaylistDone()` in MainScene handles the result. If a launch deep link was queued in `m.pendingDeepLink`, it resolves the channel index now and calls `ShowPlayer(idx)` directly. Otherwise it calls `ResolveStartupChannelIndex()` which honours the `lastChannelIndex` registry value.
  </Step>

  <Step title="MainScreen display">
    `ShowPlayer(channelIndex)` always ensures MainScreen exists before creating PlayerScreen. If `m.mainScreen` is `invalid`, `ShowMainScreen()` is called first:

    * Creates a `MainScreen` child node.
    * Sets `channels`, `categories`, and `initialIndex` fields.
    * Observes `channelSelected`, `openSettings`, `closeOverlay`, and `requestExit`.

    When `MainScreen.onChannelsChanged()` fires it calls `SignalAppLaunchCompleteOnce()`:

    ```brightscript theme={null}
    sub SignalAppLaunchCompleteOnce()
        scene = m.top.GetScene()
        if scene <> invalid
            if m.global.appLaunchBeaconSent <> true
                scene.signalBeacon("AppLaunchComplete")
                m.global.appLaunchBeaconSent = true
            end if
        end if
    end sub
    ```

    This satisfies the Roku certification requirement that `AppLaunchComplete` is sent exactly once, after content is ready to display.
  </Step>

  <Step title="PlayerScreen and live playback">
    `ShowPlayer()` creates a `PlayerScreen` node inserted *below* MainScreen in the scene tree (so MainScreen can render on top as an overlay when needed). On init, PlayerScreen:

    1. Finds the `Video` node (`videoPlayer`) and calls `ApplyResponsiveLayout()` to set its viewport.
    2. Spawns `ConnectivityTask`, `AdsPollingTask`, and `MetricsTask`.
    3. Observes `m.adManager.videoHeightReduction` and `videoOffsetY` for Format C ad viewport compression.
    4. Calls `PlayChannel(channelIndex)` via the `channelIndex` field observer.

    `PlayChannel()` builds a `ContentNode` with `url`, `streamFormat = "hls"`, and `live = true`, then sets `m.video.control = "play"`. The watchdog timer (`FREEZE_CHECK_MS` = 10 s) monitors `m.video.position` to detect frozen streams and restarts them automatically.
  </Step>

  <Step title="Ad polling">
    `AdsPollingTask` runs continuously while PlayerScreen is active:

    1. **Handshake** — POST to `ADS_PATH_HANDSHAKE` (`/app/devices/handshake`) with device identity.
    2. **Poll loop** — GET `ADS_PATH_ACTIVE` (`/app/ads/active`) every `ADS_POLL_MS` (10 000 ms).
    3. On each response, the task sets `adsSnapshot` which triggers `OnAdsSnapshot()` in PlayerScreen.
    4. PlayerScreen matches the snapshot to the current channel via `GTV_IsSnapshotForCurrentChannel()` and forwards it to `AdManager`.
    5. `AdManager` renders the appropriate format. If Format C is active it emits a `videoHeightReduction` value that shrinks the `Video` node height.
  </Step>
</Steps>

## Overlay flow

When the user presses **left** during playback, `OnRequestOverlay()` makes MainScreen visible with `overlayMode = true`. Key events route through `PlayerScreen.RouteOverlayKey()` → `MainScreen.HandleOverlayKey()`. Pressing **OK** on a channel commits `channelSelected`, which triggers `OnChannelSelected()` in MainScene and calls `m.playerScreen.channelIndex = idx`.

Pressing **Back** or selecting the same channel dismisses the overlay via `OnCloseOverlay()` → `HideOverlayState()`.

## Deep link handling

GlobalTV supports `mediaType=live` deep links at two points in the lifecycle:

**Launch deep link (cold start)** — detected in `main.brs` before the event loop:

```brightscript theme={null}
if args <> invalid
    if GTV_IsLiveDeepLink(args)
        scene.launchDeepLink = args
    end if
end if
```

Setting `scene.launchDeepLink` triggers `onLaunchDeepLink()` in MainScene (via `onChange`), which stores the link in `m.pendingDeepLink`. The link is consumed in `OnPlaylistDone()` once channels are available.

**Input deep link (warm start)** — received as `roInputEvent` in the `main.brs` event loop:

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

Setting `scene.inputDeepLink` triggers `onInputDeepLink()` → `ProcessDeepLink()`, which resolves the `contentId` to a channel index and calls `ShowPlayer(idx)` immediately if channels are loaded, or queues it in `m.pendingDeepLink` otherwise.

## Session auth re-validation

After a successful login, MainScene starts a repeating timer (`SESSION_AUTH_CHECK_MS` = 420 s ≈ 7 minutes). On each fire, `OnSessionAuthTimer()` creates a new `AuthTask` with the saved credentials. If the task fails with a classified reason code, MainScene shows the appropriate session-issue dialog and stops playback.

<Warning>
  The session timer is stopped whenever a handshake, auth, or playlist task is already in flight to avoid overlapping requests.
</Warning>

## Exit flow

Any screen can set `requestExit` to trigger `OnRequestExit()`, which creates a `ConfirmExitDialog`. On confirmation, `m.top.exitRequested` is incremented, which the `main.brs` event loop detects via `roSGNodeEvent` and calls `screen.Close()`.
