Skip to main content

Asset Download, Cache and Atomic Activation

TomorrowOS makes sure content is reachable, cached and ready before it replaces what is already on screen. There is no separate tomorrow.assets.* command surface today. Assets move through the CMS media routes and the device content policy:
This guide describes that shipped flow in @tomorrowos/sdk and the TomorrowOS players (Tizen / BrightSign), not a fictional Asset SDK.

Why asset handling matters

A signage screen may need to play:
  • Images
  • Videos
  • HTML widgets / ZIP packages
  • Brand fallback / idle content
If assets are not handled properly, screens can show black frames, broken media, half-loaded widgets, or incomplete playlists. TomorrowOS prevents the worst cases by:
  1. Verifying assets on the CMS before publish
  2. Pushing a complete policy to the device
  3. Caching media on the player (URL + content hash)
  4. Switching playlists only when the next content can play (atomic handoff)
  5. Falling back to brand idle when nothing playable is available

Core principle

Verify on publish. Cache on the device. Keep good content on screen until the next item is ready.
Content does not become “live” merely because a CMS button was clicked. The publish path checks reachability first; the player then downloads/caches and only hands off when playback can continue safely.

End-to-end lifecycle (current)

  1. Operator uploads media in the Control Panel (/media/upload*, Cloudinary direct, or Vercel Blob).
  2. CMS stores an asset record (url, optional contentHash / sha256, mime type).
  3. Operator builds a playlist with absolute https://… item URLs (or /uploads/… rewritten with mediaBaseUrl at publish).
  4. On Publish, the panel calls verifyPlaylistsAssetsReady — HEAD/GET probe each resolved URL.
  5. If verification fails, publish stops. No setPolicy is sent.
  6. If verification succeeds, CMS builds device policy and sends device.content.setPolicy over WebSocket.
  7. Player persists the policy snapshot (tomorrowos.cachedPolicy), rebuilds the download queue, and starts / continues playback.
  8. Media is fetched into local cache (by URL and by content-hash dedup where available).
  9. Playlist / item handoffs keep the current frame until the next item is ready (prefetch / first-frame rules).
  10. On failure or empty policy, player shows brand fallback idle — not a black empty stage when avoidable.
  11. Offline / resume: player may hydratePlaybackFromCache from the last persisted policy.

What the CMS owns

Media upload backends

Configured via env (priority order in the SDK): Routes:
  • GET /media/upload-capabilities
  • POST /media/upload
  • GET /media/upload-sign + POST /media/register (Cloudinary direct)
  • POST /media/upload-initupload-chunkupload-complete (chunked proxy)

Playlist item shape (simplified)

Players prefer absolute HTTPS URLs. Relative /uploads/... URLs need a reachable CMS origin (or mediaBaseUrl at publish).

Publish gate (atomic before the wire)

Before POST /device/{deviceId}/assignments, the Control Panel:
  1. Refreshes playlists from GET /playlists
  2. Rejects empty playlists
  3. Resolves each item URL (including mediaBaseUrl when needed)
  4. Probes each asset (verifyPlaylistsAssetsReady)
  5. Only then publishes playlist IDs to the device
That is the CMS-side half of atomic activation: do not push a policy whose media is already unreachable. Example (conceptual):

What the device owns

Wire commands

HTTP wrappers used by the Control Panel:
  • POST /device/{deviceId}/assignments → builds policy → device.content.setPolicy
  • POST /device/{deviceId}/content/set-policy → raw policy push
  • POST /device/{deviceId}/content/cleardevice.content.clear
  • DELETE /device/{deviceId}/assignments → clear assignments + push empty/brand policy
There are no device methods named device.assets.download / stage / activate today. Caching is internal to the player runtime after setPolicy.

Player cache behaviour

On device.content.setPolicy the player typically:
  1. Saves a policy snapshot for offline resume
  2. Rebuilds a playlist download queue for media URLs in the policy
  3. Downloads missing files into local storage
  4. Indexes cache by URL and by content-hash / dedup keys when present (same bytes under a new CDN URL can hit cache)
  5. Runs a background download worker without tearing down the current frame
  6. Prefetches the next image on a back layer where relevant (black-gap avoidance)
Partial or failed downloads are not treated as playable cache hits. Playback continues with whatever is already good; missing items may be skipped or fall through to brand fallback depending on the active playlist state.

Atomic activation on the device

“Atomic” here means playlist / item handoff, not a separate activate API:
  1. Receive new policy
  2. Persist snapshot
  3. Keep current content visible when possible (hotUpdate for same active playlist)
  4. Queue downloads for new URLs
  5. Pick the active playlist (schedules + always-on rules)
  6. Switch only through content handoff paths that wait for the next item to be ready
  7. If nothing playable remains → brand fallback idle
  8. Report command.result success/failure for the setPolicy command itself
The player does not wipe the screen to black merely because a new policy arrived. See also docs/guides/black-gap-playback.md.

Last known good and fallback

When policy is empty, clear is called, or no playable playlist exists, the player shows brand fallback — not a CMS-supplied emergency playlist API. device.content.clear explicitly:
  • Clears download queues and in-flight markers
  • Clears the policy cache
  • Stops playback
  • Shows brand idle

Offline behaviour

Screens should keep playing when the CMS is unreachable:
  • Resume from cached policy after reboot / orientation reload / re-pair (hydratePlaybackFromCache)
  • Serve media from local cache when the network path fails
  • Reconnect WebSocket (device.hello / device.resume) and accept a newer setPolicy when online again
  • Do not treat an incomplete CMS publish as applied (CMS publish gate already blocked it)

Storage pressure and cleanup

Current players prune / manage cache indexes as new policies arrive and downloads complete. Behaviour is implementation-defined per platform (Tizen file APIs, BrightSign storage, etc.). There is no public assets.getStorage() / assets.cleanup() method yet. Cleanup must not delete:
  • Files still referenced by the active policy
  • The policy snapshot used for resume
  • Brand assets required for idle

Failure modes (practical)

Device logs may surface via uplink device.log; screenshots via device.telemetry.captureScreen (HTTP: POST /device/{id}/screenshot).

Capability checks

Discover support with:
Relevant shipped capabilities include content policy commands, for example:
Older docs listed aspirational names such as asset.download, asset.stage, asset.atomic_activation. Those are design goals / certification checklists, not separate wire methods in protocol 1.0. Atomic activation is behaviour inside setPolicy + player runtime.

API examples (current)

Upload on CMS (panel / server routes), then:
Which results in the device receiving:
Clear:

Certification tests (aligned to current behaviour)

Minimum tests:
  • Image / video upload to configured media backend
  • Publish blocked when an asset URL is unreachable
  • Publish succeeds and device receives setPolicy
  • Player caches media and plays without black gap on item handoff
  • Hot update of the active playlist without full black clear
  • Offline resume from cached policy
  • content/clear → brand idle
  • Relative /uploads/ URLs with correct mediaBaseUrl
  • Cloudinary / Blob absolute URLs play on-device
  • Failed download does not replace good on-screen content prematurely

Goal

TomorrowOS should make content activation safe and predictable. The goal is not a separate Asset microservice API. The goal is: reachable media → verified publish → durable policy push → local cache → seamless handoff → brand-safe fallback.