Skip to content

[DASH Multi-Period] Player gets stuck when the next period’s first init segment (I-frame) fails to load, StuckDetection identifies it after ~10 minutes #3396

Description

@akhilesh-dubey

Version

Media3 1.10.1

More version details

In multi-period DASH, when the player transitions seamlessly from period N to period N+1 and the
first chunk of N+1 fails with a retriable IOException (EOFException, empty response, read
timeout, retriable 5xx), the error is never surfaced. Playback stays in STATE_BUFFERING
indefinitely; the only eventual signal is StuckPlayerException after the watchdog timeout (default
10 min). We have observe in production where users are happily playing and freeze exactly at a mid-roll ad
boundary or an intro to content boundary.

Analysis (against 1.10.1 source)

Based on our high level analysis on the Media3 1.10.1 source, the failure appears related to the interaction between period read-ahead, renderer stream selection, and initial-discontinuity read suppression.

During read-ahead across the period boundary:

  • The renderer finishes reading period N.
  • The renderer's reading period advances to N+1 and its sample stream is switched accordingly.
  • The Player's playing period is still N.

Both error-surfacing paths in ExoPlayerImplInternal resolve the renderer via the playing
period:

  • renderer.allowsPlayback(playingPeriodHolder)if (!allowsPlayback) maybeThrowRendererStreamError(i)
  • (in STATE_BUFFERING) if (renderers[i].isReadingFromPeriod(playingPeriodHolder)) maybeThrowRendererStreamError(i)

RendererHolder.getRendererReadingFromPeriod(period) matches a renderer only when
renderer.getStream() == period.sampleStreams[index]. Since the renderer's stream is now N+1's,
getRendererReadingFromPeriod(playingPeriod = N) returns null, so:

  • allowsPlayback(N) returns true (the renderer == null branch) → first path skipped.
  • isReadingFromPeriod(N) is false → the STATE_BUFFERING path skipped.

So the failing load lives on the reading period (N+1), but the only code that surfaces stream
errors polls the playing period (N), which no renderer is reading. maybeThrowStreamError() /
ChunkSampleStream.maybeThrowError() is therefore never invoked on the failing stream.

The one escape hatch, loader.hasFatalError(), is gated on the retry decision:
ChunkSampleStream.mayHaveInitialDiscontinuity() = (...) && !loadingFinished && !loader.hasFatalError(),
and the initial-discontinuity read suppression (DashMediaPeriod.setSuppressReadOnAllStreams) stays
engaged while hasFatalError() is false. For retriable errors,
DefaultLoadErrorHandlingPolicy.getRetryDelayMsFor never returns C.TIME_UNSET, so hasFatalError()
never becomes true, the suppression gate never lifts, and the playing period never advances to N+1 →
indefinite buffering. For non-retriable errors, getRetryDelayMsFor returns C.TIME_UNSET
DONT_RETRY_FATALhasFatalError() → gate lifts → error surfaces normally.

The remaining path depends on loader.hasFatalError(). For a persistent retriable error, the loader continues retrying and does not transition to a fatal error, so the initial-discontinuity read suppression remains active. As a result, the failing N+1 stream is not consumed far enough for the error to be surfaced, leaving the Player stuck buffering.

Suspected regression Commit f066180c — "Refactor DASH initial
discontinuity handling" (#3057) — shipped in
1.10.0 (2026-03-25). It widened the read gate in ChunkSampleStream.readData()/skipData()
from hasInitialDiscontinuity to mayHaveInitialDiscontinuity():

public boolean mayHaveInitialDiscontinuity() {
  return (needToEvaluateInitialDiscontinuity || hasInitialDiscontinuity)
      && !loadingFinished
      && !loader.hasFatalError();
}

needToEvaluateInitialDiscontinuity only clears once the period's first media chunk actually loads,
so a persistent retriable first-chunk failure keeps the gate closed indefinitely; the only unblock
is loader.hasFatalError(), which never becomes true for a retriable error. The commit message states
it deliberately "suppress[es] reading for as long as the evaluation hasn't happened" — previously
suppression only spanned the brief window between evaluating and consuming the discontinuity.

The original discontinuity feature (the suppressRead / hasInitialDiscontinuity fields) was added
earlier by commit e8664dbc (#1440), shipped in
1.5.0 and present in 1.6.x, which does not exhibit the hang because its suppression window was
narrow. The suspected regression is the 1.10.0 refactor(#3057) commit f066180 , not the original feature.

I believe this happens specifically when the Player enters STATE_BUFFERING just before the period transition and is unable to recover from the buffering state.

Suggested Direction

One possible fix would be to allow the initial-discontinuity read suppression to break once a load error has exceeded the retry budget, even if the Loader has not marked the error as fatal.
This would allow persistent retriable errors at the period boundary to surface through the normal stream-error path instead of remaining in buffering indefinitely.

Devices that reproduce the issue

All

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

No

Reproduction steps

  • Use a multi-period DASH source with at least 2 periods.
  • Allow playback to reach period N and read ahead into period N+1.
  • Inject an EOFException (RetriableException) for the first chunk of period N+1.
  • Seek to the end of period N so that the existing buffer is eventually exhausted.
  • Observe that the Player enters STATE_BUFFERING.
  • The Loader continues retrying the failing chunk with bounded backoff.
  • No onPlayerError is reported.
  • The playhead remains stuck at the period boundary and eventually StuckPlayerException is reported after the watchdog timeout.

Expected result

The load error is surfaced (or recovered) instead of retried indefinitely with no forward progress

Actual result

We’re seeing a significant increase in stall detection errors during DASH period transitions in Media3 1.10.1 (Regression compared to 1.6.0).

[ERROR_CODE_TIMEOUT
Player stuck buffering with no progress for 600000 ms
](msg:ns2: Unexpected runtime error (ERROR_CODE_TIMEOUT) cause (kh8): Player stuck buffering with no progress for 600000 ms trace:ns2: Unexpected runtime error)

Media

We can share the media URLs via email if needed to help with reproduction.

Bug Report

  • You will email the zip file produced by adb bugreport to [email protected] after filing this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions