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
useTransparency: true) for alpha blendingWhen 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: fixedorposition: absoluteso 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
refresh() only when the element is fully renderedThe 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 (loadevent orimg.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
autoHideDurationSec for your use caseThe 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 throughrefresh()/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:
- Stop any refresh interval
- Await the last in-flight
refresh()Promise (to prevent a ghost frame appearing after hide) - Call
removeElement(ownedElement) - 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)
overlay.addElement(element)Registers a DOM element as the overlay source. Only one element may be registered at a time.
| Parameter | Type | Description |
|---|---|---|
element | Element | The DOM element whose bounding rect will be captured |
Returns Promise<true>. Rejects if:
elementis not a DOMElement- A different element is already registered (call
removeElement()first)
Note:
addElementdoes not send a frame. Callrefresh()explicitly after adding the element.
overlay.refresh()
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()withoutawait) is fine for high-frequency periodic refreshes.
overlay.hideOverlay()
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 callhideOverlay()to make it disappear.
overlay.removeElement(element)
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.
| Parameter | Type | Description |
|---|---|---|
element | Element | Must 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()
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)
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
});| Key | Type | Default | Description |
|---|---|---|---|
useTransparency | boolean | true | Capture a separate alpha-channel JPEG. Set false for fully opaque overlays to reduce bandwidth. |
overlayCapturePreset | "default" | "once" | "default" | Named capture quality preset (see below). |
overlayCapturePlan | Array<step> | null | Explicit capture plan; overrides overlayCapturePreset when set. Set to null to fall back to the preset. |
autoHideDurationSec | number (0–65535) | 600 | Seconds 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()
overlay.getConfiguration()Returns a shallow copy of the current configuration object.
overlay.getElements()
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)
"default" (recommended for most UIs)Sends an immediate low-quality frame (visible quickly), then optional conditional retries, and finally a high-quality frame.
| Step | Quality | Conditional | Delay |
|---|---|---|---|
| 0 | low | false | 0 ms (immediate) |
| 1 | low | true | 150 ms |
| 2 | low | true | 300 ms |
| 3 | high | false | 600 ms |
"once"
"once"A single immediate high-quality conditional step. Use for infrequent captures where you don't need the low-quality preview.
| Step | Quality | Conditional | Delay |
|---|---|---|---|
| 0 | high | true | 0 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 capturedelay: 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:
- 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.
- 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 metadataGeneral rule
Call refresh() at each meaningful visual milestone:
- Immediately after
addElement()— shows the static skeleton - After images / fonts are loaded — shows media assets
- 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 key | Applies when | Default |
|---|---|---|
timeout.playing | Remote player is playing in the background | 60 s |
timeout.idle | No playback is active in the background | 60 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 callrefresh()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; callhideOverlay()at that point (or rely on theonstatechangehandler above). - If
autoSuspendis enabled, any call torefresh()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
| Symptom | Likely cause | Fix |
|---|---|---|
| Overlay shows a black or solid-color box | Background is not transparent | Add transparent CSS class to body/root when overlay is active |
| Ghost frame appears briefly after hide | In-flight refresh() resolved after hideOverlay() | Drain _lastRefreshPromise before calling hideOverlay() |
addElement throws "element already registered" | Previous module left an element registered | Track your own element reference; call removeElement(ownedElement) before addElement |
| No overlay visible on desktop browser | window.cefQuery is not available outside the Senza platform | Expected — overlay only works on the Senza SmartTV platform |
| Overlay disappears after 10 minutes without interaction | Default autoHideDurationSec of 600 s elapsed | Set autoHideDurationSec: 0 to disable auto-hide, or call refresh() periodically |
| Progress bar frozen on first frame | refresh() called before element is fully painted | Defer the first refresh() until after the element is visible and data is ready |
| Overlay shows broken image icons or placeholder text | refresh() was called before images finished loading | Wait for img.complete / load events, then call refresh() again |
| Video visible through a solid-colour box instead of the overlay | An ancestor element has a non-transparent background | Add a CSS class to <body>/<html> that forces all non-overlay containers to background: transparent |
Updated about 15 hours ago