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

# Ads

> Ad system overview: polling, handshake, impression tracking, and three format renderers (A, B, C) in GlobalTV Roku.

## Overview

The ad system is composed of four components that work together:

* **`AdsPollingTask`** — background task that handshakes with the ad server and polls for active ad snapshots.
* **`AdManager`** — SceneGraph node on `PlayerScreen` that receives snapshots, reconciles active ad slots, manages expiry, and emits viewport reduction signals.
* **`AdFormatA`, `AdFormatB`, `AdFormatC`** — three SceneGraph nodes that render individual ad creatives according to their layout rules.

<CardGroup cols={2}>
  <Card title="AdsPollingTask" icon="rotate">
    Runs the handshake and polls `ADS_PATH_ACTIVE` every 10 s
  </Card>

  <Card title="AdManager" icon="layer-group">
    Reconciles snapshot data into active render slots; tracks impressions
  </Card>

  <Card title="Format A" icon="rectangle-wide">
    Full-width banner overlaid on the video, anchored to top or bottom
  </Card>

  <Card title="Format B" icon="square">
    Corner badge overlay positioned in one of four screen corners
  </Card>

  <Card title="Format C" icon="table-rows">
    Viewport-reduction banner: shrinks the video area rather than overlaying it
  </Card>

  <Card title="Impression tracking" icon="chart-bar">
    Batch impression events sent to `ADS_PATH_IMPRESSIONS`
  </Card>
</CardGroup>

***

## Ad server configuration

All ad traffic is routed to a dedicated ads backend, separate from the main application server.

```brightscript theme={null}
ADS_API_BASE_URL   : "https://ads.globaltv.lat/api/v1"
ADS_USE_DEDICATED  : true
ADS_HANDSHAKE_ENABLED : true
```

| Constant                | Value                             | Description                                              |
| ----------------------- | --------------------------------- | -------------------------------------------------------- |
| `ADS_API_BASE_URL`      | `https://ads.globaltv.lat/api/v1` | Base URL for all ad endpoints                            |
| `ADS_USE_DEDICATED`     | `true`                            | Use dedicated ads server (not main server)               |
| `ADS_HANDSHAKE_ENABLED` | `true`                            | Run the device handshake on startup                      |
| `ADS_FLOW_DIAG`         | `false`                           | Diagnostic logging for the ad flow (disabled by default) |

When `ADS_USE_DEDICATED = false`, all ad requests fall back to `m.global.activeServer` (the main app server).

***

## Endpoints

| Constant               | Path                            | Method | Purpose                              |
| ---------------------- | ------------------------------- | ------ | ------------------------------------ |
| `ADS_PATH_HANDSHAKE`   | `/app/devices/handshake`        | POST   | Device registration on session start |
| `ADS_PATH_ACTIVE`      | `/app/ads/active`               | GET    | Active ad snapshot poll              |
| `ADS_PATH_IMPRESSIONS` | `/app/impressions/events/batch` | POST   | Batch impression event upload        |

Full URLs are constructed as:

```text theme={null}
https://ads.globaltv.lat/api/v1/app/devices/handshake
https://ads.globaltv.lat/api/v1/app/ads/active?device_id=...&stream_position=...
https://ads.globaltv.lat/api/v1/app/impressions/events/batch
```

***

## Handshake flow

`TryAdsHandshake()` runs once when `AdsPollingTask` starts (before the first poll). It registers the device with the ad server.

<Steps>
  <Step title="Precondition check">
    If `ADS_HANDSHAKE_ENABLED = false`, `ADS_USE_DEDICATED = false`, or `ADS_API_BASE_URL` is empty, the handshake is skipped entirely.
  </Step>

  <Step title="Payload construction">
    ```brightscript theme={null}
    payload = {
        platform           : c.PLATFORM          ' "roku"
        device_id          : deviceId
        device_model       : GTV_GetBrandedDeviceModel(di)
        os_version         : GTV_AdsNormalizeOsVersion(di)
        app_version        : GTV_GetAppVersion()
    }
    ' subscriber credentials are appended if available from Registry:
    payload.subscriber_identifier = username
    payload.subscriber_password   = password
    ```
  </Step>

  <Step title="POST request">
    ```brightscript theme={null}
    url  = c.ADS_API_BASE_URL + c.ADS_PATH_HANDSHAKE
    resp = GTV_HttpPost(url, FormatJson(payload), c.TIMEOUT_HTTP)
    ```

    If the primary path returns `404`, the task retries with `ADS_PATH_HANDSHAKE_FALLBACK` (empty by default, so no fallback occurs unless configured).
  </Step>

  <Step title="Result">
    HTTP 2xx is logged as success. Any other code logs a warning with the response body snippet. The handshake result does not block polling — polling proceeds regardless.
  </Step>
</Steps>

***

## Active snapshot polling

After the handshake, `AdsPollingTask` enters a polling loop.

### Poll interval

| Constant      | Value      | Description                    |
| ------------- | ---------- | ------------------------------ |
| `ADS_POLL_MS` | `10000` ms | Default interval between polls |

The backend may return a `next_check_at` field (ISO 8601 or epoch timestamp) to shorten the interval. The task uses the smaller of the two values, clamped to a minimum of 2 s.

### Poll parameters

Each poll request includes:

| Parameter         | Source                              | Description                                    |
| ----------------- | ----------------------------------- | ---------------------------------------------- |
| `device_id`       | `roDeviceInfo.GetChannelClientId()` | Unique device identifier                       |
| `stream_id`       | `AdsPollingTask.streamId`           | Set by `PlayerScreen` per channel              |
| `stream_position` | Channel number string               | Numeric position in the playlist               |
| `since_version`   | Last known version string           | Enables delta responses (no change → HTTP 204) |

If both `stream_id` and `stream_position` are available, two separate poll parameter sets are built and tried in order, falling back on HTTP 422.

### Response handling

| HTTP code                       | Action                                                  |
| ------------------------------- | ------------------------------------------------------- |
| `200`                           | Parse JSON body, normalize ads list, emit `adsSnapshot` |
| `204`                           | No change — retain current ad state                     |
| `422` + `since_version` error   | Reset `since_version` to `""`, retry                    |
| `401`/`403`/`404` (credentials) | Emit `sessionInvalid` → re-login flow                   |
| Auth inactive signal            | Emit `userInactive` → re-login flow                     |
| Other error                     | Log warning, retain current state, increment fail count |

### Channel change reset

When `PlayerScreen` tunes to a new channel, it toggles `AdsPollingTask.resetChannel`. The task detects this event and immediately issues a fresh poll with `since_version = ""` for the new stream:

```brightscript theme={null}
if fieldName = "resetChannel" and m.top.resetChannel = true
    sinceVersion = ""
    res = DoPollResult(sinceVersion)
    sinceVersion = ProcessPollResult(res, sinceVersion)
end if
```

***

## AdManager: snapshot reconciliation

`AdManager` receives the normalized snapshot from `PlayerScreen` and reconciles it against the currently active ad slots.

<Steps>
  <Step title="Snapshot arrives">
    `PlayerScreen.OnAdsSnapshot()` filters the snapshot to the current channel key before forwarding it to `AdManager.adsSnapshot`.
  </Step>

  <Step title="Record normalization">
    `AdManager.NormalizeAdRecord` processes each ad entry: resolves the format type (`a`, `b`, or `c`), extracts `media_url`, maps position strings (including Spanish variants), and computes `activeUntilSec` from the `active_until` field with clock-skew correction.
  </Step>

  <Step title="Slot reconciliation">
    `ReconcileSlots` diffs the desired slot map against the active slot map:

    * Slots no longer desired are destroyed (`DestroySlot`).
    * New slots are created (`CreateSlot`) — each creates the appropriate `AdFormatA/B/C` node.
    * Existing slots with an unchanged ad signature are kept in place.
  </Step>

  <Step title="Expiry sweep">
    A 1-second repeating timer calls `OnExpirySweep`, which checks each active slot's `activeUntilSec`. Expired slots are destroyed automatically.
  </Step>

  <Step title="Video reduction">
    After each reconcile, `RecomputeVideoReductionAndEmit` recalculates the combined Format C height reduction across `c:top` and `c:bottom` slots and emits `videoHeightReduction` and `videoOffsetY` to `PlayerScreen`.
  </Step>
</Steps>

### Failure threshold

If 3 consecutive image load failures occur across any slots (`m.maxFails = 3`), `AdManager` clears all slots and emits `allAdsHidden = true`. `PlayerScreen` then restores the video to its full base dimensions and the ad system remains silent for the rest of that session.

***

## Impression tracking

When an ad image loads successfully, `AdManager.StartImpressionForSlot` opens an impression session for that slot:

```brightscript theme={null}
evt = {
    event_uuid : GTV_NewImpressionUuid()
    span       : CreateObject("roTimespan")  ' starts timing
    ad_id      : rec.adId
    stream_id  : rec.streamId
    ad_format  : rec.adType
}
m.impressionBySlot[slotKey] = evt
```

When the slot is destroyed (`DestroySlot`), `CloseImpressionForSlot` fires. If the ad was visible for at least 1 s, an `ad_impression_closed` metric event is emitted via `m.top.trackMetric`:

```brightscript theme={null}
evt = {
    event_type : "ad_impression_closed"
    event_uuid : session.event_uuid
    stream_id  : session.stream_id
    ad_id      : session.ad_id
    ad_format  : session.ad_format
    visible_ms : visibleMs
    reason     : reason   ' e.g. "slot_removed", "expired", "ads_cleared"
    slot       : slotKey
}
```

`PlayerScreen.OnAdTrackMetric` forwards this to `MetricsTask`, which batches and POSTs events to `ADS_PATH_IMPRESSIONS`.

***

## Ad formats

<AccordionGroup>
  <Accordion title="Format A — banner overlay on video">
    **Slot keys:** `a:top`, `a:bottom`

    Format A renders a full-width banner image directly over the video. It does **not** reduce the video area — the ad is composited on top.

    **Default dimensions:**

    * Width: `100%` of the video viewport width
    * Height: `15%` of the video viewport height

    **Positions:** `top`, `top-left`, `top-right`, `bottom` (default), `bottom-left`, `bottom-right`

    The banner is anchored to the top or bottom edge of the video viewport. The viewport used is `m.top.videoViewport` (the current, possibly-reduced video rect) — not the base screen rect.

    **Conflict rule:** Only one Format A slot may be active at a time. A second Format A ad with a different vertical position is dropped.

    ```brightscript theme={null}
    ' AdFormatA.brs — default percentages
    wPct = GTV_ReadPercentA(cfg.width_percent,  100.0)  ' 100% of viewport width
    hPct = GTV_ReadPercentA(cfg.height_percent,  15.0)  ' 15% of viewport height
    ```

    **Fade:** The ad fades in over \~6 frames (\~0.1667 opacity per tick) after the image loads, and fades out the same way when destroyed.
  </Accordion>

  <Accordion title="Format B — corner badge">
    **Slot keys:** `b:top-left`, `b:top-right`, `b:bottom-left`, `b:bottom-right`

    Format B renders a small square badge in one of the four corners of the video. It does **not** reduce the video area.

    **Default dimensions:**

    * Width: `10%` of the viewport width
    * Height: `10%` of the viewport height

    **Positions:** `top-left` (default), `top-right`, `bottom-left`, `bottom-right`. Shorthand values (`top`, `bottom`) resolve to the right-side equivalent (`top-right`, `bottom-right`).

    **Pillarbox-aware positioning:** When the video container is wider than the 16:9 active content (letterboxed/pillarboxed), `AdManager` detects the side bars and constrains the badge to the pillarbox area rather than the content area. This keeps the badge visible without obscuring the stream.

    ```brightscript theme={null}
    ' AdFormatB.brs — default percentages
    wPct = GTV_ReadPercentB(cfg.width_percent,  10.0)  ' 10% of viewport width
    hPct = GTV_ReadPercentB(cfg.height_percent, 10.0)  ' 10% of viewport height
    ```

    **Fade:** Same fade-in/fade-out animation as Format A.
  </Accordion>

  <Accordion title="Format C — viewport reduction">
    **Slot keys:** `c:top`, `c:bottom`

    Format C is the only format that **physically shrinks the video area**. It renders a banner in the space freed by moving the video up or down, rather than compositing over it.

    **Default dimensions:**

    * Width: `100%` of the base screen viewport width
    * Height: `15%` of the base screen viewport height

    **Positions:** `top` or `bottom` (default)

    **Viewport reduction mechanism:**

    When the image loads, `AdFormatC` sets two fields on itself:

    ```brightscript theme={null}
    m.top.videoHeightReduction = bannerH   ' pixels to remove from video height
    m.top.videoOffsetY = bannerH           ' only set for top-positioned banners
    ```

    * For **bottom** ads: the video height is reduced by `bannerH`; `videoOffsetY = 0`.
    * For **top** ads: the video is offset downward by `bannerH` and its height reduced; `videoOffsetY = bannerH`.

    `AdManager.RecomputeVideoReductionAndEmit` aggregates reductions from both `c:top` and `c:bottom` slots, enforces a minimum video height of `480 px`, and emits the final `videoHeightReduction` + `videoOffsetY` values to `PlayerScreen`.

    **On image load failure:** `AdFormatC` immediately resets both reduction fields to `0` so the video returns to full size:

    ```brightscript theme={null}
    ' OnLoadStatus — failure branch
    m.top.videoHeightReduction = 0
    m.top.videoOffsetY = 0
    m.top.imageError = true
    ```

    **On destroy (fade out):** The fade-out completion callback also resets reduction fields to `0` before hiding the poster, ensuring the video snaps back at the moment the ad disappears.

    **Viewport used:** Format C uses `m.top.videoBaseViewport` (the full base screen area), not the current reduced video rect. This ensures the banner fills the freed space at the edge of the screen rather than the edge of the already-reduced video.
  </Accordion>
</AccordionGroup>

***

## Slot key reference

| Format | Position values                         | Slot key         |
| ------ | --------------------------------------- | ---------------- |
| A      | `top`, `top-left`, `top-right`          | `a:top`          |
| A      | `bottom`, `bottom-left`, `bottom-right` | `a:bottom`       |
| B      | `top-left`                              | `b:top-left`     |
| B      | `top-right`                             | `b:top-right`    |
| B      | `bottom-left`                           | `b:bottom-left`  |
| B      | `bottom-right`                          | `b:bottom-right` |
| C      | `top`, `top-left`, `top-right`          | `c:top`          |
| C      | `bottom`, `bottom-left`, `bottom-right` | `c:bottom`       |

Slot keys determine both the render position and deduplication — two ads that map to the same slot key cannot coexist; the first one wins.

***

## Diagnostic flag

`ADS_FLOW_DIAG = false` controls verbose ad flow logging. When set to `true`, `AdManager` and `AdsPollingTask` emit detailed `GTV_Warn` entries for every snapshot apply, slot create/destroy, expiry check, and viewport computation. This flag is intended for local development only.

```brightscript theme={null}
ADS_FLOW_DIAG : false
```

<Note>
  Enable `ADS_FLOW_DIAG` in `AppConstants.brs` to trace the full ad lifecycle in the Roku debug console. Remember to set it back to `false` before packaging for submission.
</Note>
