Repository navigation
Fallback ladder rung 5: recreate the decoder after a transient device failure - #196
Merged
Merged
Conversation
…come back Rung 5 of ADR-0011's ladder, the last remedy before the ladder gives up. `RecreateDecoder` decides it — one class, `Device.DecoderTransient`, its sibling `DecoderInit` refused because no decoder was there to lose — and `PlayerStateLadder` is the pair of rung decisions core is handed, a second question on the third slot rather than a fourth slot (rule 13's addendum). Core performs it by re-preparing the player and doing nothing else: a failed Media3 player holds its playlist and its position in `STATE_IDLE` and `prepare()` retries from there, so the identity, the measurement session, the CMCD `sid` and the position are all preserved by nothing touching them, and no seek is added around it. The position is written into the remembered-position map first, so `ResumeFromLastKnown` agrees at the instant of the recreation. What is core's alone is the bound, `MAX_DECODER_RECREATIONS`: two recreations at one position, the count starting over wherever playback advanced, because a decoder that will not come back must reach rung 6 rather than loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeTWwmK1KcFMrfSAre23GS
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.
Closes #182
Rung 5 of ADR-0011's ladder: the decoder recreated after a transient device failure, and the last remedy before the ladder gives up.
The shape
A second question on the third slot, not a fourth slot (rule 13's addendum, taken literally).
PlayerStateRungsgainsrecreatesDecoder(error);superplayer-resilience'sRecreateDecoderdecides it — one class,Device.DecoderTransient, withDecoderInitrefused because no decoder was there to lose — andPlayerStateLadderis the pair of rung decisions core is handed, so nothing can fill one rung and not the other.Core performs it by re-preparing and doing nothing else. A failed Media3 player holds its playlist and position in
STATE_IDLEandprepare()retries from there (// ref:Player.prepare()), so the identity, the measurement session, the CMCDsidand the position are preserved by nothing touching them. No seek is added — that would be a second position for the operation to disagree about, and on a live window the wrong one. The position does go into the remembered-position map before the re-prepare, soResumeFromLastKnownagrees at that instant (rule 9's other half, as rung 4 does it).The order is imposed in the one place both rungs are decided: rung 4 is offered the failure first, rung 5 only where it declined. It costs a real
Device.DecoderTransientnothing, since the ladder refuses that class the next source.The bound is core's,
SuperPlayer.MAX_DECODER_RECREATIONS= 2 at one position, the count starting over wherever playback advanced. It counts futile recreations, so a long viewing that loses a decoder twice hours apart has had two remedies work, while one that will not come back reaches rung 6 instead of looping. It is core's because it is a fact about this player's history that no classification can see, and correctness under rule 12 rather than policy under rule 11. Two rather than one because the second attempt answers a different cause (a decoder still held when the first re-prepare asked — the pooled-feed case); more than two at a position that has played nothing is a device that is not coming back.Which testing route, and what the test cannot show
Route 2, the honest KDoc — as the issue permits and prefers. Nothing in this repository can fail a decoder mid-stream:
ShadowMediaCodecConfigsays which codecs exist andDeviceStatement.declareVideoDecoderhow many instances a device admits; neither raises aMediaCodec.CodecExceptionpart-way through a stream, anddocs/testing.mdbars the device. I did not build a decoder-fault seam: a fake intricate enough to fail a shadow codec convincingly would be testing itself.So the rung is split across two tests, each asserting the half it can:
FallbackLadderTest(resilience) — the routing, over realPlaybackExceptions:DecoderTransientreaches rung 5,DecoderInitdoes not and escalates, and nothing that is not this device's decoding is offered it.SuperPlayerDecoderRecreationTest(core, new) — the player's half, with a hand-writtenPlayerStateRungsstanding in for the ladder and an ordinary 502 on one segment standing in for the decoder failure: the session is rescued and plays on; the resumption position is at least the position the failure interrupted (a seek beforeprepareputs the playhead twenty seconds in, so the held position is one the request does not name — a rung that read the request would restart at zero and be caught); one media id, one URI, one measurement session; rung 4 asked before rung 5; the bound spent exactly twice and then the error delivered once to the consumer; and a player with the slot empty re-prepares nothing.What remains unshown, and the test's KDoc says so: that a real transient decoder failure on a device surfaces as a
PlaybackExceptionthis path sees, and that a decoder rebuilt byprepare()renders again. A device run is where that is closed. Precedent for the stated limit isDecoderWarmupTest.ADR and docs
Rule 5 gets the second addendum it asked for (
Amended:line updated): what rung 5 must preserve — position, identity, telemetry session, CMCDsid— the same call that would break three of them (adopt, withsetMediaRequest/setMediaItembeside it), the no-seek rule, and the bound, which is the one thing rung 4 did not need. Rule 10's addendum needs no widening — it withholds a failure "a rung" repaired — and therefore ADR-0003 rule 3's addendum covers this rung with no second exception to argue, which the addendum says explicitly.CLAUDE.md's resilience paragraph and its ADR-0011 bullet are updated to match.No public API changed:
updateApiSurfaceproduces no diff, andMAX_DECODER_RECREATIONSis internal because it bounds no promise a consumer writes code against.Self-review
Standards. No new
whenovererrorCode(rule 1): the rung readsErrorClassifierlike every other. No Media3 type in a public signature; the seam types stay internal (ADR-0001 rule 2,docs/api-surface.md).// spec:/// ref:citations on theMediaCodec.CodecExceptiontransient/recoverable distinction and onPlayer.prepare()(CONTRIBUTING). Tests drive the facade and assert nothing past it (docs/testing.md); the internal seam they fill is the oneSuperPlayerResilienceSeamTestalready fills. Smells fixed while writing: a sentinelIntfor the fault's relenting became aRelentingenum, aas? TestRungscast became a typed field.PlayerStateLadderis a deliberate Middle Man, argued in its KDoc (one interface, two rungs, each decided by the object that documents it).Spec. Against #182's criteria: recreate-and-continue ✅ (core test),
DecoderInitrefused ✅ (ladder test — the criterion core cannot state), position preserved ✅, order ✅ (rung 4 before rung 5; that the failure reached the player at all is the proof rungs 1–3 declined, and rungs 2 and 3 refusingDecoderTransientis already asserted inFallbackLadderTest), bounded ✅, limit stated in KDoc ✅.CI
gh workflow run ci.ymlon this branch: https://github.com/ramesh130/superplayer/actions/runs/35063718877 — success.Skipped
Emulator and demo runs (host memory), per the batch instructions.
./gradlew assemble check --max-workers=2is green locally, golden traces included — a core-only session is unchanged, which is rule 14's other half.🤖 Generated with Claude Code
https://claude.ai/code/session_01HeTWwmK1KcFMrfSAre23GS