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 separatetomorrow.assets.* command surface today. Assets move through the CMS media routes and the device content policy:
@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
- Verifying assets on the CMS before publish
- Pushing a complete policy to the device
- Caching media on the player (URL + content hash)
- Switching playlists only when the next content can play (atomic handoff)
- 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)
- Operator uploads media in the Control Panel (
/media/upload*, Cloudinary direct, or Vercel Blob). - CMS stores an asset record (
url, optionalcontentHash/ sha256, mime type). - Operator builds a playlist with absolute
https://…item URLs (or/uploads/…rewritten withmediaBaseUrlat publish). - On Publish, the panel calls
verifyPlaylistsAssetsReady— HEAD/GET probe each resolved URL. - If verification fails, publish stops. No
setPolicyis sent. - If verification succeeds, CMS builds device policy and sends
device.content.setPolicyover WebSocket. - Player persists the policy snapshot (
tomorrowos.cachedPolicy), rebuilds the download queue, and starts / continues playback. - Media is fetched into local cache (by URL and by content-hash dedup where available).
- Playlist / item handoffs keep the current frame until the next item is ready (prefetch / first-frame rules).
- On failure or empty policy, player shows brand fallback idle — not a black empty stage when avoidable.
- Offline / resume: player may
hydratePlaybackFromCachefrom the last persisted policy.
What the CMS owns
Media upload backends
Configured via env (priority order in the SDK):
Routes:
GET /media/upload-capabilitiesPOST /media/uploadGET /media/upload-sign+POST /media/register(Cloudinary direct)POST /media/upload-init→upload-chunk→upload-complete(chunked proxy)
Playlist item shape (simplified)
/uploads/... URLs need a reachable CMS origin (or mediaBaseUrl at publish).
Publish gate (atomic before the wire)
BeforePOST /device/{deviceId}/assignments, the Control Panel:
- Refreshes playlists from
GET /playlists - Rejects empty playlists
- Resolves each item URL (including
mediaBaseUrlwhen needed) - Probes each asset (
verifyPlaylistsAssetsReady) - Only then publishes playlist IDs to the device
What the device owns
Wire commands
HTTP wrappers used by the Control Panel:
POST /device/{deviceId}/assignments→ builds policy →device.content.setPolicyPOST /device/{deviceId}/content/set-policy→ raw policy pushPOST /device/{deviceId}/content/clear→device.content.clearDELETE /device/{deviceId}/assignments→ clear assignments + push empty/brand policy
device.assets.download / stage / activate today. Caching is internal to the player runtime after setPolicy.
Player cache behaviour
Ondevice.content.setPolicy the player typically:
- Saves a policy snapshot for offline resume
- Rebuilds a playlist download queue for media URLs in the policy
- Downloads missing files into local storage
- Indexes cache by URL and by content-hash / dedup keys when present (same bytes under a new CDN URL can hit cache)
- Runs a background download worker without tearing down the current frame
- Prefetches the next image on a back layer where relevant (black-gap avoidance)
Atomic activation on the device
“Atomic” here means playlist / item handoff, not a separate activate API:- Receive new policy
- Persist snapshot
- Keep current content visible when possible (
hotUpdatefor same active playlist) - Queue downloads for new URLs
- Pick the active playlist (schedules + always-on rules)
- Switch only through content handoff paths that wait for the next item to be ready
- If nothing playable remains → brand fallback idle
- Report
command.resultsuccess/failure for thesetPolicycommand itself
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 newersetPolicywhen 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 publicassets.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: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: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 correctmediaBaseUrl - Cloudinary / Blob absolute URLs play on-device
- Failed download does not replace good on-screen content prematurely