Principles
- Local is primary: the player must never block on network for playback.
- Deterministic: schedules and transitions produce the same output online or offline.
- Recoverable: every step is crash-safe and idempotent.
- Observable: logs, heartbeats and proof-of-play survive outages.
1) System architecture
On-device
- Player app (Tizen/webOS/Android) with AV layer (AVPlay/MediaPlayer).
- Local content store with checksum-verified assets.
- Scheduler & state machine plus background sync worker.
- Health monitor handling heartbeat, temperature, free space and crash counter.
Backend
- Content API serving signed manifests and assets.
- Device registry & policy (model/firmware channels, time zone).
- Telemetry ingest & proof-of-play store.
2) Content manifest
Manifests make offline decisions deterministic. Serve a compact JSON that lists assets, versions, checksums, durations and schedule.
{
"version": "2025.07.15-09",
"valid_from": "2025-07-15T09:00:00Z",
"assets": [
{"id":"hero-8k","path":"loops/hero_8k.mp4","sha256":"...","bytes":123456789,"duration":120},
{"id":"promo","path":"loops/promo_4k.mp4","sha256":"...","bytes":45678901,"duration":30}
],
"schedule": [
{"asset":"hero-8k","start":"* 08:00","end":"* 20:00"},
{"asset":"promo","pattern":"*/15m"}
]
}
Use semantic version strings for rollout control. Include bytes to pre-allocate space and detect truncation.
3) Caching strategy
| Storage layout | /wgt-private/content/<version>/ (Tizen/webOS) or app-private directory (Android). |
| Atomic updates | Download to .part, verify checksum, then rename. Keep previous version as fallback. |
| Space management | Budget by manifest bytes; apply LRU eviction for expired assets. |
| Integrity | sha256 per file; verify on boot and before playback. |
4) Sync & scheduling
- Intervals: poll manifests every 5–15 minutes; back off when offline.
- Time: maintain device time via NTP or gateway sync; fail gracefully with last known schedule.
- Partial downloads: keep the current playlist running while background fetch continues.
- Regionalisation: select channel by tags (store-id, timezone, locale).
5) Player state machine
States: IDLE → PREPARE → PLAYING → LOOPING → ERROR → RECOVER
On PREPARE: verify file, open decoder, warm-up audio
On LOOPING: if streamcomplete → seek(0) → play()
On ERROR: log, increment counter, fallback to safe asset, schedule retry
Watchdogs: decode stall, no frames for 2s, CPU temp high → restart AV
Tizen AVPlay (local file)
const p = tizen.tvavplay;
p.open('file://wgt-private/content/2025.07.15-09/hero_8k.mp4');
p.setDisplayRect(0, 0, screen.width, screen.height);
p.prepareAsync(() => p.play());
p.setListener({ onstreamcompleted: () => { p.seekTo(0, () => p.play()); } });
webOS (webOS.video API)
// pseudo
video.src = 'file:///media/sereno/content/2025.07.15-09/hero_8k.mp4';
video.loop = true; // with exact muxing this is seamless
video.play();
6) Telemetry & proof-of-play
- Heartbeat: every 60s (device id, app version, uptime, free space, temperatures, Wi-Fi RSSI).
- Event log: play_start/stop, errors, retries, version_switched.
- Proof-of-play: CSV/JSON rows with timestamps, asset id, duration watched.
- Store-and-forward: queue locally; upload when back online; dedupe by idempotency key.
7) Security & hardening
- Sign manifests; optionally sign assets or deliver over mutually authenticated TLS.
- Lock kiosk mode and restrict remote control to your secure channel.
- Validate filenames/paths from the manifest and deny traversal.
- Stagger updates to avoid fleet-wide spikes.
8) QA & soak testing
- 72-hour soak per model/firmware channel; power-cycle and network-bounce.
- Record decode metrics, temps, reboot count and playlist accuracy.
- Simulate corrupted downloads to confirm rollback logic.
9) Rollout & ops
- Stage on a small canary group per model.
- Observe telemetry for 24–48 hours.
- Gradually expand by region/timezone.
- Rollback by pinning the previous manifest version.
Appendix: snippets
Exponential backoff (fetch manifest)
async function fetchWithBackoff(url, max = 6) {
for (let i = 0; i < max; i++) {
try {
const res = await fetch(url, { cache: 'no-store' });
return await res.json();
} catch (e) {
await new Promise(r => setTimeout(r, Math.min(60000, 2000 * Math.pow(2, i))));
}
}
throw new Error('failed');
}
Atomic download with checksum (pseudo)
download(url, tmp)
if (sha256(tmp) === manifest.sha256) rename(tmp, target)
else delete(tmp) & retry