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.
6.9 KiB
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.
- Tizen
audio.volume— dead control.set_volumehas no handler intizen/js/app.js; the only volume path is the on-deviceKEYCODE_VOLUME_*keys. The dashboard slider silently does nothing. Either implement the handler or let the capability hide the control. - Tizen
offline.cacheis 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. - BrightSign
remote.screenshotneeds 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. display.resolutionis BrightSign-only. Fine, but the dashboard should not offer it elsewhere.
Correctly impossible — do not "fix" these
system.rebooton web/Tizen. No API exists. A browser tab rebooting its host would be a browser vulnerability.display.poweron 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_updateon web. The player is the deployment; there is nothing to update.sync.nativeoff 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:
tizenclaimsaudio.volume— no handler exists (gap 1 above). Should be removed.tizenomitsremote.screenshotandremote.stream— both are implemented (captureAndSend,startStreaming). Should be added.tizenclaimsoffline.cache— true only for the playlist payload, not media. Either keep it with the partial meaning documented, or split the capability.