diff --git a/docs/architecture.md b/docs/architecture.md index e98a4a0..606225b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -152,11 +152,20 @@ point is confirmed: - **`/Users/{userId}` for token validation** was dropped — the proxy redesign no longer needs it at all (sessions are validated once at login and cached server-side, not re-checked against Emby per-request). -- **`/Videos/{id}/master.m3u8`** for HLS playback, with - `VideoCodec=h264&AudioCodec=aac` forcing a broadly-compatible transcode - target. Works for basically anything Emby can transcode, but is never the - most efficient path (no direct play, no bitrate/quality negotiation) — - fine for v1, worth revisiting once the basic flow works. +- **Streaming**: the first version hand-built + `/Videos/{id}/master.m3u8?VideoCodec=h264&AudioCodec=aac` directly, and it + broke in the field: Emby's server threw `ArgumentNullException: Value + cannot be null. (Parameter 'key')` — almost certainly an internal + dictionary keyed by something that URL never supplied (`MediaSourceId`, + `PlaySessionId`, `DeviceId` are the usual suspects). Fixed by calling + `GET /Items/{id}/PlaybackInfo?UserId=...` first and using the + `MediaSource`'s own `TranscodingUrl` — the same thing real Emby clients do + — falling back to a hand-built URL (now at least carrying + `MediaSourceId`/`PlaySessionId`/`UserId` from that response) only if no + `TranscodingUrl` comes back. Not yet confirmed against a real response: + whether `TranscodingUrl` is reliably present, and whether it's always HLS + (a direct-play-eligible source might not transcode at all, which `hls.js` + on the client wouldn't handle - see Known limitations). - **HLS playlist rewriting** (`rewritePlaylist` in `embyProxy.js`) — the logic is straightforward (rewrite every non-`#` line) but has only been reasoned about, never run against a playlist Emby actually generated. If @@ -211,8 +220,12 @@ response shapes for `/Views` and library items are not (see below). ## Known limitations -- Search-only catalog (see Non-goals) — no way to browse a show's episode - list yet. +- Library tabs show top-level items only — no drilling into a Series' + seasons/episodes yet (a Series tile links out to Emby instead, see above). +- Playback always goes through `hls.js` against whatever Emby's + `PlaybackInfo` suggests. If that ever comes back as a direct-play source + with no `TranscodingUrl` (no transcoding needed/possible), the client has + no fallback path yet - it'll try to hand hls.js something that isn't HLS. - One item per party; no shared queue. - A party member needs a browser tab open and connected for sync to apply — there's no "catch up automatically when you come back" beyond whatever diff --git a/server/src/index.js b/server/src/index.js index cbdd289..934b736 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -154,8 +154,42 @@ app.get('/api/catalog/image/:itemId', async (req, res) => { app.get('/api/catalog/stream/:itemId/master.m3u8', async (req, res) => { const session = sessionFromRequest(req); if (!session) { res.status(401).end(); return; } - const params = new URLSearchParams({ VideoCodec: 'h264', AudioCodec: 'aac' }); - await proxyEmbyResponse(req, res, `/Videos/${req.params.itemId}/master.m3u8?${params}`, session.embyToken, req.params.itemId); + const itemId = req.params.itemId; + + // Ask Emby how it wants this played rather than hand-building a + // /Videos/{id}/master.m3u8 URL ourselves: the first version did that with + // only VideoCodec/AudioCodec, and Emby's server threw "Value cannot be + // null (Parameter 'key')" - almost certainly an internal dictionary + // lookup (MediaSourceId/PlaySessionId/DeviceId) that URL never supplied. + // PlaybackInfo returns a MediaSource with its own ready-to-use + // TranscodingUrl carrying whatever Emby actually needs. + const infoRes = await embyFetch(`/Items/${itemId}/PlaybackInfo?UserId=${session.userId}`, session.embyToken); + if (!infoRes.ok) { + res.status(502).json({ error: 'Impossible de préparer la lecture de ce titre.' }); + return; + } + const info = await infoRes.json(); + const source = (info.MediaSources || [])[0]; + if (!source) { + res.status(502).json({ error: 'Aucune source de lecture disponible pour ce titre.' }); + return; + } + + // TODO(verify): TranscodingUrl not confirmed present for every source + // type/Emby version (e.g. a source Emby considers direct-playable may not + // have one at all) - the hand-built fallback below now at least supplies + // the parameters PlaybackInfo reveals (MediaSourceId, PlaySessionId, + // UserId) instead of guessing, but is still a fallback, not the primary path. + const target = source.TranscodingUrl + || `/Videos/${itemId}/master.m3u8?${new URLSearchParams({ + UserId: session.userId, + MediaSourceId: source.Id || '', + PlaySessionId: info.PlaySessionId || '', + VideoCodec: 'h264', + AudioCodec: 'aac' + })}`; + + await proxyEmbyResponse(req, res, target, session.embyToken, itemId); }); // Everything an HLS playlist references (variant playlists, segments) comes