screentinker/tizen/js/capabilities.js
ScreenTinker 684e60fc55 Offline media on every player, and a revision so the cache can still be updated
Two halves of the same problem. A screen has to keep playing when the link is
gone, and it must not keep playing the wrong thing once the link is back.

CACHING FOR OFFLINE, on the players that could not:

- Tizen cached nothing but the playlist, so a panel came back from a reboot
  knowing exactly what to show and fetched every frame of it from a server that
  was not there. tizen/js/media-cache.js caches the media itself to wgt-private
  (the store Tizen documents as surviving reboots), resumable via Range and
  If-Range, with the transfer async so a stalled chunk cannot freeze the player.
  offline.cache moves from "absent" to a runtime claim: a build with no writable
  private storage still says nothing.

- The web player's worker stored only what a single fetch() happened to
  complete, which on a marginal link is nothing at all — a 200MB asset never
  finishes in one go and every retry starts from zero. It now accumulates in
  resumable chunks, driven by the player's playlist rather than by playback, so
  the prefetch is not competing with the video that is currently on screen for
  the same scarce bandwidth. BrightSign inherits this.

STILL UPDATING, which caching quietly breaks:

PUT /api/content/:id/replace changes an asset's bytes under a stable id. Every
cache keys on that id, so before this the new bytes could not reach a panel that
already held the old ones — not until the next refresh, but never. Content now
carries a revision, stamped onto each item at send time like widget revs, and
every player keys its cache on it. The same send-time refresh fixes a second
bug: a replace writes a new randomly-named file and unlinks the old one, so the
filepath in a published snapshot pointed at a deleted file and web panels 404'd
on the item until somebody republished the playlist. The route now also pushes
to affected devices, which it never did.

Bytes are kept only where they can be built upon: no validator means no safe
resume, so the partial is discarded and the attempt backs off as the failure it
is rather than re-fetching the same prefix forever.

Server needed no new transfer support — res.sendFile already does Range,
If-Range and 416. The Tizen cache and the service worker are both driven in Node
against fakes, because neither can be exercised without hardware and "the chunks
assemble correctly" is not something to discover from a panel showing a corrupt
video.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 15:27:36 -05:00

101 lines
5.3 KiB
JavaScript

/* ScreenTinker — Tizen capability declaration.
*
* The dashboard used to offer every control to every display, so buttons a platform cannot honour
* did nothing and read as bugs. The player now DECLARES what it can actually do
* (server/lib/player-capabilities.js holds the vocabulary) and the frontend hides the rest.
*
* Declared at RUNTIME rather than from a static table, because on Tizen the answer genuinely
* varies by build and panel:
* - reboot and real panel power exist only through webapis.systemcontrol / b2bapis.b2bcontrol,
* which are injected ONLY on a Samsung panel running a .wgt signed with a Partner distributor
* certificate. The same code on an unsigned dev build, the URL-Launcher path, or a consumer TV
* has no such surface (see device-control.js).
* - tizen.tvaudiocontrol is a TV-profile API; it is absent in a plain browser context.
* A hardcoded list would claim these on every Tizen device and be wrong on most of them.
*
* ⚠️ Names must match server/lib/player-capabilities.js exactly. An unknown string is DROPPED by the
* server's parser, so a typo silently removes a control rather than failing loudly.
*/
(function () {
'use strict';
/* Native TV audio. Present on the TV profile; absent in a browser/URL-Launcher context, which is
* why this is probed rather than assumed. */
function tvAudio() {
return (window.tizen && tizen.tvaudiocontrol && typeof tizen.tvaudiocontrol.setVolume === 'function')
? tizen.tvaudiocontrol : null;
}
/* The Samsung fleet-control surface, via the module that already owns those probes. Re-asked on
* every call because the platform can inject these objects after the first script pass. */
function fleet() {
try { return window.STDeviceControl ? window.STDeviceControl.capabilities() : null; }
catch (e) { return null; }
}
function detect() {
var caps = [
// Playback surface — all implemented in player.js on every Tizen build.
'playback.video', 'playback.image', 'playback.widget', 'playback.youtube',
'playback.zones', 'playback.transitions', 'playback.pip',
// Per-item mute, honouring the shared rule in server/lib/media-mute.js (including the
// YouTube embed, which used to be hardcoded muted and unmutable).
'audio.mute',
// Volume always resolves to SOMETHING: the native TV control where the profile provides it,
// otherwise the media elements. Both change what a viewer hears, so the control is honest.
'audio.volume',
// CSS for graphics, and AVPlay's setDisplayRotation for portrait/flipped video — the Tizen
// HTML5 <video> sits on a hardware plane that ignores CSS rotate.
'display.rotation',
// Blanking works on every build: a real panel API where one exists, and otherwise the black
// overlay PLUS hardware-plane teardown (app.js showScreenOff). Declared because the screen
// genuinely goes dark either way — hiding a working control is the opposite failure to the
// one this whole model exists to fix. Which mechanism ran is reported in the device log.
'display.power',
// Images capture for real; video and YouTube return an honest status card saying live preview
// is unavailable on this platform. Declared because the operator gets a truthful frame rather
// than a dead button.
'remote.screenshot', 'remote.stream', 'remote.input',
// location.reload() — the URL-Launcher path also re-pulls content this way.
'system.restart_player',
// Clock/schedule-derived group sync, no leader.
'sync.clock'
];
var f = fleet();
// Only on a partner-signed panel with the B2B/system surface present.
if (f && f.reboot) caps.push('system.reboot');
// NOT declared, deliberately, each for a concrete reason:
// display.resolution — no web-accessible mode setting on the TV profile.
// system.self_update — a .wgt is installed by the panel, not by the app; there is no
// in-app OTA (device-control.js reports the same).
// system.kiosk — Tizen has no device-owner equivalent reachable from a web app.
// system.brightness / system.screen_timeout / system.time / system.install_apk /
// system.shell — no substantiated API on this surface. Claiming them would put back
// exactly the dead buttons this change removes.
// sync.native — no cross-player frame sync (that is BrightSign's SyncManager).
// (offline.cache is deliberately absent from this list — it is a RUNTIME check below.)
// offline.cache is a RUNTIME fact, not a platform one. app.js has always cached the payload,
// but the media bytes were fetched from the network every time — so a panel survived an outage
// knowing exactly what it could not show. media-cache.js changes that, WHERE the platform
// actually gives us persistent storage. It does not on every build, and a panel that cannot
// write to wgt-private must not claim an offline capability it does not have.
try {
if (window.MediaCache && (window.__stMediaCache || window.MediaCache.create())) caps.push('offline.cache');
} catch (e) { /* no storage: the claim stays absent, which is the honest answer */ }
return caps;
}
window.STCapabilities = { detect: detect, tvAudio: tvAudio };
})();