Files
SyncPlayer/README.md
2026-02-12 10:50:49 +01:00

170 lines
4.2 KiB
Markdown

# SyncPlayer (LAN Synchronized Video Playback)
Flask + Flask-SocketIO system for near frame-level synchronized playback across many browser-capable SoC displays on a local LAN.
## Key synchronization mechanism
### 1) Custom NTP-style time sync (client does **not** use its wall clock)
Client keeps a high-resolution monotonic clock using:
```js
client_ms = performance.timeOrigin + performance.now()
```
Every 10s the client sends `t1_ms` and the server replies with `{t1_ms, t2_ms}` where `t2_ms` is server time in ms.
Client receives the reply at `t3_ms` and calculates:
```text
rtt = t3 - t1
offset = t2 - (t1 + t3)/2
server_time ~= client_time + offset
```
Offset is smoothed to reduce jitter.
### 2) Scheduled absolute start
When an event triggers, the server schedules a future start time:
```text
start_time_ms = server_time_ms() + EVENT_LEAD_TIME_MS (default 5000)
```
Server sends each participating display:
- `video_url`
- `start_time_ms`
- `event_id`
Client preloads, pauses at 0, then uses a `setTimeout` + `requestAnimationFrame` loop to call `video.play()` *as close as possible* to `start_time_ms` using the corrected server clock.
### 3) Drift correction during playback
Every 2s client computes:
```text
expected = (serverNowMs - start_time_ms)/1000
drift = video.currentTime - expected
```
- if `|drift| > 15ms`: adjust `playbackRate` slightly (0.99 / 1.01)
- if `|drift| > 100ms`: hard seek to expected
This keeps the group synchronized on low-latency LANs.
## Project structure
```
/app
/templates
/static
/media
models.py
routes.py
sockets.py
udp_listener.py
sync.py
config.py
run.py
requirements.txt
```
## Setup
### 1) Create venv + install deps
Windows (cmd):
```bat
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
```
### 2) Initialize database
```bat
python run.py init-db
```
### 3) Run server
```bat
python run.py --host 0.0.0.0 --port 5000
```
Notes:
- On Windows / Python 3.14+, this runs in `threading` async mode by default (good enough for LAN testing). For highest scale/throughput, run on Linux and install `eventlet` or `gevent`.
- You can force an async mode with: `set ASYNC_MODE=threading` (Windows) or `export ASYNC_MODE=eventlet|gevent|threading` (Linux).
Open admin UI:
- `http://<server-ip>:5000/admin/`
Open display pages:
- `http://<server-ip>:5000/display/<public_id>`
## UDP trigger
Configure UDP Port + UDP Payload on an Event in the admin UI. The UDP listener binds ports automatically (refreshes config every 5s).
Send a UDP packet (example PowerShell):
```powershell
$udp = new-Object System.Net.Sockets.UdpClient
$bytes = [Text.Encoding]::UTF8.GetBytes("SHOW_START")
$udp.Send($bytes, $bytes.Length, "192.168.1.10", 7777) | Out-Null
$udp.Close()
```
### Firewall notes
- Allow TCP port (default 5000) for HTTP/WebSocket.
- Allow the configured UDP ports for triggers.
## HTTP URL trigger (LAN)
You can also trigger an event by opening a URL (useful for simple controllers without UDP):
- By id: `http://<server-ip>:5000/trigger/<event_id>`
- By name: `http://<server-ip>:5000/trigger_by_name/<event_name>`
Cooldown:
- Cooldown is enforced by default.
- To bypass cooldown (LAN only), add `?force=1`.
Example:
```
http://192.168.1.10:5000/trigger/1?force=1
```
## Idle image (no active event)
You can upload an **Idle Image** in the admin UI:
- `http://<server-ip>:5000/admin/idle-image`
When no triggered event is active, displays show this image fullscreen.
## Debug overlay
On display page:
- Press **F2** or **Ctrl+Shift+D**, or
- **Double click** anywhere
to toggle a small overlay showing Socket.IO connection state, offset/RTT, event id, drift and playbackRate.
## Display video cache warming
When a display page loads, it will fetch `/api/videos` and perform **background cache warming**:
- Default strategy: request the first **1MB** of each video via HTTP `Range` (`bytes=0-1048575`).
- Purpose: get the container + initial moov atoms into cache to reduce startup stutter.
- It is **best-effort**: browsers may evict cache under pressure, and some platforms ignore caching for large media.
- Cache warming automatically pauses once an event starts to avoid competing bandwidth during synchronized start.