mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Tizen declares what it can do, and volume and blanking now work
The dashboard offered every control to every display, so on a Tizen panel the volume
slider and screen_off did nothing and read as bugs. Two of them were genuinely dead:
- set_volume fell through STDeviceControl.run()'s default case and was answered
"unknown command". It is not a Samsung fleet action and must work on every build,
so it is handled in app.js instead: tizen.tvaudiocontrol where the TV profile
provides it (that is the TV's own volume, the only thing that reaches AVPlay video
on the hardware plane), otherwise the media elements. The level is remembered and
re-applied on 'play' — media elements are created per item, so a one-shot set
lasted only until the playlist advanced.
- screen_off was a z-index overlay, which covers the web layer only. Portrait and
flipped video runs through AVPlay on a separate hardware plane the DOM cannot draw
over, so the overlay went up and the video played straight through it. It now tears
the AVPlay session down as well; screen_on re-mounts via playCurrent(), because a
torn-down session cannot be resumed and gotoIndex() early-returns on an unchanged
index.
js/capabilities.js declares the rest at runtime rather than from a static table,
because on Tizen the answer varies by build: reboot exists only through the B2B
surface injected on a partner-signed .wgt, and tizen.tvaudiocontrol is absent in a
browser context. Against the server baseline this adds display.power, remote.screenshot
and remote.stream (all backed by real handlers) and drops offline.cache — the payload
is cached, but media bytes are still fetched from the network, so content does not
survive an outage and claiming it would overstate.
Adds the tv.audio privilege; without it tvaudiocontrol throws SecurityError.
This commit is contained in:
parent
6bc709d2f7
commit
c07a56b47d
|
|
@ -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 /
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
94
tizen/js/capabilities.js
Normal 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 };
|
||||
})();
|
||||
Loading…
Reference in a new issue