8.9 KiB
Host Control Mode
Overview
Host Control Mode allows the room host to control playback for all participants. Guests can attempt to play/pause/seek, but their actions are not broadcast to the room unless they explicitly choose to desync and watch on their own.
Modes
everyone (Default)
- Anyone in the room can control playback
- All play/pause/seek actions are broadcast to all peers
- Traditional KoalaSync behavior
host-only
- Only the host (and promoted controllers) can control the room
- Guest actions are blocked and they are snapped back to the host's position
- Guests can choose to desync and watch independently
Protocol Events
SET_CONTROL_MODE (Client → Server)
Sent by the host to change the room's control mode.
Payload:
{
"controlMode": "everyone" | "host-only"
}
Authorization: Only the host (room.hostPeerId) can send this event.
Server Response: Broadcasts CONTROL_MODE to all peers in the room.
CONTROL_MODE (Server → Client)
Broadcast when the control mode changes or when a peer joins a room.
Payload:
{
"controlMode": "everyone" | "host-only",
"hostPeerId": "string",
"controllers": ["string"]
}
Triggered by:
- Host changing the control mode
- Host leaving the room (fallback to 'everyone')
- Controller promotion/demotion
SET_PEER_ROLE (Client → Server)
Sent by the host to promote or demote a peer to/from controller status.
Payload:
{
"peerId": "string",
"controller": "boolean"
}
Authorization: Only the host (room.hostPeerId) can send this event.
Server Response: Broadcasts CONTROL_MODE to all peers in the room.
Server-Side Implementation
Room Object
room = {
hostPeerId: "peer-id", // First peer to join the room
controlMode: "everyone", // 'everyone' | 'host-only'
controllers: ["peer-id"], // Peers allowed to control in host-only mode
lastControlModeChangeAt: 0, // Timestamp for rate limiting
lastRoleChangeAt: 0 // Timestamp for rate limiting
}
Key Behaviors
- Host Migration: When the host leaves, the room falls back to 'everyone' mode
- Controller Set: Always includes the host, plus any promoted peers
- Rate Limiting: Control mode changes are debounced to 500ms per room
Client-Side Implementation
State Management (background.js)
let controlMode = CONTROL_MODES.EVERYONE;
let hostPeerId = null;
let controllers = [];
function amHost() { return hostPeerId === peerId; }
function amController() { return amHost() || controllers.includes(peerId); }
Event Gating
Sender-side gate (background.js):
- Blocks
PLAY,PAUSE,SEEK,FORCE_SYNC_*,EPISODE_LOBBY_*from non-controllers - Sends
HOST_BLOCKEDmessage to content script instead
Receiver-side gate (background.js):
- Ignores gated events from non-controllers in host-only mode
- Prevents malicious or outdated clients from bypassing controls
Snap-Back Logic (content.js)
When a guest's action is blocked:
- Show dialog: "Stay in sync" (default) or "Watch on my own"
- If "Stay in sync": snap player back to host's position
- If "Watch on my own": set
hcmDesynced = true, show resync button
Host Sync Target Calculation
function getHostSyncTarget() {
const host = currentRoom.peers.find(p => p.peerId === hostPeerId);
if (!host) return null;
let targetTime = host.currentTime;
// Extrapolate if host is playing
if (host.playbackState === 'playing' && host.lastHeartbeat) {
const elapsedSec = (Date.now() - host.lastHeartbeat) / 1000;
if (elapsedSec > 0 && elapsedSec <= 30) { // Max 30s extrapolation
targetTime += elapsedSec;
}
}
return { playbackState: host.playbackState, targetTime };
}
Edge Cases
1. Host Leaves Room
- Behavior: Room falls back to 'everyone' mode
- Implementation:
removePeerFromRoomchecks if leaving peer is host - Broadcast:
CONTROL_MODEevent sent to all remaining peers
2. Controller Promoted During Force Sync
- Problem: Controller's
FORCE_SYNC_EXECUTEwould be blocked - Solution:
forceSyncInitiatorfield tracks who started the sync - Behavior: Initiator's EXECUTE is allowed even if demoted mid-sync
3. Guest Seek Spam
- Problem: Guest could spam seeks to disrupt
- Solution: Cooldown period after snap-back (1000ms)
- Behavior: Subsequent actions within cooldown are ignored
4. Host State Stale
- Problem: Host heartbeat hasn't arrived recently
- Solution: Max 30s extrapolation, then use last known position
- Behavior: Prevents overshooting if host paused but heartbeat delayed
5. Old Client Without Feature
- Problem: Client doesn't know about host-only mode
- Solution: Receiver-side gate in all clients
- Behavior: Modern clients ignore events from non-controllers
User Experience
Host UI
- Toggle: "Only I can control" (visible only to host)
- Role badges: "Host", "Controller", or "Guest"
- Guest notice: "The host controls playback" (when host-only active)
- Controller management: Promote/demote peers in participant list
Guest UI
- Blocked Action: Dialog with choices:
- "Stay in sync" (default, auto-closes after 8s)
- "Watch on my own" (enters desync mode)
- Desync Mode: "Solo" badge with "Resync" button
- Resync: Returns to synchronized state with host
Co-Host UI
- Same controls as host (can play/pause/seek/force-sync)
- "Controller" badge instead of "Host"
- Cannot promote/demote other controllers
Testing Checklist
Basic Functionality
- ✅ Host can toggle between 'everyone' and 'host-only'
- ✅ Guests see "host controls playback" notice in host-only
- ✅ Guest play/pause/seek blocked in host-only
- ✅ Guest snapped back to host position when blocked
- ✅ Guest can choose "watch on my own" and desync
- ✅ Desynced guest can resync with host
Edge Cases
- ✅ Host leaving room falls back to 'everyone'
- ✅ Multiple rapid guest actions don't cause loop
- ✅ Guest can desync during buffering/throttling
- ✅ Old client events ignored by modern clients
- ✅ Force sync initiated by controller works
- ✅ Episode lobby initiated by controller works
UI/UX
- ✅ Host toggle only visible to host
- ✅ Role badges show correct roles
- ✅ Desync dialog auto-closes after timeout
- ✅ Resync button visible when desynced
- ✅ Controller promotion UI visible to host
Architecture Decisions
Why Double Gating?
Sender-side + Receiver-side gates provide defense in depth:
- Sender-side: Clean UX with dialog for guests
- Receiver-side: Protects against old/buggy/malicious clients
- Server-side: Central enforcement point (optional but recommended)
Why Extrapolation?
Linear extrapolation from last heartbeat provides ~±1s accuracy:
- Better than freezing at last known position
- Handles minor network jitter gracefully
- Capped at 30s to avoid overshooting on stale data
Why Cooldown?
600-1000ms cooldown prevents control loops:
- Guest pause → snap-back → pause → snap-back...
- Allows legitimate desync after cooldown expires
- Doesn't block deliberate user actions
Migration Path
From 'everyone' to 'host-only'
- Host toggles mode to 'host-only'
- Server validates host authority
- Server broadcasts
CONTROL_MODEto all peers - All clients update local state
- Existing playback continues uninterrupted
- Future guest actions are gated
From 'host-only' to 'everyone'
- Host toggles mode to 'everyone'
- Server validates host authority
- Server broadcasts
CONTROL_MODEto all peers - All clients update local state
- All peers regain control immediately
Performance Considerations
- Memory: Minimal overhead (~100 bytes per room for HCM state)
- CPU: Snap-back calculation is O(1), negligible impact
- Network: No additional traffic beyond existing heartbeats
- Storage: Control mode persisted in room state, no additional storage
Security Considerations
- No Authentication: Trust model is client-enforced (no tokens)
- No Encryption: Feature doesn't handle sensitive data
- Rate Limiting: Control mode changes debounced to prevent spam
- Validation: All mode changes validated server-side
Future Enhancements
Planned
- Host transfer button (manual host migration)
- Temporary controller promotion (time-limited)
- Guest request control (notification to host)
Considered but Rejected
- Password-protected host transfer (too complex)
- Vote-based control mode (social complexity)
- Per-action permissions (UI overload)
Changelog
- 2.5.0: Initial implementation (July 2026)
- 2.5.1: Added co-host support and controller promotion
- 2.5.2: Improved desync UI with resync button
- 2.6.0: Added server-side rate limiting for mode changes
See Also
- Protocol Specification — Technical event details
- Architecture — System overview