emby-watchparty-3
Docker app from Mahesvara's Toolshed
Overview
Readme
View on GitHubEmby Watch Party
A synchronized watch-party frontend for Emby and Jellyfin: watch together while your media server stays on your internal network. Any member with an account on the configured server can become the room's host; other viewers join as spectators without a media-server account.
This branch contains 3.x. The watchparty2 branch preserves the 2.x source and documentation. Read the migration guide before moving an existing 2.x deployment to 3.0.
Table of contents
- Support development
- Special thanks
- Discord
- Documentation
- Features
- Browser compatibility
- Setup
- Usage
- Configuration
- Architecture
- API endpoints
- Troubleshooting
- Security notes
- License
- Contributing
- Acknowledgments
- Educational use notice
Support development
If Emby Watch Party has saved you from the hell of trying to coordinate "3, 2, 1, play" over Discord, consider buying me a coffee:
Every tip helps fund the hardware and the late nights reverse-engineering Emby's HLS pipeline.
Special thanks
Special thanks to QuackMasterDan for his dedication in testing and providing valuable feedback throughout development.
Thanks to wlowen and JeslynMcKenzie for testing, detailed bug reports, and providing mediainfo that helped track down the HEVC transcoding issues.
Special thanks to StealthyDruid and xyxxyxxy for their absolute dedication to testing every beta release of 2.x and 3.x. Their relentless testing, detailed bug reports and feedback helped eradicate nearly every bug along the way.
Discord
Documentation
- Project wiki - hardware, deployment, troubleshooting, and FAQ
- Migration guide - upgrade path from 2.1.x to 3.0
- Appliance deployment - Compose, CasaOS, Portainer, and TrueNAS setup, diagnosis, update, and rollback; the maintainer's separate Unraid templates repository is indexed by Community Apps through
TemplateURL - Socket.IO API - developer reference for the Socket.IO event protocol
- OpenAPI reference -
GET /docs(Swagger UI) orGET /redocon a running instance - CHANGELOG.md - consolidated 3.0.0 release notes, upgrade requirements and version history summary
- SUMMARY-OF-CHANGES.md - full 3.0 beta changelogs and earlier stable release notes, with links to the older 2.0 beta log and 3.0 technical analysis
- Emby quirks we learned the hard way - Why 2.0 came to be.
Features
- Secure proxy architecture: your Emby or Jellyfin server stays on the local network; only the WatchParty app is exposed
- FastAPI + Vue 3 + TypeScript stack: async end-to-end, typed OpenAPI docs at
/docsand/redoc, single uvicorn process serves backend + compiled SPA - Real-time sync with drift correction: any party member can play, pause or seek; the shared party clock keeps independent streams aligned, with progress reports from the video selector anchoring drift correction
- Late-joiner voting flow: existing users vote to admit mid-playback joiners; a passing vote restarts on a PTS-aligned segment 0 so everyone lands together
- Host-provider auth model: UNLOCKED / PLAYING-ONLY / LOCKED states around the authenticated media-server host, with ownership recovery for quick refreshes
- Per-user playback sessions: audio, subtitles, quality and media version are personal, with separate media-server streams for each viewer
- Multi-version playback: each viewer can switch between available media versions during playback, such as theatrical/director's cut or 1080p/4K sources
- Bitrate-granular quality menu: mirrors Emby's own per-resolution table, with admin-side toggles for exposed resolutions and bitrates
- Unified subtitles: text subs via side-channel proxy, image (PGS) subs burned in per-user, both surfaced in one dropdown
- Resume, jump-to-timestamp, and binge auto-advance: Continue Watching prompt from Emby
UserData, absolute-timestamp seek input, and end-of-episode countdown with room-wide Cancel - Rooms, chat, and identity: 5-character party codes, live chat with a mobile slide-over, custom avatars (upload / Gravatar / monsterid fallback) recoverable via 3-word codes
- Refined Cyber UI: cyan/magenta/violet palette, glass surfaces, chip/pill controls, animated LIVE badge, iOS-style A-Z library jump bar
- Reverse-proxy ready:
APP_PREFIXwired end-to-end so one Docker image sits behind any subpath (https://example.com/watchparty/...) - Admin panel: runtime settings editable from
/admin, including the in-party modal; most changes apply immediately, while file-logging changes require restart - Reload-as-rejoin + healthchecks: refresh in an active party rejoins via persistent
client_id;/api/healthreports liveness and/api/readyverifies the media server is reachable and that the configured API key is accepted; rsyslog-style logging with rotation
Browser compatibility
The desktop browser targets are Chrome, Edge, Firefox, Safari and Brave. Mobile targets are Safari on iOS and Chrome on Android. Codec support and native fullscreen subtitle behaviour depend on the browser, operating system and source media.
CI exercises Chromium, Firefox and desktop WebKit. Chromium covers the full fake-media-server playback journey; Firefox adds host/admin recovery checks, and desktop WebKit covers the cross-engine smoke suite. Native HLS checks run with the iOS WebKit project on macOS. These checks do not replace a playback test on your viewers' devices.
Known issue: Brave on iOS has been reported to hide subtitles in fullscreen while normal-view subtitles still work. Use Safari on iOS when affected.
Setup
Prerequisites
- An Emby or Jellyfin server reachable from the Watch Party backend. The media server does not need to be exposed to the internet.
- A dedicated administrative API key from that server. Host credentials are supplied through the in-app login flow, rather than stored in
.env. - A Watch Party URL reachable by your viewers, with HTTPS for production deployments.
- For Docker installs: Docker Engine and Docker Compose v2.
- For manual installs: Python 3.12.x, Node.js matching
^20.19.0 || >=22.12.0, and npm. The official frontend build and CI use Node 24.
Manual installation
Use a fresh checkout and Python 3.12. Existing 2.x installations need the migration steps, including a fresh virtual environment while preserving their configuration and data.
Create and activate the virtual environment.
Linux/macOS:
python3.12 -m venv .venv . .venv/bin/activateWindows PowerShell, with Python 3.12 selected as
python:python -m venv .venv .\.venv\Scripts\Activate.ps1Install the backend dependencies with hash verification:
python -m pip install --require-hashes -r requirements.txtBuild the frontend. Vite writes the bundle to
backend/static/:cd frontend npm ci npm run build cd ..For hot-reload development and browser tests, see frontend/README.md.
Configure the backend. For a new installation, copy
.env.exampleto.env:cp .env.example .envPowerShell equivalent:
Copy-Item .env.example .envThe example is a production template. Fill in
MEDIA_SERVER_TYPE,MEDIA_SERVER_URL,MEDIA_SERVER_API_KEY, a stableSESSION_SECRET, andCORS_ALLOWED_ORIGINS. Uncomment and setSESSION_COOKIE_SECURE=trueandBEHIND_PROXY=trueorfalse; a proxy deployment also needs its actualTRUSTED_PROXY_CIDRS. Keep HLS token validation enabled.Generate the session secret once with
openssl rand -hex 32, or with Python:python -c "import secrets; print(secrets.token_hex(32))"For local plain HTTP, use
APP_ENV=developmentandSESSION_COOKIE_SECURE=false. Development relaxes production-only requirements, but provider, URL, port, prefix and proxy settings must still be valid. Browsing and playback need a reachable media server and working credentials.Run the application from the repository root with the environment active:
python -m backend.appOpen
http://localhost:5000/. WithAPP_PREFIX=/watchparty, usehttp://localhost:5000/watchparty/. API documentation stays at hostname-root/docsand/redoc, with the schema at/openapi.json.
Docker installation
Upgrading from 2.1.x requires the 3.0 migration guide.
Back up .env, the effective Compose/container configuration, config.json,
data/, images/avatars/, and every volume mapping. Keep the old image and
complete backup until login, playback, seeking, subtitles and reconnects pass a real test.
Choose the image tag deliberately:
| Tag | Purpose |
|---|---|
:2 |
Stable 2.x line |
:3, :3.0 |
Stable 3.x line, available once its first stable image is published |
:3.0.0 |
Exact 3.0.0 release, available once published |
:devel, :nightly |
Moving 3.x prerelease images |
:latest |
Latest stable release; it can move between major versions |
The generated Compose example currently selects
:devel. Before a stable 3.x image is published, use that prerelease for testing
or build this checkout locally with docker build -t emby-watchparty:local .
and set the copied Compose service's image: to emby-watchparty:local.
Changing a Git branch does not publish an image.
For the published prerelease:
docker pull ghcr.io/oratorian/emby-watchparty:devel
For a new installation, copy docker-compose.yml.example to
docker-compose.yml and .env.example to .env. Existing installations
should edit their copies using the migration guide.
Fill the production settings described above. In the copied Compose file,
uncomment the SESSION_COOKIE_SECURE and BEHIND_PROXY environment entries.
Setting them only in Compose's .env does not pass them into the container
while those mappings remain commented out.
Create persistent directories and config.json before the first start.
Keep an existing config.json unchanged; a new file must contain {},
rather than be empty or a directory.
Linux/macOS:
mkdir -p data images/avatars logs
[ -f config.json ] || echo {} > config.json
Windows PowerShell:
New-Item -ItemType Directory -Force data, images/avatars, logs | Out-Null
if (-not (Test-Path config.json)) { Set-Content config.json '{}' }
Run the read-only preflight against the exact candidate environment and volumes, then start only after resolving its errors and required actions:
docker compose run --rm --no-deps emby-watchparty python -m backend.migration_preflight --root /app --target production --deployment docker
docker compose up -d emby-watchparty
Invalid production configuration keeps the process reachable for diagnosis:
/api/health reports setup_required, /api/ready returns 503, and stderr names
the failing settings. Both API paths include APP_PREFIX when configured.
The supplied Compose artifacts set the container bind and port to
0.0.0.0:5000. To expose another host port, change only the left side of
the HOST_PORT:5000 mapping.
If you prefer docker run, this POSIX-shell example uses the same storage layout
and prerelease tag. Set the production values in .env first, or use the local
development configuration for plain HTTP:
docker run -d \
--name emby-watchparty \
-p 5000:5000 \
--env-file .env \
-e WATCH_PARTY_BIND=0.0.0.0 \
-e WATCH_PARTY_PORT=5000 \
-v "$(pwd)/data:/app/data" \
-v "$(pwd)/images/avatars:/app/images/avatars" \
-v "$(pwd)/config.json:/app/config.json" \
-v "$(pwd)/logs:/app/logs" \
ghcr.io/oratorian/emby-watchparty:devel
| Host path | Container path | Purpose |
|---|---|---|
./data |
/app/data |
Persistent avatar database |
./images/avatars |
/app/images/avatars |
Uploaded avatar images |
./config.json |
/app/config.json |
Runtime admin settings |
./logs |
/app/logs |
Optional mount to retain logs across recreation; file logging is enabled by default |
Keep SESSION_SECRET stable across restarts. A missing secret creates an
ephemeral key in development, while production rejects it. Run one application
process/worker because party state, sessions, rate limits, background tasks and
HLS tokens are process-local.
Boot settings come from the environment or .env and require restart.
Runtime settings live in config.json and the admin panel; most apply immediately,
while file-logging changes require restart. Platform guides and validation steps
are indexed in the appliance deployment guide.
Usage
Creating a watch party
The default (REQUIRE_LOGIN=false):
- Click "Create Party" on the home page (no login required)
- Share the party code or URL with your friends
- Inside the party, click "Login to Become Host" with your media-server credentials. This unlocks the library for everyone in the room. Spectators can join without a media-server login.
- Browse the library and select a video
- Everyone in the room will be synchronized
With REQUIRE_LOGIN=true (set from the admin panel):
- Click "Create Party" and supply credentials for the configured Emby or Jellyfin server
- The creator becomes host atomically; the party starts UNLOCKED
- Share the code; spectators join with no login prompt
- Browse, pick, watch
In both modes, when the host disconnects mid-playback the in-flight video keeps streaming until it ends naturally (PLAYING-ONLY state). The library re-locks immediately; any member can click "Login to Become Host" to unlock it again.
Joining a watch party
- Click "Join Watch Party" on the home page (or open a shared URL)
- Enter the party code if needed
- Enter your username
- Start watching together!
If you join before a video has been selected, you land directly in the party. If you join while a video is already playing, the existing users will see a vote modal asking whether to restart the video from the beginning so you can join in sync. See the project wiki for full details of the late-joiner vote flow.
In-party controls
- Browse library: use the sidebar to browse the configured media server's libraries, movies and TV shows
- Select video: click on any video to start watching it with the group
- Video controls: any user can play, pause, or seek - all users will sync
- Audio, subtitles, quality and media version: each viewer can change their own settings during playback without pausing the room or starting a shared ready check
- Chat: use the chat box at the bottom to communicate with other viewers
- Leave: click the "Leave" button to exit the watch party
Configuration
Configuration is split into two tiers:
- Boot-essential settings come from environment variables or
.envand require restart. - Runtime settings are editable from the admin panel at
/admin, using administrator credentials for the configured media server. Most changes apply immediately; file-logging changes are saved with a restart-required indication.
Boot precedence is exact: process environment -> .env -> defaults. Process environment and .env are one explicit-operator tier, with process values winning. Reading .env does not mutate the running process environment.
When boot configuration is invalid
The process stays reachable but serves nothing. Normal API, Socket.IO, HLS, admin and application routes return 503; liveness returns HTTP 200 with status: setup_required, and readiness returns 503 with the same status, so an orchestrator can tell "misconfigured" from "dead" without restart-looping the container.
The failing fields are named on stderr in a framed banner, and again through the application logger:
========================================================================
Emby Watch Party cannot start: invalid boot configuration.
CORS_ALLOWED_ORIGINS: must be explicit in production
SESSION_COOKIE_SECURE: must be true in production
SESSION_SECRET: must be at least 32 characters in production
Set these in the environment (container template, compose
environment:, or .env) and restart. Nothing else is served
until they are valid.
========================================================================
Fix the named variables where your deployment defines them and restart. Unraid users edit the Community App through its WebUI. CasaOS, Portainer, and TrueNAS users edit the imported Compose-based app or stack; plain Compose uses the environment: block or .env.
3.0 development builds briefly shipped an interactive setup page at
/setup, gated by a bootstrap token, that wrotedata/bootstrap.json. Both are gone. Boot configuration is environment-only, as it was in 2.x. The page could not work on the platforms this is deployed to: every setting arrives as an environment variable there, and env-provided fields were short-circuited back to their current value, so the form silently discarded edits. Any leftoverbootstrap.jsonorsetup-tokenis ignored and removed on the next successful boot.
Local plain-HTTP example:
APP_ENV=development
SESSION_SECRET=generate-one-stable-64-character-random-hex-value
SESSION_COOKIE_SECURE=false
CORS_ALLOWED_ORIGINS=*
MEDIA_SERVER_TYPE=emby
MEDIA_SERVER_URL=http://localhost:8096
MEDIA_SERVER_API_KEY=your-dedicated-emby-api-key
ENABLE_HLS_TOKEN_VALIDATION=true
Jellyfin 10.10+ uses the same two settings and only changes the provider:
MEDIA_SERVER_TYPE=jellyfin
MEDIA_SERVER_URL=http://jellyfin:8096
MEDIA_SERVER_API_KEY=your-dedicated-jellyfin-api-key
There is one URL and one key for both providers, because neither was ever
provider-specific; only MEDIA_SERVER_TYPE is. It stays explicit, with no
auto-detection: it names which server MEDIA_SERVER_URL points at and which
server issued MEDIA_SERVER_API_KEY, and changing it requires a restart. Mint
the key on that server, from its own admin dashboard, and keep it dedicated to
Watch Party.
Renamed in 3.0, with no fallback. EMBY_SERVER_URL and
JELLYFIN_SERVER_URL are now MEDIA_SERVER_URL; EMBY_API_KEY and
JELLYFIN_API_KEY are now MEDIA_SERVER_API_KEY. The old names are not read
at all, so leaving one in place is a boot error naming its replacement rather
than a silently ignored line. See the migration guide.
Jellyfin playback is HLS-only: HLS stream copy/remux/transcode are supported. Progressive/direct-file playback, Live TV, Jellyfin SyncPlay, and reuse of GPL Jellyfin Web code are out of scope. Browser receives opaque local HLS resource IDs; Jellyfin credentials remain server-side.
Skip Intro on Jellyfin needs detected intro segments. Watch Party reads
timings from Jellyfin's /MediaSegments/{item_id} API. A segment provider such
as Intro Skipper must detect them first; install a compatible plugin version
and let it analyse your library.
File Transformation is not required by Watch Party's segment lookup. It is an optional part of Intro Skipper's enhancements to Jellyfin's own web interface; see the plugin's settings.
When intro data is absent or unavailable, Skip Intro stays hidden and playback continues. On Emby the equivalent data comes from the server itself.
Use this only for local development over plain HTTP. Do not expose this permissive configuration to untrusted networks.
Production reverse-proxy example:
APP_ENV=production
BEHIND_PROXY=true
SESSION_SECRET=generate-one-stable-64-character-random-hex-value
SESSION_COOKIE_SECURE=true
CORS_ALLOWED_ORIGINS=https://watchparty.example.com
MEDIA_SERVER_TYPE=emby
MEDIA_SERVER_URL=http://emby:8096
MEDIA_SERVER_API_KEY=your-dedicated-emby-api-key
ENABLE_HLS_TOKEN_VALIDATION=true
TRUSTED_PROXY_CIDRS=172.16.0.0/12
If boot configuration becomes invalid, the process stays up and names the failing fields on stderr and in the log. Repair those deployment settings and restart.
.env
Boot-essential, restart required.
| Variable | Description | Default |
|---|---|---|
| Application | ||
WATCH_PARTY_BIND |
IP address to bind to. Supplied container artifacts pin this to 0.0.0.0. |
0.0.0.0 |
WATCH_PARTY_PORT |
Runtime listen port. Supplied container artifacts pin this to 5000; edit only the published host port. |
5000 |
APP_ENV |
development or strict startup-validated production mode |
development |
APP_PREFIX |
URL prefix for reverse proxy deployments (e.g. /watchparty) |
(empty) |
SESSION_EXPIRY |
Idle session-cookie and administrator-session lifetime in seconds; the default is 24 hours | 86400 |
| Session cookie (new in 2.0.0-beta18) | ||
SESSION_SECRET |
Stable cookie-signing key, required in production with at least 32 characters. Generate once with openssl rand -hex 32; an empty development value uses a new key on each boot. |
(generated in development) |
SESSION_COOKIE_SECURE |
When true, the session cookie carries the Secure flag (HTTPS-only). Set true in every deployment behind TLS; leave false for http://localhost dev. |
false |
| Socket.IO CORS (new in 2.0.0-beta18) | ||
CORS_ALLOWED_ORIGINS |
Comma-separated origin allowlist for the Socket.IO server (https://a.example.com,https://b.example.com). * accepts any origin (historical default). Pin to your real origin(s) in production. |
* |
BEHIND_PROXY |
Explicitly declare true behind a reverse proxy or false for direct access. Production requires a declaration. |
(unset) |
TRUSTED_PROXY_CIDRS |
Comma-separated proxy source CIDRs allowed to supply forwarding headers; required when BEHIND_PROXY=true. Trust only the actual proxy addresses. |
(empty) |
ENABLE_HLS_TOKEN_VALIDATION |
Restart-required stream-access protection. Production requires true. |
true |
| Media server | ||
MEDIA_SERVER_TYPE |
Explicit provider: emby or jellyfin. Not auto-detected; restart to change. |
emby |
MEDIA_SERVER_URL |
URL of the server named by MEDIA_SERVER_TYPE, reachable from the backend. |
http://localhost:8096 |
MEDIA_SERVER_API_KEY |
Administrative API key issued by that same server. Required in production. | (empty) |
EMBY_SERVER_URL, JELLYFIN_SERVER_URL, EMBY_API_KEY and JELLYFIN_API_KEY were retired in 3.0 and are never read; a leftover one fails the boot with the name of its replacement.
REQUIRE_LOGIN was previously here. It now lives in the admin panel as a runtime, hot-reloadable setting; see the host-provider authentication model in Architecture for the full semantics.
Admin panel
Runtime settings are editable at /admin. Most apply immediately. Changing file logging enablement, path, format or rotation size requires restart; log levels apply immediately. See the project wiki for a walkthrough.
Logging
| Setting | Description | Default |
|---|---|---|
| Log level | Application log verbosity (DEBUG, INFO, WARNING, ERROR) | INFO |
| Console log level | Terminal output verbosity | WARNING |
| Log to file | Write logs to disk; restart required after changing | true |
| Log file | Path to log file; restart required after changing | logs/emby-watchparty.log |
| Log format | rsyslog or standard; restart required after changing |
rsyslog |
| Max log size (MB) | Rotation threshold; restart required after changing | 10 |
Security
| Setting | Description | Default |
|---|---|---|
| Max users per party | 0 = unlimited | 0 |
| HLS token expiry (s) | Token lifetime | 86400 |
| Rate limiting | Enable API rate limiting | true |
| Party creation limit | Max per IP | 5 per hour |
| API rate limit | Max per IP | 1000 per minute |
Session
| Setting | Description | Default |
|---|---|---|
| Static session mode | Auto-create a fixed party on startup | false |
| Static session ID | Party code when static mode is enabled | PARTY |
Late join vote (new in 2.0)
| Setting | Description | Default |
|---|---|---|
| Enable late join vote | Require a majority vote to admit users who join mid-playback | true |
| Vote timeout (s) | Seconds before the selector tiebreak kicks in | 20 |
| Post-vote cooldown (s) | Delay after a failed vote before another join attempt is allowed (0 disables) | 30 |
Architecture
Backend (FastAPI + python-socketio)
- FastAPI: async REST API, dependency-injected components, Pydantic schemas for every request/response body (
backend/src/schemas.py). - python-socketio
AsyncServer(ASGI mode) mounted underAPP_PREFIX/socket.io: WebSocket-based real-time sync, hardened with a 128 KiB frame cap and tightened ping timings. - httpx: async media-server requests and HLS proxying; Emby and Jellyfin adapters keep upstream requests and credentials on the backend
- uvicorn ASGI worker, bound to
WATCH_PARTY_BIND:WATCH_PARTY_PORT. - HLS.js-compatible playlists: token-gated (
HLSTokenManager) short-lived URLs prevent segment scraping.
Frontend (Vue 3 + TypeScript)
- Vue 3 SPA authored in TypeScript, bundled with Vite (base
./for prefix-agnostic asset resolution). - Pinia stores for auth, party, playback, and socket state; Vue Router for
/party/:code,/admin, and landing routes. - socket.io-client for the sync channel, native fetch for REST, hls.js for adaptive playback.
Per-user transcode model
Each viewer has a separate media-server playback session. Personal audio, subtitle, quality and media-version changes replace that viewer's stream. Play, pause and seek update the shared party state; a seek while playing coordinates the room through a ready check. Disconnect cleanup releases the departed viewer's active stream when one exists.
Host-provider authentication
There is no application user database. Any party member can become host by authenticating with media-server credentials inside the party (POST /api/v2/auth/login). The access token remains server-side in memory. Host disconnect marks the host absent and locks library browsing immediately; the 5-second grace period allows ownership recovery before final host-left handling. A retained private host grant lets the same browser reclaim ownership while that party and grant remain valid.
- UNLOCKED - host present, all playback and library browsing allowed.
- PLAYING-ONLY - host absent during playback; the retained token keeps the current video available, including seeks and personal stream changes. Library browsing and selecting a new video are locked.
- LOCKED - host gone with no active video; token cleared, party waits for someone to log in as the new host.
Key components
PartyManager- party lifecycle, sid <-> client_id mapping, host state machine, ready-check and late-join vote bookkeeping.- Media-server providers - Emby and Jellyfin adapters for authentication, library operations, playback reporting and HLS resources.
EmbyClient- async wrapper for Emby's REST API, used by the Emby provider.HLSTokenManager- mints and validates short-lived per-user HLS URL tokens.StreamBuilder- assembles per-user transcode URLs, honouring theFORCE_TRANSCODEadmin toggle.AvatarStore- SQLite-backed avatar metadata plus a sibling images directory (both Docker-mountable).
Deployment
Ships as a single multi-stage Docker image: a Node stage runs npm run build to produce the Vue bundle, then a Python stage installs backend dependencies and copies the built static/ into the FastAPI app. One container, one port, one process - uvicorn serves the API, the SPA, and the WebSocket mount under the configurable APP_PREFIX.
Party state, standalone admin sessions, and rate-limit buckets are process-local. Run one uvicorn worker unless those stores are moved to shared infrastructure.
API endpoints
Emby Watch Party 3.0 exposes a FastAPI REST API and a Socket.IO event protocol. The canonical references are:
- REST / OpenAPI:
/docs(Swagger UI),/redocand/openapi.jsonon the running backend. These stay at the hostname root even whenAPP_PREFIXis set. - Socket.IO:
docs/SOCKET_API.md- every inbound / outbound event, payload shapes, and the multi-step flows.
REST API (representative endpoints)
Library, media metadata and host-authentication routes live under /api/v2, the surface used by the bundled web client. The legacy routes were removed in 3.0; the migration guide maps them to their replacements. Party, HLS, avatar and admin routes have their own prefixes. Examples:
v2-POST /api/v2/auth/login(media-server login, host-claim),GET /api/v2/libraries,POST /api/v2/items/query,GET /api/v2/items/{item_id}/streamsparty-POST /api/party/create,POST /api/party/{id}/join,GET /api/party/{id}/infohls-GET /hls/{item_id}/master.m3u8, segment and nested-playlist proxy passthroughquality-GET /api/quality-optionsavatar-POST /api/avatar/upload,GET /api/avatar/{avatar_uuid}admin-GET/PUT /api/admin/config,POST /api/admin/loginhealth-GET /api/health(liveness),GET /api/ready(media server reachable and credentials accepted),GET /api/version
See /docs for the full parameter, response, and auth-scope details.
WebSocket events (Socket.IO)
Mounted under ${APP_PREFIX}/socket.io. Events group into these categories:
- Connection lifecycle -
connect/disconnect/connected, transparent reconnect via stableclient_id. - Party join / leave / vote -
join_party,leave_party,user_joined/user_left, plusjoin_vote,join_vote_pending,join_vote_started,join_vote_update,join_vote_resolvedandjoin_rejected. - Host state and visibility -
host_changed,host_left,host_reclaimed,set_party_hiddenandparty_visibility_changed. - Playback -
select_video,retry_video_selection,cancel_video_selection,stop_video,change_streams(per-user),play,pause,seek,video_selection_started,video_selection_failed,video_selection_cancelled,video_selected,streams_changed,video_stopped,video_ended. - Sync coordination -
stream_ready,ready_check_update,all_ready,force_pause_before_seek,heartbeat+drift_correction,report_progress. - Chat / UI -
chat_message,toggle_library,update_avatar+members_update,sync_state. - Binge-watch / auto-advance -
set_binge_watch_active,binge_watch_state_changed,auto_advance_pending,auto_advance_cancel(led),auto_advance_fired,binge_finished. - Admin / errors -
party_dissolved,rate_limitedanderror.
Full payloads, auth requirements, and flow diagrams live in docs/SOCKET_API.md.
Troubleshooting
Common issues and fixes are catalogued on the wiki: Troubleshooting.
Quick checks before opening an issue:
- App unreachable. Confirm the backend is bound on
WATCH_PARTY_BIND/WATCH_PARTY_PORTand that clients can reach it. Behind a reverse proxy, verifyAPP_PREFIXmatches the mount path and that WebSocket upgrades are forwarded. - Can't browse or play media. The admin
MEDIA_SERVER_API_KEYin.envmust be valid for the server named byMEDIA_SERVER_TYPE, and that server (MEDIA_SERVER_URL) must be reachable from the backend. A key minted on the other server is a valid key against the wrong server and reads as "credentials rejected". To play, someone in the party has to click Login to Become Host and supply their own credentials - nothing plays until a host provider is attached. - Playback works, sync doesn't. Confirm the Socket.IO transport isn't being downgraded or blocked by an intermediate proxy, and that
CORS_ALLOWED_ORIGINSincludes your real origin. - Session cookie rejected / kicked out on every restart.
SESSION_SECRETmust be set to a stable value (openssl rand -hex 32). An unset secret regenerates on boot and invalidates every existing cookie; with--workers >1each worker signs differently. - Logs. File logging is enabled by default at
logs/emby-watchparty.log, with application levelINFO; console output defaults toWARNING. Change log levels live in /admin -> Logging. File-logging enablement, path, format and rotation changes need a restart.
Security notes
- Proxy architecture. The Emby or Jellyfin server stays on your internal network. The backend fetches and re-serves media resources, so viewers do not need direct access to that server.
- Credential model.
MEDIA_SERVER_API_KEYis the dedicated API key for the configured media server. Keep it andSESSION_SECRETout of version control. Do not commit.env.- Host credentials authenticate against the configured Emby or Jellyfin server. The resulting access token stays in server memory and is used for that party's media operations and per-viewer playback sessions.
- Standalone admin login also keeps its media-server token in an expiring server-side session. The browser cookie contains only an opaque handle.
- Production mode. Set
APP_ENV=production, declareBEHIND_PROXY, and supply trusted proxy CIDRs when needed. Startup rejects missing stable session keys, insecure cookies, wildcard CORS, disabled HLS token validation or missing media-server credentials. - Session secret and process model.
SESSION_SECRETmust remain stable across restarts. Run exactly one application process/worker; party state, admin sessions, limiters, background tasks, streams, and HLS tokens are intentionally in-memory and are not shared between workers. - HLS URLs. Since beta18, HLS playlist and segment URLs served to the browser no longer carry the admin
MEDIA_SERVER_API_KEYas a query parameter. Access is gated by a per-stream HLS token bound to the session cookie; the API key stays server-side. - Party codes are generated with cryptographically secure random tokens.
- Built-in controls. HLS token validation is on by default and is a restart-required boot setting,
ENABLE_HLS_TOKEN_VALIDATION. Bounded per-IP API/party-creation rate limiting and party size limits remain tunable in /admin -> Security. ConfigureTRUSTED_PROXY_CIDRSwhen a reverse proxy supplies client addresses. - For internet-facing deployments, terminate TLS at a reverse proxy (nginx, Caddy, Traefik), set
SESSION_COOKIE_SECURE=true, and pinCORS_ALLOWED_ORIGINSto your real origin(s) instead of*.
License
MIT License - feel free to modify and use as you wish.
Contributing
Contributions welcome. See CONTRIBUTING.md for branch conventions, PR flow, and coding guidelines. Issues and feature requests go in GitHub Issues.
Wiki edits are open - if you deploy on hardware or a platform not yet documented, please add your findings.
Acknowledgments
- Integrates with Emby Media Server and Jellyfin
- UI design: Refined Cyber by Christian Gillinger
- The community testers on Discord who lived through every beta and kept filing the bug reports that made 2.0 what it is
Educational use notice
This project is intended for educational purposes and private use only. Please use it responsibly and follow the terms and copyright rules that apply to your media server and content.
Requirements
Related apps
Explore more like this
Explore allDetails
ghcr.io/oratorian/emby-watchparty:3Runtime arguments
- Network
bridge- Shell
sh- Privileged
- false
Template configuration
Full public HTTPS URL for the Unraid Web UI launcher, for example https://watchparty.example.com or https://example.com/watchparty/. Include APP_PREFIX when used. The reverse proxy routes this URL to the host port below; the container itself serves HTTP. When retaining 2.x, use a different public hostname: ports and subpaths do not isolate session cookies.
- Target
- net.unraid.docker.webui
Published host port for Watch Party 3.x. Default 5001 avoids the 2.x template's 5000. Change this host value if needed; keep container port and WATCH_PARTY_PORT at 5000. Point your reverse proxy at this host port.
- Target
- 5000
- Default
- 5001
- Value
- 5001
Select the configured provider. MEDIA_SERVER_URL and MEDIA_SERVER_API_KEY must belong to that same Emby or Jellyfin server. Changing providers requires container recreation. Jellyfin playback uses HLS; progressive files, Live TV, and SyncPlay are outside the supported path.
- Target
- MEDIA_SERVER_TYPE
- Default
- emby|jellyfin
- Value
- emby
HTTP(S) media-server URL reachable from this container, for example http://192.168.1.100:8096. localhost means this container. A service hostname needs a shared Docker network with name resolution.
- Target
- MEDIA_SERVER_URL
Dedicated administrative API key issued by the selected media server. Host usernames/passwords are entered through Watch Party, not here. Masking hides this field in the editor; server administrators can still read container environment values.
- Target
- MEDIA_SERVER_API_KEY
Stable signing secret of at least 32 characters. Generate once with openssl rand -hex 32 and paste the result. Keep it across updates; changing it invalidates signed cookies. Do not reuse a placeholder or leave it blank.
- Target
- SESSION_SECRET
Production requires true and an HTTPS browser URL. HTTP between your TLS reverse proxy and this container is allowed. false is only for a separately configured development deployment.
- Target
- SESSION_COOKIE_SECURE
- Default
- true|false
- Value
- true
Comma-separated public origins, for example https://watchparty.example.com. Include scheme and any nondefault public port, without a path or trailing slash. For https://example.com/watchparty/ use https://example.com. Production rejects blank values and *.
- Target
- CORS_ALLOWED_ORIGINS
Declare whether a reverse proxy terminates viewer connections. Normally true for this HTTPS deployment, which requires TRUSTED_PROXY_CIDRS. Set false only for a genuinely direct topology; it does not remove production's HTTPS and secure-cookie requirements.
- Target
- BEHIND_PROXY
- Default
- true|false
- Value
- true
Required when Behind Reverse Proxy is true. Comma-separated CIDRs for the proxy's actual connecting address as Watch Party sees it, such as its Docker-network address. Use the narrowest relevant networks; do not trust all private networks or 0.0.0.0/0. Leave blank only for a direct deployment.
- Target
- TRUSTED_PROXY_CIDRS
Production enables strict startup checks for secrets, secure cookies, explicit origins, proxy topology, and HLS token validation. Keep production for this stable deployment.
- Target
- APP_ENV
- Default
- production
- Value
- production
Keep 0.0.0.0 inside the container so the published port and reverse proxy can reach it. This is not the Unraid host address.
- Target
- WATCH_PARTY_BIND
- Default
- 0.0.0.0
- Value
- 0.0.0.0
Keep the container-side port at 5000. Change only Web UI Host Port to select a different published host port.
- Target
- WATCH_PARTY_PORT
- Default
- 5000
- Value
- 5000
Production requires true. This is a restart-required boot setting in 3.x, no longer an admin toggle. An old disabled config.json value is overridden by this environment setting.
- Target
- ENABLE_HLS_TOKEN_VALIDATION
- Default
- true
- Value
- true
Optional path such as /watchparty. Leave blank for a dedicated hostname. The proxy must preserve this prefix; include it in Public HTTPS Web UI but not in Allowed Public Origins.
- Target
- APP_PREFIX
Signed session-cookie lifetime, default 86400 seconds (24 hours). Use 1209600 to retain the former 2.x 14-day cookie lifetime. Active party state does not survive a process restart.
- Target
- SESSION_EXPIRY
- Default
- 86400
- Value
- 86400
Writable application data, including the avatar identity database. Use separate storage from a running 2.x installation; copy a stopped backup only as part of a deliberate migration.
- Target
- /app/data
- Default
- /mnt/user/appdata/emby-watchparty-3/data
- Value
- /mnt/user/appdata/emby-watchparty-3/data
Persistent uploaded avatar files. Keep this paired with Application Data when backing up or migrating.
- Target
- /app/images/avatars
- Default
- /mnt/user/appdata/emby-watchparty-3/avatars
- Value
- /mnt/user/appdata/emby-watchparty-3/avatars
Writable runtime settings file. Before a fresh installation, create this host path as a FILE containing {} if it does not exist. Do not use touch or let Docker create a directory. Preserve an existing config.json when migrating.
- Target
- /app/config.json
- Default
- /mnt/user/appdata/emby-watchparty-3/config.json
- Value
- /mnt/user/appdata/emby-watchparty-3/config.json
Optional persistent application logs. File logging and console levels are managed in /admin.
- Target
- /app/logs
- Default
- /mnt/user/appdata/emby-watchparty-3/logs
- Value
- /mnt/user/appdata/emby-watchparty-3/logs