Fix streaming: use Emby's PlaybackInfo instead of a hand-built m3u8 URL
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
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>
This commit is contained in:
parent
07613c1cec
commit
1891e87c83
2 changed files with 56 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue