Jellyfin SyncPlay Reference
This document describes Jellyfin’s native SyncPlay implementation for reference when developing OpenWatchParty. Last updated: January 2026.
Overview
Jellyfin SyncPlay is the built-in synchronized playback feature that allows multiple users to watch content together. Understanding its architecture helps inform design decisions for OpenWatchParty.
Key differences from OpenWatchParty:
| Aspect | Jellyfin SyncPlay | OpenWatchParty |
|---|---|---|
| Architecture | Integrated into Jellyfin | Standalone plugin + server |
| Transport | REST API + Jellyfin messages | Dedicated WebSocket |
| Time sync | Min-delay selection | EMA smoothing |
| Server | C# (same as Jellyfin) | Rust |
| Client support | All official clients | Web only (currently) |
Source Code Locations
Server (C#)
Repository: jellyfin/jellyfin
Emby.Server.Implementations/SyncPlay/
├── SyncPlayManager.cs # Main orchestrator
└── Group.cs # Group/room management
MediaBrowser.Controller/SyncPlay/
├── ISyncPlayManager.cs # Main interface
├── IGroupState.cs # State machine interface
├── IGroupStateContext.cs # State context
├── IGroupPlaybackRequest.cs
├── ISyncPlayRequest.cs
├── GroupMember.cs # Participant model
├── GroupStates/ # State implementations
├── PlaybackRequests/ # Command handlers
├── Queue/ # Queue management
└── Requests/ # Request types
Client (JavaScript/TypeScript)
Repository: jellyfin/jellyfin-web
src/plugins/syncPlay/
├── plugin.ts # Plugin entry point
├── ui/ # UI components
└── core/
├── Manager.js # Main orchestrator
├── Controller.js # Control flow
├── PlaybackCore.js # Playback synchronization
├── QueueCore.js # Queue management
├── Helper.js # Utilities
├── Settings.js # Configuration
├── index.js # Module exports
├── players/ # Player-specific implementations
└── timeSync/
├── TimeSync.js # Base sync algorithm
├── TimeSyncCore.js # Facade/conversions
└── TimeSyncServer.js # Server ping implementation
Time Synchronization Algorithm
Overview
SyncPlay uses an NTP-like algorithm with min-delay selection (not averaging).
Four Timestamps
Each ping collects four timestamps:
| Timestamp | Source | Description |
|---|---|---|
requestSent |
Client | Before API call |
requestReceived |
Server | When request arrives |
responseSent |
Server | When response leaves |
responseReceived |
Client | After response arrives |
Offset Calculation
// NTP-like formula
offset = ((requestReceived - requestSent) + (responseSent - responseReceived)) / 2
This assumes symmetric network latency and accounts for server processing time.
Round-Trip Delay
delay = (responseReceived - requestSent) - (responseSent - requestReceived)
// └── total round trip ──────────┘ └── server processing time ──┘
Min-Delay Selection Strategy
Instead of averaging or EMA smoothing, SyncPlay:
- Maintains a sliding window of 8 measurements
- Sorts measurements by round-trip delay (lowest first)
- Selects the measurement with minimum delay as the best estimate
Rationale: Measurements with minimal latency are assumed to be most accurate, as network jitter typically adds delay rather than reducing it.
Polling Intervals
| Phase | Interval | Trigger |
|---|---|---|
| Greedy | 1000ms | Initial sync |
| Low-profile | 60000ms | After 3 successful pings |
const NumberOfTrackedMeasurements = 8;
const PollingIntervalGreedy = 1000; // 1 second
const PollingIntervalLowProfile = 60000; // 60 seconds
const GreedyPingCount = 3;
Time Conversion
// TimeSyncCore.js
// Server time → Local time
localTime = serverTime - extraTimeOffset;
// Local time → Server time
serverTime = localTime + extraTimeOffset;
// Total offset includes user-configurable adjustment
totalOffset = timeSyncServer.getTimeOffset() + extraTimeOffset;
Playback Synchronization
PlaybackCore.js
Manages synchronized playback with two correction strategies:
1. SpeedToSync
Adjusts playback rate to catch up gradually:
// Activates when drift is within thresholds
if (drift >= minDelaySpeedToSync && drift <= maxDelaySpeedToSync) {
// Adjust playback rate between 0.2x and 2.0x
// Corrects within configurable duration
}
Characteristics:
- Smooth, imperceptible correction
- Works when player supports
playbackRate - Limited correction range
2. SkipToSync
Direct seek to correct position:
// Activates when drift exceeds threshold
if (drift > minDelaySkipToSync) {
player.seek(estimatedServerPosition);
}
Characteristics:
- Immediate correction
- Visible jump in playback
- Used for large drifts
Local Control Methods
localUnpause() // Resume playback
localPause() // Pause playback
localSeek(ticks) // Seek to position (in ticks)
localStop() // Stop playback
Drift Calculation
// Expected position based on server sync
const expected = lastSyncPosition + (now - lastSyncTime);
// Actual player position
const actual = player.currentTime;
// Drift to correct
const drift = expected - actual;
Manager Coordination
Initialization Flow
1. SyncPlay enabled
2. timeSyncCore.forceUpdate()
3. Wait for 'time-sync-server-update' event
4. syncPlayReady = true
5. Process queued commands
Event Categories
1. Group Updates (processGroupUpdate)
Handles:
- User join/leave notifications
- Queue changes
- Group state transitions (“GroupJoined”, “UserLeft”, “PlayQueue”)
2. Playback Commands (processCommand)
Command structure:
{
Command: "Play" | "Pause" | "Seek",
When: serverTimestamp, // Execution time
PositionTicks: number, // Position in ticks
PlaylistItemId: string // Queue item ID
}
Processing:
- Validate command isn’t stale (
When > syncPlayEnabledAt) - Verify playlist alignment
- Queue if not ready, otherwise execute
- Delegate to
playbackCore.applyCommand()
3. State Changes (processStateChange)
Handles group state modifications and emits events to observers.
Command Flow
Server API Call
↓
Manager.processCommand()
↓
Validate timing & playlist
↓
playbackCore.applyCommand()
↓
Schedule execution at 'When' timestamp
↓
Player executes action
Communication Protocol
Transport
SyncPlay uses Jellyfin’s existing infrastructure:
- REST API for time sync (
apiClient.getServerTime()) - Jellyfin message system for commands (WebSocket-based but shared with other features)
API Endpoints
GET /SyncPlay/Time # Get server time (for sync)
POST /SyncPlay/New # Create group
POST /SyncPlay/Join # Join group
POST /SyncPlay/Leave # Leave group
POST /SyncPlay/Play # Send play command
POST /SyncPlay/Pause # Send pause command
POST /SyncPlay/Seek # Send seek command
POST /SyncPlay/SetPlaylist # Set queue
Server Time Response
{
"RequestReceptionTime": "2024-01-15T10:30:00.123Z",
"ResponseTransmissionTime": "2024-01-15T10:30:00.125Z"
}
Server Architecture
SyncPlayManager.cs
Main orchestrator responsibilities:
- Create/destroy groups
- Route messages to appropriate groups
- Manage user sessions
- Handle authentication/authorization
Group.cs
Group (room) management:
- Track members
- Maintain playback state
- Process commands from host
- Broadcast state updates
State Machine
Groups use a state machine pattern:
States: Idle, Waiting, Paused, Playing
Transitions:
Idle → Waiting (play requested, waiting for ready)
Waiting → Playing (all ready)
Playing → Paused (pause requested)
Paused → Playing (unpause requested)
Any → Idle (stop/leave)
Settings and Configuration
Client Settings (Settings.js)
{
// Sync correction
enableSyncCorrection: true,
// SpeedToSync
useSpeedToSync: true,
minDelaySpeedToSync: 50, // ms
maxDelaySpeedToSync: 3000, // ms
speedToSyncDuration: 1000, // ms
// SkipToSync
useSkipToSync: true,
minDelaySkipToSync: 400, // ms
// Extra offset (user adjustable)
extraTimeOffset: 0 // ms
}
Server Configuration
Configured via Jellyfin’s standard configuration system:
- Group size limits
- Timeout values
- Feature toggles
Known Limitations
Based on GitHub issues:
- Transcoding delay: Users requiring transcoding tend to be ~2 seconds behind
- Sync correction issues: Can cause problems when precise sync isn’t needed
- Pause/resume desync: Occasional further desync after pause/resume cycles
Comparison with OpenWatchParty
Time Sync Approach
| Aspect | Jellyfin SyncPlay | OpenWatchParty |
|---|---|---|
| Algorithm | Min-delay selection | EMA smoothing (α=0.4) |
| Samples | 8 (sliding window) | Continuous |
| Selection | Best (lowest RTT) | Weighted average |
| Initial sync | 3 fast pings | First measurement direct |
| Maintenance | 60s polling | 10s polling |
Trade-offs:
- Min-delay is more resistant to outliers
- EMA provides smoother transitions
- Min-delay requires more samples for accuracy
Drift Correction
| Aspect | Jellyfin SyncPlay | OpenWatchParty |
|---|---|---|
| Strategy | SpeedToSync + SkipToSync | Continuous rate adjustment |
| Rate range | 0.2x - 2.0x | 0.85x - 2.0x |
| Deadzone | Configurable thresholds | 40ms |
| Hard seek | Above threshold | Above 2s drift |
Architecture
| Aspect | Jellyfin SyncPlay | OpenWatchParty |
|---|---|---|
| Deployment | Integrated | Plugin + external server |
| Dependencies | None | Rust server required |
| Client support | All Jellyfin clients | Web only |
| Maintenance | Jellyfin team | Independent |
References
GitHub Repositories
- jellyfin/jellyfin - Server
- jellyfin/jellyfin-web - Web client
Key Pull Requests
- PR #1011 - Original SyncPlay implementation
- PR #1945 - TV series support, code refactor
- PR #1990 - WebRTC time syncing proposal
- PR #2204 - SyncPlay settings UI
- PR #3976 - Move to plugin architecture