Skip to content

Route: one capability value per playback request; the planner reads no global - #1054

Merged
GLinnik21 merged 7 commits into
mainfrom
series/attempt-caps
Oct 26, 2026
Merged

GLinnik21 merged 7 commits into
mainfrom
series/attempt-caps

Conversation

@GLinnik21

Copy link
Copy Markdown
Owner

First PR of the playback-refusal series (#918). It is a pure refactor: each playback request carries one capability value, read once at play start, and every plan, profile and recovery in that request uses it. The planner no longer reads the process-wide devcaps::caps().

Closes #1044

What changed

  • One read per request, at the app's play start. app/playback.rs, app/content.rs and app/playback/queue_transport.rs read devcaps::caps() once and pass the Arc<Caps> into route::request_play*. That calls PlaybackSession::begin_attempt, the field's only start writer.
    • PlaybackSession.caps is an Option<Arc<Caps>> because PlaybackSession::IDLE is a const. The Option is handled only in attempt_caps(); the planner takes &Caps.
    • Retries and re-resolves reuse the same request's value. A new play or preview always begins a fresh one.
  • Every reader follows the request's value. That covers the planner, the profile builders (TranscodeSpec.caps), the MDE ask, the Encode claim, RetranscodeClaimInputs, RebasePlan, OriginalRecoveryPlan and HlsAbrControl.
    • ResolveEnv carries the value into build_stream, classify_outcome and the preview paths.
    • The zero-argument is_dp_audio, is_dp_audio_track, profile_extra and forced_direct_profile are deleted; is_dp_audio_track_with(&Caps, ..) is the only form left.
  • Caps::without(&Facet) narrows a copy by the same rule a learned refusal applies (refusal_ceiling, shared with apply_learned).
  • Readers left on the boot snapshot, by design: engine.rs (×3), the Forced preview, the track menu's "converted by the server" read-out, app/diagnostics.rs and lab/snapshot.rs.

Gate: coverage expansion, not a shrink

ci/check-deps.sh's globals rule now also matches devcaps::caps, is_dp_audio and is_dp_audio_track. Both qualified and bare-after-import spellings match, with the same import rule as peek/revision; the _with forms do not.

  • After this change no machine in scope reads them, so the widening lands at zero ledger lines and PINNED_LEDGER_TOTALS is unchanged.
  • route/plan.rs is now pinned at zero global capability reads. A later change that puts one back fails make check.
  • New self-test: DepGates.test_globals_ledger_sees_the_capability_answer_readers_in_a_machine.

Tests

New, all green:

  • devcaps::tests::without_an_audio_facet_narrows_a_copy_by_the_learned_refusal_rule
  • plan::track_selection_tests::an_attempt_without_dts_picks_the_ac3_sibling_while_the_wider_attempt_keeps_the_dts
  • decision::plan_tests::a_transcode_profile_names_the_codecs_of_the_attempts_own_caps. It needs no global installed or read.
  • player::pump::flight_tests::a_native_audio_refusal_learned_during_the_original_trial_reaches_the_rollbacks_profile. Written first; it failed on the branch before the fix ("the rollback's profile must carry the Opus ceiling the pipeline just refused"), then passed.

The flight rig now begins its attempt from the thread's table, so rig tests plan from the table they install. No rig test changed outcome.

Crate suites, on the committed tree:

  • make test-crate C=plx_platform: test result: ok. 307 passed; 0 failed
  • make test-crate C=plx_plex: test result: ok. 735 passed; 0 failed
  • make test-crate C=plx_media: test result: ok. 1766 passed; 0 failed; 64 ignored
  • make test-hostsim C=plx_media: test result: ok. 1934 passed; 0 failed; 64 ignored
  • make test-crate C=app: test result: ok. 1059 passed; 0 failed
  • make test-hostsim: test result: ok. 1172 passed; 0 failed
  • bash ci/check-deps.sh: check-deps: all gates green
  • python3 ci/check-module-layers.py: green

make check, --no-default-features and the ARM build are left to this PR's CI.

Docs

  • session::learn_native_audio_refusal and devcaps::learn_refusal no longer claim the conversion plans against the global answer.
  • The Caps doc says the value is read once per attempt.
  • plex/mod.rs's dead-code list drops profile_for, which production now reaches.
  • docs/webos5-port.md's "codec capability is still asserted, not probed" bullet had been false since the profile started deriving from the set's own codec table. It now says what is probed and what is still asserted (bitDepth=10, the unreadable-table fallback).
  • docs/sans-io.md describes the gate.

Notes

🤖 Generated with Claude Code

GLinnik21 and others added 7 commits October 26, 2026 04:05
… media follows)

Refs #1044

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… recovery flight read it (wip, tests next)

PlaybackSession holds an Arc<Caps> read once where an attempt starts
(request_play_with_queue, and the native-audio conversion's dispatch);
ResolveEnv, the rebase/recovery/retranscode/ABR captures and TranscodeSpec
carry it, so the route's gates and the profile string PMS reads come from the
same value instead of two reads of the process-wide answer.

Refs #1044

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d refusal reaching its flight)

Caps A (a DTS row) and B (= A.without(Audio dts)) pick differently and profile
differently with no global installed; the native-audio recovery flight's
replacement /decision carries the ceiling learned mid-attempt.

Refs #1044

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ost-lesson caps

Refs #1044

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… none

The app's play start (request_movie / request_episode / the content and
queue paths) reads devcaps::caps() ONCE and hands it to the route's
request_play* entry points, which begin the attempt on it. The planner,
the resolve worker and every mid-film rebuild plan from that value
(PlaybackSession::caps, ResolveEnv::caps); route no longer reads the
process-wide answer to plan, and plan.rs holds no devcaps::caps( and no
zero-argument is_dp_audio( / is_dp_audio_track( (the cfg(test) pick
wrappers take the &Caps too).

The pump's ConvertNativeAudio no longer re-reads the global after the
lesson: dispatch_native_audio_conversion narrows the playing attempt by
what the demuxer fed (Caps::without), so the replacement /decision
carries the learned ceiling from the attempt's own value. The #1034
pin test (the refusal reaching its recovery flight) is unchanged.

ci/check-deps.sh: the `globals` rule is widened to devcaps::caps( and the
zero-argument is_dp_audio( / is_dp_audio_track( (qualified, or bare after
an import; the _with forms do not match). It is a coverage expansion, not
a shrink: no machine reads them, so zero ledger lines were seeded and
PINNED_LEDGER_TOTALS is unchanged. A self-test covers the shapes.

Refs #1044

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, not only the conversion's

The failed-open recovery chose the Original trial's rollback over the conversion when both
applied, and only the conversion narrowed the attempt's Caps. The refusal was learned (the
process-wide answer lost the audio), but the attempt kept the value the play started on, so the
rollback's rebase profile, a later retry and a quality change offered native Opus again. The
pipeline refused it again, `learn` returned false (known combination), and a manual Original ended
on the read-out where main converted.

recover_failed_source_route now narrows the attempt (PlaybackSession::narrow_attempt) before it
picks the recovery, once per learned refusal; dispatch_native_audio_conversion no longer repeats it.
The pump rig test a_native_audio_refusal_learned_during_the_original_trial_reaches_the_rollbacks_profile
failed on the rollback's /decision profile before this and passes after.

Also: the docs the change made false (learn_native_audio_refusal, devcaps::learn_refusal, the Caps
struct, plex/mod.rs's allow list, webos5-port.md's codec-capability bullet, sans-io.md), and
FlightRig now begins its attempt on the thread's devcaps::caps() so rig tests plan from the table
they install rather than the pre-probe fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… does without HEVC

The previous wording said "H.264 alone" for a set whose table lists no HEVC decoder, which drops
the VP9 / AV1 copy lanes a table may still list. The direct-play list omits hevc
(direct_play_video_codecs) and the transcode target's encode head is h264 (Caps::encode_vcodec).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GLinnik21 GLinnik21 added playback Playback, the player and its controls internal Tooling, CI, tests and code health; no user-facing change labels Oct 26, 2026
@GLinnik21
GLinnik21 merged commit 7163318 into main Oct 26, 2026
19 checks passed
@GLinnik21
GLinnik21 deleted the series/attempt-caps branch October 26, 2026 09:37
GLinnik21 added a commit that referenced this pull request Oct 26, 2026
…ture (#1060)

A play the TV refuses before its first picture is rescued by one registered state machine, `OpenRecovery` (`player/recovery.rs`, `ci/machines.ini`), which replaces the `open_failure_action` decision function. A manual-quality DTS play refused with `num=601` (#695) now retries with the server converting the DTS track instead of ending on the error screen.

- The machine holds the request's capability value (#1054), reads no global and performs no IO; `player/pump.rs` drives it and `engine.rs` steps `LoadStarted`. Effects: `ConfirmRefusal`, `Rollback`, `ConvertFacet`, `StartAutoHls`, `ShowError`.
- The ladder for a refusal before the refusing Engine's first picture: roll back a pending HLS→Original trial; Force Direct Play reads out; the audio facet rung converts a refused non-AAC/AC3/E-AC3 track once, and only if excluding it narrows the request; Auto then starts the retained HLS bootstrap; else the read-out. At most `MAX_RECOVERY_FLIGHTS = 2` flights per request.
- Fixed: a native-audio stall of an already-learned combination left the viewer on "Buffering…" forever; it now reads out.
- Fixed: a refused foreground-resume Load was handled twice (pump read-out plus lifecycle retry). The Load call's synchronous refusal belongs to the foreground lifecycle (`Engine.foreground_follows_load`).
- A refused restored Load after Home no longer spins on "Connecting"; after a DTS rescue the ABR climb re-grades the Original candidate's audio (`regrade_original_audio`).
- New delivery reason `refusal_recovery`; debug-only `plxnative-refuse601` trigger.
- The conversion keeps the refused track (0.8.0 landed on the AC3 sibling); the stored refusal makes the next play pick the sibling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dr-Amp pushed a commit to Dr-Amp/plx-native that referenced this pull request Oct 26, 2026
Brings in main's GLinnik21#1054 (one capability value per playback request), GLinnik21#1060
(OpenRecovery state machine for refusals before the first picture) and GLinnik21#1053
(tools/pr-land.py).

Live TV adapts to the capability value: request_live takes the Arc<Caps> the
way request_play does, and the app passes it at play start. ResolveEnv no
longer derives Default, so the channel tests build their environment from
ResolveEnv::snapshot and set the live channel on it. The LiveTune type moves
above the ResolveEnv doc comment, which it had split.

Signed-off-by: Dr-Amp <dr_amp@users.noreply.github.com>
GLinnik21 added a commit that referenced this pull request Oct 26, 2026
…1063)

Tests pin that Force Direct Play refuses audio the install has learned the TV refuses before any Load, naming the codec and offering "Switch to Auto and play". No production change: the Forced gate reads the attempt's start value (#1054, #1062), and main has refused this case since #1034.

- `forced_direct_play_refuses_a_learned_refusal_before_the_load`: Forced plus a stored `dca 6` row gives `ForcedFailure::Audio { codec: "DTS" }` and no part URL.
- `forced_direct_play_plays_the_ac3_sibling_of_a_learned_dts`: with an AC3 sibling, Forced plays the sibling.
- `the_play_start_value_lacks_an_applied_refusal`: the first test of the app's `start_caps()`.
- Red observed by mutation (the wrapper returning the boot value; the Forced pick reading the global; the Forced arm using the codec-level check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Tooling, CI, tests and code health; no user-facing change playback Playback, the player and its controls

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The route and the transcode profile read one capability value per attempt

1 participant