mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
e7b59d61ac
- ARCHITECTURE.md: startup -> lazy connect, reconnect when in room - HOW_IT_WORKS.md: on-demand connection, heartbeat while connected - PRIVACY.md: clarify alarms only during active sessions - SYNC_GUIDE.md: build-extension.cjs + episode-utils injection - StoreDescription.md: on-demand relay, no persistent connection - README.md + extension/README.md: npm run build:extension - extension/README.md: replace Dual Heartbeat with On-Demand Connection - website/privacy.html: add lazy-connect privacy note
214 lines
12 KiB
Markdown
214 lines
12 KiB
Markdown
# 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
|
|
|
|
1. Download the extension from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page (or install from the Chrome Web Store / Firefox Add-ons).
|
|
2. The extension adds a small icon to your browser toolbar.
|
|
3. 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`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.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:
|
|
|
|
1. **WebSocket Handshake** (on demand): `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket` only when needed (popup opened or active room).
|
|
2. **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_VERSION` to reject outdated clients.
|
|
3. **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:
|
|
|
|
1. The extension generates a random Room ID (e.g., `happy-koala-42`) and a random 6-character password.
|
|
2. Room IDs are restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only).
|
|
3. The extension emits a `JOIN_ROOM` event to the server.
|
|
|
|
> **Data sent in `JOIN_ROOM`**:
|
|
> ```json
|
|
> {
|
|
> "roomId": "happy-koala-42",
|
|
> "password": "x7k2m9",
|
|
> "peerId": "a1b2c3d4",
|
|
> "username": "MyName",
|
|
> "tabTitle": "YouTube - My Video",
|
|
> "protocolVersion": "1.0.0"
|
|
> }
|
|
> ```
|
|
|
|
4. **Server-side processing**:
|
|
- All fields are **sanitized**: `roomId` is stripped of invalid characters and clamped to 64 chars; `peerId` clamped to 16 chars; `password` clamped to 128 chars; `username` clamped 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_DATA` containing the list of peers in the room.
|
|
|
|
5. **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:
|
|
|
|
1. The extension constructs a URL in this format:
|
|
```
|
|
https://sync.koalastuff.net/join.html#join:<roomId>:<password>:<serverFlag>:<encodedServerUrl>
|
|
```
|
|
- `serverFlag`: `0` for official server, `1` for custom server.
|
|
- `encodedServerUrl`: Only populated if using a custom server.
|
|
|
|
2. **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.
|
|
|
|
3. 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:
|
|
|
|
1. **`join.html` loads** on `sync.koalastuff.net`. The page displays "INVITATION DETECTED" with the Room ID.
|
|
|
|
2. **Extension detection**: The page checks for `document.documentElement.dataset.koalasyncInstalled`, which is set by `bridge.js` (a content script injected only on `sync.koalastuff.net`).
|
|
|
|
3. **If the extension IS installed**:
|
|
- The page shows "Joining room automatically..."
|
|
- After 500ms, the page dispatches a `KOALASYNC_JOIN_REQUEST` custom DOM event with `{ roomId, password, useCustomServer, serverUrl }`.
|
|
- `bridge.js` catches this event and forwards it to `background.js` via `chrome.runtime.sendMessage`.
|
|
- `background.js` stores the credentials in `chrome.storage.sync` and emits `JOIN_ROOM` to the server.
|
|
- The server validates the password against the stored keyed SHA-256 HMAC hash.
|
|
- On success, the server responds with `ROOM_DATA` and broadcasts `PEER_STATUS { status: 'joined' }` to all existing peers.
|
|
- The join page updates to show "✅ Successfully joined!".
|
|
|
|
4. **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:
|
|
|
|
1. Open a video on any website (YouTube, Twitch, Netflix, etc.).
|
|
2. In the extension popup → **Sync** tab → use the **"Target Tab"** dropdown.
|
|
3. The dropdown lists all open tabs, filtered to exclude noise (search engines, social media — configurable via Settings).
|
|
4. Tabs with a **matching video title** are highlighted with a ⭐ prefix for easy identification.
|
|
5. Selecting a tab causes `background.js` to set `currentTabId` and inject `content.js` into that tab via `chrome.scripting.executeScript`.
|
|
|
|
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, and `loadeddata`. (Time and volume state are tracked via a 15-second heartbeat interval, not continuous event listeners). It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
|
|
|
|
---
|
|
|
|
## Step 7: Synchronized Playback
|
|
|
|
When User A presses **Play** on their video:
|
|
|
|
1. `content.js` detects the native `play` event on the `<video>` element.
|
|
2. It checks the `expectedEvents` Set — if this event was expected (caused by a remote command), it's consumed silently. If not, it's a **user action**.
|
|
3. For user actions, `content.js` sends `{ type: 'CONTENT_EVENT', action: 'play', payload: { currentTime, ... } }` to `background.js`.
|
|
4. `background.js` adds an `actionTimestamp` and emits the `PLAY` event to the server.
|
|
5. **Server relay**: The server sanitizes all fields (strings clamped, numbers validated, booleans type-checked) and constructs a clean `relayPayload` with `senderId` set to User A's `peerId`. The raw client data is never forwarded directly.
|
|
6. The server broadcasts the sanitized payload to all other peers in the room.
|
|
7. User B's `background.js` receives the `PLAY` event and calls `routeToContent()`, which sends a `SERVER_COMMAND` message to User B's `content.js`.
|
|
8. User B's `content.js` adds `'playing'` to its `expectedEvents` Set (so it won't echo the event back), then calls `video.play()`.
|
|
|
|
> **The same flow applies to Pause and Seek**, with Seek additionally sending `targetTime` for 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
|
|
1. The initiator's `content.js` captures the current `video.currentTime` as the `targetTime`.
|
|
2. `background.js` emits `FORCE_SYNC_PREPARE` with `{ targetTime }` to all peers.
|
|
3. All peers (including the initiator) **pause** their video and **seek** to `targetTime`.
|
|
4. Each peer's `content.js` polls `video.readyState` until it reaches `≥ 3` (buffered enough to play), with an 8-second timeout.
|
|
5. Once buffered, each peer sends `FORCE_SYNC_ACK` back.
|
|
|
|
### Phase 2 — Execute
|
|
6. Once all ACKs are received (or after 8.5 seconds), the initiator emits `FORCE_SYNC_EXECUTE`.
|
|
7. 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`, `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:
|
|
|
|
1. `background.js` emits `LEAVE_ROOM` (or the WebSocket `disconnect` fires automatically).
|
|
2. The server calls `removePeerFromRoom()`, which:
|
|
- Removes the peer from the room's `peers` Set, `peerIds` Map, and `peerData` Map.
|
|
- Removes the socket from the global `socketToRoom` and `peerToSocket` maps.
|
|
- Broadcasts `PEER_STATUS { status: 'left' }` to remaining peers.
|
|
- If the room is now empty, **deletes the room entirely** — no data persists.
|
|
3. 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:
|
|
|
|
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
|
|
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected new title.
|
|
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
|
|
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
|
|
5. Once a peer detects the matching title, they send `EPISODE_READY`.
|
|
6. 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.
|