HLS: IndexOutOfBoundsException happens if no segment files inside HLS playlist
xqq opened this issue · comments
Version
Media3 1.3.1
More version details
Could be reproduced across very long time-span versions.
I have confirmed that it has been happening since at least from ExoPlayer 2.18.2, upon to the newest media3 (release branch).
Devices that reproduce the issue
Pixel 8 running Android 14
Devices that do not reproduce the issue
It should be a device-independent bug.
Reproducible in the demo app?
Yes
Reproduction steps
Generate an HLS m3u8 playlist which doesn't include any segment files like this:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:0
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-ENDLIST
Put the playlist above onto an HTTP server, append the m3u8 URL into samples inside media.exolist.json
,
and then try playback inside the Media3 demo app.
Expected result
e.g. The player stays in a buffering state
Actual result
m3u8 playlist without segment files inside will always cause an IndexOutOfBoundsException which may lead to crash:
2024-06-13 15:27:26.067 11695-11695 EventLogger androidx.media3.demo.main D loading [eventTime=0.06, mediaPos=0.00, window=0, period=0, true]
2024-06-13 15:27:26.593 11695-11695 VRI[Sample...rActivity] androidx.media3.demo.main D visibilityChanged oldVisibility=true newVisibility=false
2024-06-13 15:27:26.627 11695-11743 HWUI androidx.media3.demo.main D endAllActiveAnimators on 0xb400007186b40080 (ExpandableListView) with handle 0xb400007306f4f9b0
2024-06-13 15:27:26.693 11695-11815 LoadTask androidx.media3.demo.main E Unexpected exception handling load completed
java.lang.IndexOutOfBoundsException: index (0) must be less than size (0)
at com.google.common.base.Preconditions.checkElementIndex(Preconditions.java:1371)
at com.google.common.base.Preconditions.checkElementIndex(Preconditions.java:1353)
at com.google.common.collect.RegularImmutableList.get(RegularImmutableList.java:82)
at androidx.media3.exoplayer.hls.HlsChunkSource.getNextMediaSequenceAndPartIndex(HlsChunkSource.java:857)
at androidx.media3.exoplayer.hls.HlsChunkSource.createMediaChunkIterators(HlsChunkSource.java:716)
at androidx.media3.exoplayer.hls.HlsChunkSource.getNextChunk(HlsChunkSource.java:415)
at androidx.media3.exoplayer.hls.HlsSampleStreamWrapper.continueLoading(HlsSampleStreamWrapper.java:776)
at androidx.media3.exoplayer.hls.HlsSampleStreamWrapper.continuePreparing(HlsSampleStreamWrapper.java:262)
at androidx.media3.exoplayer.hls.HlsMediaPeriod.buildAndPrepareSampleStreamWrappers(HlsMediaPeriod.java:558)
at androidx.media3.exoplayer.hls.HlsMediaPeriod.prepare(HlsMediaPeriod.java:183)
at androidx.media3.exoplayer.source.MaskingMediaPeriod.createPeriod(MaskingMediaPeriod.java:133)
at androidx.media3.exoplayer.source.MaskingMediaSource.onChildSourceInfoRefreshed(MaskingMediaSource.java:213)
at androidx.media3.exoplayer.source.WrappingMediaSource.onChildSourceInfoRefreshed(WrappingMediaSource.java:154)
at androidx.media3.exoplayer.source.WrappingMediaSource.onChildSourceInfoRefreshed(WrappingMediaSource.java:49)
at androidx.media3.exoplayer.source.CompositeMediaSource.lambda$prepareChildSource$0$androidx-media3-exoplayer-source-CompositeMediaSource(CompositeMediaSource.java:117)
at androidx.media3.exoplayer.source.CompositeMediaSource$$ExternalSyntheticLambda0.onSourceInfoRefreshed(Unknown Source:4)
at androidx.media3.exoplayer.source.BaseMediaSource.refreshSourceInfo(BaseMediaSource.java:90)
at androidx.media3.exoplayer.hls.HlsMediaSource.onPrimaryPlaylistRefreshed(HlsMediaSource.java:565)
at androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker.onPlaylistUpdated(DefaultHlsPlaylistTracker.java:428)
at androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker.access$1500(DefaultHlsPlaylistTracker.java:54)
at androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker$MediaPlaylistBundle.processLoadedPlaylist(DefaultHlsPlaylistTracker.java:726)
at androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker$MediaPlaylistBundle.access$200(DefaultHlsPlaylistTracker.java:514)
at androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker.onLoadCompleted(DefaultHlsPlaylistTracker.java:276)
at androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker.onLoadCompleted(DefaultHlsPlaylistTracker.java:53)
at androidx.media3.exoplayer.upstream.Loader$LoadTask.handleMessage(Loader.java:487)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:317)
at android.os.HandlerThread.run(HandlerThread.java:85)
Media
Just construct a m3u8 playlist which doesn't include any segments like this:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:0
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-ENDLIST
Bug Report
- You will email the zip file produced by
adb bugreport
to android-media-github@google.com after filing this issue.
Just construct a m3u8 playlist which doesn't include any segments like this:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-TARGETDURATION:0 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-DISCONTINUITY-SEQUENCE:0 #EXT-X-ENDLIST
I don't think this is a valid HLS playlist. The HLS spec section 4.1 defines two types of playlist:
A Playlist is a Media Playlist if all URI lines in the Playlist identify Media Segments. A Playlist is a Multivariant Playlist if all URI lines in the Playlist identify Media Playlists. A Playlist MUST be either a Media Playlist or a Multivariant Playlist; all other Playlists are invalid.
The playlist you've provided has zero 'URI lines'. This means that both "all URI lines in the Playlist identify Media Segments" and "all URI lines in the Playlist identify Media Playlists" are vacuously true. This then means that the playlist is both a Media Playlist and a Multivariant Playlist, and is therefore invalid:
A Playlist MUST be either a Media Playlist or a Multivariant Playlist; all other Playlists are invalid.
This is actually appeared as a Media Playlist provided by some streaming server. Before that, there's a valid Multivariant Playlist in front of that and provided several Media Playlists, but all of the Media Playlists didn't contain segments just like this.
Although this Media Playlist comes to be invalid, ExoPlayer should not access any element of the empty segments list which is causing an IndexOutOfBoundsException. In case the segments
list is empty, the get() method should not be called.
This is triggered by a bad media, but obviously, it is a bug that shows the HLS module is lacking robustness for some input.
ExoPlayer's behaviour when presented with invalid input media is not defined (so the exception you're seeing is somewhat expected) - we expect the media to conform to the relevant specs. In this case I'd suggest asking the streaming server to stop producing invalid HLS playlists.
Section 4 of the HLS spec makes it clear that ExoPlayer is correct in failing when parsing this invalid playlist:
Playlists that violate these rules are invalid; clients MUST fail to parse them.
No, ExoPlayer is not acting as failing when parsing this invalid playlist.
For this malformed m3u8 playlist, the HlsPlaylistParser didn't throw any exceptions to notify the failure of parsing. Actually, Failure did not occur during parsing. The parsing for this kind of playlist is passed without any warning or error.
Playlists that violate these rules are invalid; clients MUST fail to parse them.
ExoPlayer does not fail to parse it at this time.
I'll repeat my claim: It is a bug that shows the HLS module is lacking robustness for some input.
The IndexOutOfBoundsException shouldn't occur. Invalid playlists that violate rules should be refused during parsing.
Can you clarify why it makes a difference to you whether the exception is thrown from HlsPlaylistParser
or a little bit later where it's currently thrown?
HlsPlaylistParser
throws an internal ParserException which will be handled by the upper-layer eventually. Then the upper-layer could report an error to the user by existing interface to notify that the content(playlist) is malformed or invalid.
The IndexOutOfBoundsException here caused the app to crash immediately here, for some unknown reasons.
Playlists that violate these rules are invalid; clients MUST fail to parse them.
I firmly believe that the HlsPlaylistParser
MUST fail to parse this kind of playlist, rather than passing them.
The IndexOutOfBoundsException here caused the app to crash immediately here, for some unknown reasons.
I'm surprised you see the app crash. I would expect the exception to be reported out of Player.Listener.onPlayerError
rather than crashing the app.
I tried this with the demo app, and I don't see the app crash, just a message on screen saying 'playback failed', and the stack trace twice in logcat (once from ExoPlayerImplInternal
and once from EventLogger
).
If you're seeing your app crash, is it possible that you re-throw exceptions from Player.Listener.onPlayerError
?
I also tried adding checkState(!segments.isEmpty())
to HlsPlaylistParser
, in order to implement your requested change, and it is also routed out of Player.Listener.onPlayerError
- so I'm afraid I'm not really sure I see the difference.