Merge: platform-native capability declaration

This commit is contained in:
ScreenTinker 2026-08-05 14:25:46 -05:00
commit f8e359895d
4 changed files with 186 additions and 7 deletions

View file

@ -23,6 +23,10 @@
<tizen:privilege name="http://tizen.org/privilege/internet"/>
<tizen:privilege name="http://tizen.org/privilege/application.launch"/>
<tizen:privilege name="http://tizen.org/privilege/display"/>
<!-- tizen.tvaudiocontrol: sets the TV's OWN volume, which is what reaches AVPlay video on the
hardware plane. Without this privilege the API throws SecurityError and the player falls
back to per-element media volume, which cannot touch that plane. -->
<tizen:privilege name="http://tizen.org/privilege/tv.audio"/>
<tizen:privilege name="http://developer.samsung.com/privilege/network.public"/>
<!-- #125: Samsung B2B fleet control (reboot / panel power via b2bcontrol /

View file

@ -59,6 +59,7 @@
<script src="js/player.js"></script>
<script src="js/device-control.js"></script>
<script src="js/pip-overlay.js"></script>
<script src="js/capabilities.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View file

@ -15,7 +15,7 @@
// packaged config.xml via the Tizen application API; fall back to a constant that
// build-wgt.sh stamps from config.xml's version="" so the dashboard always shows the
// version that is actually installed (never the old hardcoded '1.0.0').
var APP_VERSION_FALLBACK = '1.9.11'; // st:app-version — stamped by build-wgt.sh
var APP_VERSION_FALLBACK = '1.9.29'; // st:app-version — stamped by build-wgt.sh
var APP_VERSION = (function () {
try {
var v = tizen.application.getCurrentApplication().appInfo.version;
@ -388,6 +388,12 @@
// independent of (and in addition to) the panel API.
if (type === 'screen_on' || type === 'launch') { clearScreenOff(); keepAwake(); }
// Volume is handled here, not in STDeviceControl: it is not a Samsung fleet-control action
// and it must work on EVERY build, not only a partner-signed panel. Previously it fell
// through to STDeviceControl's default case and was answered "unknown command" — the
// dashboard slider did nothing on Tizen at all.
if (type === 'set_volume') { applyVolume(payload); return; }
if (!window.STDeviceControl) { reportCmd('error', type, 'device-control unavailable'); return; }
STDeviceControl.run(type, payload).then(function (res) {
var note = res.note;
@ -477,6 +483,12 @@
msg.client_version = APP_VERSION; // config.xml version (stamped by build-wgt.sh)
msg.platform = 'Tizen ' + (tizenVersion() || '');
msg.contract_version = 'v4';
// What this player can ACTUALLY do, probed at runtime (js/capabilities.js). The dashboard hides
// every control we do not declare, so a Tizen panel stops showing buttons for things the
// platform cannot honour. Omitted entirely if the module failed to load: the server then falls
// back to its per-platform baseline, which is the right behaviour for an older .wgt and much
// better than declaring an empty set, which would read as "supports nothing".
try { if (window.STCapabilities) msg.capabilities = STCapabilities.detect(); } catch (e) {}
if (deviceId && deviceToken) { msg.device_id = deviceId; msg.device_token = deviceToken; }
else { msg.pairing_code = pairingCode(); }
socket.emit('device:register', msg);
@ -508,16 +520,84 @@
// ---- remote control + dashboard preview (#120 / #121) ----
// Screen on/off uses a black overlay (a sideloaded web app can't power the panel
// off cleanly), mirroring the web player.
// Volume, 0-100 from the dashboard.
//
// Prefers tizen.tvaudiocontrol — that is the TV's OWN volume, so it applies to whatever is
// playing including AVPlay video, which lives on a hardware plane the media elements know
// nothing about. Setting el.volume alone would leave portrait video at full blast.
//
// Falls back to the media elements where the TV profile is absent (URL-Launcher / browser
// context), and remembers the level so items mounted LATER inherit it — media elements are
// created per item, so a one-shot set would last only until the playlist advanced.
var mediaVolume = null; // 0..1, null = never set
function applyVolume(payload) {
var pct = payload && (payload.value !== undefined ? payload.value : payload.volume);
var n = Number(pct);
if (!isFinite(n)) { reportCmd('warn', 'set_volume', 'no usable value in payload'); return; }
n = Math.max(0, Math.min(100, n));
mediaVolume = n / 100;
var tv = null;
try { tv = window.STCapabilities ? STCapabilities.tvAudio() : null; } catch (e) {}
if (tv) {
try {
tv.setVolume(Math.round(n));
reportCmd('info', 'set_volume', 'TV volume set to ' + Math.round(n) + '% (tvaudiocontrol)');
return;
} catch (e) {
// Fall through to the media elements rather than reporting success for nothing.
reportCmd('warn', 'set_volume', 'tvaudiocontrol refused (' + (e && e.message ? e.message : e) + ') — using media volume');
}
}
applyMediaVolume();
reportCmd('info', 'set_volume', 'media volume set to ' + Math.round(n) + '%');
}
function applyMediaVolume() {
if (mediaVolume === null) return;
try {
var els = document.querySelectorAll('video, audio');
for (var i = 0; i < els.length; i++) {
try { els[i].volume = mediaVolume; } catch (e) {}
}
} catch (e) {}
}
// Media elements are created per item across several render paths, so re-apply on every 'play'.
// Captured, because media events do not bubble.
try {
document.addEventListener('play', function () { applyMediaVolume(); }, true);
} catch (e) {}
// Blanking has to reach the HARDWARE PLANE, not just the DOM.
//
// A z-index overlay covers the web layer only. Portrait/flipped video runs through AVPlay
// (#170), which composites on a separate hardware plane the DOM cannot draw over — so on a
// portrait panel the old overlay went up and the video kept playing straight through it. The
// screen never went dark, which is the whole point of the command. Tearing the AVPlay session
// down is what actually blanks it; the same trap bit the BrightSign port from the other side.
//
// Landscape <video> is paused too: cheap, and it stops audio continuing behind a black screen.
function showScreenOff() {
if (document.getElementById('screenOffOverlay')) return;
var o = document.createElement('div');
o.id = 'screenOffOverlay';
o.style.cssText = 'position:fixed;inset:0;background:#000;z-index:9999';
document.body.appendChild(o);
if (!document.getElementById('screenOffOverlay')) {
var o = document.createElement('div');
o.id = 'screenOffOverlay';
o.style.cssText = 'position:fixed;inset:0;background:#000;z-index:9999';
document.body.appendChild(o);
}
try { if (player && player.avActive && player.avStop) player.avStop(); } catch (e) {}
try {
var vids = document.querySelectorAll('video');
for (var i = 0; i < vids.length; i++) { try { vids[i].pause(); } catch (e2) {} }
} catch (e) {}
}
function clearScreenOff() {
var o = document.getElementById('screenOffOverlay');
if (o && o.parentNode) o.parentNode.removeChild(o);
// Not blanked: nothing to restore, and re-mounting would restart the current item for no reason.
if (!o) return;
if (o.parentNode) o.parentNode.removeChild(o);
// A torn-down AVPlay session cannot be resumed, so re-mount the current item from scratch.
// playCurrent(), not gotoIndex(): gotoIndex early-returns when the index has not changed, so it
// would leave a blanked portrait panel dark after screen_on.
try { if (player && player.playCurrent) player.playCurrent(); } catch (e) {}
}
// Diagnostic info overlay (parity with the web player). Toggled by the dashboard remote BACK key —
// NOT the physical TV BACK (10009), which still exits to setup. A quick on-site troubleshooting panel.

94
tizen/js/capabilities.js Normal file
View file

@ -0,0 +1,94 @@
/* 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 — app.js caches the PAYLOAD so a reboot during an outage replays the
// last playlist, but the media bytes are still fetched from the
// network. Content does not survive an outage, so claiming offline
// capability would overstate it.
return caps;
}
window.STCapabilities = { detect: detect, tvAudio: tvAudio };
})();