diff --git a/README.md b/README.md index 33b28fa..1d31258 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ including why this replaced an earlier Emby-plugin attempt (still in just hit "Épisode suivant" instead of restarting a party each time. If the host leaves, the party hands off to another member automatically (their player unlocks live, no reload needed) — only closing once - everyone's gone. + everyone's gone. Everyone also shares the exact same stream from + Emby — one transcode per party, not one per person — so the host's + language/quality pick applies for the whole party too. 5. Everyone present shows up as a small avatar overlay on the video (their initials, on the player itself). If you're not the host, a small toolbar lets you ask the host to pause or resume — everyone sees the @@ -37,8 +39,8 @@ including why this replaced an earlier Emby-plugin attempt (still in 6. Subtitles render natively in the browser for any text-based track. A Blu-ray/DVD-style image subtitle (PGS/VobSub/DVB) is different — the host can pick one to have it burned into the video by this app's own - ffmpeg, shared live by everyone in the party (audio/quality selection - is fixed while that's active, and it restarts the stream for + ffmpeg, shared live by everyone in the party (picking one restarts + the stream for everyone, so it's a host-only choice, like play/pause already is). 7. Music works differently, on purpose: pick a track (browsing Artist → Album → Track) to start a listening party. There's no host for @@ -104,7 +106,13 @@ thing to double-check here too. Same again for the Emby-playstate sync (the exact `PlaystateService` endpoint/body shape) and the Playlists endpoint - three Emby-API assumptions total now waiting on a real server to actually confirm, all otherwise built and unit-tested the -same way. +same way. The one-shared-stream-per-party work is a different kind of +unverified: its own Emby API calls were already live, confirmed code, +just newly shared instead of per-viewer - what's untested is behavior +under real multi-viewer load (whether a dead Emby session actually +surfaces as an HTTP failure the way the cache-invalidation logic +assumes, how a real transcode's seek handling feels once several +viewers depend on the exact same one). ## Project layout @@ -221,7 +229,9 @@ Then put it behind your existing reverse proxy like everything else - [x] Design + scaffold the standalone app - [x] Proxy all Emby access server-side; Emby-themed UI (sidebar, carousels) - [x] TV show navigation (Series → Seasons → Episodes) + episode auto-queue -- [x] Player language/subtitle/quality pickers +- [x] Player language/subtitle/quality pickers (language/quality are now + host-only, party-wide choices — see docs/architecture.md's "One + shared Emby stream per party") - [x] Socket.IO instead of raw WebSockets; fixed two real memory leaks (dead-connection cleanup server-side, view teardown on navigation client-side) — see docs/architecture.md @@ -251,6 +261,8 @@ Then put it behind your existing reverse proxy like everything else queue or the participative music queue) — see docs/architecture.md - [x] Sync watch-party progress back to Emby's own PlaystateService, per member's own account — see docs/architecture.md +- [x] One shared Emby stream per party instead of one per viewer — see + docs/architecture.md - [ ] Try a full party end-to-end with two real accounts (still the main gap) - [ ] Tune transcoding target / support direct play when possible diff --git a/client/src/api/embyApi.ts b/client/src/api/embyApi.ts index 11389b6..13d71c8 100644 --- a/client/src/api/embyApi.ts +++ b/client/src/api/embyApi.ts @@ -116,21 +116,19 @@ export function getPlaybackOptions(itemId: string): Promise return apiFetch(`/api/catalog/item/${itemId}/playback-options`); } -/** `options` may carry AudioStreamIndex/MaxStreamingBitrate/mediaType - not - * subtitles, which are their own separate request now (see subtitleUrl - * below and Player.ts's applySubtitleTrack for why). */ -export function streamUrl(itemId: string, options: Record = {}): string { - const entries = Object.entries(options).filter(([, v]) => v !== '' && v != null) as [string, string][]; - const qs = new URLSearchParams(entries).toString(); - return `/api/catalog/stream/${itemId}/master.m3u8${qs ? '?' + qs : ''}`; -} - -/** Same stream, tagged for a music track - see server/src/services/ - * CatalogService.ts's proxyStream for why mediaType is the one thing - * that matters here (its hand-built-fallback path is genuinely different - * for audio). */ -export function audioStreamUrl(itemId: string): string { - return streamUrl(itemId, { mediaType: 'audio' }); +/** The party's own shared Emby stream (server/src/services/ + * PartyStreamService.ts) - one negotiated PlaybackInfo session reused + * by every member, instead of each viewer's own