screentinker/docs/player-parity.md
ScreenTinker c4ee7d008f web player: declare capabilities at runtime, persist them, and audit all four players
The dashboard offered every control to every display, so a browser tab showed a
reboot button that could never work. server/lib/player-capabilities.js defines the
vocabulary; this makes the web player actually speak it.

The declaration is computed, not constant, because the same index.html is BOTH the
browser player and the BrightSign player. system.reboot / display.power /
display.resolution / system.self_update are claimed only when BS.hasHost() answers —
deliberately hasHost() and not isBrightSign(), since the UA check is also true for a
widget built without node integration, which can reach none of them. Screenshots,
offline cache, transitions and native sync are each probed the same way.

Capabilities were never persisted: the column and the handler did not exist, so a
declaration would have been sent and silently dropped. Added the migration and
applyCapabilities(). An ABSENT declaration leaves the column NULL so the baseline
still applies — several hundred fielded displays declare nothing and would otherwise
lose every control at once — while an EMPTY declaration is stored as '[]' and honoured.

docs/player-parity.md records every capability against all four players with a reason
for each "no", and flags three Tizen baseline errors found while verifying it.

Tests: 1109/1109. Both inline <script> blocks in index.html parse clean.
2026-08-05 14:17:40 -05:00

119 lines
6.9 KiB
Markdown

# Player parity matrix
What each player can actually do, verified against the code rather than assumed. This is the
document that says where the remaining work is, so a wrong "yes" here is worse than a missing row:
it puts a control on the dashboard that cannot work.
Capability names come from `server/lib/player-capabilities.js`. Players declare their own set at
registration; a player that declares nothing falls back to the per-platform baseline in that file.
**Legend** — ✅ supported · ⚠️ partial/conditional (reason given) · ❌ not supported (reason given)
BrightSign runs the *same* `server/player/index.html` as the browser, so it differs only where the
`autorun.brs` host bridge adds something the browser cannot reach.
## Playback
| capability | Android | Web | Tizen | BrightSign |
|---|---|---|---|---|
| `playback.video` | ✅ ExoPlayer | ✅ `<video>` | ✅ AVPlay | ✅ hardware plane |
| `playback.image` | ✅ | ✅ | ✅ | ✅ |
| `playback.widget` | ✅ WebView | ✅ iframe | ✅ iframe | ✅ iframe |
| `playback.youtube` | ✅ WebView embed | ✅ IFrame API | ✅ iframe embed | ✅ IFrame API |
| `playback.zones` | ✅ | ✅ | ✅ | ✅ |
| `playback.transitions` | ✅ GL wipes (#204) | ⚠️ declared only when the bundle loads — a failed load hard-cuts rather than breaking playback | ✅ | ⚠️ as web |
| `playback.pip` | ✅ `PipOverlay` | ✅ `#pipContainer` | ✅ | ✅ |
## Audio
| capability | Android | Web | Tizen | BrightSign |
|---|---|---|---|---|
| `audio.mute` | ✅ incl. YouTube via IFrame bridge | ✅ | ✅ incl. YouTube via `postMessage` | ✅ as web |
| `audio.volume` | ✅ `set_volume` | ✅ `set_volume` | ❌ **no `set_volume` handler exists** — the dashboard slider does nothing today | ✅ as web |
## Display
| capability | Android | Web | Tizen | BrightSign |
|---|---|---|---|---|
| `display.rotation` | ✅ native `rootView.rotation` | ✅ CSS transform | ✅ CSS + AVPlay for video | ⚠️ host rotates the output via `roVideoMode`; CSS alone cannot turn the hardware video plane |
| `display.power` | ✅ `screen_off` / `lock_now` | ❌ a browser tab cannot power a panel — the overlay only paints black | ❌ `screen_off` draws a black overlay, deliberately, "so the command still does something visible" | ⚠️ media teardown always works; CEC is best-effort and absent on some units |
| `display.resolution` | ❌ no video-mode control in the app | ❌ not addressable from a browser | ❌ | ✅ `roVideoMode` via the host |
## Remote view and control
| capability | Android | Web | Tizen | BrightSign |
|---|---|---|---|---|
| `remote.screenshot` | ⚠️ view capture always; full-screen only with accessibility or MediaProjection | ⚠️ canvas only — same-origin content, and the alpha probe rejects frames where no pixels arrived | ✅ `captureAndSend` | ⚠️ host framebuffer capture **requires primary storage**; falls back to canvas, which cannot read the video plane |
| `remote.stream` | ✅ | ✅ 1fps | ✅ | ⚠️ as web |
| `remote.input` | ✅ | ✅ | ✅ | ✅ |
## Lifecycle
| capability | Android | Web | Tizen | BrightSign |
|---|---|---|---|---|
| `system.restart_player` | ✅ | ✅ `location.reload()` | ✅ | ✅ host rebuilds the widget — a page reload does not reliably return |
| `system.reboot` | ✅ device owner | ❌ a browser tab cannot reboot its host | ❌ no Tizen API exposed to the app | ✅ `RebootSystem()` via the host |
| `system.self_update` | ✅ APK OTA (`UpdateChecker`) | ❌ the server deploys the player; there is nothing for it to update | ❌ `.wgt` updates go through Tizen's own store/CLI | ✅ `autorun.zip` package update |
## Device management
Android device-owner territory. Everything here is ❌ elsewhere for the same reason — no equivalent
privilege model exists on those platforms — so the column is collapsed.
| capability | Android | Web / Tizen / BrightSign |
|---|---|---|
| `system.kiosk` | ✅ lock-task, now persisted across reboot | ❌ no device-owner concept |
| `system.brightness` | ✅ Tier 0/1 | ❌ |
| `system.screen_timeout` | ✅ Tier 1 | ❌ |
| `system.install_apk` | ✅ Tier 2 | ❌ not an APK platform |
| `system.shell` | ✅ Tier 2, handled in `WebSocketService` | ❌ |
| `system.time` | ✅ Tier 2 | ❌ |
## Synchronisation and resilience
| capability | Android | Web | Tizen | BrightSign |
|---|---|---|---|---|
| `sync.clock` | ✅ | ✅ | ✅ | ✅ |
| `sync.native` | ❌ no native protocol | ❌ | ❌ | ⚠️ SyncManager, BOS 8.2.10+; multicast so all members must share one L2 network |
| `offline.cache` | ✅ content downloaded to disk | ✅ service worker | ⚠️ **playlist payload only**`st_payload_cache` replays the last renderable payload, but there is no service worker, so media still needs the network | ✅ service worker + 1GB storage quota |
---
## Real gaps worth closing
Ordered by how visible the failure is to an operator.
1. **Tizen `audio.volume` — dead control.** `set_volume` has no handler in `tizen/js/app.js`; the
only volume path is the on-device `KEYCODE_VOLUME_*` keys. The dashboard slider silently does
nothing. Either implement the handler or let the capability hide the control.
2. **Tizen `offline.cache` is partial.** The playlist survives a reboot; the media does not. A
Tizen panel that loses its uplink keeps its schedule and cannot play it. This is the largest
functional gap in the table.
3. **BrightSign `remote.screenshot` needs primary storage.** Reachable today only via the canvas
fallback, which cannot read the video plane, so screenshots show everything except the video.
Resolves itself when a card or SSD is fitted.
4. **`display.resolution` is BrightSign-only.** Fine, but the dashboard should not offer it
elsewhere.
## Correctly impossible — do not "fix" these
- **`system.reboot` on web/Tizen.** No API exists. A browser tab rebooting its host would be a
browser vulnerability.
- **`display.power` on web.** The overlay is the honest maximum; the panel stays lit.
- **All of device management off Android.** No equivalent privilege model exists on Tizen or
BrightSign, and a web player has no device to manage.
- **`system.self_update` on web.** The player *is* the deployment; there is nothing to update.
- **`sync.native` off BrightSign.** It is BrightSign's own protocol, and the clock-derived one is
the cross-platform answer that already works everywhere.
## ⚠️ Corrections needed in `player-capabilities.js`
Found while verifying this table. The baselines only apply to displays that declare nothing, so
these are wrong for the existing fleet until each player ships its declaration:
- **`tizen` claims `audio.volume`** — no handler exists (gap 1 above). Should be removed.
- **`tizen` omits `remote.screenshot` and `remote.stream`** — both are implemented
(`captureAndSend`, `startStreaming`). Should be added.
- **`tizen` claims `offline.cache`** — true only for the playlist payload, not media. Either keep
it with the partial meaning documented, or split the capability.