Remote UI Overlay

Describes the UI overlay feature

Overlay Developer Guide

Overview

The overlay API lets your web application display a UI element as an image frame on top of full-screen remote video playback. The SDK captures a DOM element's bounding rectangle, encodes it as a JPEG image (with optional transparency), and delivers it to the streaming connector.

Use the overlay when you need to:

  • Show a player banner (progress bar, title, controls) over full-screen video
  • Show a channel zap banner while the video is playing
  • Display any UI widget on top of remote video without interrupting the stream

The overlay is available in any lifecycle state (foreground or background).


Web UI Requirements

Before integrating the overlay, make sure your UI meets the following requirements. Violating any of these is the most common source of visual defects.

1. All non-overlay content must be transparent

The SDK captures the entire viewport region occupied by the registered element. Any ancestor element, sibling, or page background with an opaque fill will be captured and will block the video underneath — even if that element has nothing to do with your UI widget.

What to do: When the overlay is active, force every container between the page root and the overlay element to background: transparent. The recommended pattern is to add a CSS class to <body> and <html> that applies the transparent rules, and remove it when the overlay is hidden.

body.my-overlay-active,
html.my-overlay-active,
body.my-overlay-active #app-root,
body.my-overlay-active .page-container {
    background: transparent !important;
    background-color: transparent !important;
}

2. Use transparency (useTransparency: true) for alpha blending

When your overlay element has transparent or semi-transparent areas (e.g. rounded corners, drop shadows, gradients), the SDK must capture a separate alpha channel alongside the color image. This is the default (useTransparency: true in configure()). Only set it to false for fully opaque overlays — doing so on a semi-transparent element will fill the transparent areas with black.

3. The overlay element must fully contain the UI area you want to render

The SDK captures exactly the element's bounding rectangle via getBoundingClientRect(). Any part of your UI that falls outside that rectangle — including children that overflow it — will not appear in the captured frame.

Make sure the registered element:

  • Covers the entire visual area you need to display. If your banner has elements that overflow their container (e.g. a tooltip, a popup, a drop shadow), either register a larger wrapper element or ensure overflowing children are clipped within bounds.
  • Has a non-zero width and height. If the element is not yet laid out, has display: none, or has zero dimensions, the capture is skipped silently and no frame is sent.
  • Is positioned within the viewport. Use position: fixed or position: absolute so the element stays at the correct on-screen coordinates. The captured coordinates map directly to the client's video frame, so an off-viewport element will either be skipped or appear at the wrong position on screen.

4. Only one element can be registered at a time

The API accepts a single element. If you call addElement() while another element is already registered, the call is rejected. Always call removeElement(previousElement) before registering a new one. If multiple independent UI components may show overlays simultaneously (e.g. a player banner and a zap banner), coordinate between them — see Multi-Module Safety.

5. Call refresh() only when the element is fully rendered

The overlay captures whatever is painted in the browser at call time. Calling refresh() before the element is ready results in a frame with broken image icons, empty text, or missing content. Defer the first refresh() until:

  • All <img> elements inside the overlay element have loaded (load event or img.complete === true)
  • Custom fonts are available (document.fonts.ready)
  • Any async data driving the UI has been applied to the DOM

For a better user experience, call refresh() multiple times as each part of the UI becomes ready (the SDK cancels the previous capture plan automatically on each call — see Incremental Refresh).

6. CSS animations and transitions are not animated on the client

The overlay is a static JPEG snapshot. CSS transitions, @keyframes animations, and requestAnimationFrame loops inside the overlay element do not animate on the TV client — only the captured still frames are shown. To display motion (e.g. a progress bar advancing), call refresh() on a timer; each call sends a new snapshot.

7. Configure autoHideDurationSec for your use case

The client automatically hides the overlay after autoHideDurationSec seconds (default: 600 / 10 minutes) without receiving a new frame. Choose a value that matches the expected lifetime of your UI:

  • Player banner with periodic refresh: set to 0 (never auto-hide) and drive visibility entirely through refresh() / hideOverlay()
  • Static notification: set to the maximum number of seconds it should remain visible
  • Zap banner with known timeout: set to Math.ceil(zapBannerTimeoutMs / 1000)

8. Always clean up on unmount or hide

When the UI component is hidden or unmounted, always:

  1. Stop any refresh interval
  2. Await the last in-flight refresh() Promise (to prevent a ghost frame appearing after hide)
  3. Call removeElement(ownedElement)
  4. Call hideOverlay()

Failing to call hideOverlay() leaves the last captured frame visible on the TV indefinitely.

9. Remove the overlay when transitioning to foreground

When the application returns to the foreground the full UI is visible again, so any overlay still showing from the background state must be removed. The recommended place to do this is the lifecycle onstatechange callback:

import { lifecycle, overlay } from "senza-sdk";

lifecycle.onstatechange = async (event) => {
    if (event.state === "foreground") {
        await overlay.hideOverlay();
    }
};

Keeping the overlay visible in the foreground is unnecessary (the live UI is already on screen) and can cause a stale frame to appear over the interactive UI.


Quick Start

import { overlay } from "senza-sdk";

// 1. (Optional) Configure before or after registering an element
overlay.configure({
    autoHideDurationSec: 30,           // hide after 30 s of no refresh
    overlayCapturePreset: "default"    // progressive low → high quality
});

// 2. Register the DOM element you want to display
const bannerElement = document.getElementById("player-banner");
await overlay.addElement(bannerElement);

// 3. Render the first frame (and trigger follow-up quality steps)
await overlay.refresh();

// --- Later, when the UI updates (e.g. progress bar advances) ---
overlay.refresh();    // fire-and-forget is fine for periodic refreshes

// 4. When the banner disappears
await overlay.removeElement(bannerElement);
await overlay.hideOverlay();

API Reference

overlay.addElement(element)

Registers a DOM element as the overlay source. Only one element may be registered at a time.

ParameterTypeDescription
elementElementThe DOM element whose bounding rect will be captured

Returns Promise<true>. Rejects if:

  • element is not a DOM Element
  • A different element is already registered (call removeElement() first)

Note: addElement does not send a frame. Call refresh() explicitly after adding the element.


overlay.refresh()

Captures the registered element and sends the frame to the streaming client.

Runs the active capture plan — a sequence of captures with varying quality and timing. Pending plan steps from a previous refresh() are automatically cancelled when a new refresh() is called.

Returns Promise<true> when the first step is dispatched. Rejects with an error message string if no element is registered.

Call refresh() every time your UI content changes (e.g. progress bar tick, text update, layout change). Fire-and-forget (overlay.refresh() without await) is fine for high-frequency periodic refreshes.


overlay.hideOverlay()

Sends a removeOverlay command to the streaming client. The visible overlay frame is immediately removed on the client side.

Returns Promise<true>. Safe to call even if no element is registered.

removeElement() alone does not hide the last rendered frame on the client — you must call hideOverlay() to make it disappear.


overlay.removeElement(element)

Unregisters the element from the SDK. Does not send a hide command to the client — the last rendered frame remains visible until hideOverlay() is called.

ParameterTypeDescription
elementElementMust match the registered element. Omit to remove unconditionally.

Returns Promise<true> on success, Promise<false> if nothing was registered. Rejects if the provided element does not match.


overlay.removeAllElements()

Clears the registered element without sending a hide command. Equivalent to removeElement() without an argument. Use hideOverlay() afterward if you want the client frame to disappear.


overlay.configure(configuration)

Updates overlay settings. Can be called before or after addElement(). Calling with overlayCapturePreset or overlayCapturePlan also cancels any pending capture-plan timers.

overlay.configure({
    useTransparency: true,
    overlayCapturePreset: "default",
    autoHideDurationSec: 600
});
KeyTypeDefaultDescription
useTransparencybooleantrueCapture a separate alpha-channel JPEG. Set false for fully opaque overlays to reduce bandwidth.
overlayCapturePreset"default" | "once""default"Named capture quality preset (see below).
overlayCapturePlanArray<step>nullExplicit capture plan; overrides overlayCapturePreset when set. Set to null to fall back to the preset.
autoHideDurationSecnumber (0–65535)600Seconds after which the client auto-hides the overlay if no new frame arrives. 0 = never auto-hide.

Invalid values are silently ignored and the previous setting is kept.


overlay.getConfiguration()

Returns a shallow copy of the current configuration object.


overlay.getElements()

Returns an array of the registered element(s) — either [element] or [].


Capture Presets

Capture presets define a sequence of quality steps executed after each refresh() call.

"default" (recommended for most UIs)

Sends an immediate low-quality frame (visible quickly), then optional conditional retries, and finally a high-quality frame.

StepQualityConditionalDelay
0lowfalse0 ms (immediate)
1lowtrue150 ms
2lowtrue300 ms
3highfalse600 ms

"once"

A single immediate high-quality conditional step. Use for infrequent captures where you don't need the low-quality preview.

StepQualityConditionalDelay
0hightrue0 ms

Custom capture plans

For full control, pass an explicit overlayCapturePlan array to configure():

overlay.configure({
    overlayCapturePlan: [
        { quality: "low",  conditional: false, delay: 0 },
        { quality: "high", conditional: false, delay: 50 }
    ]
});

Each step must have:

  • quality: "low", "mid", or "high"
  • conditional: true — skip if no paint change since last non-conditional step; false — always capture
  • delay: non-negative number of milliseconds after the previous step

Set overlayCapturePlan: null to revert to the preset.


Transparency Requirements

The overlay is a pixel-accurate screenshot of the registered DOM element composited against the video stream on the client. For this to work correctly:

  1. Every part of the page that is NOT your overlay element must be transparent. Any opaque background behind or around the element will be captured and will block the video, even if that background belongs to a parent container or the page root — not to the overlay element itself.
  2. The overlay element's own background should be transparent wherever you want the video to show through, relying on its alpha channel to blend with the video behind it.

Making the rest of the page transparent

The safest approach is to use a CSS class toggled on <body> and <html> when the overlay is active. This scopes the transparency rules to the overlay-active state and avoids breaking the normal non-overlay layout.

/* Force the page root and all intermediate containers to be transparent
   so the video shows through the overlay element's alpha channel */
body.my-overlay-active,
html.my-overlay-active,
body.my-overlay-active #app-root,
body.my-overlay-active .page-container {
    background: transparent !important;
    background-color: transparent !important;
}

/* The overlay element itself: transparent where you want video to show */
.player-banner {
    background: transparent;
}
// When showing the overlay
document.body.classList.add("my-overlay-active");
document.documentElement.classList.add("my-overlay-active");

// When hiding the overlay
document.body.classList.remove("my-overlay-active");
document.documentElement.classList.remove("my-overlay-active");

Why this matters: If any ancestor element has a non-transparent background, that color will appear in the captured frame and cover the video. This is the most common cause of an overlay that looks correct in the browser but shows a solid-color box on the TV.


Periodic Refresh

When your UI contains live-updating content (e.g. a video progress bar), call refresh() on a fixed interval:

let intervalId = null;

function startRefreshInterval() {
    if (intervalId) return;
    intervalId = setInterval(() => {
        overlay.refresh();          // fire-and-forget
    }, 500);                        // capture every 500 ms
}

function stopRefreshInterval() {
    if (intervalId) {
        clearInterval(intervalId);
        intervalId = null;
    }
}

Stop the interval before calling hideOverlay() to avoid sending frames after the overlay is hidden.


Incremental Refresh — Refreshing as Content Becomes Ready

The SDK captures whatever is painted in the browser at the moment refresh() is called. If your overlay element contains assets that load asynchronously — images, fonts, data-driven content — a premature refresh() will capture a partially-rendered frame (broken image icons, placeholder text, etc.).

Strategy: call refresh() multiple times as each part of the UI becomes ready. Each call cancels the previous capture plan and starts a new one, so the client always receives the freshest frame.

Waiting for images

async function showOverlay(bannerElement) {
    await overlay.addElement(bannerElement);

    // First refresh immediately with whatever is already painted
    // (e.g. text, background colour) — gives the user instant feedback
    overlay.refresh();

    // Wait for all <img> elements inside the banner to finish loading,
    // then refresh again for the complete frame
    const images = Array.from(bannerElement.querySelectorAll("img"));
    if (images.length > 0) {
        await Promise.all(
            images.map(
                (img) =>
                    img.complete
                        ? Promise.resolve()
                        : new Promise((resolve) => {
                              img.addEventListener("load",  resolve, { once: true });
                              img.addEventListener("error", resolve, { once: true }); // don't block on broken images
                          })
            )
        );
        overlay.refresh();   // full frame with all images
    }
}

Waiting for data-driven content

If parts of the UI update after an async data fetch (e.g. programme metadata, thumbnails), refresh after the data is applied:

// After registering the element and doing the first refresh...
const metadata = await fetchProgrammeMetadata(channelId);
applyMetadataToUI(metadata);          // updates DOM
overlay.refresh();                    // re-capture with full metadata

General rule

Call refresh() at each meaningful visual milestone:

  1. Immediately after addElement() — shows the static skeleton
  2. After images / fonts are loaded — shows media assets
  3. After async data arrives — shows the complete, correct content

Because each refresh() cancels the previous capture plan's pending steps, calling it more often than necessary is safe and inexpensive.


Multi-Module Safety

Only one element can be registered at a time across the entire application. If multiple components independently manage overlays (e.g. a player banner and a channel-zap banner), they must coordinate to avoid overwriting each other's registration.

Recommended pattern:

// Each module tracks the element it registered
let _myOverlayElement = null;

async function showMyOverlay(element) {
    // Remove our own element if we already registered one
    if (_myOverlayElement) {
        await overlay.removeElement(_myOverlayElement);
    }
    await overlay.addElement(element);
    _myOverlayElement = element;
    await overlay.refresh();
}

async function hideMyOverlay() {
    if (_myOverlayElement) {
        await overlay.removeElement(_myOverlayElement);
        _myOverlayElement = null;
    }
    await overlay.hideOverlay();
}

Avoid removeAllElements() in shared code paths — it can remove an element registered by another module.


Race Condition: Show/Hide

If your UI shows and hides the overlay rapidly (e.g. key presses), a stale refresh() Promise may resolve after hideOverlay() has already been sent, drawing a ghost frame.

Pattern to avoid this:

let _lastRefreshPromise = null;

async function hideSafely(ownedElement) {
    // Drain in-flight refresh before hiding
    if (_lastRefreshPromise) {
        await _lastRefreshPromise.catch(() => {});
        _lastRefreshPromise = null;
    }
    await overlay.removeElement(ownedElement);
    await overlay.hideOverlay();
}

function show(element) {
    _lastRefreshPromise = overlay.refresh();
}

Suspend Mode and the Overlay

Auto-suspend

The SDK supports an auto-suspend feature that automatically suspends the application after a configurable period of inactivity in the background state. Enable and tune it via lifecycle.configure():

import { lifecycle } from "senza-sdk";

lifecycle.configure({
    autoSuspend: {
        enabled: true,
        timeout: {
            playing: 3600,  // suspend after 1 hour of background playback with no key press
            idle: 300       // suspend after 5 minutes when not playing
        }
    }
});
Timeout keyApplies whenDefault
timeout.playingRemote player is playing in the background60 s
timeout.idleNo playback is active in the background60 s

Any key press from the user while in the background resets the auto-suspend timer. The timer only runs when the lifecycle state is "background".

To intercept an imminent auto-suspend (e.g. to save state or show a "still watching?" prompt before it fires), listen to beforestatechange:

lifecycle.addEventListener("beforestatechange", async (e) => {
    if (e.state === lifecycle.UiState.SUSPENDED && e.isSystemTriggered) {
        // Optional: cancel the suspend and handle it yourself
        // e.preventDefault();
        await saveApplicationState();
    }
});

Wakeup from suspend — showing the overlay on the resumed session

When the application is suspended, it is not running at all — but playback continues uninterrupted on the connector device. A key press from the user causes the platform to relaunch the application. The app starts directly in background state (the same state it uses during normal background playback), and the connectReason will be "ui_release" or "reconnect_app".

The first thing the application should do on wakeup is restore its state — re-establish session context, reload configuration, and resync with the remote player. This is standard background-mode initialisation and is not specific to the overlay.

Once state is restored, decide what to show based on the key that triggered the wakeup:

  • If the key intent is to display UI over the playing video (e.g. the user pressed OK to see the player banner, or a directional key to open the channel zap), stay in background and show the overlay. Do not call lifecycle.moveToForeground() — moving to foreground would interrupt the remote playback. Instead, register your UI element and call refresh() to display it as an overlay while the video keeps playing behind it.
  • If the key should stop or navigate away from playback, move to foreground normally.
import { lifecycle, remotePlayer, overlay } from "senza-sdk";

// Called after state has been restored on wakeup
async function handleWakeupKey(keyCode) {
    if (isBannerKey(keyCode)) {
        // Stay in background — show player banner as overlay over live video
        await showPlayerBannerOverlay();
    } else {
        // Navigate away from playback — bring full UI to foreground
        lifecycle.moveToForeground();
    }
}

Overlay while in background state

The overlay can be used in any lifecycle state, including "background". This enables patterns where the application stays in background — keeping remote playback running at full quality — while displaying interactive UI panels (channel zap, player banner, info screens) via the overlay:

lifecycle.onstatechange = (event) => {
    if (event.state === "background") {
        // App just entered background — remote playback is now active.
        // The overlay can be shown at any point from here.
    }
    if (event.state === "foreground") {
        // App returned to foreground — hide any overlay, the live UI is visible.
        overlay.hideOverlay();
    }
};

Key behaviours to keep in mind when using the overlay in background:

  • Key events are still delivered to the application in background state — you can react to them to refresh() or swap the overlay element.
  • Calling lifecycle.moveToForeground() from background state will bring the full UI back; call hideOverlay() at that point (or rely on the onstatechange handler above).
  • If autoSuspend is enabled, any call to refresh() does not reset the suspend timer — only key presses do. If you want the overlay to stay visible, either disable auto-suspend or ensure the user's key activity is reaching the application.

Complete Example

import { overlay } from "senza-sdk";

const USE_OVERLAY = window.isSmartTVPlatform?.() && overlay != null;

class PlayerBanner {
    constructor() {
        this._element = null;
        this._refreshInterval = null;
        this._lastRefreshPromise = null;
    }

    async show(bannerElement) {
        if (!USE_OVERLAY) {
            bannerElement.style.visibility = "visible";
            return;
        }

        // Make backgrounds transparent so video shows through
        document.body.classList.add("player-overlay-active");

        overlay.configure({
            overlayCapturePlan: [
                { quality: "low",  conditional: false, delay: 0 },
                { quality: "high", conditional: false, delay: 50 }
            ],
            autoHideDurationSec: 0  // manual hide only
        });

        await overlay.removeAllElements();
        await overlay.addElement(bannerElement);
        this._element = bannerElement;

        // Kick off first capture
        this._lastRefreshPromise = overlay.refresh();
        await this._lastRefreshPromise;

        // Keep the progress bar live
        this._refreshInterval = setInterval(() => {
            this._lastRefreshPromise = overlay.refresh();
        }, 500);
    }

    async hide() {
        if (!USE_OVERLAY) return;

        clearInterval(this._refreshInterval);
        this._refreshInterval = null;

        // Wait for any in-flight capture to finish before hiding
        if (this._lastRefreshPromise) {
            await this._lastRefreshPromise.catch(() => {});
            this._lastRefreshPromise = null;
        }

        document.body.classList.remove("player-overlay-active");

        if (this._element) {
            await overlay.removeElement(this._element);
            this._element = null;
        }
        await overlay.hideOverlay();
    }
}

Troubleshooting

SymptomLikely causeFix
Overlay shows a black or solid-color boxBackground is not transparentAdd transparent CSS class to body/root when overlay is active
Ghost frame appears briefly after hideIn-flight refresh() resolved after hideOverlay()Drain _lastRefreshPromise before calling hideOverlay()
addElement throws "element already registered"Previous module left an element registeredTrack your own element reference; call removeElement(ownedElement) before addElement
No overlay visible on desktop browserwindow.cefQuery is not available outside the Senza platformExpected — overlay only works on the Senza SmartTV platform
Overlay disappears after 10 minutes without interactionDefault autoHideDurationSec of 600 s elapsedSet autoHideDurationSec: 0 to disable auto-hide, or call refresh() periodically
Progress bar frozen on first framerefresh() called before element is fully paintedDefer the first refresh() until after the element is visible and data is ready
Overlay shows broken image icons or placeholder textrefresh() was called before images finished loadingWait for img.complete / load events, then call refresh() again
Video visible through a solid-colour box instead of the overlayAn ancestor element has a non-transparent backgroundAdd a CSS class to <body>/<html> that forces all non-overlay containers to background: transparent

Did this page help you?