13 KiB
KoalaSync — How It Works (Step-by-Step)
This guide walks through the complete user flow of KoalaSync, from creating a room to synchronized playback. It is designed for store reviewers, end-users, and manual testers to understand exactly what happens at each step, what data is sent, and where it goes.
Step 1: Installing the Extension
- Download the extension from the Releases page (or install from the Chrome Web Store / Firefox Add-ons).
- The extension adds a small icon to your browser toolbar.
- On first install, a unique 8-character Peer ID is generated locally and stored in
chrome.storage.local. This ID is never sent to any external service — it only travels to the relay server when you join a room.
What's stored locally:
peerId(8-char hex),username(customizable, defaults to a readable adjective-noun pair),serverUrl,filterNoisepreference. All stored viachrome.storage.syncandchrome.storage.local.
Step 2: Connecting to the Relay Server
When you open the extension popup (with saved room credentials) or when a saved room configuration exists from a previous session, the background service worker connects to the relay server:
- WebSocket Handshake (on demand):
background.jsopens a WebSocket towss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocketonly when needed (popup opened or active room). - Security Checks (server-side):
- The server checks the client's IP rate limit (max 10 connections per 60 seconds).
- The server validates the authentication token (hardcoded in
shared/constants.js) to verify this is a legitimate KoalaSync client. - The server checks the extension version against
MIN_VERSIONto reject outdated clients.
- Connection Established: The server responds with an Engine.IO handshake (
0{...}), followed by a Socket.IO namespace join (40). The connection status dot in the popup turns green.
Data sent to server:
token(authentication),version(e.g.,1.3.1). No personal data is transmitted during connection.
Step 3: Creating a Room
Click "Create Room" in the popup's Room tab:
- The extension generates a random Room ID (e.g.,
happy-koala-42) and a random 6-character password. - Room IDs are restricted to
[a-zA-Z0-9-](alphanumeric + hyphens only). - The extension emits a
JOIN_ROOMevent to the server.
Data sent in
JOIN_ROOM:{ "roomId": "happy-koala-42", "password": "x7k2m9", "peerId": "a1b2c3d4", "username": "MyName", "tabTitle": "YouTube - My Video", "protocolVersion": "1.0.0" }
-
Server-side processing:
- All fields are sanitized:
roomIdis stripped of invalid characters and clamped to 64 chars;peerIdclamped to 16 chars;passwordclamped to 128 chars;usernameclamped to 30 chars. - The server hashes the password with a keyed SHA-256 HMAC and stores only that hash in RAM (the plaintext is never stored).
- A new room object is created in memory with the peer's data.
- The server responds with
ROOM_DATAcontaining the list of peers in the room.
- All fields are sanitized:
-
Popup updates: The Room tab switches to the "Active Room" view, showing your Room ID and an invitation link.
Step 4: Sharing an Invitation Link
Click the 📋 Copy button next to the invite link:
-
The extension constructs a URL in this format:
https://sync.koalastuff.net/join.html#join:<roomId>:<password>:<serverFlag>:<encodedServerUrl>serverFlag:0for official server,1for custom server.encodedServerUrl: Only populated if using a custom server.
-
Important: The room credentials are in the URL hash (
#), which means they are never sent to the web server — the hash fragment stays entirely in the browser. The landing page server never sees your room ID or password. -
Send this link to your friend via any messaging app.
Step 5: Your Friend Opens the Invitation Link
When your friend opens the link in their browser:
-
join.htmlloads onsync.koalastuff.net. The page displays "INVITATION DETECTED" with the Room ID. -
Extension detection: The page checks for
document.documentElement.dataset.koalasyncInstalled, which is set bybridge.js(a content script injected only onsync.koalastuff.net). -
If the extension IS installed:
- The page shows "Joining room automatically..."
- After 500ms, the page dispatches a
KOALASYNC_JOIN_REQUESTcustom DOM event with{ roomId, password, useCustomServer, serverUrl }. bridge.jscatches this event and forwards it tobackground.jsviachrome.runtime.sendMessage.background.jsstores the credentials inchrome.storage.syncand emitsJOIN_ROOMto the server.- The server validates the password against the stored keyed SHA-256 HMAC hash.
- On success, the server responds with
ROOM_DATAand broadcastsPEER_STATUS { status: 'joined' }to all existing peers. - The join page updates to show "✅ Successfully joined!".
-
If the extension is NOT installed:
- The page shows download links (Chrome Web Store / GitHub).
- The user installs the extension, returns to the link, and the flow continues from step 3.
Step 6: Selecting a Video Tab
Both users now need to select which browser tab contains the video to sync:
- Open a video on any website (YouTube, Twitch, Netflix, etc.).
- In the extension popup → Sync tab → use the "Target Tab" dropdown.
- The dropdown lists all open tabs, filtered to exclude noise (search engines, social media — configurable via Settings).
- Tabs with a matching video title are highlighted with a ⭐ prefix for easy identification.
- Selecting a tab causes
background.jsto setcurrentTabIdand injectcontent.jsinto that tab viachrome.scripting.executeScript.
What
content.jsdoes on injection: Finds the first<video>element on the page and attaches event listeners forplay,pause,seeked, andloadeddata. (Time and volume state are tracked via a 15-second heartbeat interval, not continuous event listeners). It uses anexpectedEventsSet to distinguish between user actions and programmatic actions (loop prevention).
Step 7: Synchronized Playback
When User A presses Play on their video:
content.jsdetects the nativeplayevent on the<video>element.- It checks the
expectedEventsSet — if this event was expected (caused by a remote command), it's consumed silently. If not, it's a user action. - For user actions,
content.jssends{ type: 'CONTENT_EVENT', action: 'play', payload: { currentTime, ... } }tobackground.js. background.jsadds anactionTimestampand emits thePLAYevent to the server.- Server relay: The server sanitizes all fields (strings clamped, numbers validated, booleans type-checked) and constructs a clean
relayPayloadwithsenderIdset to User A'speerId. The raw client data is never forwarded directly. - The server broadcasts the sanitized payload to all other peers in the room.
- User B's
background.jsreceives thePLAYevent and callsrouteToContent(), which sends aSERVER_COMMANDmessage to User B'scontent.js. - User B's
content.jsadds'playing'to itsexpectedEventsSet (so it won't echo the event back), then callsvideo.play().
The same flow applies to Pause and Seek, with Seek additionally sending
targetTimefor the time position.
Step 8: Force Sync (Two-Phase Protocol)
If videos drift out of sync, either user can click "Force Sync":
Phase 1 — Prepare
- The initiator's
content.jscaptures the currentvideo.currentTimeas thetargetTime. background.jsemitsFORCE_SYNC_PREPAREwith{ targetTime }to all peers.- All peers (including the initiator) pause their video and seek to
targetTime. - Each peer's
content.jspollsvideo.readyStateuntil it reaches≥ 3(buffered enough to play), with an 8-second timeout. - Once buffered, each peer sends
FORCE_SYNC_ACKback.
Phase 2 — Execute
- Once all ACKs are received (or after 8.5 seconds), the initiator emits
FORCE_SYNC_EXECUTE. - All peers call
video.play()simultaneously, achieving synchronized playback.
Why two phases? Without buffering confirmation, peers with slower connections would start playing before they've loaded the target timestamp, causing immediate desync.
Step 9: Heartbeat & Peer Health
While in a room, two heartbeats keep the session alive:
| Heartbeat | Interval | Source | Purpose |
|---|---|---|---|
| Background | 30 seconds | background.js |
While connected, signals "I'm still connected" and triggers automatic reconnect (500ms base, max 5s). No heartbeats fire when idle (lazy connect). |
| Content | 15 seconds | content.js |
Sends video metadata: currentTime, privacy-filtered mediaTitle, playbackState, volume, muted |
- Server Reaper: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
- Room Cleanup: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
Step 10: Leaving a Room
When a user clicks "Leave" or closes their browser:
background.jsemitsLEAVE_ROOM(or the WebSocketdisconnectfires automatically).- The server calls
removePeerFromRoom(), which:- Removes the peer from the room's
peersSet,peerIdsMap, andpeerDataMap. - Removes the socket from the global
socketToRoomandpeerToSocketmaps. - Broadcasts
PEER_STATUS { status: 'left' }to remaining peers. - If the room is now empty, deletes the room entirely — no data persists.
- Removes the peer from the room's
- The event rate-limit counter for that socket is also cleaned up.
After disconnect, zero data about the user remains on the server. There is no database, no log file, no analytics record. The session existed only in RAM and is now gone.
Episode Auto-Sync Flow
When watching a series and an episode ends:
content.jsmonitors the Media Session API for title changes.- When a new title is detected, the peer broadcasts
EPISODE_LOBBYwith the expected title after applying the local media title privacy setting. In episode-only mode this is an identifier such asS01E04; when media titles are not sent, the client does not create a new episode lobby. - All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
- Each peer's
content.jspolls for the new title to appear in the<video>element's metadata. - Once a peer detects the matching title, they send
EPISODE_READY. - When all peers report ready, the lobby resolves and playback resumes simultaneously.
Data Flow Summary
┌─────────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────────┐
│ Extension │ ←─────────────────→│ Relay Server │←──────────────────→│ Extension │
│ (User A) │ JOIN_ROOM │ (RAM only) │ JOIN_ROOM │ (User B) │
│ │ PLAY/PAUSE/SEEK │ │ PLAY/PAUSE/SEEK │ │
│ │ FORCE_SYNC_* │ Sanitizes & │ FORCE_SYNC_* │ │
│ │ PEER_STATUS │ relays only │ PEER_STATUS │ │
│ │ EPISODE_* │ │ EPISODE_* │ │
└──────┬──────┘ └───────────────┘ └──────┬──────┘
│ │
┌────┴─────┐ ┌─────┴────┐
│ content │ Listens to <video> events │ content │
│ .js │ Controls playback │ .js │
└──────────┘ └──────────┘
The relay server is a pure message forwarder. It never interprets video content, accesses URLs, or stores session history. All media control happens locally inside each user's browser via the
<video>DOM API.