diff --git a/docs/host-control-mode-EDGECASES.md b/docs/host-control-mode-EDGECASES.md new file mode 100644 index 0000000..884f0dd --- /dev/null +++ b/docs/host-control-mode-EDGECASES.md @@ -0,0 +1,212 @@ +# Host Control Mode — Branch Overview, Goals & Edge Cases + +> **This is the canonical entry-point doc for branch `feature/host-control-mode`.** +> If you're an agent or contributor picking this up: read this file first, then the +> implementation plan in [`host-control-mode-plan.md`](./host-control-mode-plan.md). +> Temporary working doc — delete or fold into permanent docs before merging to `main`. +> +> Status legend: 🔴 open / unresolved · 🟡 idea, needs testing · 🟢 decided + +--- + +## 1. What this branch is for + +Origin: a GitHub feature request. When watching with larger groups, anyone can pause +or seek and disrupt everyone else. The requester wants the room creator to optionally +restrict control to a single **host**, the way Teleparty works — guests who try to +pause get asked whether they want to pause *only their own* player (and desync), and +otherwise get snapped back to the room's position. + +**Goal:** Add an optional per-room **Host Control Mode**. A room can be switched +between: +- **`everyone`** (default, current behavior): anyone can play/pause/seek for the room. +- **`host-only`**: only the host drives the room. A guest's deliberate play/pause/seek + is not broadcast; instead they're snapped back to the room position — unless they + explicitly choose to desync (go solo) with a "Resync" escape hatch. + +## 2. Trust model (read this before over-engineering) + +This is **client-side trust, by design**. It's a watch party, not a security boundary. +The point is preventing *accidental* and *casual* disruption, not stopping someone +determined to patch their own extension. We do **not** add auth, tokens, or +cryptographic host identity. `peerId` is unauthenticated and that's fine here. + +(We still gate server-side as the robust chokepoint — see plan — but that's about +killing spam reliably, not about defeating a hostile client.) + +## 3. Scope / non-goals + +In scope: +- Host designation (first joiner = host), mode toggle, host-only gating of all + room-moving events, guest snap-back, deliberate-desync flow + resync, host UI. + +Explicit non-goals (for this branch): +- Authenticated / spoof-proof host identity. +- Persistent host across server restarts (room state is in-memory). +- Syncing around personalized ad breaks. +- Host transfer UI (auto-fallback to `everyone` when host leaves; manual transfer + is a possible later add). + +## 4. Architecture summary + +Three-layer gate for room-moving events from a non-host in host-only mode +(`PLAY`, `PAUSE`, `SEEK`, `FORCE_SYNC_PREPARE`, `FORCE_SYNC_EXECUTE`, `EPISODE_LOBBY`): +1. **Server** — doesn't relay them (robust chokepoint, kills spam regardless of client). +2. **Sender (guest)** — doesn't emit; shows confirm dialog / disables host-only buttons. +3. **Receiver** — drops any that slip through (covers old/buggy/modified clients). + +Snap-back reuses the existing `_setSuppress` mechanism (content.js:442) so applying +the room state programmatically doesn't echo back as a new event. Target position is +extrapolated from the host's last known state (±1s). Full detail + code hooks in the +plan doc. + +--- + +## 5. The central challenge + +Everything hard about this feature reduces to **one question** (see EC-9): + +> How do we reliably tell a **deliberate** guest pause/seek from an **involuntary** +> player/browser event (buffering, ads, tab throttling, source swaps, DRM hiccups)? + +If we get this wrong, guests get spammed with desync dialogs and snap-back loops for +things they never did. The host/role plumbing is the easy part; this classifier is the +real work. **Design the intent-classifier before writing the gate.** + +--- + +## 6. Edge cases + +### EC-1 🔴 Buffering / loading fires a `pause` event +content.js listens to `play`/`pause`/`seeked`/`loadeddata` only (content.js:1000-1003), +not `waiting`/`stalled`. Pure HTML5 buffering fires `waiting` → harmless. But custom +players (Netflix/YouTube/Twitch/JW) often call `video.pause()` during buffering/ads → +real `pause` → guest gate would mis-classify as deliberate. Sub-cases: (a) initial load +sits paused, no event, fine; (b) mid-stream stall, player-dependent; (c) seek-induced +re-buffering may outlast the suppress window and leak. Mitigation: `isBuffering` flag +from `waiting`/`playing`, or grace window; in host-only the guest's own state is +irrelevant so just ignore involuntary pauses and let catch-up logic (content.js:489) +re-sync them. + +### EC-2 🟢 Force-Sync / Episode-Lobby abuse by guests +Guest could seek + spam Force-Sync to drag everyone, or spam Episode-Lobby to pause +everyone. Decision: host-only blocks guest *initiation* of `FORCE_SYNC_*` and +`EPISODE_LOBBY`; guests may only respond (`FORCE_SYNC_ACK`, `EPISODE_READY`). Guests' +legitimate path is the personal "Resync" button. + +### EC-3 🟢 Host leaves the room +Fall back to `controlMode = 'everyone'`, broadcast `CONTROL_MODE`. Never a stuck locked +room. (Auto-promote next peer deferred.) + +### EC-4 🔴 Snap-back fight loop (pause/play/skip back/pause/play) +Mashing controls or a janky player → each snap-back may emit events → ping-pong. +Mitigation: cooldown (~600ms) after snap-back; ensure snap-back runs fully under +suppress. Also: if target is unreachable (seek past buffered range), retry K times then +give up — no infinite loop. + +### EC-5 🔴 Ad breaks (YouTube/Twitch/…) +Mid-roll ads pause/swap the media element, differ per peer → desync is unavoidable and +must NOT spam the dialog. Probably covered by EC-1 buffering grace; flag for explicit +testing. + +### EC-6 🟡 Snap-back target accuracy +No continuous room clock; extrapolate from host's `currentTime` + `lastHeartbeat` +(±1s, worse if stale). The follow-up host correction must also be suppressed so it +doesn't read as guest input. + +### EC-7 🟢 Old / buggy / modified guest client +Covered by receiver-side + server-side gates. + +### EC-8 🔴 Tab throttling / background tab +Backgrounding throttles timers and may pause media. There's existing +`visibilityGraceUntil` handling for seeks (content.js:892). Confirm a +background-induced pause isn't treated as deliberate in host-only; reuse the grace flag. + +### EC-9 🔴 What counts as "deliberate" — the central unresolved question +Collapses EC-1/EC-5/EC-8. Candidate signals: `readyState`/`networkState`/`video.seeking` +at event time; recent `waiting`; recent user gesture (`navigator.userActivation`, +keydown/click); visibility/focus. Build one shared **intent-classifier** helper in +content.js that all host-only gating flows through. + +### EC-10 🔴 Host brief disconnect / reconnect (network blip) +Host's wifi drops for 3s and reconnects. With "host leaves → fallback to everyone", +a blip would silently unlock the room and demote the host (peerId persists in +chrome.storage so they rejoin with the same id, but the server already cleared +`hostPeerId`). Mitigation idea: short **host grace period** (e.g. keep `hostPeerId` +reserved for ~30s after disconnect; if the same peerId rejoins, restore host + mode). +Needs the server reaper (server:644) and `removePeerFromRoom` (server:168) to cooperate. + +### EC-11 🔴 New guest joins mid-session in host-only mode +On join they must (a) immediately sync to the host's current position without the host +doing anything, and (b) see they're a guest in the UI. ROOM_DATA already carries peers; +add `hostPeerId`/`controlMode` so a fresh client knows its role instantly. Verify the +existing "newcomer syncs without waiting" path (content.js:542) still fires. + +### EC-12 🔴 Desync semantics — what does "solo" actually mean? +When a guest chooses "pause only me", do they (a) fully ignore all subsequent host +events until they Resync, or (b) keep receiving but not auto-applying? Define clearly. +Proposed: full solo — ignore host play/pause/seek while desynced; Resync re-attaches and +snaps to current host position. Also: what state does Resync land them in if the host is +currently paused vs playing? + +### EC-13 🔴 Race: host flips to host-only exactly as a guest pauses +Event ordering between `SET_CONTROL_MODE`/`CONTROL_MODE` and an in-flight guest `PAUSE`. +The `seq` ordering helps, but define the tie-break. Likely: server is authoritative — +once it has `host-only`, it drops the guest event regardless of client-side timing. + +### EC-14 🟡 Volume / mute / audio-options must NOT be gated +Those are per-peer, not room control. The gate must target only play/pause/seek + +forcesync/episode — not `PEER_STATUS` volume/mute fields. Easy to over-block; add a test. + +### EC-15 🔴 Live streams (Twitch live, live DVR) +"Room timestamp" is fuzzy on live edge; seeking semantics differ. Decide whether +host-only even makes sense for live, or degrade gracefully. Low priority but log it. + +### EC-16 🟡 Host's own involuntary events still drive the room +If the host buffers and the player auto-pauses, that pause is "allowed" and pauses +everyone. That's existing behavior, but in host-only it means the host's buffering +stalls the whole room. Acceptable? Probably yes (host is authoritative), but note it. + +### EC-17 🟡 Server restart drops room state +`hostPeerId`/`controlMode` are in-memory. After a server restart, whoever rejoins first +becomes the new host and mode resets to `everyone`. Acceptable for now (non-goal), but +document so it's not a surprise. + +### EC-18 🟡 Dialog dismissed without choosing +Guest clicks away / presses Esc on the desync prompt. Default = treat as "No" → snap +back. Make sure an un-answered dialog can't leave them in limbo (paused + not desynced + +no dialog). + +### EC-19 🟡 Multiple video elements / element swap (SPA, ad → content) +Players that swap the `