Skip to content

HLS: fatal IllegalArgumentException in HlsMediaChunk.feedDataToExtractor when a byte-range EXT-X-PART chunk is re-loaded after full consumption #3350

Description

@davotoula

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

  1. Play a Low-Latency HLS stream whose EXT-X-PART entries are byte ranges into the parent
    segment
    rather than separate part files.
  2. 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

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions