Handling UI Release (Suspend)
Saving and Restoring State for Senza UI Release (Suspend)
1. What suspend means
Senza runs your app in a browser in the cloud (a pod) and streams the browser's screen to a client device. The client device can also play video directly from the CDN. In that mode the UI can be shown as a JPEG overlay, and the cloud browser is only needed while the user interacts with the UI.
To save resources, Senza releases the UI (suspend): the pod running the browser and your app is shut down completely.
foreground ──► background ──► suspended (pod gone)
(UI shown) (playback, (no browser, no JS, no timers,
UI hidden) no network; playback continues)
│
key press / end of stream / error
▼
new pod, app loads from scratch (cold start)
What this means for your app:
- Suspend can happen while video is playing. The user keeps watching; they do not notice.
- While suspended nothing of your app runs. All in-memory state (variables, framework state, caches, in-flight requests, timers) is lost.
- The app is loaded again on an event: a key press, end of stream (EOS), a playback error, an alarm, or a license request.
- On reload the app gets the connect reason (
ui_release), the trigger event (for example which key was pressed), and can query the current playback state fromremotePlayer. - The app must restore its last state as if the app had never gone down and the user had just pressed that key. The user must not be able to tell a reload happened.
Disconnect is different. When the session ends (power off, inactivity, disconnect()), the app is not coming back with its state, and sessionStorage is not available in the userdisconnected handler. You do not need to save UI state for disconnect. Use the handler only for cleanup that needs no storage (close server sessions, send analytics) from data you hold in memory. Data that must outlive a session belongs in cookies or localStorage.
2. Modes and states
| Mode | Page runs | Can draw |
|---|---|---|
| Foreground | yes | yes |
| Background | yes | no (frozen last frame, or Remote Player video passes through) |
| Suspended (UI release) | no | no |
lifecycle.UiState values: unknown, foreground, inTransitionToForeground, background, inTransitionToBackground, suspended. Flow: foreground, inTransitionToBackground, background, inTransitionToForeground, foreground. suspended is reported only in beforestatechange; onstatechange does not fire when entering suspend, because once suspending the application and sdk is going down and can not receive it.
How suspend is triggered
Suspend is only entered from background. Moving to suspend from foreground state will result in an error. There are two ways:
| Trigger | How | beforestatechange fires |
|---|---|---|
| App request | The app calls lifecycle.moveToSuspended(). It returns a promise that resolves when the suspend command was sent and rejects if it could not be processed. It fails if the app is not in background. | true |
| Auto Suspend | Senza suspends the app by itself once it has stayed in background for a configured time. Enable and tune it with lifecycle.configure({autoSuspend: {...}}). It is off by default. | true |
Auto Suspend settings (seconds):
lifecycle.configure({
autoSuspend: {
enabled: true,
timeout: {
playing: 60, // suspend after 60 seconds of no activity after moving to background mode while playing
idle: 30 // suspend after 30 seconds of no activity after moving to background when there is no playback.
}
}
});Notes:
- The countdown of auto suspend timer runs only while the app is in background. Moving back to foreground stops it. Pressing a key while in background will reset the timer.
- Save state in the
beforestatechangecallback whene.state === "suspended". Saving the state must be synchronous and end before the callback ends. - To block a suspend, whether from
moveToSuspended()or from Auto Suspend (for example during a critical flow), calle.preventDefault()inbeforestatechangewhene.state === "suspended".e.isSystemTriggeredtells a timer-triggered suspend from an app-triggered one. A prevented Auto Suspend restarts its countdown. - Auto Suspend usually follows Auto Background: the app first moves to background (by
moveToBackground()or Auto Background after inactivity), and then suspends after the Auto Suspend timeout.
Typical timelines:
Auto: foreground ─(idle timeout, Auto Background)─► background ─(idle timeout, Auto Suspend)─► suspended
Manual: foreground ─ moveToBackground() ─► background ─ moveToSuspended() ─► suspended
Playing: foreground ─ moveToBackground() ─► background (video plays) ─(playing timeout, Auto Suspend)─► suspended, video keeps playing
3. Where to keep state
| Storage | Survives suspend | Survives disconnect or reboot | Use for |
|---|---|---|---|
| In-memory JS | no | no | nothing needed after wake-up |
sessionStorage | yes | no (not available on disconnect) | state to restore after suspend |
localStorage | yes | yes (per user, about 400 KB gzip) | preferences, settings |
| Cookies | yes | yes (third-party cookies: no) | long-lived tokens or flags, data needed after disconnect |
Use sessionStorage for resume state. It lives exactly as long as the session, which is what you want.
4. What to save:
Save only what you need to rebuild the screen the user was on and apply the key that woke the app. Each category below has an example from the reference web UI (all keys in sessionStorage).
4.1 Navigation
Where the user is, and how Back should behave.
- Route or view stack.
- Reference web UI:
routerStack(array of route paths). On wake-up the last route is popped and re-entered, so the player or screen mounts exactly once.
4.2 Focus and selection
What the user had focused or selected, so the screen looks the same.
- Focused list or row id and tile id, selected tab or menu id, scroll anchor index, last selected item in a detail page (episode, season tab), last search keyword and selected suggestion.
- Reference web UI: focused swimlane id and tile id, first valid swimlane index, selected hub menu id, last selected episode and series tab, last searched keyword, last selected trending search item.
4.3 Playback context
What was playing, so you can continue it, offer the right controls, and react to EOS or error.
- Asset identity and type (live, restart or catch-up, VOD), channel, program or episode, source.
- Play session objects and the URL needed to close the server session later.
- The reason for a special playback mode (for example why a restart session was created).
- Next-item information (next episode) for the "up next" UI.
- Playback position and state: do not save them. Read them from
remotePlayeron wake-up, because the stream kept playing (or stayed paused) while the app was down:remotePlayer.currentTime: current position (seconds).remotePlayer.isPlaying:trueif playing,falseif paused or not loaded.remotePlayer.duration: duration, for VOD assets.remotePlayer.getPresentationStartTimeAsDate(): presentation start of a live stream,nullif no live stream is loaded.
- Reference web UI: current program info (
playback.programInfo), streaming session object and second session object, restart reason, session tear-down URLs, next episode detail.
4.4 Session and authentication
Whatever avoids a new login or a new server handshake on every wake-up.
- Session tokens or session objects issued by your backend.
- Anything you validate again on wake-up (tokens can expire during suspend).
- Reference web UI: the backend session token, so wake-up restores it instead of requesting a new one; the analytics session info.
4.5 Bootstrap and configuration caches
Responses that take time to fetch and rarely change. Caching them makes wake-up fast, because the user is waiting on a key press.
- Tenant or app configuration, initial API response, feature flags, user profile settings, channel list.
- Version-stamp the cache. When your app version changes, drop it.
- Reference web UI: tenant config, initial bootstrap response, apps config, user profile settings, channel list, plus a cache version that clears the storage when it changes.
4.6 Time-based state
Timers do not run while suspended, so store timestamps and recompute on wake-up.
- Start time of a grace period or timeout (for example a PIN grace period), expiry times, "last activity" times.
- Schedule anything that must happen while suspended with
alarmManagerbefore going down. - Reference web UI: PIN grace period start time (remaining time is recomputed on restore), an alarm set for the end of the current live event.
4.7 Flow flags
Small markers that change how the next wake-up behaves.
- Why the app went to background (idle timeout vs user action), the last lifecycle state, a pending deep link, the launcher URL.
- Reference web UI: "idle freeze happened" markers (so the next key goes straight to the last screen), last app state, deep link URL, launcher URL.
4.8 Pending outbound data
Anything queued for a server that must not be lost or duplicated.
- Buffered analytics events, scheduled reporting, queued writes.
- Flush early, before background (for example on
userinactivityor inbeforestatechange), because there is no network while suspended. If you cannot flush, store the schedule and send it on wake-up. - Reference web UI: buffered content-update event schedule, analytics events flushed on inactivity warning.
4.9 What not to save
Popups, spinners, toasts, hover and animation state, anything derived or cheap to refetch, large lists you can reload, and anything sensitive that should not sit in storage.
5. When to save
Save while the page is still alive, before it goes down:
- On
onstatechangewithstate === "background". Suspend is only reachable from background. - Or in
beforestatechangewhene.state === lifecycle.UiState.SUSPENDED. It fires for bothmoveToSuspended()and Auto Suspend, and the listener can be async (Senza waits up to 10 s).e.isSystemTriggeredtells timer from app. CallingpreventDefault()cancels the change. - Always before a
moveToSuspended()call you make yourself. - Network-dependent work (analytics flush, closing sessions) should happen earlier, on
userinactivity(countdown inevent.timeout) or inbeforestatechange.
Keep saving synchronous and small, wrap storage access in try/catch, store JSON, and include a schema version.
import {lifecycle} from "senza-sdk";
lifecycle.addEventListener("onstatechange", e => {
if (e.state === lifecycle.UiState.BACKGROUND) saveState();
});
function saveState() {
try {
sessionStorage.setItem("myapp.state", JSON.stringify({
v: 1,
savedAt: Date.now(),
routes: router.stack,
focus: ui.focusState(),
playback: player.contextInfo(), // ids and types, not position
nextItem: ui.nextItem()
}));
} catch (err) {
console.warn("saveState failed", err);
}
}6. Detecting why the app loaded
lifecycle.connectReason (also getConnectReason(); the docs page says connectionReason). Values in lifecycle.ConnectReason:
| Constant | Value | Meaning | What to do |
|---|---|---|---|
INITIAL_CONNECTION | initial_connection | First load | Fresh start, ignore saved state |
UI_RELEASE | ui_release | Reloaded after suspend | Restore saved state |
APPLICATION_RELOAD | reload_app | Reloaded, for example after HOME key | Usually start fresh |
UI_TERMINATION | ui_termination | UI was terminated | Restore if state is valid, else start fresh |
WEBRTC_ERROR | webrtc_error | Reloaded after a WebRTC error | Same as above |
UI_WATCHDOG | ui_watchdog | Watchdog got no UI frames | Same as above |
UNKNOWN | unknown | SDK not initialized | Start fresh |
lifecycle.triggerEvent ({type, data}, {} if none) says what woke the app:
type | data | Meaning |
|---|---|---|
keyPressEvent | {keyValue} | Key press woke the app |
alarmFiredEvent | {alarmName, payload} | alarmManager alarm fired |
userAlertEvent | {eventCode, message} | User alert from the Message Manager |
externalEvent | {eventName, payload} | External event sent to the app |
videoPlaybackEvent | {eventCode, errorCode} | Remote Player ended or failed (eventCode is EOS or ERR) |
getLicense | none | A DRM license request is pending. The request arrives on the Remote Player license-request event after uiReady() |
hdmiStatusChanged | none | HDMI status changed. Details arrive in a callback after uiReady() |
keyValue identifies the key. Compare both numeric and named forms when checking specific keys (seen in the reference web UI, not documented).
7. Restoring: act as if the key was pressed in the running app
The goal is not only to reload data. The user pressed a key on a screen that was alive a moment ago. Rebuild that screen, then apply the key to it.
Order:
- Read
connectReason,triggerEvent, and the Remote Player state. - If
ui_release, load saved state; validate its version and shape. On any problem fall back to a safe home screen. - Restore caches and session data (4.4, 4.5), recompute time-based state (4.6).
- Rebuild navigation and focus (4.1, 4.2).
- Reconcile playback (4.3): see below. Never restart content that is still playing.
- Call
uiReady(). The SDK delivers the waking key press to your app only after this call. - Apply the trigger (below), call
lifecycle.moveToForeground()when you are ready to draw, and clear one-shot saved data.
7.1 Apply the trigger
| Trigger | Typical behaviour |
|---|---|
| Select/OK key during playback | Bring the app to the screen the user was on (player), show the playback controls |
| Back key | Go to the previous screen, do not resume the player UI |
| Home or app key | Leave as the platform expects (launcher or home), do not restart playback |
| Arrow keys during live playback | Do what the key would do in the running app, for example change channel |
videoPlaybackEvent EOS | Report end of playback, show your end-of-content UI or go back |
videoPlaybackEvent ERR | Show an error UI, move to foreground |
alarmFiredEvent | Run whatever you scheduled (for example PIN or event-end handling) |
getLicense | Renew the DRM license (create a new play session if needed) and answer the request after uiReady() |
| unknown | Safe home screen |
The waking key may also arrive as a normal DOM key event. Ignore that duplicate, for example with a one-shot flag and timestamp, or the key is handled twice.
7.2 Reconcile with playback
- The stream kept playing (or stayed paused) while you were down. Read
remotePlayer.isPlayingandremotePlayer.currentTimeand rebuild the player UI from them: show the progress bar atcurrentTime, and the play or pause icon fromisPlaying. Do not callload()again for content that is already loaded; it restarts the content. Useplay()orpause()only if the key that woke the app asks for it (for example a play/pause key). - A paused stream that has been waiting long may need a fresh play session; revalidate before resuming.
- While the restore is in progress, guard against code paths that would start playback or ask for a PIN (a flag such as "wake-up in progress"). Reset it when restore completes or fails.
- Server sessions may have expired. Revalidate and recreate them.
7.3 Example
import {lifecycle, uiReady} from "senza-sdk";
async function start() {
const reason = lifecycle.connectReason;
const trigger = lifecycle.triggerEvent;
let saved = null;
if (reason === lifecycle.ConnectReason.UI_RELEASE) {
try {
saved = JSON.parse(sessionStorage.getItem("myapp.state"));
if (saved?.v !== 1) saved = null;
} catch { saved = null; }
}
restoreCaches(saved);
router.restore(saved?.routes ?? ["/home"]);
ui.restoreFocus(saved?.focus);
uiReady();
switch (trigger?.type) {
case "keyPressEvent": handleWakeKey(trigger.data.keyValue, saved); break;
case "videoPlaybackEvent": handlePlaybackEnd(trigger.data); break;
case "alarmFiredEvent": handleAlarm(trigger.data); break;
case "getLicense": break; // handled on the license-request event
default: break;
}
const s = lifecycle.state;
if (s === lifecycle.UiState.BACKGROUND || s === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND) {
await lifecycle.moveToForeground();
}
}8. Things that behave differently while suspended
- Timers:
setTimeoutandsetIntervaldo not fire. UsealarmManagerfor anything that must happen while suspended, and store timestamps. - Foreground: with timer-based management a key press brings the app to foreground. With function-based management it does not, so check
lifecycle.stateand callmoveToForeground()yourself. - Remote Player end or error: a suspended app moves to background. Move to foreground to show UI or play more. The Shaka Player integration does this automatically.
- Inactivity: after the default 3 hours without input, Senza sends
userinactivitywith a countdown, then disconnects. - Disconnect:
userdisconnectedfires on session end (inactivity, app switch,disconnect(), power off). It is guaranteed to be delivered, can be async (10 s), and must be enabled by your Synamedia representative.sessionStorageis not available there.
Auto Background and Auto Suspend
Configure with lifecycle.configure(); read the active values with lifecycle.getConfiguration(). Both features have two timeouts, chosen by whether the Remote Player is playing video when the countdown starts:
playing: video is playing in the Remote Player.idle: no video is playing.
| Auto Background (foreground to background) | Auto Suspend (background to suspended) | |
|---|---|---|
playing measures | time in foreground while video plays | time in background while video plays |
idle measures | time in foreground with no video (UI only) | time in background with no video |
Defaults in seconds. Values set for your app or community in the session settings can override them, so read getConfiguration() instead of assuming:
| Setting | Default |
|---|---|
autoBackground.enabled | true |
autoBackground.timeout.playing | 600 (false disables) |
autoBackground.timeout.idle | 600 (false disables) |
autoSuspend.enabled | false |
autoSuspend.timeout.playing | 60 |
autoSuspend.timeout.idle | 60 (time in background with no video playing) |
9. Checklist
- Resume state is saved on
backgroundor inbeforestatechange(suspended), insessionStorage, withtry/catchand a schema version. - State covers navigation, focus, playback context, session, caches, time stamps, flow flags and pending outbound data (section 4).
- Playback position and playing/paused state are read from
remotePlayer(currentTime,isPlaying), not from saved data. - Network-dependent cleanup (analytics, sessions) happens before background, or on disconnect from in-memory data.
- Startup branches on
connectReasonandtriggerEvent; unknown values lead to a safe home screen. - Restore rebuilds the screen, then applies the key, EOS, error or alarm as if the app had been running.
-
uiReady()is called after restore,moveToForeground()when needed. - Duplicate wake-up key is ignored; "wake-up in progress" flags are always reset.
- Tokens and server sessions are revalidated after wake-up.
- Data needed after disconnect is in cookies or
localStorage, notsessionStorage. - Tested: wake by each key (select, back, home, arrows), by EOS, by error, by alarm, by license request, with playback running and with no playback, after reboot (empty
sessionStorage), and after tokens expire during suspend.
Updated 3 days ago