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 from remotePlayer.
  • 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

ModePage runsCan draw
Foregroundyesyes
Backgroundyesno (frozen last frame, or Remote Player video passes through)
Suspended (UI release)nono

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:

TriggerHowbeforestatechange fires
App requestThe 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 SuspendSenza 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 beforestatechange callback when e.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), call e.preventDefault() in beforestatechange when e.state === "suspended". e.isSystemTriggered tells 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

StorageSurvives suspendSurvives disconnect or rebootUse for
In-memory JSnononothing needed after wake-up
sessionStorageyesno (not available on disconnect)state to restore after suspend
localStorageyesyes (per user, about 400 KB gzip)preferences, settings
Cookiesyesyes (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 remotePlayer on wake-up, because the stream kept playing (or stayed paused) while the app was down:
    • remotePlayer.currentTime: current position (seconds).
    • remotePlayer.isPlaying: true if playing, false if paused or not loaded.
    • remotePlayer.duration: duration, for VOD assets.
    • remotePlayer.getPresentationStartTimeAsDate(): presentation start of a live stream, null if 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 alarmManager before 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 userinactivity or in beforestatechange), 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:

  1. On onstatechange with state === "background". Suspend is only reachable from background.
  2. Or in beforestatechange when e.state === lifecycle.UiState.SUSPENDED. It fires for both moveToSuspended() and Auto Suspend, and the listener can be async (Senza waits up to 10 s). e.isSystemTriggered tells timer from app. Calling preventDefault() cancels the change.
  3. Always before a moveToSuspended() call you make yourself.
  4. Network-dependent work (analytics flush, closing sessions) should happen earlier, on userinactivity (countdown in event.timeout) or in beforestatechange.

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:

ConstantValueMeaningWhat to do
INITIAL_CONNECTIONinitial_connectionFirst loadFresh start, ignore saved state
UI_RELEASEui_releaseReloaded after suspendRestore saved state
APPLICATION_RELOADreload_appReloaded, for example after HOME keyUsually start fresh
UI_TERMINATIONui_terminationUI was terminatedRestore if state is valid, else start fresh
WEBRTC_ERRORwebrtc_errorReloaded after a WebRTC errorSame as above
UI_WATCHDOGui_watchdogWatchdog got no UI framesSame as above
UNKNOWNunknownSDK not initializedStart fresh

lifecycle.triggerEvent ({type, data}, {} if none) says what woke the app:

typedataMeaning
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)
getLicensenoneA DRM license request is pending. The request arrives on the Remote Player license-request event after uiReady()
hdmiStatusChangednoneHDMI 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:

  1. Read connectReason, triggerEvent, and the Remote Player state.
  2. If ui_release, load saved state; validate its version and shape. On any problem fall back to a safe home screen.
  3. Restore caches and session data (4.4, 4.5), recompute time-based state (4.6).
  4. Rebuild navigation and focus (4.1, 4.2).
  5. Reconcile playback (4.3): see below. Never restart content that is still playing.
  6. Call uiReady(). The SDK delivers the waking key press to your app only after this call.
  7. Apply the trigger (below), call lifecycle.moveToForeground() when you are ready to draw, and clear one-shot saved data.

7.1 Apply the trigger

TriggerTypical behaviour
Select/OK key during playbackBring the app to the screen the user was on (player), show the playback controls
Back keyGo to the previous screen, do not resume the player UI
Home or app keyLeave as the platform expects (launcher or home), do not restart playback
Arrow keys during live playbackDo what the key would do in the running app, for example change channel
videoPlaybackEvent EOSReport end of playback, show your end-of-content UI or go back
videoPlaybackEvent ERRShow an error UI, move to foreground
alarmFiredEventRun whatever you scheduled (for example PIN or event-end handling)
getLicenseRenew the DRM license (create a new play session if needed) and answer the request after uiReady()
unknownSafe 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.isPlaying and remotePlayer.currentTime and rebuild the player UI from them: show the progress bar at currentTime, and the play or pause icon from isPlaying. Do not call load() again for content that is already loaded; it restarts the content. Use play() or pause() 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: setTimeout and setInterval do not fire. Use alarmManager for 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.state and call moveToForeground() 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 userinactivity with a countdown, then disconnects.
  • Disconnect: userdisconnected fires 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. sessionStorage is 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 measurestime in foreground while video playstime in background while video plays
idle measurestime 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:

SettingDefault
autoBackground.enabledtrue
autoBackground.timeout.playing600 (false disables)
autoBackground.timeout.idle600 (false disables)
autoSuspend.enabledfalse
autoSuspend.timeout.playing60
autoSuspend.timeout.idle60 (time in background with no video playing)

9. Checklist

  • Resume state is saved on background or in beforestatechange (suspended), in sessionStorage, with try/catch and 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 connectReason and triggerEvent; 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, not sessionStorage.
  • 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.

Did this page help you?