The audio negotiation never answers DirectStream, so a remux picks the wrong container #187

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

The audio negotiation only ever answers DirectPlay or nothing
(crates/ephyra-app/src/streaming/playback/verdict.rs:260-313). Upstream has a
third answer for audio, and reaches it often.

StreamBuilder.BuildAudioItem
(upstream/MediaBrowser.Model/Dlna/StreamBuilder.cs:483-500) returns
DirectStream whenever the only reason direct play failed was
ContainerNotSupported — a codec the client takes, in a container it does not.
It does not honour EnableDirectStream=false for audio, so the branch is not
one a client can decline.

Two things follow that we get wrong:

The container. StreamBuilder.cs:157 sets
item.TranscodingContainer = remuxContainer — the matched direct-play
profile's container where that is ts or mp4, and transcodingContainer ?? "ts" otherwise. /Audio/{id}/universal then transcodes into that, not into
the transcoding profile's container. So a codec-compatible, container-
incompatible source asked for with no transcodingContainer is .ts upstream
and .mp3 here.

What the client is told. PlaybackInfo reports the play method for a source
it cannot hand over directly; ours never says DirectStream for audio.

jellyfin-web masks the first half by always sending transcodingContainer,
which is why this has not shown up in the click-through.

How to know it is done

A source whose codec a profile takes and whose container it does not is
negotiated as DirectStream, and the harness agrees with upstream on both the
PlaybackInfo body and the container /universal picks for it.

House rules

Tests run under cargo nextest; upstream's source is the checkout in
upstream/ at the pin. Anything touching the negotiation gets reviewed.

The audio negotiation only ever answers `DirectPlay` or nothing (`crates/ephyra-app/src/streaming/playback/verdict.rs:260-313`). Upstream has a third answer for audio, and reaches it often. `StreamBuilder.BuildAudioItem` (`upstream/MediaBrowser.Model/Dlna/StreamBuilder.cs:483-500`) returns `DirectStream` whenever the *only* reason direct play failed was `ContainerNotSupported` — a codec the client takes, in a container it does not. It does not honour `EnableDirectStream=false` for audio, so the branch is not one a client can decline. Two things follow that we get wrong: **The container.** `StreamBuilder.cs:157` sets `item.TranscodingContainer = remuxContainer` — the matched direct-play profile's container where that is `ts` or `mp4`, and `transcodingContainer ?? "ts"` otherwise. `/Audio/{id}/universal` then transcodes into *that*, not into the transcoding profile's container. So a codec-compatible, container- incompatible source asked for with no `transcodingContainer` is `.ts` upstream and `.mp3` here. **What the client is told.** `PlaybackInfo` reports the play method for a source it cannot hand over directly; ours never says `DirectStream` for audio. jellyfin-web masks the first half by always sending `transcodingContainer`, which is why this has not shown up in the click-through. ## How to know it is done A source whose codec a profile takes and whose container it does not is negotiated as `DirectStream`, and the harness agrees with upstream on both the `PlaybackInfo` body and the container `/universal` picks for it. ### House rules Tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**. Anything touching the negotiation gets reviewed.
Author
Collaborator

Done in 879ee69, with dc9354e after a review.

audio_verdict now answers DirectStream where the container was the only
complaint, in upstream's own shape: the direct-play profile search, then a
profile that names the codec in a container this file is not in, then the
bitrate check on each branch, then the exactly-ContainerNotSupported gate.
The container it hands over in is item.TranscodingContainer ?? "ts", overridden
by the matched profile's where that is one a segmented stream can carry, and the
HLS codec restriction is applied where the request asked for segments.

One thing came with it that the issue did not name: a file whose container no
profile takes is blamed for its codecs as well — the video one included,
which a music file has none of. That is upstream's
GetTranscodeReasonsFromDirectPlayProfile, and it is what a transcoding URL
carries.

Measured

Both servers over the anchor, POST /Items/{flac}/PlaybackInfo with a profile
that takes flac in webm, before → after:

                        upstream          ours (before)      ours (after)
TranscodingContainer    ts                mp3                ts
TranscodingUrl          …/stream.ts       …/stream.mp3       …/stream.ts
                        no AudioCodec     AudioCodec=mp3     no AudioCodec
                        no AudioBitrate   AudioBitrate=…     no AudioBitrate
                        Vbr=false         Vbr=true           Vbr=false

and a plain transcode's reasons, before → after:

upstream  TranscodeReasons=ContainerNotSupported,VideoCodecNotSupported,AudioCodecNotSupported
ours      TranscodeReasons=ContainerNotSupported   →   the same three

Whole-body PlaybackInfo diffs agree on three profiles — the remux, a direct
play, and what jellyfin-web posts — with nothing left but the session id and the
token each server draws for itself.

Done in `879ee69`, with `dc9354e` after a review. `audio_verdict` now answers `DirectStream` where the container was the only complaint, in upstream's own shape: the direct-play profile search, then a profile that names the codec in a container this file is not in, then the bitrate check on each branch, then the exactly-`ContainerNotSupported` gate. The container it hands over in is `item.TranscodingContainer ?? "ts"`, overridden by the matched profile's where that is one a segmented stream can carry, and the HLS codec restriction is applied where the request asked for segments. One thing came with it that the issue did not name: a file whose container no profile takes is blamed for its **codecs** as well — the video one included, which a music file has none of. That is upstream's `GetTranscodeReasonsFromDirectPlayProfile`, and it is what a transcoding URL carries. ## Measured Both servers over the anchor, `POST /Items/{flac}/PlaybackInfo` with a profile that takes `flac` in `webm`, before → after: ``` upstream ours (before) ours (after) TranscodingContainer ts mp3 ts TranscodingUrl …/stream.ts …/stream.mp3 …/stream.ts no AudioCodec AudioCodec=mp3 no AudioCodec no AudioBitrate AudioBitrate=… no AudioBitrate Vbr=false Vbr=true Vbr=false ``` and a plain transcode's reasons, before → after: ``` upstream TranscodeReasons=ContainerNotSupported,VideoCodecNotSupported,AudioCodecNotSupported ours TranscodeReasons=ContainerNotSupported → the same three ``` Whole-body `PlaybackInfo` diffs agree on three profiles — the remux, a direct play, and what jellyfin-web posts — with nothing left but the session id and the token each server draws for itself.
Sign in to join this conversation.
No description provided.