A playing session reports neither what is playing nor how it is being encoded #166

Closed
opened 2026-08-21 11:08:46 +02:00 by Claude · 1 comment
Collaborator

The playback stats overlay on a file playing over HLS:

Player       Html Video Player  (null)
Stream type  HLS  (http)

and then straight to Original Media Info. Upstream shows (Transcoding)
there and a whole Transcoding Info section between the two.

Both holes are the session we report for the playing client.

NowPlayingItem is never sent. crates/ephyra-http/src/map/session.rs's
info fills now_playing_queue and leaves now_playing_item at
SessionInfoDto::default(), though the registry has held it all along —
crates/ephyra-app/src/session/sessions.rs:47, set from the playback report at
389 and cleared at 406. The bundle's getDisplayPlayMethod returns null for
a session with no NowPlayingItem before it ever looks at PlayState, and the
caller concatenates that into the label — which is the literal (null) above.
Anything else that asks what a session is doing reads the same field.

TranscodingInfo is never sent either. Nothing outside the generated DTO
mentions it. The stats panel gates the entire section on it —
o.TranscodingInfo && p.push({stats: m(o, t, i), name: h}) — so target codecs,
output container, bitrate, framerate, completion percentage and the transcode
reasons are all absent, and the section is titled Transcoding, Remuxing or
Direct streaming off the same object.

Upstream. SessionManager.cs:430 puts the item on the session from the
playback report and 1252/1256 copies both fields onto the DTO.
MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs:351 reports
TranscodingInfo on every progress line ffmpeg writes: bitrate, audio and video
codec, container, framerate, completion percentage, width, height, channels,
IsAudioDirect/IsVideoDirect, the hardware acceleration backend, and the
transcode reasons.

Most of that is already computed here. crates/ephyra-ffmpeg/src/progress.rs
parses TranscodingProgress { position_ticks, framerate, percent_complete, bytes_transcoded, bit_rate } per job, and its only reader today is the
supervisor that kills a stalled one — streaming/encoder/refusing.rs. The
codecs, the container and the dimensions are the job's own request, and
IsAudioDirect/IsVideoDirect are whether either stream was a copy.

Where to start

NowPlayingItem on its own settles the (null), and the item id is already on
the live session. The mapping has no repository to materialise it with, which is
presumably why it was skipped: upstream's session carries the built item rather
than an id, so the decision is where that materialisation happens rather than
whether the data is there.

TranscodingInfo is the larger half: progress has to reach the session registry,
keyed by device id, which is what upstream keys it by.

How to know it is done

With the pinned bundle: a file playing over HLS shows the play method rather
than (null), and a Transcoding Info section whose completion percentage
moves; a direct play shows (Direct playing) and no such section. A test that
asks for a session mid-playback and fails today.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
; generated files are regenerated rather than edited; a comment says what
the code does, not how it got there.

A test that cannot fail is worse than none.

The playback stats overlay on a file playing over HLS: Player Html Video Player (null) Stream type HLS (http) and then straight to *Original Media Info*. Upstream shows `(Transcoding)` there and a whole *Transcoding Info* section between the two. Both holes are the session we report for the playing client. **`NowPlayingItem` is never sent.** `crates/ephyra-http/src/map/session.rs`'s `info` fills `now_playing_queue` and leaves `now_playing_item` at `SessionInfoDto::default()`, though the registry has held it all along — `crates/ephyra-app/src/session/sessions.rs:47`, set from the playback report at 389 and cleared at 406. The bundle's `getDisplayPlayMethod` returns `null` for a session with no `NowPlayingItem` before it ever looks at `PlayState`, and the caller concatenates that into the label — which is the literal `(null)` above. Anything else that asks what a session is doing reads the same field. **`TranscodingInfo` is never sent either.** Nothing outside the generated DTO mentions it. The stats panel gates the entire section on it — `o.TranscodingInfo && p.push({stats: m(o, t, i), name: h})` — so target codecs, output container, bitrate, framerate, completion percentage and the transcode reasons are all absent, and the section is titled *Transcoding*, *Remuxing* or *Direct streaming* off the same object. **Upstream.** `SessionManager.cs:430` puts the item on the session from the playback report and 1252/1256 copies both fields onto the DTO. `MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs:351` reports `TranscodingInfo` on every progress line ffmpeg writes: bitrate, audio and video codec, container, framerate, completion percentage, width, height, channels, `IsAudioDirect`/`IsVideoDirect`, the hardware acceleration backend, and the transcode reasons. **Most of that is already computed here.** `crates/ephyra-ffmpeg/src/progress.rs` parses `TranscodingProgress { position_ticks, framerate, percent_complete, bytes_transcoded, bit_rate }` per job, and its only reader today is the supervisor that kills a stalled one — `streaming/encoder/refusing.rs`. The codecs, the container and the dimensions are the job's own request, and `IsAudioDirect`/`IsVideoDirect` are whether either stream was a copy. ## Where to start `NowPlayingItem` on its own settles the `(null)`, and the item id is already on the live session. The mapping has no repository to materialise it with, which is presumably why it was skipped: upstream's session carries the built item rather than an id, so the decision is where that materialisation happens rather than whether the data is there. `TranscodingInfo` is the larger half: progress has to reach the session registry, keyed by device id, which is what upstream keys it by. ## How to know it is done With the pinned bundle: a file playing over HLS shows the play method rather than `(null)`, and a *Transcoding Info* section whose completion percentage moves; a direct play shows `(Direct playing)` and no such section. A test that asks for a session mid-playback and fails today. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**; generated files are regenerated rather than edited; a comment says what the code does, not how it got there. A test that cannot fail is worse than none.
Author
Collaborator

Landed in 1d565f8, with review fixes in 9f43537.

NowPlayingItem. The registry held the id all along; the materialising
happens in the route, which has the repository — one query for every session
rather than one per session. Described on nobody's behalf, which is what
upstream's GetItemInfo does: a session is read by whoever may see it, and
describing the item as its own user would put their watched state on somebody
else's screen.

The review caught the field set. Upstream strips eighteen fields from a
session's copy (SessionManager.GetItemInfo) — MediaSources, MediaStreams,
People, Tags, ItemCounts and the rest — and we were sending all of them on
every /Sessions poll and every socket broadcast. The media sources among them
carry the path on disk, on a route anyone with EnableRemoteControlOfOtherUsers
can reach.

TranscodingInfo. Keyed by device, which is what upstream keys it by. What
the job was asked to produce is recorded when it starts — the codecs, the
container, the dimensions, the channels, the two direct flags and the reasons,
all settled before ffmpeg runs. The rate, the bitrate and the completion
percentage come off the progress ephyra-ffmpeg already parses, whose only
reader was the supervisor that kills a stalled job.

The review caught the device lookup. It used rfind over a BTreeMap keyed by
job id, and a job id is a counter rendered decimally — so "9" sorts after
"10", and a device that had seeked mid-playback would report the abandoned
encode's container and codecs and ask the transcoder for its progress. It orders
by when a job was last heard from now, with a test over the 9→10 boundary.

A direct play shows no transcoding section, as upstream's does.

Landed in 1d565f8, with review fixes in 9f43537. **`NowPlayingItem`.** The registry held the id all along; the materialising happens in the route, which has the repository — one query for every session rather than one per session. Described on nobody's behalf, which is what upstream's `GetItemInfo` does: a session is read by whoever may see it, and describing the item as its own user would put their watched state on somebody else's screen. The review caught the field set. Upstream strips eighteen fields from a session's copy (`SessionManager.GetItemInfo`) — `MediaSources`, `MediaStreams`, `People`, `Tags`, `ItemCounts` and the rest — and we were sending all of them on every `/Sessions` poll and every socket broadcast. The media sources among them carry the path on disk, on a route anyone with `EnableRemoteControlOfOtherUsers` can reach. **`TranscodingInfo`.** Keyed by device, which is what upstream keys it by. What the job was asked to produce is recorded when it starts — the codecs, the container, the dimensions, the channels, the two direct flags and the reasons, all settled before ffmpeg runs. The rate, the bitrate and the completion percentage come off the progress `ephyra-ffmpeg` already parses, whose only reader was the supervisor that kills a stalled job. The review caught the device lookup. It used `rfind` over a `BTreeMap` keyed by job id, and a job id is a counter rendered decimally — so `"9"` sorts after `"10"`, and a device that had seeked mid-playback would report the abandoned encode's container and codecs and ask the transcoder for its progress. It orders by when a job was last heard from now, with a test over the 9→10 boundary. A direct play shows no transcoding section, as upstream's does.
Sign in to join this conversation.
No description provided.