Skip to main content

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.

AdsPollingTask

Runs the handshake and polls ADS_PATH_ACTIVE every 10 s

AdManager

Reconciles snapshot data into active render slots; tracks impressions

Format A

Full-width banner overlaid on the video, anchored to top or bottom

Format B

Corner badge overlay positioned in one of four screen corners

Format C

Viewport-reduction banner: shrinks the video area rather than overlaying it

Impression tracking

Batch impression events sent to ADS_PATH_IMPRESSIONS

Ad server configuration

All ad traffic is routed to a dedicated ads backend, separate from the main application server.
ADS_API_BASE_URL   : "https://ads.globaltv.lat/api/v1"
ADS_USE_DEDICATED  : true
ADS_HANDSHAKE_ENABLED : true
ConstantValueDescription
ADS_API_BASE_URLhttps://ads.globaltv.lat/api/v1Base URL for all ad endpoints
ADS_USE_DEDICATEDtrueUse dedicated ads server (not main server)
ADS_HANDSHAKE_ENABLEDtrueRun the device handshake on startup
ADS_FLOW_DIAGfalseDiagnostic 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

ConstantPathMethodPurpose
ADS_PATH_HANDSHAKE/app/devices/handshakePOSTDevice registration on session start
ADS_PATH_ACTIVE/app/ads/activeGETActive ad snapshot poll
ADS_PATH_IMPRESSIONS/app/impressions/events/batchPOSTBatch impression event upload
Full URLs are constructed as:
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.
1

Precondition check

If ADS_HANDSHAKE_ENABLED = false, ADS_USE_DEDICATED = false, or ADS_API_BASE_URL is empty, the handshake is skipped entirely.
2

Payload construction

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
3

POST request

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).
4

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.

Active snapshot polling

After the handshake, AdsPollingTask enters a polling loop.

Poll interval

ConstantValueDescription
ADS_POLL_MS10000 msDefault 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:
ParameterSourceDescription
device_idroDeviceInfo.GetChannelClientId()Unique device identifier
stream_idAdsPollingTask.streamIdSet by PlayerScreen per channel
stream_positionChannel number stringNumeric position in the playlist
since_versionLast known version stringEnables 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 codeAction
200Parse JSON body, normalize ads list, emit adsSnapshot
204No change — retain current ad state
422 + since_version errorReset since_version to "", retry
401/403/404 (credentials)Emit sessionInvalid → re-login flow
Auth inactive signalEmit userInactive → re-login flow
Other errorLog 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:
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.
1

Snapshot arrives

PlayerScreen.OnAdsSnapshot() filters the snapshot to the current channel key before forwarding it to AdManager.adsSnapshot.
2

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

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

Expiry sweep

A 1-second repeating timer calls OnExpirySweep, which checks each active slot’s activeUntilSec. Expired slots are destroyed automatically.
5

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.

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

Slot keys: a:top, a:bottomFormat 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-rightThe 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.
' 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.
Slot keys: b:top-left, b:top-right, b:bottom-left, b:bottom-rightFormat 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.
' 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.
Slot keys: c:top, c:bottomFormat 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:
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:
' 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.

Slot key reference

FormatPosition valuesSlot key
Atop, top-left, top-righta:top
Abottom, bottom-left, bottom-righta:bottom
Btop-leftb:top-left
Btop-rightb:top-right
Bbottom-leftb:bottom-left
Bbottom-rightb:bottom-right
Ctop, top-left, top-rightc:top
Cbottom, bottom-left, bottom-rightc: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.
ADS_FLOW_DIAG : false
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.