Repository navigation
Tests: Force Direct Play refuses a learned refusal before the Load - #1063
Merged
Merged
Conversation
… 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>
…very lands Step 0 of the playback-refusal S1 lane. Two hostsim rig tests drive the real foreground actuator and the pump -> poll_foreground_load order of playback_tick: - an asynchronous type=18 after the Load call returned (route Started, then load_failed): the lifecycle reads Started and lets go (Idle); the pump's failed-open ladder fails the Engine. This is the case OpenRecovery will own. - a synchronous refusal (StartFailed together with load_failed, as threads.rs publishes it): the pump fails the Engine (read-out) AND the lifecycle then settles the same refusal as #1018's retry (Prepared, Engine retired). Two machines act on one refusal today; the test names say so, and the split that fixes it (the lifecycle keeps the Load call's own result) follows. launch_restore is the first half of refuse_restore, extracted unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng main's four arms
`open_failure_action` was a pure function over four booleans the pump gathered after it
had already run the install's learner and narrowed the attempt. It is now `OpenRecovery`
(`player/recovery.rs`): a tested `State + Event -> Effects` machine owned per request by
`PlaybackSession::recovery`, driven by `pump.rs` (`drive_open_recovery`) the way
`ForegroundLifecycle` is driven by `foreground.rs`.
Characterization only: the ladder is main's, in main's order (pending HLS->Original rollback,
the conversion of native audio the install just learned the pipeline refuses, a cold Auto
Original's HLS contingency, the read-out), and a native-audio STALL of a combination the
install already knew still only logs (that is the next change's bug, pinned by a test).
- Events `Refused{refusal, facts}`, `RefusalConfirmed{newly}`, `FlightLanded`, `FlightRefused`,
`FirstPicture`; effects `ConfirmRefusal`, `Rollback`, `StartFlight`, `StartAutoHls`,
`ShowError{refusal}`. The learner is IO, so the machine asks for it (`ConfirmRefusal`) and the
pump answers with whether the combination was new, in the same drain; the rung is picked on
that answer. `LoadRefusal` is the typed payload: `Refused{num}`, `TimedOut`, plus `Stalled` and
`SourceFailed` for the watchdog and the demuxer/transport branches that share the ladder.
- The attempt's capabilities moved into the machine (`start` plus the audio facets excluded, the
attempt always re-derived from `start`): `PlaybackSession::caps` and `narrow_attempt` are gone,
`attempt_caps()` delegates, and the narrowing happens inside `step` on a newly learned refusal,
on every arm, as `recover_failed_source_route` did.
- `FirstPicture` is stepped from the pump's decoded-frame branch; the machine is idempotent there.
- `ci/machines.ini` registers the machine; `player/recovery.rs` joins the workers/globals scope
of `ci/check-deps.sh` with no ledger line. `open_failure_action`'s unit test moved into the
machine's table tests with the same seven expectations.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…w ends on the read-out, not on "Buffering..."
TV run G (2026-10-26): a stored {opus, 6ch} refusal and a transcode fixture serving 1600 B
6-channel Opus again. `native_audio_stall_fired` returned false for a combination the install
already knew ("only logs"), so the pump neither failed the Engine nor showed a read-out, the
watchdog was spent, and the viewer stayed on the buffering spinner for good.
The stall is a refusal of the same class as `type=18` (`LoadRefusal::Stalled`). OpenRecovery
now runs the ladder on the learner's answer whatever the refusal: a known combination is a
refusal with no conversion left, so it takes the next rung (a pending Original trial's rollback,
a cold Auto Original's HLS contingency) and else `ShowError`.
Red first: the rig test (a stored {opus,6ch}, both AU queues holding data, nothing accepted for
NATIVE_AUDIO_STALL_MS, the watchdog's STALL line asserted as its precondition) failed with
"a stall of a combination the install already knew left the viewer on Connecting" before the
change and passes after it. player/CLAUDE.md's "a known refusal fails as before" held only for
the `type=18` path; it now says what both paths do.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, owned by OpenRecovery Comment-only. `PlaybackSession::caps` no longer exists (the value lives in the request's `OpenRecovery`); three doc comments in plan.rs, devcaps.rs and session.rs still named the field or the route as the narrower. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ecycle's alone; the ladder owns refusals observed after the call returned The pump's failed-open ladder and the foreground lifecycle both acted on a synchronous refusal of a foreground-resume Load (StartFailed + load_failed): the pump set Error, then the lifecycle settled the same refusal as #1018's retry and retired the Engine. Split by who observes the refusal. The foreground actuator marks the Engine whose exact Load attempt it follows (Engine::foreground_follows_load, set where it starts or takes over following an attempt). The pump decides before stepping the machine: a failed Load on a followed Engine whose route_start_status is Failed is the Load call's own result, so it returns and the lifecycle's Failed settlement owns it. A refusal seen after the call returned, and a fresh play's synchronous refusal (no lifecycle behind it), stay the ladder's. The step-0 SYNC test is flipped to the desired behaviour (red first: the pump left Error), the ASYNC test is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-facets rung and a 2-flight cap A pipeline refusal before the first picture now converts the suspect audio (a codec outside aac/ac3/eac3, named by recovery::suspect from the stream the pipeline was fed) in one recovery flight, then takes the HLS contingency of a cold Auto Original, then the read-out; MAX_RECOVERY_FLIGHTS bounds the flights of one request. The learner's answer (newly) no longer selects a rung: a facet the attempt's start already lacks narrows nothing and gets no flight. Forced Direct Play ends on the read-out after a pending rollback. StartFlight is ConvertFacet and dispatch_native_audio_conversion is dispatch_refusal_conversion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efusalRecovery The Retranscode landing of a recovery flight notes the delivery (and the app's own recovery as the open's origin) through report::note_refusal_recovery_delivery; the reason joins the closed list, the consent legend and PRIVACY.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y classes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… landing re-grades the Original candidate's audio; the #695 rig tests plxnative-refuse601 (dev builds) publishes type=18 num=601 through sf_on_event once the Load returned and before a picture, once per playback trace generation, so the converted reload of the same request plays. The flag is read only inside that open window (a process-wide latch cannot be armed per test). The refusal-recovery landing calls regrade_original_audio, so the stored Original candidate's audio is graded against the narrowed attempt and the ABR climb plans the remux, not the refused audio's Part. Hostsim tests: a DTS 601 converts in one flight (#695, with and without a sibling), an Auto Original flies the conversion not HLS, a refused conversion ends on the read-out, the climb after a rescue plans no DTS direct play, the landing notes its delivery reason and counts the attempt once, refuse601 publishes once per request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1a ladder made false is rewritten
ConvertFacet carries the facet and the flight ordinal, so the driver logs 'converting {facet}
(flight n/2)'. The learner's doc, the pump's refusal comments, dispatch_refusal_conversion's doc,
the type=18 callback comment and the player guide describe the ladder as it is: the facet rung is
chosen by suspect() and the loop guard, not by the learner's answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the ladder did not ask for re-opens it; an unhandled refusal fails the Engine The ladder lives per request but a picture is per Engine: the facet rung is gated on the refusal's own Engine having no decoded frame (facts.no_frames), not on the request's history, so a native-audio refusal on a reloaded Engine converts again. RecoveryEvent::LoadStarted, raised by engine::start_bufferfeed_for (the funnel every Engine start passes), takes a Flight or Failed machine back to Opening when a park and restore re-Loaded the route without a landing; the recovery landing's own reload finds the machine already in Opening and changes nothing. recover_failed_open fails the Engine on a refusal the machine does not handle, as before the machine. The stall branch gets the preview check the other failure branches have; the DTS rig tests are renamed and tightened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ladder change made false is corrected The pure table now walks the full cap as a fixpoint over (state, flights, excluded) with FirstPicture, a free RefusalConfirmed, FlightRefused, LoadStarted and a Refused of an Engine that did show a picture, and pins every (state, event) pair once. The CLAUDE.md ladder paragraph, the Sans-IO scope notes, the check-deps header, the pump and decision docs and parity-gaps item 5 now describe the per-Engine facet rung, the LoadStarted re-entry, and the driver failing the Engine for a refusal the machine does not handle. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… proof (S2a, #1046) A DTS 601 names no codec, so a refusal of it was never learned and a refusal that does name one was applied from a single event. The OpenRecovery machine now separates the two kinds of evidence: - Named (num=200, the stall): ConfirmRefusal{Named} is emitted in the same step as the ladder's rung; there is no Confirming state and no RefusalConfirmed event. The row applies at once (no `proofs`). - Proof: a facet flight whose /decision lanes are audio=transcode and video=copy|directplay (FlightLanded{lanes}) enters Proving; the first picture of the converted reload emits ConfirmRefusal{Proof}. Both-facets (HLS), rollback, timeout and source-failure rescues prove nothing. The stored NativeAudioRefusal gains `proofs: Option<u32>` (absent = named or pre-S2a row = applied). A counted row applies at LEARNED_REFUSAL_PROOFS (2); named wins on merge, proofs add capped, a proof of unknown width is not stored. The writers (record_named_refusal / record_refusal_proof) only read devcaps::learning_firmware() and queue the merge on the storage worker; devcaps no longer carries a write door (learn_refusal is gone), the row reaches the capability answer through the record's projection of applied rows. Lanes are parsed from /decision by route/plan.rs::decision_lanes and carried PreparedEncode -> AppliedRetranscode -> RecoveryLanding -> FlightLanded. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed proof, not devcaps::learn_refusal Prose the S2a change made false: player/CLAUDE.md's learned-refusal section, docs/sans-io.md and a test_harness docstring still named learn_refusal and described a refusal as applying at once from the pump. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rves it The non-hostsim test build compiled decision_test_support.rs without the rig, so MDE_TRANSCODE_BOTH was dead code there (-D warnings). Defined where it is used. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e its picture, proves nothing (S2a, #1046) A facet flight proved its conversion whenever PMS answered the lanes clean, even when the request had already played the facet natively: the picture is per Engine, so the facet rung is reachable from Playing (a Home, a return, a restore Load refused before its picture), and two such restores would persist a wrong row. OpenRecovery now remembers that a picture showed (pictured) and enters Proving only for a request that showed none. A Load between the clean landing and the first picture (a track switch, a restore after Home, a seek reload) read as the landing's own reload and kept the pending proof for a route whose lanes were never read. FlightLanded moves the machine BEFORE the driver reloads, so Proving carries `loaded`: the first LoadStarted is the landing's own reload, a second one drops to Opening and loses the proof. Tests: the first_picture helper asserts its refusal flag arrived (5 s, not a silent 500 ms fall-through); confirm_refusal's recorder test runs the real recorder under a temporary session and reads the answer before the queued write lands; the proof and named-opus rig tests project the real record before their capability assert; decision_lanes and Proving x named Refused get direct pins; the debug-flag test goes through project_stored_refusals (nativeaudioforget applies NONE, so a row recorded during the run reaches caps() on the next launch). Prose corrected where it described the old landing and the old forget behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… assertion can fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow reads the start value R1 fails on the base (the record's projection swaps the Arc: opus ceiling None -> Some(2)); M2 fails on the base (the preview reads the boot snapshot and promises Direct Play for a refused DTS 5.1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d its test doors go Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d on Session Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds the start value; pump tests read the record at the start edge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sals (start_caps), at every play-start site Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alue from the stored refusals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1048) No production change. Since #1034 the Forced audio gate honours a learned ceiling (native_ceiling), and S0/S2b moved it to the attempt's start value. These tests pin that composition: - route tier: a set that announces DTS 5.1 with an applied `dca 6` row is refused as ForcedFailure::Audio("DTS") with no part URL (no Load); with an AC3 sibling, Forced plays the sibling. - app tier: the play-start wrapper `start_caps()` returns a value lacking the applied refusal while the write-once boot snapshot is untouched. - cold_audio_resolve_held: cold_audio_resolve for a caller that already holds testlock::serial() and set up its own fixtures under it. The guide gains one sentence under the start-value paragraph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fusal of their Load before the next frame
The rig's Engine is never built by a real sf_load (the rig forces the host object ready), so its teardown cannot prove
the release and quarantines the host sink; every Load a test then starts (the foreground restore, the facet landing's
reload) is refused by the stub from the MEDIA thread (ok=0, no type=18 callback, so a refusal with num=0). The tests
assert "not Error" after a frame that follows the start; they pass only while that frame beats the media thread. When
the media thread wins, the frame reads a second refusal of an open whose facet is spent and the ladder ends on the
read-out ("no rung left for type=18 num=0"). The CI hostsim job failed on S3 (#1063) in two of these tests with no
S3 code involved; a loop of only the pump tests failed the same way (1 of 60 and 4 of 250 runs under load).
pipeline_takes_the_load awaits the stub's refusal of the Load and takes it down, and is called after each Load these
tests go on to grade: park_and_restore (Scenario B), Scenario A's restore, and refuse601's reload. The refusal is now
always absorbed before the frame, so every run takes the ordering that used to lose; the test's own
pipeline_refuses_601 still ends the open. No production change: on a set the restore's Load returns ok=1 and the
pipeline then refuses it with 601.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
first_picture takes the hostsim sink's refusal through pipeline_takes_the_load (S1a's helper) instead of repeating its wait-and-clear body. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
audio_converted_by_server reads the attempt since S2b; the merged #1049 test still used the three-argument form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fifth PR of the playback-refusal series (#918). It is stacked on #1062 (S2b), so the diff shown is S3 only.
No production change. Under Force Direct Play, audio this install has learned the TV refuses is already refused before any Load. The refusal names the codec and offers "Switch to Auto and play". This PR adds the tests that pin that behaviour, so a later change cannot quietly send the refused stream to the pipeline again.
Closes #1048
Why there is no code change
devcaps::start_capsover the session's applied rows. Here is the chain for a DTS 5.1 file on a set that announces DTS 6ch and has an applieddts 6row:Caps::minussetsnative_ceiling["dts"] = 2.forced_feeds_audio_track("dca", 6)rejects the track, so the Forced pick has no eligible track.route/plan.rs) returnsForcedFailure::Audio { codec: "DTS" }beforeplan.urlis set, so no Engine starts and no Load is issued.forced_direct_play_keeps_its_stand_in.Tests
forced_direct_play_refuses_a_learned_refusal_before_the_load(route tier). Forced plus a stored nameddca 6row, planned fromstart_caps, gives:ForcedFailure::Audio { codec: "DTS" };plan.url;forced_direct_play_plays_the_ac3_sibling_of_a_learned_dts(route tier). The same row with an AC3 sibling: there is no verdict, the part URL is planned, and everyhasMDE=1probe namesaudioStreamID=2.the_play_start_value_lacks_an_applied_refusal(app crate). This is the first test of the app'sstart_caps()wrapper:Arcitself;cold_audio_resolve_held, a split ofcold_audio_resolvefor a caller that already holds the test serial lock (TempSessionand the firmware door must be built under it). The 15 existing callers are unchanged.Red, observed by mutation. No landed tree fails these tests, because main since #1034 also refuses. Each mutation was reverted before the commit:
devcaps::caps():the_play_start_value_lacks_an_applied_refusalfails witha row in force makes a new value.devcaps::caps()instead of&env.caps: both route tests fail. The refusal test gets no verdict; the sibling test's probe namesaudioStreamID=1.audio_direct_plays_with's Forced arm callsforced_feeds_audioinstead offorced_feeds_audio_track. Both fail with:a learned DTS refusal must refuse under Force Direct Play before the Load, got NoneForced must name the AC3 sibling (stream 2), not the learned DTS (stream 1)There is no ui-sim capture: the read-out code is unchanged, and
forced_audio_refusal_names_the_codecalready pins its sentence.Crate runs on the committed tree:
make test-crate C=plx_media T=forced_direct_play:test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 1863 filtered outmake test-crate C=plx_media T=forced:test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 1812 filtered outmake test-crate C=app T=the_play_start_value:test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1059 filtered outThe full gates are left to this PR's CI.
Docs
player/CLAUDE.md: one sentence saying that under Force Direct Play a row in force refuses before any Load, with the codec named.🤖 Generated with Claude Code