Skip to content

Hold 0111's ladder as values: which source, which streams, and what the handover carries (#111) - #294

Merged
iderex merged 1 commit into
mainfrom
the-ladder-the-streams-and-the-handover-111
Sep 3, 2026
Merged

Hold 0111's ladder as values: which source, which streams, and what the handover carries (#111)#294
iderex merged 1 commit into
mainfrom
the-ladder-the-streams-and-the-handover-111

Conversation

@iderex

@iderex iderex commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The issue this belongs to

#111. The last section says which of its conditions this reaches, which is one of the five as written, and what it reaches instead of the other four.

What changed

src/playback/handover.rs is 0111's ladder as values. docs/decisions/0111-which-source-is-played-and-the-handover.md had landed on 2026-08-16 and nothing in the tree held any of it: no rung, no stream rule, no handover type, and no refusal for nothing playable. That is exactly the state the record was written against, in its own words: the first playback code answers five records at once, and the shortest correct-looking code takes the first source the server listed.

git rev-parse origin/the-ladder-the-streams-and-the-handover-111
eae60866cf1ec45029e2119928534e7561aaac98

git show eae60866cf1ec45029e2119928534e7561aaac98:src/playback/handover.rs | grep -n '^pub enum \|^pub struct \|    pub fn chosen'
75:pub enum StreamKind {
90:pub struct Picture {
119:pub struct OfferedStream {
217:pub struct RoutesOffered {
247:pub struct ConversionAddress {
281:pub struct OfferedSource {
515:pub enum Rung {
572:pub struct Ceiling {
600:pub struct Preferences<'a> {
626:pub struct WhatTheServerOffered {
653:pub struct ChosenStream {
691:pub enum WhatThePlayerOpens {
711:pub struct Handover {
740:    pub fn chosen(
837:pub enum NothingPlayable {

What the server offered arrives as values, in the shape both supported lines answer with: per source its container, its streams with kind, codec, language, default flag and picture size, the three route flags, the address the server supplies for a route below the top one, its rate, and its default stream indices. The fields were read off MediaSourceInfo.cs and MediaStream.cs at the two commits 0010 names, and the diff between the two commits moves none of the fields this reads:

git -C jellyfin diff --stat 1fbd8739292cce610231be93daf43368733edf63 c3ed1407ca698b0905de99da87b67415e6a62dbd -- MediaBrowser.Model/Dto/MediaSourceInfo.cs MediaBrowser.Model/Entities/MediaStream.cs MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs MediaBrowser.Model/Dlna/PlaybackErrorCode.cs
 MediaBrowser.Model/Dlna/PlaybackErrorCode.cs |  16 +-
 MediaBrowser.Model/Entities/MediaStream.cs   | 299 +++++++++++++--------------
 2 files changed, 163 insertions(+), 152 deletions(-)
git -C jellyfin diff 1fbd8739292cce610231be93daf43368733edf63 c3ed1407ca698b0905de99da87b67415e6a62dbd -- MediaBrowser.Model/Entities/MediaStream.cs | grep -E '^[-+] *public' 
+        public string LocalizedLanguage { get; set; }
+        public string LocalizedOriginal { get; set; }
+        public bool IsOriginal { get; set; }
+        public bool IsVobSubSubtitleStream
-        public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream;
+        public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream || IsVobSubSubtitleStream;

Handover::chosen is the rule. Each source has its streams chosen first, by 0111's stream rule, and is judged on those and its container; it lands on the highest rung the description admits or on none; the highest rung across sources wins, with fewer converted streams winning on the conversion rung and the server's own order breaking a tie. The start is 0058's answer applied: Resume::At is where playback starts and the two answers that offer no resume start from the beginning. Nothing playable is told apart as the server's refusal or the core's, and NothingPlayable::as_failure hands both to Failure::nothing_playable, which is the one further entrance this change adds to 0037's mapping point in src/failure/mod.rs. The module cannot name a variant of Failure in an expression, exactly as before.

The other files register the module where every landing registers one: src/playback/mod.rs, the crate's thread assertions in src/lib.rs, and the same assertions asked from outside in tests/thread_statements.rs. src/playback/ is not an area on .github/coverage/pinned-surface, so that register is untouched.

The means is the one the tree already carries, which is the check ## Choosing the means asks for: values in the language 0011 measured and chose, judged by the suite that already exists, with no dependency, no runtime and no parser added. What the answer body looks like on the wire is not parsed here and is not decided here.

What this decided that 0111 left to whoever built it

A picture whose size the server did not state is not covered by the description. 0036's second part is "at what sizes", a size nobody stated cannot be inside it, and 0036 names the direction this fails in as the cheap one: a conversion the device did not need, rather than a stream that fails inside a decoder. The alternative reads as generous and ships the failure 0111 spends its argument on.

Every rung below the top one is built from the address the server supplied, and a source with none clears no lower rung. Both supported lines supply an address only where the source is not played as it stands, read at the commits 0010 names, and the address is what the player opens. The one-line removal of that guard does not compile, which is written in the guard section below rather than claimed as a run that went red. A source the server marks as playable as it stands, that the description does not cover, and that carries no address, is therefore nothing playable from the core: that is the mismatch 0111 names, arriving here rather than inside a decoder.

Two things can mark a default stream and both are read, the server's own index first. The source's DefaultAudioStreamIndex is the server's statement about this source and the flag on a stream is the file's about itself; where they disagree the server's is the one that reflects what it will serve. Reading the flag first reddens the language case.

A source describing no stream clears no rung. It is judged by nothing, and the alternative admits a file on the strength of its container alone.

Names are compared without regard to letter case. A container or a codec is the same platform fact spelled by two parties, and a client writing MKV would otherwise have every file converted with nothing saying why.

A source whose rate the server did not state is not above any ceiling, because the core cannot say that it is, and 0111 has the core invent no ceiling of its own.

What failure it prevents

The first source the server listed being taken, because it compiles and it works on every machine where every file is already playable. 0111 names that as the decision that gets taken by accident, and says it ships a stream the first narrower device cannot decode, with the failure arriving from inside the platform's decoder where 0004 has no kind and the core no visibility. The case named for it went red with the ladder's comparison deleted.

A source judged on a track nobody will hear. A file whose second audio track is undecodable would be converted for a stream the person was never going to play; the ladder judges only the streams chosen for it, and the case named for it went red with every stream judged.

A token in a formatted line. Both supported lines write the session's token into the address they supply for a conversion, so ConversionAddress formats by hand and the case that reads the output went red with the derived formatting restored.

Evidence

All at eae60866cf1ec45029e2119928534e7561aaac98, with a clean working tree:

git status --short | wc -l
0

cargo test --locked --lib playback::handover
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 556 filtered out; finished in 0.00s

cargo test --locked --lib failure::tests::nothing_playable
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 576 filtered out; finished in 0.00s

cargo test --locked
test result: ok. 577 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 28 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.03s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 125 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.35s

cargo build --locked --all-targets
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

bash .github/lint/lint.sh check | tail -1
Every lint the groups above carry was refused, outside the register printed with it.

bash .github/format/format.sh check | tail -1
Every tracked source file above is written the way the formatter would write it.

git diff --stat origin/main...HEAD | tail -1
 5 files changed, 1474 insertions(+)

The invariants leg was run at the same commit, alone, because the machine this was written on was under load from other sessions and the run took 263 seconds where it had taken a few earlier in the day:

bash .github/invariants/invariants.sh check | tail -1
Every rule above was applied to its subject and refused nothing.

Nothing under src/ reaches the network, before or after this change:

git grep -n 'std::net' HEAD -- src/ ; echo "exit=$?"
exit=1

What a guard here refuses, and the proof it bites

Each violation below was applied to src/playback/handover.rs on the working tree at this commit's content, the module's cases were run, and the file was restored and compared byte for byte against the kept copy before the next one. Every run is cargo test --locked --lib playback::handover, and the green run without any violation is the first line under Evidence.

Deliberate violation What went red
The ladder's comparison replaced by false, so the first source listed is kept the_first_source_listed_is_not_taken_when_a_later_one_sits_higher, fewer_converted_streams_wins_between_two_sources_on_the_conversion_rung
Every stream judged rather than the ones that will be played a_stream_nobody_will_play_is_not_judged, a_subtitle_is_chosen_by_the_same_rule_and_counts_when_judged
A picture of unstated size treated as covered (is_some_and to is_none_or) a_picture_larger_than_the_largest_or_of_unstated_size_is_not_played_as_it_stands
The ceiling not applied a_source_above_the_ceiling_is_not_a_candidate_and_one_of_unstated_rate_is
A tie going to the later source (< to <=) a_tie_on_one_rung_goes_to_the_source_the_server_listed_first, a_source_above_the_ceiling_is_not_a_candidate_and_one_of_unstated_rate_is
The hand-written formatting of the address replaced by the derived one the_conversion_address_is_written_out_nowhere
The file's default flag read before the server's index languages_are_read_in_order_then_the_default_then_the_first_listed
The server's error code ignored a_server_that_offers_nothing_is_its_refusal_and_its_code_rides_the_kind

Each of those eight runs reported exactly the cases named and no other, and the summary line of each was test result: FAILED with 19 or 20 passed.

The ninth guard is that a rung below the top one is built from the server's address, so a source without one cannot land there. Its one-line removal, replacing let address = self.conversion_address.as_ref()?; with a constructed address, does not compile:

error[E0515]: cannot return value referencing temporary value
error: could not compile `flowfin-core` (lib test) due to 1 previous error

That is a proof by construction rather than a run that went red, and I am saying so rather than counting it among the eight. The case a_route_below_the_top_needs_the_address_the_server_supplies_for_it asserts the behaviour from outside.

What this does not cover

None of #111's four test conditions. A test producing a handover from a play call against the fake server, a test covering a source the device cannot decode and proving the conversion path is the one requested, a test covering a server that offers nothing playable, and the handover crossing nothing 0112 draws are each a run with a request in it, and nothing here sends a byte. The fifth condition, the source-selection rule in docs/decisions/ with its reason, was met by the record on 2026-08-16 and is untouched. The issue stays open on the four.

The address for a source played as it stands. 0111 says the handover carries an address the platform's own player opens. For every rung but the top one the server supplies it in its answer and WhatThePlayerOpens::TheServersConversion carries it as it arrived. For a source played as it stands the server supplies none, the path is one the core would build, and 0010's table has no row for it:

git show eae60866cf1ec45029e2119928534e7561aaac98:docs/decisions/0010-the-server-surface-and-what-an-absence-does.md | grep -c '/Videos/\|/stream'
0

0010 is the authority for which paths the core may name and says of itself that a path outside its table arrives by a superseding record, so WhatThePlayerOpens::TheSourceAsItStands names the source and carries no address, and the row is a rule question I have not answered. It is written on #111 rather than decided here.

Nothing parses an answer body. WhatTheServerOffered::answered takes values; the reading that turns bytes into them sits at 0037's mapping point for a body nothing in this tree reads, and 0037 already names that half as owed.

Nothing emits an event. NothingPlayable says which of the two refusals it was so that an event under 0100 can carry the difference, and nothing here holds a diagnostics facility at a point where a play call is answered.

Nothing measures anything. The interval 0008 opens on this call is play.core, its span is declared in src/measurement/ and opened by nothing outside that module's own cases, and #65 is where a number comes from.

One language list serves both kinds of stream, which is 0111's sentence as written; a caller wanting one list for audio and another for subtitles is a reversal for the record and not a field added here.

The change is 1474 added lines in five files, of which the module is 1391 and its cases are about half of that. One property holds across every changed byte: values and their cases, with no request, no store and no clock. I am stating the size rather than leaving it to be found.

Who has read it

Nobody other than me has read this change. There was no second reader available for it, and the evidence above stands in place of one.

…he handover carries (#111)

0111 fixes that the play call carries the client's own capability description
and the answer is read against it, that a source sits on the highest of four
rungs the description admits and is judged only on the streams that will be
played, that a tie goes to the order the server listed, how the audio and
subtitle streams are chosen, what the handover carries and nothing else, and
that nothing playable is request-refused rather than an empty answer. The
record had landed and nothing in the tree held any of it, which is the state
0111 was written against: the first playback code answers five records at
once, and the shortest correct-looking code takes the first source listed.

src/playback/handover.rs is the rule. What the server offered arrives as
values, each source has its streams chosen first and is judged on those and
its container, and the highest rung wins with fewer converted streams winning
on the conversion rung and the server's order breaking a tie. The start is
0058's answer applied. A source played as it stands names itself and carries
no address, because the server supplies one only for the rungs below and
0010's table has no row for the path the core would build. Every lower rung
is built from the address the server supplied, so a rung without one cannot
be written. Nothing playable is told apart as the server's refusal or the
core's, and both become one kind through a further entrance to 0037's mapping
point in src/failure/mod.rs.

Eight guards were watched failing, each reddening the case named for it:
taking the first source listed, judging every stream rather than the ones to
be played, treating a picture of unstated size as covered, ignoring the
ceiling, giving a tie to the later source, formatting the address the server
put the token into, reading the file's default flag before the server's
index, and ignoring the server's error code. The ninth is the address guard,
and its one-line removal does not compile.

Nothing here sends a byte. #111's four test conditions are each a run against
the fake server and every one of them is untouched; the issue stays open on
them and on the address for a source played as it stands.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit d9de3c7 into main Sep 3, 2026
27 checks passed
@iderex
iderex deleted the the-ladder-the-streams-and-the-handover-111 branch September 3, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant