Repository navigation
java.lang.ArrayIndexOutOfBoundsException: length=0; index=0 on live streams with preroll advert when stop player #3423
Copy link
Copy link
Closed
Labels
Description
Version
Media3 1.11.1
More version details
Reproducible from different versions (for example 1.8.1, 1.9.4, 1.10.1, 1.11.1)
Devices that reproduce the issue
Device independent, but there are some examples:
Dreame Smart 4KTV (android 14)
Honor X6c (android 15)
Samsung Galaxy A07 (android 16)
Devices that do not reproduce the issue
N/A
Reproducible in the demo app?
Not tested
Reproduction steps
We have a video-based application with custom ads logic (not IMA).
High level idea for reproducing:
- you have ExoPlayer with analytics listener added
- you have a live stream content (endless hls stream) with preroll advert (advert at 0 position)
- when preroll is playing you need to stop player (reason from real life - user doesn't want watch advert and closes player) and after this you need to do anything with player (in our case there is volume ajustment).
There is simplified test with robolectric but you can try to reproduce on real player:
Test (robolectric) code with comments
// Required libs:
// androidx.media3:media3-test-utils: 1.11.1
// androidx.media3:media3-test-utils-robolectric:1.11.1
// androidx.media3:media3-exoplayer-hls:1.11.1
@RunWith(RobolectricTestRunner::class)
class LiveAdStopAnalyticsCrashTest {
private companion object {
const val LIVE_URL = "https://demo.unified-streaming.com/k8s/live/scte35.isml/.m3u8"
const val VOD_URL = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
const val AD_URL = "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4"
val PREROLL_ONLY_AD_STATE = AdPlaybackState(1234, 0L).withAdCount(0, 1)
val PREROLL_MEDIA_ITEM = MediaItem.fromUri(AD_URL)
}
private class SimpleAdsLoader : AdsLoader {
private var listener: AdsLoader.EventListener? = null
private var adPlaybackState: AdPlaybackState? = null
override fun start(s: AdsMediaSource, d: DataSpec, adsId: Any, v: AdViewProvider, l: AdsLoader.EventListener) {
listener = l
adPlaybackState = PREROLL_ONLY_AD_STATE
l.onAdPlaybackState(adPlaybackState!!)
}
fun showAdNow() {
adPlaybackState = adPlaybackState!!.withAvailableAdMediaItem(0, 0, PREROLL_MEDIA_ITEM)
listener!!.onAdPlaybackState(adPlaybackState!!)
}
override fun stop(s: AdsMediaSource, l: AdsLoader.EventListener) = Unit
override fun setPlayer(p: Player?) = Unit
override fun release() = Unit
override fun setSupportedContentTypes(vararg c: Int) = Unit
override fun handlePrepareComplete(s: AdsMediaSource, g: Int, i: Int) = Unit
override fun handlePrepareError(s: AdsMediaSource, g: Int, i: Int, e: IOException) = Unit
}
/** Lets the player run in real time (the test plays real streams over the network). */
private fun ExoPlayer.runFor(millis: Long) {
val end = System.currentTimeMillis() + millis
while (System.currentTimeMillis() < end) {
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(50))
Thread.sleep(50)
}
}
private fun ExoPlayer.waitUntilStreamIsLoaded() {
val window = Timeline.Window()
val deadline = System.currentTimeMillis() + 20_000
while (currentTimeline.isEmpty || currentTimeline.getWindow(0, window).isPlaceholder) {
check(System.currentTimeMillis() < deadline) { "stream was not loaded" }
runFor(50)
}
}
private fun userClosesPlayerDuringAd(streamUrl: String) {
val context = RuntimeEnvironment.getApplication()
val adsLoader = SimpleAdsLoader()
// 1. Create the player
val player = TestExoPlayerBuilder(context)
.setClock(Clock.DEFAULT)
.setMediaSourceFactory(DefaultMediaSourceFactory(context).setLocalAdInsertionComponents({ adsLoader }, AdViewProvider { null }))
.build()
// !! Critical - use listener impl which calls DefaultPlaybackSessionManager inside
player.addAnalyticsListener(PlaybackStatsListener(/* keepHistory= */ false, /* callback= */ null))
// 2. Put a stream (live or regular video) with ads support into the player and start it
player.setMediaItem(
MediaItem.Builder()
.setUri(streamUrl)
.setAdsConfiguration(MediaItem.AdsConfiguration.Builder(Uri.parse("https://example.com/ad-tag")).build())
.build()
)
player.prepare()
player.play()
player.waitUntilStreamIsLoaded()
player.runFor(10)
// 3. The app inserts an ad, the ad starts, the user watches it for 1 second
adsLoader.showAdNow()
TestPlayerRunHelper.run(player).untilPlayingAdIs(true)
player.runFor(1000)
assertTrue("the ad must still be playing", player.isPlayingAd)
// 4. The user gets angry and closes the player
player.stop()
player.runFor(300)
// 5. Example side effect on leaving player: volume is changed
player.volume = 0.0f
// 6. Host app still working...
player.runFor(500) // <- crash for the live stream
player.release()
}
// Will fail
@Test
fun liveStream_userClosesPlayerDuringAd() = userClosesPlayerDuringAd(LIVE_URL)
// Will success
@Test
fun regularVideo_userClosesPlayerDuringAd() = userClosesPlayerDuringAd(VOD_URL)
}Expected result
No player crash (behavior like on VOD or simple video as mp4 for example)
Actual result
Production stacktrace (test stacktrace is a little different in some lines but the idea is the same):
Fatal Exception: java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
at androidx.media3.common.AdPlaybackState.getAdGroup(AdPlaybackState.java:1035)
at androidx.media3.common.Timeline$Period.getAdGroupTimeUs(Timeline.java:754)
at androidx.media3.exoplayer.analytics.DefaultPlaybackSessionManager.updateSessions(DefaultPlaybackSessionManager.java:148)
at androidx.media3.exoplayer.analytics.MediaMetricsListener.maybeAddSessions(MediaMetricsListener.java:334)
at androidx.media3.exoplayer.analytics.MediaMetricsListener.onEvents(MediaMetricsListener.java:311)
at androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector.lambda$setPlayer$0(DefaultAnalyticsCollector.java:138)
at androidx.media3.common.util.ListenerSet$ListenerHolder.iterationFinished(ListenerSet.java:477)
at androidx.media3.common.util.ListenerSet.handleMessage(ListenerSet.java:421)
at android.os.Handler.dispatchMessage(Handler.java:128)
at android.os.Looper.dispatchMessage(Looper.java:358)
at android.os.Looper.loopOnce(Looper.java:288)
at android.os.Looper.loop(Looper.java:392)
at android.app.ActivityThread.main(ActivityThread.java:10346)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:638)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:972)
Test stacktrace:
Index 0 out of bounds for length 0
java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at androidx.media3.common.AdPlaybackState.getAdGroup(AdPlaybackState.java:1118)
at androidx.media3.common.Timeline$Period.getAdGroupTimeUs(Timeline.java:753)
at androidx.media3.exoplayer.analytics.PlaybackStatsListener.findBestEventTime(PlaybackStatsListener.java:325)
at androidx.media3.exoplayer.analytics.PlaybackStatsListener.onEvents(PlaybackStatsListener.java:250)
at androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector.lambda$setPlayer$0(DefaultAnalyticsCollector.java:138)
at androidx.media3.common.util.ListenerSet$ListenerHolder.iterationFinished(ListenerSet.java:477)
at androidx.media3.common.util.ListenerSet.handleMessage(ListenerSet.java:421)
at android.os.Handler.dispatchMessage(Handler.java:106)
at org.robolectric.shadows.ShadowPausedLooper$IdlingRunnable.doRun(ShadowPausedLooper.java:677)
at org.robolectric.shadows.ShadowPausedLooper$IdleForRunnable.doRun(ShadowPausedLooper.java:658)
at org.robolectric.shadows.ShadowPausedLooper$ControlRunnable.run(ShadowPausedLooper.java:599)
at org.robolectric.shadows.ShadowPausedLooper$HandlerExecutor.execute(ShadowPausedLooper.java:849)
at org.robolectric.shadows.ShadowPausedLooper.executeOnLooper(ShadowPausedLooper.java:765)
at org.robolectric.shadows.ShadowPausedLooper.idleFor(ShadowPausedLooper.java:114)
at LiveAdStopAnalyticsCrashTest.runFor(LiveAdStopAnalyticsCrashTest.kt:69)
at LiveAdStopAnalyticsCrashTest.userClosesPlayerDuringAd(LiveAdStopAnalyticsCrashTest.kt:122)
at LiveAdStopAnalyticsCrashTest.liveStream_userClosesPlayerDuringAd(LiveAdStopAnalyticsCrashTest.kt:128)
Possible crash root cause (AI-accelerated investigation):
player.stop()→ExoPlayerImplInternal.resetInternal(releaseMediaSourceList = true)→PlaylistTimeline.copyWithPlaceholderTimeline(). For live windows this replaces the period'sAdPlaybackStatewithAdPlaybackState.NONE(adGroupCount == 0); for non-live windows the ad state is kept (onlyisPlaceholder = true). That is why only live streams crash.resetInternalitself also drops the ad metadata from the player's ownMediaPeriodIdfor live windows, so the player is consistent.DefaultAnalyticsCollector.MediaPeriodQueueTrackeris not:playingMediaPeriod/readingMediaPeriodare@MonotonicNonNulland are not cleared when the queue becomes empty inonQueueUpdated, so they still hold the adMediaPeriodId(adGroupIndex = 0).updateMediaPeriodTimelines(preferredTimeline = player.getCurrentTimeline())/addTimelineForMediaPeriodIdmaps that stale ad id to the new placeholder timeline because the period uid still exists in it. The ad id is now paired with a period that has no ad groups.- Any later event that is created through
generateReadingMediaPeriodEventTime()/generatePlayingMediaPeriodEventTime()(onVolumeChanged,onSurfaceSizeChanged,onVideoDecoderReleased,onAudioDisabled,onDroppedFrames,onRenderedFirstFrame, ...) gets anEventTimewithmediaPeriodId.isAd() == trueand the ad-less timeline. A surface size change explains the rotation we sometimes see before the crash in production. DefaultPlaybackSessionManager.updateSessions(lines 146-148) andPlaybackStatsListener.findBestEventTime(line 325) callperiod.getAdGroupTimeUs(adGroupIndex)on that period →adGroups[0]on an empty array.- Some of these events are created on the playback thread (renderer events) while
MediaPeriodQueueTrackeris updated from both the playback thread (updateMediaPeriodQueueInfo) and the app thread (onTimelineChanged), so the exact ordering, and therefore the crash, is timing dependent. That matches how sporadic it is in production.
Media
Any endless hls live stream, (see example urls in attached test above)
Bug Report
- You will email the zip file produced by
adb bugreportto android-media-github@google.com after filing this issue.
Reactions are currently unavailable
Activity
Preserve ad timeline after stopping live streams
marcbaechinger commentedon Sep 25, 2026
We pushed the commit above to main that fixes the
ArrayIndexOutOfBoundsException. Thanks for reporting!