# 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** — ✅ verified in source · ⚠️ partial/conditional (reason given) · ❌ not supported (reason given) · 💀 **dead**: the capability is declared or baselined but the control cannot work · ❓ **unverifiable from source** — needs hardware, and is marked as such rather than asserted. 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. The bridge has two halves: the JS (`brightsign/st-bridge.js`, served by us at `/player/st-bridge.js`, always current) and the on-device BrightScript that must create the widget with `nodejs_enabled:true`. `BS.hasHost()` is false unless BOTH are present, and everything host-backed hangs off it. Verified at `2237eda`. Where a row cites "the fielded build" it means `v1.9.28` — the last release before any player declared anything, and therefore the build every baseline is describing. --- ## 🔴 Read this first: three dead controls found by this audit These are not gaps in coverage. They are controls a customer can press today that do nothing. ### 1. The volume slider works on Android only `frontend/js/views/device-detail.js` sends `set_volume` as **`{ level: 0..1 }`**: ```js el?.addEventListener('change', () => sendCommand(device.id, cmd, { level: parseInt(el.value, 10) / 100 })); ``` | player | what the handler reads | result | |---|---|---| | Android | `payload.optDouble("level", -1.0)` | ✅ works | | web | `data.payload?.value ?? data.value` | 💀 `undefined` → `isFinite` fails → silent no-op | | Tizen | `payload.value ?? payload.volume` | 💀 `undefined` → logs `no usable value in payload` | | BrightSign | (the web player) | 💀 as web | Three of the four players have a complete, working volume implementation that cannot be driven, because nobody checked the payload key against the sender. **Fix: one line in `server/player/index.html` and one in `tizen/js/app.js` — accept `level` (0..1) as well.** Until then `audio.volume` has been removed from the `web` and `brightsign` baselines, and `test/player-parity-baselines.test.js` holds that as a **biconditional**: fix the player and the test fails, telling you to put the baseline entry back. ### 2. Every #161 Tier-2 command was refused for the entire fleet — FIXED here `lock_now`, `power_menu`, `status_bar`, `block_uninstall` and `unblock_uninstall` were gated on `system.device_owner`. **No player declares that name** — not `PlayerCapabilities.kt`, not `tizen/js/capabilities.js`, not `declaredCapabilities()`, not `st-bridge.js` — and no baseline granted it. So `supports()` returned false for every device on every platform and all five commands were refused, *including on the device-owner panels the whole feature was built for*. The dashboard still drew the buttons, because `device-detail.js` gates that block on `device.tier === 2 ||` too, so an operator on a real owner panel pressed "Lock now" and got a silent server-side refusal. Fixed in `player-capabilities.js`: those five now accept `system.device_owner` **or** `system.kiosk`. That is an exact stand-in, not a loose one — `PlayerCapabilities.kt` declares `system.kiosk` under `if (isOwner)` and nothing else, which is precisely when `STPolicy`'s `owned()` actions do anything, and no non-Android player declares it. **Follow-up owned by Android:** `PlayerCapabilities.kt` should declare `system.device_owner` under `if (isOwner)`, at which point the stand-in becomes redundant. ### 3. The capture bootstrap required the capability it creates — half FIXED here `enable_system_capture` raises Android's MediaProjection consent dialog: it is how a panel *gains* full-screen capture. It was gated on `remote.screenshot`, so the only panel that needs it — no accessibility, no projection grant, therefore no declared `remote.screenshot` — was the one panel that could not be sent it. The command is now ungated. **Still broken, and it is a frontend change:** `device-detail.js` renders the button behind `can('remote.screenshot')`, so it is still hidden on exactly those panels. --- ## Playback No command routes to any `playback.*` capability and no dashboard control is gated on one, so these describe content rendering. They are informational, and shown to the operator in the Info tab. | capability | Android | Web | Tizen | BrightSign | |---|---|---|---|---| | `playback.video` | ✅ ExoPlayer (`MediaPlayerManager`) | ✅ `