Planning docs for the optional per-room Host Control Mode (Teleparty-style host-only playback control) requested via GitHub issue. - host-control-mode-EDGECASES.md: canonical branch entry-point — goals, scope, trust model, architecture summary, 21 edge cases, test matrix, open decisions. - host-control-mode-plan.md: layered implementation plan with concrete code hooks (server room state, three-layer gate, snap-back, guest desync flow, UI/i18n). Temporary working docs for this branch; to be folded in or removed before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.8 KiB
Host Control Mode — Implementierungsplan
Branch: feature/host-control-mode
Issue: GitHub feature request (wasserrutschentester) — nur Host darf den Raum steuern; Gäste werden zurückgesnappt oder gehen bewusst in Desync.
Ziel
Ein Raum kann zwischen zwei Modi umgeschaltet werden:
everyone(Default, heutiges Verhalten): jeder kann play/pause/seek für alle auslösen.host-only: nur der Host steuert den Raum. Pause/Seek eines Gasts wird nicht gebroadcastet; stattdessen snappt die eigene Extension den Gast zurück auf den Raum-Zustand — es sei denn, der Gast entscheidet sich bewusst für Desync.
Trust-Modell: client-seitig durchgesetzt. Kein Token, keine Auth. Es geht um versehentliches Stören, nicht um Angriffsschutz.
Datenmodell
Server (server/index.js, Room-Objekt ~Z.331)
Room bekommt zwei neue Felder:
room = {
...,
hostPeerId: peerId, // gesetzt beim Anlegen = erster Joiner
controlMode: 'everyone', // 'everyone' | 'host-only'
}
- In
ROOM_DATA(~Z.418) mitschicken:hostPeerId,controlMode. - Neues Event
SET_CONTROL_MODE(siehe unten): nur akzeptieren, wennsenderPeerId === room.hostPeerId. Server setztroom.controlMode, broadcastet die Änderung an alle. - Host-Migration: in
removePeerFromRoom(~Z.168) — wenn der gehende PeerhostPeerIdwar: entweder neuen Host bestimmen (nächster Peer) odercontrolModeaufeveryonezurückfallen lassen. → Entscheidung: Fallback aufeveryone(simpel, nie verwaister gesperrter Raum). Optional später: Host-Transfer-Button.
Shared Constants (shared/constants.js)
Neue Events im EVENTS-Objekt:
SET_CONTROL_MODE: "set_control_mode", // Client->Server: Host ändert Modus
CONTROL_MODE: "control_mode", // Server->Client: Modus geändert { controlMode, hostPeerId }
⚠️ Danach node scripts/build-extension.cjs laufen lassen (Single Source of Truth propagieren). Ggf. PROTOCOL_VERSION bumpen — nein, nur wenn alte Clients brechen würden. Da alles additiv ist und alte Clients die neuen Felder/Events einfach ignorieren, ist KEIN Protokoll-Bump nötig. (Alter Client in host-only-Raum kennt den Modus nicht und sendet weiter → Host-Extensions ignorieren fremde Events nicht... → siehe Edge Case 7. Evtl. doch Bump erwägen.)
Extension State (background.js)
let controlMode = 'everyone';
let hostPeerId = null;
// abgeleitet: const amHost = () => hostPeerId === peerId;
Aus ROOM_DATA / CONTROL_MODE befüllen, in chrome.storage.session persistieren (wie currentRoom).
Implementierung nach Schichten
1. Server (server/index.js)
- Room-Objekt um
hostPeerId+controlModeerweitern (~Z.331). ROOM_DATA-Payload erweitern (~Z.418).- Handler
SET_CONTROL_MODE: validieren (Host-Check + Wert in {everyone, host-only}), setzen,CONTROL_MODEan Raum broadcasten. - Host-Migration in
removePeerFromRoom: Fallback aufeveryone+ neuesCONTROL_MODEbroadcasten, wenn Host geht. SET_CONTROL_MODEin dierelayEvents-Liste? Nein — eigener Handler, da Sonderlogik + Host-Check. (relayEvents broadcastet blind.)
2. Shared / Build
EVENTS.SET_CONTROL_MODE,EVENTS.CONTROL_MODEergänzen.node scripts/build-extension.cjs.
3. background.js (Gast-Logik = Kern)
controlMode/hostPeerIdausROOM_DATA(~Z.875) und neuemCONTROL_MODE-Case übernehmen + persistieren + an Popup/Content pushen.- Emit-Gate im SEND-Pfad (~Z.1786): bei
host-only && !amHost()und action ∈ {play, pause, seek}: - NICHTemiten. - Stattdessen Content-Script anweisen: "snap back" ODER Desync-Confirm anzeigen. - Snap-Back-Zielzeit berechnen: aus Host-Peer-State (
playbackState,currentTime,lastHeartbeat) extrapolieren:targetTime = host.currentTime + (host.playbackState==='playing' ? (now - host.lastHeartbeat)/1000 : 0). Genauigkeit ~±1s, für Watchparty ok. (Force-Sync-Maschinerie als Referenz für Ziel-Zeit-Koordination.) - Nachricht an content.js:
{ type: 'HOST_BLOCK', action, targetTime, hostPlaybackState }.
4. content.js (Player-Reaktion + Dialog)
- Handler für
HOST_BLOCK: Confirm-Dialog im Player-Overlay rendern: "Pause only your own player and desync from the group? [Yes] [No]". - No (Default): Player via bestehende_setSuppress-Mechanik (content.js:442) wieder in Raum-Zustand zwingen (play + seek auf targetTime). Suppress verhindert Re-Broadcast. - Yes: lokal pausiert lassen,isDesynced = truesetzen, dezenten "Desynced — Resync"-Button zeigen. - "Resync"-Button → snappt zurück auf aktuelle Raum-Zeit,
isDesynced = false. - Loop-Schutz: nach einer Snap-Back-Aktion kurzes Cooldown-Fenster (z.B. 600ms), in dem weitere lokale pause/seek-Events nicht erneut den Dialog triggern (verhindert pause→play→pause-Pingpong).
5. popup (Host-UI)
- Host-Toggle "Only I can control" (nur sichtbar wenn
amHost()), sendetSET_CONTROL_MODE. - Rollen-Badge: "Host" / "Guest" + aktueller Modus.
- Gast-Hinweis wenn host-only aktiv: "The host controls playback".
- i18n-Keys in
extension/_locales/localesfür ~15 Sprachen.
Edge Cases (Test-Checkliste)
- Pause nicht verhinderbar, nur revidierbar → kurzer Flicker (~½s) beim Gast ist erwartet/akzeptabel.
- Snap-Back-Zielzeit aus Heartbeat extrapoliert, ±1s. Bei stark veraltetem Host-State (kein Heartbeat) → letzten bekannten Wert nehmen.
- Kampf-Loop pause/play/skip back/pause/play → Cooldown-Fenster nach Snap-Back. Testen mit aggressivem Mashing.
- Desync-Escape: Gast kann bewusst pausieren (Klo/Telefon) → "Yes" → solo, dann Resync.
- Host verlässt Raum → Fallback auf
everyone, alle bekommenCONTROL_MODE-Update. Testen: Host schließt Tab / Disconnect / Netzabbruch. - host-only + Episode-Auto-Sync / Force-Sync (server/index.js:503-516): Gast darf NICHT initiieren. Force-Sync trägt eine
targetTimeund zwingt ALLE darauf (background.js:1261) — ein Gast könnte seeken → Force-Sync spammen und damit host-only komplett aushebeln. Episode-Lobby pausiert ebenfalls alle. → Im host-only-Modus dürfenFORCE_SYNC_PREPARE/FORCE_SYNC_EXECUTEundEPISODE_LOBBYnur vom Host initiiert werden. Gäste dürfen weiterhin nur reagieren:FORCE_SYNC_ACK,EPISODE_READY. Gäste brauchen Force-Sync nicht — ihr legitimer Fall ist der "Resync"-Button (snappt nur sie selbst, nicht alle). - Alter Client (ohne Feature) in host-only-Raum → kennt Modus nicht, sendet weiter pause/seek → andere Extensions wenden es an. Mitigation: Empfänger-seitiges Gate (host-only-Clients ignorieren play/pause/seek von Nicht-Host) ODER
MIN_VERSION/Protokoll-Bump. → Empfehlung: zusätzlich Empfänger-seitig filtern (robuster als nur Sender-Gate). - Seek getrennt von Pause → host-only blockt auch Gast-Seeks, nicht nur Pausen.
- Mehrere Tabs / Multi-Peer mit gleicher peerId (Dedup, server:381) → Host-Identität bleibt an peerId hängen, ok.
- DAU-Verwirrung "warum kann ich nicht mehr pausieren?" → klare UI-Botschaft + der Desync-Dialog erklärt sich selbst.
Architektur-Entscheidung zu Edge Case 7 (wichtig)
Wir setzen das Gate doppelt und über alle raum-verschiebenden Events, nicht nur play/pause/seek:
Geblockte Initiierungen für Nicht-Host im host-only-Modus:
PLAY, PAUSE, SEEK, FORCE_SYNC_PREPARE, FORCE_SYNC_EXECUTE, EPISODE_LOBBY.
Weiterhin erlaubt für Gäste (reine Reaktion, verschiebt niemanden): FORCE_SYNC_ACK, EPISODE_READY, PEER_STATUS, PING/PONG.
- Sender-seitig (Gast sendet erst gar nicht) → saubere UX, Confirm-Dialog bei play/pause/seek; Force-Sync-/Episode-Lobby-Buttons im Gast-UI deaktiviert/ausgeblendet.
- Empfänger-seitig (in
handleServerEvent, background.js:969 + Force-Sync-/Episode-Cases): wennhost-onlyunddata.senderId !== hostPeerId→ Event verwerfen (nicht an Content routen, keine State-Mutation). So sind auch alte/buggy/manipulierte Clients abgedeckt, ohne harten Protokoll-Bump.
Optional zusätzlich server-seitig in den relayEvents (server/index.js:445): im host-only-Modus Initiierungs-Events von Nicht-Host gar nicht erst relayen. Spart Traffic + deckt alles zentral ab. Empfehlenswert, da der Server hostPeerId/controlMode ohnehin kennt.
Reihenfolge der Umsetzung (kleine, testbare Schritte)
- Constants + Build (Events da, nichts kaputt).
- Server: hostPeerId/controlMode + ROOM_DATA + SET_CONTROL_MODE + Migration.
- background.js: State übernehmen + Empfänger-seitiges Gate (Edge 7) — testbar ohne UI.
- background.js: Sender-seitiges Gate + Snap-Back-Zielzeit.
- content.js: Snap-Back-Apply + Confirm-Dialog + Loop-Cooldown.
- popup: Host-Toggle + Badge + i18n.
- Durchtesten der Edge-Case-Liste auf YT / Netflix / generischem HTML5-Player.