|
| 1 | +# PRD — Live View Ergonomics |
| 2 | + |
| 3 | +**Status**: Draft |
| 4 | +**Created**: 2026-04-22 |
| 5 | +**Owner**: TBD |
| 6 | +**Driving signal**: [#326 (CDx4f3kCAf3Y)](https://github.com/opensensor/lightNVR/issues/326), [#397 (AndyIsHereBoi)](https://github.com/opensensor/lightNVR/issues/397), and the Live View / mobile items from [#399](https://github.com/opensensor/lightNVR/issues/399). |
| 7 | +**Scope**: `web/js/components/preact/LiveView.jsx`, `HLSVideoCell.jsx`, `MSEVideoCell.jsx`, `GridPicker.jsx`, `FullscreenManager.jsx`, plus a new per-stream "Playback profile" model. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## 1. Problem |
| 12 | + |
| 13 | +Live View is the page operators use most, often from a phone. Today it has three distinct ergonomic gaps: |
| 14 | + |
| 15 | +1. **No control over which playback transport is offered.** A single global "WebRTC enabled" checkbox is the only knob (#397). Operators who find HLS too jittery and WebRTC too flaky want to force MSE; some want WebRTC for one camera and HLS for another. Right now they can't. |
| 16 | +2. **Grid placement isn't durable.** Reordering streams in WebRTC view doesn't stick when the user switches to HLS/MSE, and vice versa (#326). Different state stores per renderer mean the user reorders the same cameras three times. |
| 17 | +3. **Mobile gestures don't exist.** No swipe-to-cycle, no pinch-to-zoom on a tile, no long-press menu. Fullscreen requires precision-tapping a small icon. Pulling to refresh does nothing. |
| 18 | + |
| 19 | +Layered on these: the recording-state badge on each tile is ambiguous (#399 — "Enabled (Detection)" hides the constant-recording case), and there's no visual cue when a tile is unhealthy vs just buffering. |
| 20 | + |
| 21 | +## 2. Goals & non-goals |
| 22 | + |
| 23 | +**Goals** |
| 24 | +- Per-stream playback transport preference + per-instance fallback chain. |
| 25 | +- Single source of truth for grid placement, shared across renderers. |
| 26 | +- First-class mobile gestures with discoverable affordances. |
| 27 | +- Clear, glanceable health/recording badges. |
| 28 | + |
| 29 | +**Non-goals** |
| 30 | +- New playback engine. We're managing existing HLS/MSE/WebRTC, not adding LL-HLS or DASH. |
| 31 | +- Audio mixing across tiles. Continue to mute by default; audio-on is per-tile and exclusive (already today). |
| 32 | +- PTZ redesign — that's its own epic. |
| 33 | + |
| 34 | +## 3. Users & top tasks |
| 35 | + |
| 36 | +| Persona | Task | Today's friction | |
| 37 | +|---|---|---| |
| 38 | +| Phone user reacting to a doorbell ping | Open the app → see front-door tile in 2 taps → expand to full screen | Front door isn't first; fullscreen tap target is small | |
| 39 | +| Power user with 8 cameras on a wall display | Arrange tiles, expect arrangement to persist regardless of renderer | Arrangement diverges across HLS/MSE/WebRTC views (#326) | |
| 40 | +| Operator on flaky LTE | Force MSE for the wired indoor cams, accept HLS fallback for cellular outdoor cams | Single global toggle; no per-stream choice (#397) | |
| 41 | +| Anyone | Tell at a glance "is this tile recording? is the stream healthy?" | Badges are text-only and ambiguous | |
| 42 | + |
| 43 | +## 4. Requirements |
| 44 | + |
| 45 | +### 4.1 Per-stream playback transport preference |
| 46 | + |
| 47 | +Add a new field to `stream_config_t` (and `db_streams`) and a UI control on the Stream Edit modal: |
| 48 | + |
| 49 | +- **`playback_transport`** enum: `auto` (default), `webrtc_only`, `mse_only`, `hls_only`, `webrtc_then_mse`, `mse_then_hls`. |
| 50 | +- **`auto`** behavior matches today's: try WebRTC if globally enabled, else MSE, else HLS. |
| 51 | +- The Live View tile reads the per-stream value and only attempts the listed transports in order, surfacing a "fallback used" badge when it had to drop down. |
| 52 | + |
| 53 | +UI: |
| 54 | +- Stream edit modal grows a "Playback transport" select, default Auto. |
| 55 | +- Settings → Streams Defaults gets the same control as a global default. |
| 56 | +- The existing global "WebRTC enabled" checkbox becomes a 3-checkbox group (WebRTC / MSE / HLS) per #397, gating which transports the system advertises at all. Per-stream overrides cannot select a globally-disabled transport — UI greys those out with a tooltip. |
| 57 | + |
| 58 | +**Acceptance**: Disable WebRTC globally; per-stream "WebRTC only" overrides surface a clear "WebRTC disabled at server" warning instead of silently degrading. |
| 59 | + |
| 60 | +### 4.2 Unified grid placement model |
| 61 | + |
| 62 | +Today the placement state lives in three independent places (one per renderer). Consolidate: |
| 63 | + |
| 64 | +- Single canonical `grid_layout` per user (or per-device if logged out demo mode), keyed by stream UUID, storing `(slot_index, layout_size)`. |
| 65 | +- `LiveView.jsx` reads/writes this single store regardless of active renderer. |
| 66 | +- Migrating: on first load after this lands, merge the three legacy stores using last-write-wins per stream. |
| 67 | +- Layout-size changes (e.g. 4 → 9 cells) preserve relative order; new slots fill from the top. |
| 68 | +- Touch reorder: drag-handle becomes long-press anywhere on the tile (mobile) / corner grip (desktop). |
| 69 | + |
| 70 | +**Acceptance**: User reorders in HLS view, switches to MSE view, sees same arrangement. Resizing browser shrinks/grows the grid without losing user-defined slot assignments where slots remain. |
| 71 | + |
| 72 | +### 4.3 Mobile gestures |
| 73 | + |
| 74 | +| Gesture | Action | |
| 75 | +|---|---| |
| 76 | +| Tap on tile | Reveal tile chrome (badges, fullscreen, audio toggle) for 4 s | |
| 77 | +| Double-tap on tile | Toggle fullscreen | |
| 78 | +| Long-press on tile | Open context menu (Mute/Unmute, Snapshot, Open Recordings for this stream, Reorder mode) | |
| 79 | +| Pinch-zoom inside fullscreen tile | Digital zoom on the video element (record offset for snapshots) | |
| 80 | +| Pull-down at top of grid | Refresh streams list + reload manifests | |
| 81 | +| Swipe left/right on a fullscreen tile | Cycle to next/previous stream in current grid order | |
| 82 | + |
| 83 | +Discoverability: first time each gesture would have helped (e.g. user taps fullscreen icon manually 3×), show a one-time tip toast: "Tip: double-tap the tile to fullscreen." |
| 84 | + |
| 85 | +### 4.4 Recording & health badges |
| 86 | + |
| 87 | +Replace the current text-only badge with a 2-glyph layout in the tile's top-right: |
| 88 | + |
| 89 | +- **Recording glyph**: filled red dot (constant), red dot with motion arc (detection), red dot with clock (schedule), or stacked combinations. Tooltip / aria-label spells it out: "Constant + Detection". |
| 90 | +- **Health glyph**: green ✓ (streaming, no recent stalls), yellow • (buffering / recovering), red ⚠ (no stream / consecutive errors). Drives off existing `stream_state` from the Health API. |
| 91 | + |
| 92 | +Both glyphs live in a 28×28 pill on desktop, full-size 36×36 on mobile. |
| 93 | + |
| 94 | +**Acceptance**: Fixes #399 specific item ("recording indicator only says 'Enabled (Detection)' even if constant + detection are enabled"). All combinations of the three recording modes render distinctly. |
| 95 | + |
| 96 | +### 4.5 Fullscreen polish |
| 97 | + |
| 98 | +- Native picture-in-picture button on tiles where the browser supports it (Chrome/Edge desktop, Safari mobile iOS 14+). |
| 99 | +- "Always-fullscreen on tap" preference (off by default) for kiosk-style wall displays. |
| 100 | +- Fullscreen exit gesture: swipe-down on mobile, Esc on desktop. Today's pinch-out collision risk is mitigated by reserving pinch for digital zoom only. |
| 101 | + |
| 102 | +## 5. Data model changes |
| 103 | + |
| 104 | +``` |
| 105 | +ALTER TABLE streams |
| 106 | + ADD COLUMN playback_transport TEXT NOT NULL DEFAULT 'auto' |
| 107 | + CHECK (playback_transport IN |
| 108 | + ('auto','webrtc_only','mse_only','hls_only', |
| 109 | + 'webrtc_then_mse','mse_then_hls')); |
| 110 | +``` |
| 111 | + |
| 112 | +`system_settings` adds: |
| 113 | +- `transport_webrtc_offered` (bool, default true) |
| 114 | +- `transport_mse_offered` (bool, default true) |
| 115 | +- `transport_hls_offered` (bool, default true) |
| 116 | +- `live_view_grid_layout_v2` (JSON, single source of truth) |
| 117 | + |
| 118 | +Migration: run-once on boot (similar to T14 from the go2rtc work) — merges legacy `grid_layout_webrtc/mse/hls` keys into the new field, last-write-wins. |
| 119 | + |
| 120 | +## 6. Phasing |
| 121 | + |
| 122 | +| Phase | Scope | Estimate | |
| 123 | +|---|---|---| |
| 124 | +| P0 — Glyph badges + fullscreen polish | Pure-frontend; no schema work | 2 days | |
| 125 | +| P1 — Per-stream transport (#397) | Schema + migration + UI + LiveView routing | 3–5 days | |
| 126 | +| P2 — Unified grid model (#326) | Migration + LiveView refactor | 3 days | |
| 127 | +| P3 — Mobile gestures | Add gesture library, instrumentation for the tip toasts | 4–5 days | |
| 128 | + |
| 129 | +## 7. Acceptance criteria |
| 130 | + |
| 131 | +- A user can set `front_door` to `mse_only` and `garage` to `webrtc_then_mse` independently and Live View honors both. |
| 132 | +- Grid arrangement persists across renderer changes (issue #326 closed). |
| 133 | +- All recording-mode combinations render with distinct, accessible badges. |
| 134 | +- Mobile reachability: every interactive element on Live View is reachable with one thumb on a 6.7" phone in portrait. |
| 135 | + |
| 136 | +## 8. Risks |
| 137 | + |
| 138 | +| Risk | Mitigation | |
| 139 | +|---|---| |
| 140 | +| Per-stream transport explodes the test matrix | Start with the 3 single-transport modes; chained fallback in P2 | |
| 141 | +| Gesture library bloats the bundle | Hand-roll the small set we need (tap, dbltap, longpress, swipe) — no library | |
| 142 | +| Grid migration loses placements when slot count differs | Stable-sort by legacy slot then by name, fill empties top-to-bottom; show a one-time "We restored your layout" toast with an Undo | |
| 143 | +| iOS Safari fullscreen quirks | Use the existing `FullscreenManager.jsx` with a Safari-specific pseudo-fullscreen path | |
| 144 | + |
| 145 | +## 9. Related issues |
| 146 | + |
| 147 | +- [#326 — Live View grid placement not remembered](https://github.com/opensensor/lightNVR/issues/326) |
| 148 | +- [#397 — Per-stream-type checkboxes for dashboard](https://github.com/opensensor/lightNVR/issues/397) |
| 149 | +- [#399 — UI and styling issues](https://github.com/opensensor/lightNVR/issues/399) (specifically: logo→home, recording badge clarity, mobile reachability) |
0 commit comments