170 lines
4.2 KiB
Markdown
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.
|