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_FATAL → hasFatalError() → 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
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, readtimeout, retriable 5xx), the error is never surfaced. Playback stays in
STATE_BUFFERINGindefinitely; the only eventual signal is
StuckPlayerExceptionafter the watchdog timeout (default10 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:
Both error-surfacing paths in
ExoPlayerImplInternalresolve the renderer via the playingperiod:
renderer.allowsPlayback(playingPeriodHolder)→if (!allowsPlayback) maybeThrowRendererStreamError(i)STATE_BUFFERING)if (renderers[i].isReadingFromPeriod(playingPeriodHolder)) maybeThrowRendererStreamError(i)RendererHolder.getRendererReadingFromPeriod(period)matches a renderer only whenrenderer.getStream() == period.sampleStreams[index]. Since the renderer's stream is now N+1's,getRendererReadingFromPeriod(playingPeriod = N)returnsnull, so:allowsPlayback(N)returnstrue(therenderer == nullbranch) → first path skipped.isReadingFromPeriod(N)isfalse→ theSTATE_BUFFERINGpath 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) staysengaged while
hasFatalError()is false. For retriable errors,DefaultLoadErrorHandlingPolicy.getRetryDelayMsFornever returnsC.TIME_UNSET, sohasFatalError()never becomes true, the suppression gate never lifts, and the playing period never advances to N+1 →
indefinite buffering. For non-retriable errors,
getRetryDelayMsForreturnsC.TIME_UNSET→DONT_RETRY_FATAL→hasFatalError()→ 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 initialdiscontinuity handling" (#3057) — shipped in
1.10.0 (2026-03-25). It widened the read gate in
ChunkSampleStream.readData()/skipData()from
hasInitialDiscontinuitytomayHaveInitialDiscontinuity():needToEvaluateInitialDiscontinuityonly 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 statesit 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/hasInitialDiscontinuityfields) was addedearlier by commit
e8664dbc(#1440), shipped in1.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
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).
Media
We can share the media URLs via email if needed to help with reproduction.
Bug Report
adb bugreportto [email protected] after filing this issue.