Version
Media3 1.10.1
More version details
Also verified present in 1.11.0-rc01 — HlsMediaChunk.feedDataToExtractor and the DataSpec constructor precondition are byte-identical between the two tags, so this is not fixed on main as of that RC.
Devices that reproduce the issue
Pixel 9a (tegu), Android 17 (SDK 37) — stock build.
Devices that do not reproduce the issue
Not established. I have only tested the one device. The failure is in playlist/chunk handling rather than in a decoder, so I would not expect it to be device-specific.
Reproducible in the demo app?
Yes
Reproduction steps
- Play a Low-Latency HLS stream whose
EXT-X-PART entries are byte ranges into the parent
segment rather than separate part files.
- Playback fails within about a second of
ExoPlayerImpl.Init.
Live example (a zap-stream-core deployment; it is a live stream, so it may not be up by the time you
read this):
https://api-uk.zap.stream/537a365c-f1ec-44ac-af10-22d14a7319fb/hls/live.m3u8
The relevant shape of the media playlist, captured verbatim 2026-07-26:
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-PART-INF:PART-TARGET=0.49007290601730347
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:191184
#EXT-X-MAP:URI="init.mp4"
#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=1.715,CAN-BLOCK-RELOAD=YES
...
#EXT-X-PROGRAM-DATE-TIME:2026-07-26T20:18:32.611Z
#EXTINF:1.961,
191198.m4s
#EXT-X-PART:URI="191199.m4s",DURATION=0.5009999999892898,INDEPENDENT=YES,BYTERANGE="359712@0"
#EXT-X-PART:URI="191199.m4s",DURATION=0.5,BYTERANGE="153946@359712"
#EXT-X-PART:URI="191199.m4s",DURATION=0.5010000000474975,BYTERANGE="174788@513658"
#EXT-X-PART:URI="191199.m4s",DURATION=0.4589999999734573,BYTERANGE="185603@688446"
#EXT-X-PROGRAM-DATE-TIME:2026-07-26T20:18:34.572Z
#EXTINF:1.96,
191199.m4s
The BYTERANGE attribute is the important part — it is what gives the resulting chunk a bounded
DataSpec.length.
Expected result
Expected result
Playback proceeds, or at worst the load is retried / the part is skipped.
Actual result
Actual result
Fatal ExoPlaybackException: Source error roughly 0.6 s after ExoPlayerImpl.Init
(10:36:21.084 → 10:36:21.673 in my capture). Playback never recovers.
E/LoadTask: Unexpected exception loading stream
java.lang.IllegalArgumentException
at com.google.common.base.Preconditions.checkArgument
at androidx.media3.datasource.DataSpec.<init>
at androidx.media3.datasource.DataSpec.subrange
at androidx.media3.datasource.DataSpec.subrange
at androidx.media3.exoplayer.hls.HlsMediaChunk.feedDataToExtractor
at androidx.media3.exoplayer.hls.HlsMediaChunk.loadMedia
at androidx.media3.exoplayer.hls.HlsMediaChunk.load
at androidx.media3.exoplayer.upstream.Loader$LoadTask.run
at java.util.concurrent.ThreadPoolExecutor.runWorker
at java.util.concurrent.ThreadPoolExecutor$Worker.run
at java.lang.Thread.run
E/ExoPlayerImplInternal: Playback error
androidx.media3.exoplayer.ExoPlaybackException: Source error
at androidx.media3.exoplayer.ExoPlayerImplInternal.handleIoException
at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage
...
Caused by: androidx.media3.exoplayer.upstream.Loader$UnexpectedLoaderException:
Unexpected IllegalArgumentException
at androidx.media3.exoplayer.upstream.Loader$LoadTask.run
...
Caused by: java.lang.IllegalArgumentException
(as above)
Note on the trace: this was captured from an R8-minified build, so class and method names survived
but the numbers in the original frames are R8 map ids, not source line numbers. I have stripped
them to avoid confusion. The source line references below come from reading the 1.10.1 tag
directly, not from the trace.
Analysis
HlsMediaChunk.feedDataToExtractor re-enters by subranging past the bytes already fed to the
extractor:
// HlsMediaChunk.java:526 (1.10.1)
loadDataSpec = dataSpec.subrange(nextLoadPosition);
nextLoadPosition is a field (HlsMediaChunk.java:299) written in the finally of that same method
(:546) as input.getPosition() - dataSpec.position. After a read that consumed the whole chunk it
therefore equals the chunk's full length. It is reset to 0 only after the init segment is loaded
(:494), not per media load.
// DataSpec.java:517-519
public DataSpec subrange(long offset) {
return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset);
}
// DataSpec.java:528-531
public DataSpec subrange(long offset, long length) {
if (offset == 0 && this.length == length) {
return this; // only saves the offset == 0 case
}
...
// DataSpec.java:474
checkArgument(length > 0 || length == C.LENGTH_UNSET); // throws
So when nextLoadPosition == length, subrange computes length - offset == 0, the early-return
guard does not apply because offset != 0, and the constructor's length > 0 precondition throws a
bare IllegalArgumentException.
This is reachable only for a bounded chunk. An unbounded one has length == C.LENGTH_UNSET,
which short-circuits to LENGTH_UNSET and is accepted. In these playlists the only bounded chunks
are the byte-range EXT-X-PARTs, which is why the crash is specific to LL-HLS output of this shape.
Why it is fatal rather than retried: Loader$LoadTask.run wraps the RuntimeException as
UnexpectedLoaderException (Loader.java:474) and posts it as MSG_IO_EXCEPTION, so
LoadErrorHandlingPolicy is consulted — but DefaultLoadErrorHandlingPolicy lists
UnexpectedLoaderException among its non-retriable exceptions (DefaultLoadErrorHandlingPolicy.java:113-123),
returning C.TIME_UNSET and hence DONT_RETRY_FATAL.
Note that retrying would not help anyway: the HlsMediaChunk instance keeps its nextLoadPosition,
so a retry re-enters feedDataToExtractor with the same zero-length subrange and throws identically.
What I am less sure of: the precondition violation itself is unambiguous from the stack, but I
have not pinned down exactly which path re-invokes load() on a chunk that was already fully
consumed. My guess is that parts are arbitrary byte cuts rather than extractor-aligned, so the
extractor reads to the end of the bounded range, needs more input for a sample spanning the part
boundary, and throws — after which the Loader retries the same chunk. Someone who knows this code
will place it faster than I can.
Suggested fix
Either guard the call site:
// HlsMediaChunk.feedDataToExtractor
if (dataSpec.length != C.LENGTH_UNSET && nextLoadPosition == dataSpec.length) {
return; // nothing left to feed
}
…or make DataSpec.subrange tolerate an exhausted range rather than constructing an illegal
zero-length spec. The call-site guard seems the safer of the two, since relaxing the length > 0
precondition would affect every DataSpec user.
Workaround (for anyone hitting this before it is fixed)
Install a HlsPlaylistParserFactory via HlsMediaSource.Factory.setPlaylistParserFactory(...) that
strips #EXT-X-PART, #EXT-X-PART-INF, #EXT-X-PRELOAD-HINT and #EXT-X-SERVER-CONTROL from the
playlist text before delegating to DefaultHlsPlaylistParserFactory. No parts means no bounded
chunks, so the path becomes unreachable. Costs low latency — the playlists still list their complete
segments, so they play a little further behind the live edge instead of failing.
(Do not strip #EXT-X-SKIP: removing it while its segments remain omitted would corrupt a delta
playlist. Dropping #EXT-X-SERVER-CONTROL already stops media3 requesting deltas.)
Verified on the device above: 2 minutes of continuous playback on the same stream that previously
died in 0.6 s.
Media
https://api-uk.zap.stream/537a365c-f1ec-44ac-af10-22d14a7319fb/hls/live.m3u8
Bug Report
Version
Media3 1.10.1
More version details
Also verified present in 1.11.0-rc01 — HlsMediaChunk.feedDataToExtractor and the DataSpec constructor precondition are byte-identical between the two tags, so this is not fixed on main as of that RC.
Devices that reproduce the issue
Pixel 9a (tegu), Android 17 (SDK 37) — stock build.
Devices that do not reproduce the issue
Not established. I have only tested the one device. The failure is in playlist/chunk handling rather than in a decoder, so I would not expect it to be device-specific.
Reproducible in the demo app?
Yes
Reproduction steps
EXT-X-PARTentries are byte ranges into the parentsegment rather than separate part files.
ExoPlayerImpl.Init.Live example (a zap-stream-core deployment; it is a live stream, so it may not be up by the time you
read this):
The relevant shape of the media playlist, captured verbatim 2026-07-26:
The
BYTERANGEattribute is the important part — it is what gives the resulting chunk a boundedDataSpec.length.Expected result
Expected result
Playback proceeds, or at worst the load is retried / the part is skipped.
Actual result
Actual result
Fatal
ExoPlaybackException: Source errorroughly 0.6 s afterExoPlayerImpl.Init(10:36:21.084 → 10:36:21.673 in my capture). Playback never recovers.
Analysis
HlsMediaChunk.feedDataToExtractorre-enters by subranging past the bytes already fed to theextractor:
nextLoadPositionis a field (HlsMediaChunk.java:299) written in thefinallyof that same method(
:546) asinput.getPosition() - dataSpec.position. After a read that consumed the whole chunk ittherefore equals the chunk's full length. It is reset to
0only after the init segment is loaded(
:494), not per media load.So when
nextLoadPosition == length,subrangecomputeslength - offset == 0, the early-returnguard does not apply because
offset != 0, and the constructor'slength > 0precondition throws abare
IllegalArgumentException.This is reachable only for a bounded chunk. An unbounded one has
length == C.LENGTH_UNSET,which short-circuits to
LENGTH_UNSETand is accepted. In these playlists the only bounded chunksare the byte-range
EXT-X-PARTs, which is why the crash is specific to LL-HLS output of this shape.Why it is fatal rather than retried:
Loader$LoadTask.runwraps theRuntimeExceptionasUnexpectedLoaderException(Loader.java:474) and posts it asMSG_IO_EXCEPTION, soLoadErrorHandlingPolicyis consulted — butDefaultLoadErrorHandlingPolicylistsUnexpectedLoaderExceptionamong its non-retriable exceptions (DefaultLoadErrorHandlingPolicy.java:113-123),returning
C.TIME_UNSETand henceDONT_RETRY_FATAL.Note that retrying would not help anyway: the
HlsMediaChunkinstance keeps itsnextLoadPosition,so a retry re-enters
feedDataToExtractorwith the same zero-length subrange and throws identically.What I am less sure of: the precondition violation itself is unambiguous from the stack, but I
have not pinned down exactly which path re-invokes
load()on a chunk that was already fullyconsumed. My guess is that parts are arbitrary byte cuts rather than extractor-aligned, so the
extractor reads to the end of the bounded range, needs more input for a sample spanning the part
boundary, and throws — after which the
Loaderretries the same chunk. Someone who knows this codewill place it faster than I can.
Suggested fix
Either guard the call site:
…or make
DataSpec.subrangetolerate an exhausted range rather than constructing an illegalzero-length spec. The call-site guard seems the safer of the two, since relaxing the
length > 0precondition would affect every
DataSpecuser.Workaround (for anyone hitting this before it is fixed)
Install a
HlsPlaylistParserFactoryviaHlsMediaSource.Factory.setPlaylistParserFactory(...)thatstrips
#EXT-X-PART,#EXT-X-PART-INF,#EXT-X-PRELOAD-HINTand#EXT-X-SERVER-CONTROLfrom theplaylist text before delegating to
DefaultHlsPlaylistParserFactory. No parts means no boundedchunks, so the path becomes unreachable. Costs low latency — the playlists still list their complete
segments, so they play a little further behind the live edge instead of failing.
(Do not strip
#EXT-X-SKIP: removing it while its segments remain omitted would corrupt a deltaplaylist. Dropping
#EXT-X-SERVER-CONTROLalready stops media3 requesting deltas.)Verified on the device above: 2 minutes of continuous playback on the same stream that previously
died in 0.6 s.
Media
https://api-uk.zap.stream/537a365c-f1ec-44ac-af10-22d14a7319fb/hls/live.m3u8
Bug Report
adb bugreportto android-media-github@google.com after filing this issue.