feat(protocol): add host control mode events & constants

Step 1 of host control mode. Adds the protocol surface only — no behavior yet:
- EVENTS.SET_CONTROL_MODE (client->server: host sets mode)
- EVENTS.CONTROL_MODE   (server->client: mode changed)
- CONTROL_MODES { EVERYONE, HOST_ONLY }

Purely additive; old clients ignore the new events. Propagated to
extension/shared via build-extension. Also records the audited design
decisions in the edge-case log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
KoalaDev
2026-06-25 23:27:19 +02:00
parent 2995afe970
commit a968f708b3
2 changed files with 34 additions and 6 deletions
+22 -6
View File
@@ -204,9 +204,25 @@ Host hammering the toggle → many `SET_CONTROL_MODE`. Covered by existing
| Disney+ / DRM | | | | | |
| Jellyfin / Emby | | | | | |
## 8. Open decisions to lock before/while coding
- [ ] Intent-classifier signal set (EC-9) — design first.
- [ ] Host grace period on disconnect, yes/no + duration (EC-10).
- [ ] Desync semantics: full solo vs receive-only (EC-12).
- [ ] Snap-back cooldown duration + retry cap (EC-4).
- [ ] Does host-only apply to live streams or degrade (EC-15)?
## 8. Decisions (audited)
- [x] **Intent-classifier (EC-9):** A `pause`/`seek` is **involuntary** if ANY of:
`readyState < 3`, `video.seeking`, a `waiting` fired < ~1500ms ago (`isBuffering`
flag), inside `visibilityGraceUntil`, OR no own-tracked user gesture
(`Date.now() - lastUserGestureAt < 1000`, via capturing keydown/pointerdown — do
NOT use sticky `navigator.userActivation.hasBeenActive`). Bias: only *clearly*
involuntary is ignored; everything else = deliberate. **Note:** in host-only the
guest never broadcasts anyway, so this only decides dialog-vs-silent — a UX call,
not a room-integrity call. Start simple, tune later.
- [x] **Host grace on disconnect (EC-10): NOT in v1.** Immediate fallback to
`everyone` (EC-3). A grace window risks a multi-second hard-lock if the host never
returns. Revisit as polish once the core flow works.
- [x] **Desync semantics (EC-12): full solo.** Ignore host play/pause/seek while
desynced; Resync snaps to host position + adopts host play/pause state. Desync
auto-clears on new media/episode. Requires a persistent, obvious "You are desynced"
UI.
- [x] **Snap-back cooldown (EC-4): until-settled, not fixed.** Suppress re-trigger
until `readyState>=3 && playing && |Δt|<tol`, hard-cap ~1500ms. Retry target up to
3×, then give up (no infinite loop).
- [x] **Live streams (EC-15): degrade.** Disable the gate when
`video.duration === Infinity`. Caveat: live-DVR may report finite duration and slip
through — acceptable for v1.
+12
View File
@@ -33,6 +33,10 @@ export const EVENTS = {
ROOM_DATA: "room_data", // Server -> Client: current room state
ERROR: "error",
// Host Control Mode
SET_CONTROL_MODE: "set_control_mode", // Client -> Server: host sets room control mode ('everyone' | 'host-only')
CONTROL_MODE: "control_mode", // Server -> Client: room control mode changed { controlMode, hostPeerId }
// Media Control
PLAY: "play",
PAUSE: "pause",
@@ -57,6 +61,14 @@ export const EVENTS = {
PONG: "pong" // server responds with same { t } for client RTT calculation
};
// Room control modes (Host Control Mode feature).
// NOTE: content.js does not import this module — it uses the string literals
// 'everyone' / 'host-only' directly. Keep these values in sync there.
export const CONTROL_MODES = {
EVERYONE: 'everyone', // default: anyone can play/pause/seek for the room
HOST_ONLY: 'host-only' // only the host drives the room
};
export const HEARTBEAT_INTERVAL = 15000; // 15s
export const FORCE_SYNC_TIMEOUT = 8500; // 8.5s timeout for force sync ACKs (must be > content.js poll timeout of 8s)
export const EPISODE_LOBBY_TIMEOUT = 60000; // 60s timeout for episode lobby