diff --git a/docs/player-parity.md b/docs/player-parity.md index 1380573..60e52a7 100644 --- a/docs/player-parity.md +++ b/docs/player-parity.md @@ -7,129 +7,303 @@ 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) +**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. +`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 | ✅ `