{"id":41943847,"url":"https://github.com/oratorian/emby-watchparty","last_synced_at":"2026-05-05T01:03:40.991Z","repository":{"id":319748404,"uuid":"1079485790","full_name":"Oratorian/emby-watchparty","owner":"Oratorian","description":"A synchronized watch party application for Emby media servers. Watch videos together with friends in real-time, no matter where you are!","archived":false,"fork":false,"pushed_at":"2026-02-27T05:09:42.000Z","size":217,"stargazers_count":21,"open_issues_count":5,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-27T11:52:33.950Z","etag":null,"topics":["chat-application","emby","emby-server","flask","flask-socketio","hls-streaming","html5-video","media-server","multi-user","open-source","python","real-time-sync","self-hosted","socketio","subtitle-support","synchronized-playback","video-streaming","watch-party","watch-together","websocket"],"latest_commit_sha":null,"homepage":"https://emby.media/community/index.php?/topic/143565-emby-watch-party-synchronized-viewing-for-friends-family/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Oratorian.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-19T22:31:36.000Z","updated_at":"2026-02-27T01:09:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"53229345-89ca-405a-99f2-6703df40e24f","html_url":"https://github.com/Oratorian/emby-watchparty","commit_stats":null,"previous_names":["oratorian/emby-watchparty"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/Oratorian/emby-watchparty","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Oratorian%2Femby-watchparty","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Oratorian%2Femby-watchparty/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Oratorian%2Femby-watchparty/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Oratorian%2Femby-watchparty/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Oratorian","download_url":"https://codeload.github.com/Oratorian/emby-watchparty/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Oratorian%2Femby-watchparty/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29962004,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T05:59:08.471Z","status":"ssl_error","status_checked_at":"2026-03-01T05:58:04.208Z","response_time":124,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chat-application","emby","emby-server","flask","flask-socketio","hls-streaming","html5-video","media-server","multi-user","open-source","python","real-time-sync","self-hosted","socketio","subtitle-support","synchronized-playback","video-streaming","watch-party","watch-together","websocket"],"created_at":"2026-01-25T19:12:38.507Z","updated_at":"2026-05-05T01:03:40.981Z","avatar_url":"https://github.com/Oratorian.png","language":"Python","funding_links":["https://ko-fi.com/jedziah"],"categories":[],"sub_categories":[],"readme":"# Emby Watch Party\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)\n[![Flask](https://img.shields.io/badge/flask-3.1.3-green.svg)](https://flask.palletsprojects.com/)\n[![GitHub release](https://img.shields.io/github/release/Oratorian/emby-watchparty.svg)](https://github.com/Oratorian/emby-watchparty/releases)\n[![GitHub stars](https://img.shields.io/github/stars/Oratorian/emby-watchparty.svg)](https://github.com/Oratorian/emby-watchparty/stargazers)\n\nA synchronized watch party application for Emby media servers. Watch videos together with friends in real-time, no matter where you are — no Emby account required for your guests.\n\n---\n\n### Support development\n\nIf Emby WatchParty has saved you from the hell of trying to coordinate \"3, 2, 1, play\" over Discord, consider buying me a coffee:\n\n☕ **[ko-fi.com/jedziah](https://ko-fi.com/jedziah)**\n\nEvery tip helps fund the hardware and the late nights reverse-engineering Emby's HLS pipeline.\n\n---\n\n### 🎉 Special Thanks\n\nSpecial thanks to **[QuackMasterDan](https://emby.media/community/index.php?/profile/1658172-quackmasterdan/)** for his dedication in testing and providing valuable feedback throughout development.\n\nThanks to **[wlowen](https://github.com/wlowen)** and **[JeslynMcKenzie](https://github.com/JeslynMcKenzie)** for testing, detailed bug reports, and providing mediainfo that helped track down the HEVC transcoding and HLS seeking issues.\n\n---\n\n### Discord for more personal support\n\nhttps://discord.gg/RWUpxq9xsA\n\n---\n\n## Documentation\n\n### Wiki\n\nThe [project wiki](https://github.com/Oratorian/emby-watchparty/wiki) has everything you need for deployment and operations:\n\n- **[Home](https://github.com/Oratorian/emby-watchparty/wiki/Home)** — start here\n- **[Hardware Requirements](https://github.com/Oratorian/emby-watchparty/wiki/Hardware-Requirements)** — how many users your hardware can actually handle, with real measurements from stress tests\n- **Deployment guides:**\n  - **[Docker Compose](https://github.com/Oratorian/emby-watchparty/wiki/Deployment-Docker-Compose)** — full stack including GPU passthrough per vendor (Intel QSV, NVIDIA NVENC, AMD VAAPI)\n  - **[Unraid](https://github.com/Oratorian/emby-watchparty/wiki/Deployment-Unraid)** — Extra Parameters, render group GID, Nvidia Driver plugin\n  - **[TrueNAS SCALE](https://github.com/Oratorian/emby-watchparty/wiki/Deployment-TrueNAS-SCALE)** — K8s and Fangtooth-Docker app configurations\n  - **[TrueNAS CORE](https://github.com/Oratorian/emby-watchparty/wiki/Deployment-TrueNAS-CORE)** — honest about FreeBSD's no-QSV/NVENC limitation and your options\n- **[Troubleshooting](https://github.com/Oratorian/emby-watchparty/wiki/Troubleshooting)** — seeking, CPU usage, network, container, and Emby-side issues by symptom\n- **[FAQ](https://github.com/Oratorian/emby-watchparty/wiki/FAQ)** — recurring questions\n\n### Technical deep dive: Emby quirks\n\n[Emby quirks we learned the hard way](docs/Emby%20quirks%20we%20learned%20the%20hard%20way.md) documents the undocumented Emby server behaviors discovered through reverse-engineering the HLS API and countless hours reading raw ffmpeg commands in server logs. Essential reading if you're building a third-party HLS client against Emby, or just curious why this app works the way it does.\n\nThe ten quirks covered:\n\n1. `VideoCodec=h264` does **not** force a transcode (Emby interprets it as \"client accepts h264\" and stream-copies)\n2. `EnableDirectPlay=false` and `EnableDirectStream=false` on PlaybackInfo are **advisory only** — the HLS endpoint makes its own decision\n3. `EnableAutoStreamCopy=false` is the actual flag that forces a real re-encode — used throughout this app\n4. Emby's `PlaybackInfo` does not expose **peak bitrate** — only average, making peak-based logic impossible\n5. `AutoOpenLiveStream=true` pre-starts ffmpeg so the first segment is ready faster\n6. Emby's own web client **restarts the entire pipeline on seek** — a trick HLS.js can't replicate\n7. HLS playlist segment durations **lie** when stream-copying — they're uniform on paper, irregular in reality\n8. The \"triple failsafe\" (`MaxStreamingBitrate` + `h264-profile` + `h264-level` + `TranscodeReasons`) does **not** prevent stream-copy\n9. Concurrent seeks on a shared `PlaySessionId` cause **ffmpeg race conditions** — the reason 2.0 moved to per-user transcodes\n10. The CPU cost of forced re-encoding is real on software, near-zero with Intel QSV / NVENC / VAAPI — with measured numbers on a 9900K + UHD 630\n\n### Changelog\n\n[CHANGELOG.md](CHANGELOG.md) — per-release details including every fix and the reasoning behind it.\n\n## Features\n\n- **Secure proxy architecture**: Emby server stays on your local network, never exposed to the internet\n- **HLS streaming**: HTTP Live Streaming with forced re-encoding for reliable seek behavior (see the quirks doc for why this matters)\n- **Real-time sync**: play, pause, and seek events propagate to every viewer instantly\n- **Library browsing**: browse your entire Emby library and pick videos to watch together\n- **Skip Intro button**: overlay button that appears during intro segments (uses Emby's chapter/intro data)\n- **Autoplay / binge mode**: optional auto-queue of the next episode when the current one ends, with a countdown and cancel option\n- **Quality presets**: pick from 360p up to 1080p-high (10 Mbps), or let the app decide\n- **Subtitle \u0026 audio support**: native HLS manifest subtitles (VTT), automatic image-subtitle burn-in (PGS/VobSub), per-party audio track selection\n- **Room system**: 5-character party codes, no accounts needed for guests\n- **Live chat**: built-in chat alongside the video\n- **Random usernames**: 554,400+ auto-generated usernames for users who don't set one\n- **Multiple users**: unlimited concurrent viewers per party (bounded by your Emby server's hardware — see the wiki)\n- **Static session mode**: optional single-party-always-open deployment for private groups\n- **Professional logging**: rsyslog-style logging with rotation\n\n## Browser compatibility\n\n### Desktop\n- ✅ **Chrome / Edge / Brave** — full support (recommended)\n- ✅ **Firefox** — full support\n- ✅ **Safari** — full support\n\n### Mobile\n- ✅ **Safari (iOS)** — full support including subtitles (recommended for iOS)\n- ✅ **Chrome (Android)** — full support (recommended for Android)\n- ⚠️ **Brave (iOS)** — video plays, but subtitles don't show in fullscreen. Use Safari on iOS if you need subtitles.\n\n## Setup\n\n### Prerequisites\n\n- Python 3.8 or higher\n- An Emby server (stays on your local/internal network)\n- **Emby API key AND username/password** — both are required. The API key is used for admin API calls (library browsing, item lookup), and the username/password are needed to get a stream access token (Emby only serves HLS to authenticated user sessions).\n- The Flask app must be reachable by your guests. If you can't port-forward, use Tailscale, ZeroTier, or a similar overlay VPN.\n- **Emby Premiere** is required on the Emby side if you want hardware-accelerated transcoding (QSV, NVENC, VAAPI). Without it, Emby transcodes in software only.\n\n### Option 1: Manual installation\n\n1. Install dependencies:\n```bash\npip install -r requirements.txt\n```\n\n2. Configure your settings — copy `.env.example` to `.env`:\n```bash\ncp .env.example .env\n```\n\n3. Edit `.env`:\n```env\n# Emby Server Configuration\nEMBY_SERVER_URL=http://your-emby-server:8096\nEMBY_API_KEY=your-api-key-here\nEMBY_USERNAME=your-emby-username\nEMBY_PASSWORD=your-emby-password\n\n# Application Configuration\nWATCH_PARTY_BIND=0.0.0.0\nWATCH_PARTY_PORT=5000\n```\n\n4. Run:\n```bash\npython run_production.py\n```\n\n5. Open your browser at `http://localhost:5000` (or your server's IP).\n\n### Option 2: Docker installation\n\nPull from GitHub Container Registry:\n```bash\ndocker pull ghcr.io/oratorian/emby-watchparty:latest\n```\n\nRun with a `.env` file:\n```bash\ndocker run -d \\\n  --name emby-watchparty \\\n  -p 5000:5000 \\\n  --env-file .env \\\n  ghcr.io/oratorian/emby-watchparty:latest\n```\n\nOr with inline environment variables:\n```bash\ndocker run -d \\\n  --name emby-watchparty \\\n  -p 5000:5000 \\\n  -e EMBY_SERVER_URL=http://your-emby-server:8096 \\\n  -e EMBY_API_KEY=your-api-key \\\n  -e EMBY_USERNAME=your-emby-username \\\n  -e EMBY_PASSWORD=your-emby-password \\\n  -e LOG_TO_FILE=false \\\n  ghcr.io/oratorian/emby-watchparty:latest\n```\n\n**Note:** for Docker deployments, set `LOG_TO_FILE=false` so logs go to stdout (readable via `docker logs`).\n\nFor Unraid, TrueNAS SCALE, and full-stack Docker Compose setups (including GPU passthrough for hardware-accelerated transcoding), see the **[deployment guides in the wiki](https://github.com/Oratorian/emby-watchparty/wiki)**.\n\n## Usage\n\n### Creating a watch party\n\n1. Click **\"Create Party\"** on the home page\n2. Share the 5-character party code with your friends\n3. Browse the Emby library and pick a video\n4. Everyone stays synchronized automatically\n\n### Joining a watch party\n\n1. Click **\"Join Watch Party\"** on the home page\n2. Enter the party code\n3. Enter a username (or leave blank for a random one)\n4. Start watching together\n\n### In-party controls\n\n- **Browse Library**: sidebar browser for your Emby libraries, movies, and TV shows\n- **Select Video**: click any video to start it for the whole party\n- **Video Controls**: any user can play, pause, or seek — sync happens automatically\n- **Skip Intro**: when the overlay appears, click to skip the intro segment\n- **Autoplay toggle**: enable \"Autoplay: ON\" to queue the next episode automatically when the current one ends (with a cancel option during the countdown)\n- **Quality selector**: pick a quality preset if automatic selection isn't what you want\n- **Chat**: bottom chat box for talking to other viewers\n- **Leave**: exit the party\n\n## Configuration\n\nAll configuration is via `.env`. Copy `.env.example` and customize:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| **Application** | | |\n| `WATCH_PARTY_BIND` | IP to bind to | `0.0.0.0` |\n| `WATCH_PARTY_PORT` | Port to listen on | `5000` |\n| `APP_PREFIX` | URL prefix for reverse proxy deployments (e.g. `/watchparty`) | (empty) |\n| `REQUIRE_LOGIN` | Require Emby login to access the web UI | `false` |\n| `SESSION_EXPIRY` | Session expiry in seconds | `86400` |\n| `STATIC_SESSION_ENABLED` | Keep a single persistent party alive across restarts | `false` |\n| `STATIC_SESSION_ID` | Fixed party ID when `STATIC_SESSION_ENABLED=true` | `PARTY` |\n| **Emby Server** | | |\n| `EMBY_SERVER_URL` | Your Emby server URL | `http://localhost:8096` |\n| `EMBY_API_KEY` | Emby API key | (required) |\n| `EMBY_USERNAME` | Emby username | (required) |\n| `EMBY_PASSWORD` | Emby password | (required) |\n| **Logging** | | |\n| `LOG_LEVEL` | DEBUG, INFO, WARNING, ERROR | `INFO` |\n| `LOG_TO_FILE` | Enable file logging | `true` |\n| `LOG_FILE` | Path to log file | `logs/emby-watchparty.log` |\n| `CONSOLE_LOG_LEVEL` | Console log level | `WARNING` |\n| `LOG_MAX_SIZE` | Max log file size in MB | `10` |\n| **Security** | | |\n| `MAX_USERS_PER_PARTY` | Max users per party (0 = unlimited) | `0` |\n| `ENABLE_HLS_TOKEN_VALIDATION` | Validate HLS stream tokens | `true` |\n| `HLS_TOKEN_EXPIRY` | HLS token expiry in seconds | `86400` |\n| `ENABLE_RATE_LIMITING` | Enable API rate limiting | `true` |\n| `RATE_LIMIT_PARTY_CREATION` | Max party creations per IP per hour | `5` |\n| `RATE_LIMIT_API_CALLS` | Max API calls per IP per minute | `1000` |\n\n## Architecture\n\n### Backend (Flask + SocketIO)\n\n- **Flask**: web server and REST API\n- **Flask-SocketIO**: WebSocket-based real-time sync between party members\n- **EmbyClient**: custom Emby API wrapper that handles authentication, playback info, HLS stream URL construction, and playback progress reporting\n- **HLS streaming**: every stream is **force-transcoded** (not stream-copied) via `EnableAutoStreamCopy=false` to guarantee reliable seeking with HLS.js. See the [quirks doc](docs/Emby%20quirks%20we%20learned%20the%20hard%20way.md) for the full backstory.\n\n### Frontend\n\n- **Vanilla JavaScript**: no frameworks, no build step\n- **Socket.IO client**: real-time bidirectional sync\n- **HLS.js**: adaptive-bitrate HLS player with buffering and error recovery\n- **HTML5 Video**: native player with custom overlay controls\n\n### Key concepts\n\n**Watch party rooms** — each room tracks connected users, the selected video, and playback state (playing/paused, current time, last update timestamp).\n\n**Synchronization** — play/pause/seek events are broadcast to all users in the room via WebSocket. The server authoritatively tracks party clock state; clients reconcile their local `video.currentTime` against the server's projection.\n\n**Transcoding** — the Emby server does all video work. WatchParty just proxies HLS segments and tells Emby exactly how to transcode (quality, subtitles, audio track). Forced re-encoding is required for HLS.js seek reliability and is non-negotiable on this branch.\n\n**Authentication** — WatchParty authenticates with Emby using the configured username/password to obtain an AccessToken, then uses that token for HLS streaming. The API key is used separately for metadata calls. All guest traffic is proxied through the Flask app so your Emby server never sees guest IPs.\n\n## API endpoints\n\n### REST API\n\n- `GET /` — home page\n- `GET /party/\u003cparty_id\u003e` — watch party room page\n- `GET /api/libraries` — list media libraries\n- `GET /api/items?parentId=\u003cid\u003e\u0026type=\u003ctype\u003e\u0026recursive=\u003cbool\u003e` — list items in a library\n- `GET /api/item/\u003citem_id\u003e` — item details\n- `GET /api/stream/\u003citem_id\u003e` — get a video stream URL for a party\n- `POST /api/party/create` — create a new watch party\n- `GET /api/party/\u003cparty_id\u003e/info` — party state\n\n### WebSocket events\n\n**Client → Server:**\n- `join_party` — join a room\n- `leave_party` — leave a room\n- `select_video` — pick a video for the party\n- `play` / `pause` / `seek` — playback control\n- `change_quality` — switch quality preset\n- `change_audio` / `change_subtitle` — switch tracks (party-wide in 1.x)\n- `chat_message` — send chat\n- `autoplay_toggle` — toggle binge mode\n\n**Server → Client:**\n- `connected` — connection established\n- `user_joined` / `user_left` — room membership change\n- `sync_state` — playback state sync (authoritative)\n- `video_selected` — a new video started\n- `play` / `pause` / `seek` — control events from other users\n- `chat_message` — chat broadcast\n- `intro_data` — skip-intro chapter/timestamp info\n- `autoplay_countdown` — next-episode autoplay UI\n- `error` — error with context\n\n## Troubleshooting\n\nSee the **[Troubleshooting wiki page](https://github.com/Oratorian/emby-watchparty/wiki/Troubleshooting)** for detailed diagnosis of seeking, CPU usage, networking, and configuration issues.\n\nQuick checks:\n\n**Videos won't play**\n- Check `logs/emby-watchparty.log` for authentication or proxy errors\n- Verify `EMBY_SERVER_URL`, `EMBY_API_KEY`, `EMBY_USERNAME`, `EMBY_PASSWORD` are correct\n- Confirm the Emby user account has library access permissions\n- Verify the Flask app is reachable from guest browsers\n\n**Seeking is broken, jumps to wrong scenes, or 404s on segments**\n- Update to v1.6.3 or later — the `EnableAutoStreamCopy=false` fix is required\n\n**CPU at 100% with a single user**\n- Enable hardware acceleration in Emby (Settings → Transcoding → Hardware acceleration) and verify the container has GPU access. See the [wiki hardware guide](https://github.com/Oratorian/emby-watchparty/wiki/Hardware-Requirements).\n\n**Sync drifts or one user keeps buffering**\n- Check that user's network speed and device CPU usage\n- Try refreshing the page\n- Confirm WebSocket traffic isn't being dropped by a firewall or reverse proxy\n\n## Security notes\n\n- **Proxy architecture**: Emby stays on your local network. Only the Flask app is reachable by guests.\n- Credentials live in `.env` — **do not commit this to public repos**.\n- Party codes are generated using cryptographically secure random tokens.\n- HLS tokens (optional) prevent direct stream URL sharing outside the party.\n- AccessTokens are obtained at runtime and not stored persistently.\n- Built-in security features:\n  - HLS token validation (prevents direct stream access bypass)\n  - Rate limiting on API endpoints and party creation\n  - Configurable per-party user limits\n- For production, add:\n  - HTTPS via reverse proxy (Caddy, Nginx, Traefik)\n  - A VPN or overlay network if guests aren't on the public internet\n\n## License\n\nMIT License — feel free to modify and use as you wish.\n\n## Contributing\n\nContributions welcome. Fork, branch, PR against `dev`. Issues and feature requests go in GitHub Issues.\n\nWiki edits are open — if you deploy on hardware or a platform not yet documented, please add your findings.\n\n## Acknowledgments\n\n- Built on Flask, Flask-SocketIO, and HLS.js\n- Integrates with [Emby Media Server](https://emby.media/)\n- Inspired by various watch-party applications that didn't quite do what we needed\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foratorian%2Femby-watchparty","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foratorian%2Femby-watchparty","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foratorian%2Femby-watchparty/lists"}