Universal audio ignores transcodingProtocol=hls and answers with audio instead of a playlist #186

Closed
opened 2026-08-22 22:13:13 +02:00 by Claude · 1 comment
Collaborator

GET /Audio/{itemId}/universal?transcodingProtocol=hls asks for a playlist.
Upstream answers with one; we answer with raw audio.

Upstream's route
(upstream/Jellyfin.Api/Controllers/UniversalAudioController.cs:182-229) builds
an HlsAudioRequestDto and returns DynamicHlsHelper.GetMasterHlsPlaylist,
Content-Type: application/x-mpegURL. The segment container is restricted to
ts or mp4 — anything else the client names falls back to ts — and the
audio codec becomes copy where the only reason to transcode was
ContainerNotSupported.

We do not read transcodingProtocol at all
(crates/ephyra-http/src/routes/streaming/audio.rs), so every non-direct-play
verdict takes the progressive branch and answers 200 with
Content-Type: audio/mpeg and a body of audio bytes. A client that asked for a
playlist gets something it cannot parse.

The default is http
(UniversalAudioController.cs:298, Protocol = transcodingProtocol ?? http),
so the common path is right and this is the explicit-request case. The master
playlist itself is already written — crates/ephyra-http/src/routes/streaming/hls.rs
serves /Audio/{itemId}/master.m3u8 — so this is mostly a matter of reading the
parameter and handing the request to it with the fields upstream fills in.

How to know it is done

A request naming transcodingProtocol=hls comes back as a master playlist, and
the harness agrees with upstream on its body. The progressive default is
unchanged.

House rules

Tests run under cargo nextest; upstream's source is the checkout in
upstream/ at the pin.

`GET /Audio/{itemId}/universal?transcodingProtocol=hls` asks for a playlist. Upstream answers with one; we answer with raw audio. Upstream's route (`upstream/Jellyfin.Api/Controllers/UniversalAudioController.cs:182-229`) builds an `HlsAudioRequestDto` and returns `DynamicHlsHelper.GetMasterHlsPlaylist`, `Content-Type: application/x-mpegURL`. The segment container is restricted to `ts` or `mp4` — anything else the client names falls back to `ts` — and the audio codec becomes `copy` where the only reason to transcode was `ContainerNotSupported`. We do not read `transcodingProtocol` at all (`crates/ephyra-http/src/routes/streaming/audio.rs`), so every non-direct-play verdict takes the progressive branch and answers `200` with `Content-Type: audio/mpeg` and a body of audio bytes. A client that asked for a playlist gets something it cannot parse. The default is `http` (`UniversalAudioController.cs:298`, `Protocol = transcodingProtocol ?? http`), so the common path is right and this is the explicit-request case. The master playlist itself is already written — `crates/ephyra-http/src/routes/streaming/hls.rs` serves `/Audio/{itemId}/master.m3u8` — so this is mostly a matter of reading the parameter and handing the request to it with the fields upstream fills in. ## How to know it is done A request naming `transcodingProtocol=hls` comes back as a master playlist, and the harness agrees with upstream on its body. The progressive default is unchanged. ### House rules Tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**.
Author
Collaborator

Done in 879ee69, with dc9354e after a review.

The route reads transcodingProtocol. hls and not a direct play is answered
with a master playlist from the routes that already serve one: the segments cut
into ts or mp4 — the request's container where it is one of those, else the
one the negotiation settled, else ts — and the audio codec copy where the
container was the whole complaint. The variant URL carries the request's own
query with SegmentContainer and TranscodeReasons appended where the client
did not send them.

One thing that came with it: the variant URL names the codec the encoder will
really produce rather than the one the client asked for. Upstream rewrites that
in DynamicHlsHelper for every master playlist, so ours had been getting it
wrong on the video route too — a request for audioCodec=aac,mp3 named the
whole list where upstream names aac.

Measured

Both servers over the anchor. Before: upstream 200 application/vnd.apple.mpegurl
against ours 500. After, byte for byte, 254–344 bytes each:

/Audio/{flac}/universal?container=mp3&transcodingProtocol=hls&transcodingContainer=ts&audioCodec=aac
  #EXT-X-STREAM-INF:BANDWIDTH=256000,AVERAGE-BANDWIDTH=256000,CODECS="mp4a.40.2"
  main.m3u8?…&SegmentContainer=ts&TranscodeReasons=ContainerNotSupported, VideoCodecNotSupported, AudioCodecNotSupported

/Audio/{flac}/universal?container=webm|flac&transcodingProtocol=hls&transcodingContainer=mp4
  #EXT-X-STREAM-INF:BANDWIDTH=256000,AVERAGE-BANDWIDTH=256000,CODECS="fLaC"
  main.m3u8?…&AudioCodec=copy&SegmentContainer=mp4&TranscodeReasons=ContainerNotSupported

and two video master.m3u8 shapes, which the codec rewrite now also reaches,
agree the same way.

One shape still disagrees, and it is not this issue's: a request naming no
audioCodec at all. Upstream infers one from the request path — which carries
no extension here, so what it infers is the path — and ours never passes a path
to the encoder to infer from. Filed as #189.

Done in `879ee69`, with `dc9354e` after a review. The route reads `transcodingProtocol`. `hls` and not a direct play is answered with a master playlist from the routes that already serve one: the segments cut into `ts` or `mp4` — the request's container where it is one of those, else the one the negotiation settled, else `ts` — and the audio codec `copy` where the container was the whole complaint. The variant URL carries the request's own query with `SegmentContainer` and `TranscodeReasons` appended where the client did not send them. One thing that came with it: the variant URL names the codec the encoder will really produce rather than the one the client asked for. Upstream rewrites that in `DynamicHlsHelper` for **every** master playlist, so ours had been getting it wrong on the video route too — a request for `audioCodec=aac,mp3` named the whole list where upstream names `aac`. ## Measured Both servers over the anchor. Before: `upstream 200 application/vnd.apple.mpegurl` against `ours 500`. After, byte for byte, 254–344 bytes each: ``` /Audio/{flac}/universal?container=mp3&transcodingProtocol=hls&transcodingContainer=ts&audioCodec=aac #EXT-X-STREAM-INF:BANDWIDTH=256000,AVERAGE-BANDWIDTH=256000,CODECS="mp4a.40.2" main.m3u8?…&SegmentContainer=ts&TranscodeReasons=ContainerNotSupported, VideoCodecNotSupported, AudioCodecNotSupported /Audio/{flac}/universal?container=webm|flac&transcodingProtocol=hls&transcodingContainer=mp4 #EXT-X-STREAM-INF:BANDWIDTH=256000,AVERAGE-BANDWIDTH=256000,CODECS="fLaC" main.m3u8?…&AudioCodec=copy&SegmentContainer=mp4&TranscodeReasons=ContainerNotSupported ``` and two video `master.m3u8` shapes, which the codec rewrite now also reaches, agree the same way. **One shape still disagrees, and it is not this issue's**: a request naming no `audioCodec` at all. Upstream infers one from the request path — which carries no extension here, so what it infers is the path — and ours never passes a path to the encoder to infer from. Filed as #189.
Sign in to join this conversation.
No description provided.