Skip to content

SyncPlay Internals

TheMRX13 edited this page Jun 29, 2026 · 1 revision

SyncPlay Internals

🌐 English · Deutsch

SyncPlay is a native, server-authoritative watch-together feature built entirely inside the app — it does not speak the external Syncplay protocol and needs no third-party server. All clients are browsers on the same instance.

Files

File Role
web/syncplay_rooms.py In-memory room/member service (the heart). Pure logic, unit-testable.
web/transcoder.py HLS transcoder + shared/refcounted sessions for a room.
web/app.py All /api/syncplay/* routes, guest-access gating, room persistence.
web/static/syncplay_page.js Page controller: SSE client, player embedding, all UI.
web/templates/syncplay.html The /syncplay page (lobby, room, picker, modals).
web/static/syncplay.css Page-specific, responsive styling.
web/static/player.js Reused player; forwards the room token to the stream API.

Transport

  • Down: Server-Sent Events — GET /api/syncplay/stream?token=… streams JSON events.
  • Up: plain POST /api/syncplay/* for actions.
  • The token is a per-member room session token (secrets.token_urlsafe). It identifies the member and doubles as the guest auth token.

Rooms & members

  • Registry keyed by room name; members keyed by token (_token_index maps token → room).
  • Host = the member who created the room; if the host leaves it passes to the longest-present remaining member.
  • The server holds the authoritative playstate: position + updated_at + paused. effective_position() advances while playing, so a mid-stream joiner gets the exact current position.
  • A seek is not echoed to its originator (broadcast(..., exclude=token)) — they already sought locally, and an echo would needlessly restart their transcode.

Constants

Constant Value Meaning
MEMBER_TIMEOUT 30 s Drop a member with no heartbeat/poll.
ROOM_GRACE 60 s (legacy) empty-room grace; rooms now persist and are only removed on explicit close.
CHAT_HISTORY 100 Bounded chat backlog for late joiners.
EVENT_BACKLOG 500 Per-member SSE queue cap → emits a resync instead of leaking.
SHARE_EPSILON 3 s Position window within which viewers reuse one transcode session.
MAX_TRANSCODE_SESSIONS 8 Global cap on concurrent ffmpeg sessions.

Routes

All under /api/syncplay/. Guest-reachable endpoints are exempt from login_required and gated instead by a valid room token + syncplay_enabled. Host-only actions check token == room.host_token.

Method Path Purpose
GET config enabled, logged-in username, can_manage.
POST join Create/join; returns token + room snapshot.
GET stream?token= SSE event stream.
POST control play / pause / seek.
POST report Position heartbeat (+ buffering, current file).
POST ready Ready toggle.
POST chat Send a chat message.
POST episode Host announces media; with countdown → synced auto-next.
GET snapshot?token= Re-fetch state to resume after a page reload.
POST leave Leave the room.
GET rooms Room directory (name, count, watching, host, password/lock).
POST close-room Close a room by name (logged-in owner only).
POST kick / ban / transfer-host / close Host moderation.
POST host-lock / max / password Room settings (host only).
POST away / typing / reaction / track Presence & social.

SSE events

members, chat, left, play, pause, seek, sync, waiting, buffering, all_ready, media, history, countdown, host, host_lock, denied, typing, reaction, track, kicked, closed, resync.

Guest access & streaming

  • Guests (not logged in) reach the SyncPlay page and API because those endpoints are exempt; security comes from the room token + the syncplay_enabled flag.
  • For video, the relevant /api/stream/* endpoints (check, start, index.m3u8, segments, status, stop, active) are exempt and gated by a before_request guard: logged-in OR a valid sp_guest session token (stashed at join).

Shared transcode session

transcoder.start_or_join_session(file, start_pos, share_key):

  • share_key = "sp:" + room_name (derived server-side in api_stream_start from the syncplay_token the client sends).
  • Reuses the room's current session when the same file is requested within SHARE_EPSILON of its start_pos; otherwise starts a fresh one and points _shared[share_key] at it.
  • Sessions are refcounted: stop_session decrements and only kills ffmpeg at zero. player.js does stop-then-start on a seek, so refs stay balanced; synced seeks make all members converge on one session at the new position.
  • Without a share_key (normal Library playback) behaviour is unchanged.

Persistence & resume

  • Open room names are saved to the syncplay_rooms setting and restored on startup via ensure_room(), so rooms survive a restart.
  • The client stores its {token, room} in localStorage; on load it calls snapshot to silently rejoin (within MEMBER_TIMEOUT). The leave-on-reload beacon was removed so a refresh doesn't drop membership.

Settings keys

syncplay_enabled ("0"/"1"), syncplay_rooms (JSON list of persisted room names). A syncplay_enabled flag is also injected into the template context so the sidebar entry renders conditionally.

Clone this wiki locally