Emby Server plugin: watch party feature (session sync via Emby's remote-control commands) + companion web control panel
Find a file
Nicolas 1891e87c83
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
Fix streaming: use Emby's PlaybackInfo instead of a hand-built m3u8 URL
Real error from the field: "Value cannot be null. (Parameter 'key')" -
Emby's own server (.NET) throwing an ArgumentNullException. The
/api/catalog/stream/:itemId/master.m3u8 route was hand-building
/Videos/{id}/master.m3u8?VideoCodec=h264&AudioCodec=aac directly,
missing whatever parameter (MediaSourceId/PlaySessionId/DeviceId are
the likely culprits) Emby's server needs for an internal lookup.

Fixed by calling GET /Items/{id}/PlaybackInfo?UserId=... first and
using the returned MediaSource's own TranscodingUrl - the same thing
real Emby clients do, rather than guessing the URL shape ourselves.
Falls back to a hand-built URL only if no TranscodingUrl comes back,
now at least carrying MediaSourceId/PlaySessionId/UserId from that
response instead of nothing.

docs/architecture.md updated: the real bug and fix, plus a known
limitation this doesn't yet handle (a direct-play source with no
TranscodingUrl at all - hls.js has nothing to fall back to for that
case yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:59:54 +02:00
.forgejo/workflows Add Forgejo Actions pipeline to build/publish the Docker image 2026-09-13 20:59:55 +02:00
.github/workflows Scaffold Emby Watch Party plugin 2026-09-13 14:02:43 +02:00
client Redesign home page: library tabs, poster tiles, ongoing parties list 2026-09-13 21:54:14 +02:00
docs Fix streaming: use Emby's PlaybackInfo instead of a hand-built m3u8 URL 2026-09-13 21:59:54 +02:00
server Fix streaming: use Emby's PlaybackInfo instead of a hand-built m3u8 URL 2026-09-13 21:59:54 +02:00
src/WatchParty Fix the real crash: partyPage.html was missing data-controller 2026-09-13 20:17:14 +02:00
.gitignore Untrack .claude/ (local tooling config, not project source) 2026-09-13 20:53:31 +02:00
docker-compose.yml Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +02:00
LICENSE Scaffold Emby Watch Party plugin 2026-09-13 14:02:43 +02:00
README.md Proxy all Emby access through our own backend; drop server-url field; use Emby's real theme colors 2026-09-13 21:36:26 +02:00

Watch Party for Emby

A standalone web app for watching the same title, in sync, with other users of the same Emby server. It reads your Emby catalog and streams your Emby media, but it's its own app with its own player — not an Emby plugin.

See docs/architecture.md for the full design, including why this replaced an earlier Emby-plugin attempt (still in src/WatchParty/ for reference — see below).

How it works

  1. Sign in with your Emby account (just username/password — the server address is fixed server-side, see Deploying below).
  2. Search your Emby catalog for a title.
  3. Start a party (you become the host) or join one with a 6-character code.
  4. The host's play/pause/seek drives everyone else's player automatically.

Everything — catalog, images, video, sync — flows through this app's own backend, which proxies whatever it needs from Emby server-side. The browser never talks to Emby directly and never holds an Emby access token; it only ever has a session cookie for this app.

Status

Login has been exercised end to end against the real Emby server (with intentionally-wrong credentials, to confirm the round trip without needing real ones) — see docs/architecture.md for exactly what that confirms and what's still unverified beyond it (mainly: HLS playback once actually logged in).

Project layout

server/           Node.js backend: proxies ALL Emby access (auth, catalog,
                  images, HLS streaming) and coordinates party/room state +
                  realtime sync. The browser only ever talks to this.
  src/index.js       Express app + REST endpoints
  src/embyAuth.js    server-side login against Emby + session cookies
  src/embyProxy.js   generic Emby request/response proxying (incl. HLS)
  src/wsHandler.js   WebSocket protocol (see comments in the file)
  src/partyManager.js  in-memory room state

client/           Static frontend (plain JS, ES modules, no build step).
                  Talks only to server/ - never to Emby directly.
  js/embyApi.js      calls our own /api/* routes (auth, search, item, stream URL)
  js/party.js        our own backend's party REST + WebSocket client
  js/views/          one file per screen (login, browse, item, player)

src/WatchParty/   The earlier Emby-plugin attempt. Builds fine and installs,
                  but Emby has no supported extension point for a player
                  button, and even menu integration required reverse-
                  engineering undocumented internals - see
                  docs/architecture.md for the full story. Kept for
                  reference; not the active direction.

docs/architecture.md   full design doc for the standalone app

Running locally

cd server
npm install
EMBY_SERVER_URL=https://your-emby-server npm start

Then open http://localhost:3000.

Deploying

Build the Docker image from the repo root (it needs both server/ and client/):

docker build -f server/Dockerfile -t watchparty .
docker run -d --name watchparty -p 3000:3000 -e EMBY_SERVER_URL=https://theater.media.kyuno.fr watchparty

Then put it behind your existing reverse proxy like everything else (theater.media.kyuno.fr, git.dev.kyuno.fr, ...), e.g. watchparty.kyuno.fr → http://<container>:3000.

Roadmap

  • Design + scaffold the standalone app
  • Smoke-test locally (routing, error handling) without a real Emby server
  • Verify against the real Emby server (CORS is the main risk - see above)
  • Try a full party end-to-end with two accounts
  • TV show navigation (seasons/episodes) beyond flat search
  • Tune transcoding target / support direct play when possible

No chat, no OIDC — deliberately out of scope (Emby-only auth was the explicit choice; see docs/architecture.md).

License

MIT — see LICENSE.