Version
Media3 1.10.1
More version details
1.10.1
Devices that reproduce the issue
All
Any reproduced deterministically in a Robolectric unit test (inlined below) and on physical devices (Android 14) playing a live DASH stream with AWS MediaTailor server-side ad insertion via ServerSideAdInsertionMediaSource.
Devices that do not reproduce the issue
No response
Reproducible in the demo app?
Not tested
Reproduction steps
Scenario: a viewer tunes into a live DASH stream while an ad break is on air and the SSAI ad metadata is already known.
- Prepare a
ServerSideAdInsertionMediaSource wrapping a live, dynamic, multi-period timeline. Mark one mid-window period as a single whole-period server-side ad group (ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState(state, 0, periodDurationUs, periodDurationUs)).
- Let the window default position fall inside that ad period. The player resolves the start position into the ad (
isPlayingAd == true). Do not play any content.
- Refresh the content timeline (normal live manifest refresh) with the default position now past the ad period, inside a following ad-free period.
The refresh crashes the playback thread (stack from 1.10.1):
java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at androidx.media3.common.AdPlaybackState.getAdGroup(AdPlaybackState.java:1035)
at androidx.media3.common.Timeline$Period.isServerSideInsertedAdGroup(Timeline.java:890)
at androidx.media3.exoplayer.ExoPlayerImplInternal.isIgnorableServerSideAdInsertionPeriodChange(ExoPlayerImplInternal.java:3798)
at androidx.media3.exoplayer.ExoPlayerImplInternal.resolvePositionForPlaylistChange(ExoPlayerImplInternal.java:3706)
at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed(ExoPlayerImplInternal.java:2497)
at androidx.media3.exoplayer.ExoPlayerImplInternal.mediaSourceListUpdateRequestedInternal(ExoPlayerImplInternal.java:1054)
at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:803)
Surfaced to the app as ExoPlaybackException: Unexpected runtime error, ending playback. Joining a live channel during an ad break is a very common user action, so the production crash volume is significant.
Self-contained Robolectric test — Media3 (media3-exoplayer, media3-test-utils, media3-test-utils-robolectric), Robolectric and JUnit only:
package be.persgroep.lfvp.libs.player.repro
import androidx.media3.common.AdPlaybackState
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil
import androidx.media3.test.utils.FakeMediaSource
import androidx.media3.test.utils.FakeTimeline
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition
import androidx.media3.test.utils.TestExoPlayerBuilder
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.collect.ImmutableMap
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
/**
* Standalone reproduction of a Media3 1.8.0 crash, written against public Media3 + Robolectric
* test APIs only (no project code) so it can be shared verbatim on the androidx/media tracker.
*
* Scenario (live DASH stream with server-side ad insertion, e.g. AWS MediaTailor):
* 1. A viewer tunes in while an ad break is on air and its SSAI metadata is already known: the
* window default position falls inside a period marked as a server-side ad group, so the
* session starts playing the ad (`isPlayingAd == true`).
* 2. A live manifest refresh moves the window default position past the ad period, into an
* ad-free period. No content has been played since join.
*
* Handling that refresh crashes the playback thread:
*
* ```
* java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
* at androidx.media3.common.AdPlaybackState.getAdGroup
* at androidx.media3.common.Timeline$Period.isServerSideInsertedAdGroup
* at androidx.media3.exoplayer.ExoPlayerImplInternal.isIgnorableServerSideAdInsertionPeriodChange
* at androidx.media3.exoplayer.ExoPlayerImplInternal.resolvePositionForPlaylistChange
* at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed
* ```
*/
@RunWith(AndroidJUnit4::class)
@Config(sdk = [33])
public class Media3LiveSsaiJoinCrashReproTest {
private lateinit var player: ExoPlayer
@After
public fun tearDown() {
player.release()
}
@Test
public fun `manifest refresh while playing the ad the session joined in`() {
// window: ├─ p0 content (0..20s) ─┼─ p1 ad (20..40s) ─┼─ p2 content (40..60s) ─┤
player = TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build()
val initialTimeline = liveTimeline(defaultPositionUs = 30_000_000)
val content = FakeMediaSource(initialTimeline)
val ssai = ServerSideAdInsertionMediaSource(content) { false }
// 1. Join: p1 (20s..40s) is a known 20s server-side ad; the live default position (30s)
// falls inside it, so the session starts playing the ad.
ssai.setAdPlaybackStates(adPlaybackStates(initialTimeline, adPeriodIndex = 1), initialTimeline)
player.setMediaSource(ssai)
player.prepare()
TestPlayerRunHelper.advance(player).untilState(Player.STATE_READY)
assertTrue(player.isPlayingAd)
assertEquals(1, player.currentPeriodIndex)
// 2. Live manifest refresh: the default position moved past the ad period (45s, in p2).
content.setNewSourceInfo(liveTimeline(defaultPositionUs = 45_000_000))
TestPlayerRunHelper.advance(player).untilPendingCommandsAreFullyHandled()
// Asserting on the cause's stack trace puts the ArrayIndexOutOfBoundsException and its
// full ExoPlayerImplInternal stack directly into the failure message.
assertNull(player.playerError?.cause?.stackTraceToString())
}
/** FakeTimeline period uids are Pair(windowUid, periodIndex): stable across refreshes. */
private fun liveTimeline(defaultPositionUs: Long): Timeline =
FakeTimeline(
TimelineWindowDefinition.Builder()
.setUid("window")
.setLive(true)
.setDynamic(true)
.setSeekable(true)
.setPeriodCount(3)
.setDurationUs(60_000_000)
.setWindowStartTimeUs(1_720_000_000_000_000)
.setWindowPositionInFirstPeriodUs(0)
.setDefaultPositionUs(defaultPositionUs)
.build(),
)
/** One state per period, keyed by period uid (`ServerSideAdInsertionTimeline` requires full coverage). */
private fun adPlaybackStates(timeline: Timeline, adPeriodIndex: Int): ImmutableMap<Any, AdPlaybackState> {
val builder = ImmutableMap.builder<Any, AdPlaybackState>()
for (periodIndex in 0 until timeline.periodCount) {
builder.put(
timeline.getUidOfPeriod(periodIndex),
if (periodIndex == adPeriodIndex) {
ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState(
AdPlaybackState(ADS_ID),
/* fromPositionUs = */ 0,
/* contentResumeOffsetUs = */ 20_000_000,
/* adDurationsUs... = */ 20_000_000,
)
} else {
AdPlaybackState(ADS_ID)
},
)
}
return builder.build()
}
private companion object {
const val ADS_ID = "repro-ads-id"
}
}
Expected result
The refresh is applied and the session keeps playing the ad it joined in.
Actual result
ArrayIndexOutOfBoundsException on the playback thread; playback fails.
java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at androidx.media3.common.AdPlaybackState.getAdGroup(AdPlaybackState.java:1035)
at androidx.media3.common.Timeline$Period.isServerSideInsertedAdGroup(Timeline.java:890)
at androidx.media3.exoplayer.ExoPlayerImplInternal.isIgnorableServerSideAdInsertionPeriodChange(ExoPlayerImplInternal.java:3798)
at androidx.media3.exoplayer.ExoPlayerImplInternal.resolvePositionForPlaylistChange(ExoPlayerImplInternal.java:3706)
at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed(ExoPlayerImplInternal.java:2497)
at androidx.media3.exoplayer.ExoPlayerImplInternal.mediaSourceListUpdateRequestedInternal(ExoPlayerImplInternal.java:1054)
at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:803)
Media
Not applicable
Bug Report
Version
Media3 1.10.1
More version details
1.10.1
Devices that reproduce the issue
All
Any reproduced deterministically in a Robolectric unit test (inlined below) and on physical devices (Android 14) playing a live DASH stream with AWS MediaTailor server-side ad insertion via
ServerSideAdInsertionMediaSource.Devices that do not reproduce the issue
No response
Reproducible in the demo app?
Not tested
Reproduction steps
Scenario: a viewer tunes into a live DASH stream while an ad break is on air and the SSAI ad metadata is already known.
ServerSideAdInsertionMediaSourcewrapping a live, dynamic, multi-period timeline. Mark one mid-window period as a single whole-period server-side ad group (ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState(state, 0, periodDurationUs, periodDurationUs)).isPlayingAd == true). Do not play any content.The refresh crashes the playback thread (stack from 1.10.1):
Surfaced to the app as
ExoPlaybackException: Unexpected runtime error, ending playback. Joining a live channel during an ad break is a very common user action, so the production crash volume is significant.Self-contained Robolectric test — Media3 (
media3-exoplayer,media3-test-utils,media3-test-utils-robolectric), Robolectric and JUnit only:Expected result
The refresh is applied and the session keeps playing the ad it joined in.
Actual result
ArrayIndexOutOfBoundsExceptionon the playback thread; playback fails.Media
Not applicable
Bug Report
adb bugreportto android-media-github@google.com after filing this issue.