Skip to content

Fallback ladder rung 5: recreate the decoder after a transient device failure - #196

Merged
ramesh130 merged 1 commit into
mainfrom
issue-182-decoder-recreate
Sep 16, 2026
Merged

ramesh130 merged 1 commit into
mainfrom
issue-182-decoder-recreate

Conversation

@ramesh130

@ramesh130 ramesh130 commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

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). PlayerStateRungs gains recreatesDecoder(error); superplayer-resilience's RecreateDecoder decides it — one class, Device.DecoderTransient, with DecoderInit refused because no decoder was there to lose — and PlayerStateLadder is 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_IDLE and prepare() retries from there (// ref: Player.prepare()), so the identity, the measurement session, the CMCD sid and 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, so ResumeFromLastKnown agrees 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.DecoderTransient nothing, 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: ShadowMediaCodecConfig says which codecs exist and DeviceStatement.declareVideoDecoder how many instances a device admits; neither raises a MediaCodec.CodecException part-way through a stream, and docs/testing.md bars 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 real PlaybackExceptions: DecoderTransient reaches rung 5, DecoderInit does 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-written PlayerStateRungs standing 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 before prepare puts 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 PlaybackException this path sees, and that a decoder rebuilt by prepare() renders again. A device run is where that is closed. Precedent for the stated limit is DecoderWarmupTest.

ADR and docs

Rule 5 gets the second addendum it asked for (Amended: line updated): what rung 5 must preserve — position, identity, telemetry session, CMCD sid — the same call that would break three of them (adopt, with setMediaRequest/setMediaItem beside 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: updateApiSurface produces no diff, and MAX_DECODER_RECREATIONS is internal because it bounds no promise a consumer writes code against.

Self-review

Standards. No new when over errorCode (rule 1): the rung reads ErrorClassifier like 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 the MediaCodec.CodecException transient/recoverable distinction and on Player.prepare() (CONTRIBUTING). Tests drive the facade and assert nothing past it (docs/testing.md); the internal seam they fill is the one SuperPlayerResilienceSeamTest already fills. Smells fixed while writing: a sentinel Int for the fault's relenting became a Relenting enum, a as? TestRungs cast became a typed field. PlayerStateLadder is 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), DecoderInit refused ✅ (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 refusing DecoderTransient is already asserted in FallbackLadderTest), bounded ✅, limit stated in KDoc ✅.

CI

gh workflow run ci.yml on 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=2 is 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

…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
@ramesh130
ramesh130 merged commit d6c8509 into main Sep 16, 2026
1 check passed
@ramesh130
ramesh130 deleted the issue-182-decoder-recreate branch September 18, 2026 06:47
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.

Fallback ladder rung 5: recreate the decoder after a transient device failure

1 participant