From df0eb2b6d28f5c73cfe1684bbfe60305ac06c59d Mon Sep 17 00:00:00 2001 From: oguzhaneksi Date: Fri, 14 Aug 2026 16:31:55 +0300 Subject: [PATCH 1/2] Fix crash when the live default position moves past a playing SSAI ad --- RELEASENOTES.md | 3 + .../exoplayer/ExoPlayerImplInternal.java | 6 +- .../media3/exoplayer/ExoPlayerAdTest.java | 60 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0fce50000dc..27c3c6dd181 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,9 @@ * Fix an issue where Player.getCurrentPosition() could return stale values (updating only a few times per second) when dynamic scheduling is enabled ([#3286](https://github.com/androidx/media/issues/3286)). + * Fix `ArrayIndexOutOfBoundsException` when a live timeline refresh moves + the default position past a server-side inserted ad that is currently + being played ([#3348](https://github.com/androidx/media/issues/3348)). * CompositionPlayer: * Support configuring the frame rate of video frame aggregation via `Composition.Builder.setVideoFrameAggregationParameters` for playback diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index c395bbbb50b..d1841abd735 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -4096,14 +4096,16 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( && isOldAdGroupWithinNewPeriod && earliestAdGroupIsUnchangedOrLater; // Drop update if the change is from/to server-side inserted ads at the same content position to - // avoid any unintentional renderer reset. + // avoid any unintentional renderer reset. Note that the resolved period may be a period + // preceding newPeriodUid, because resolveMediaPeriodIdForAdsAfterPeriodPositionChange rolls + // back to unplayed ad periods, so the period has to be looked up by the resolved period uid. boolean isInStreamAdChange = isIgnorableServerSideAdInsertionPeriodChange( isUsingPlaceholderPeriod, oldPeriodId, oldContentPositionUs, periodIdWithAds, - timeline.getPeriodByUid(newPeriodUid, period), + timeline.getPeriodByUid(periodIdWithAds.periodUid, period), newContentPositionUs); MediaPeriodId newPeriodId = onlyNextAdGroupIndexIncreased || isInStreamAdChange ? oldPeriodId : periodIdWithAds; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java index d4fe1a9835f..f32167e9ea9 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java @@ -1058,6 +1058,66 @@ public void adInMovingLiveWindow_keepsContentPosition() throws Exception { assertThat(contentPositionAfterLiveWindowUpdateMs).isEqualTo(2000); } + @Test + public void timelineRefresh_movingLiveDefaultPositionPastPlayingSsaiAd_keepsPlayingAd() + throws Exception { + // Live window with three 20s periods: |- p0 content -|- p1 ad -|- p2 content -|. + Object adsId = new Object(); + TimelineWindowDefinition liveWindowDefinition = + new TimelineWindowDefinition.Builder() + .setDynamic(true) + .setLive(true) + .setSeekable(true) + .setPeriodCount(3) + .setDurationUs(60_000_000) + .setWindowStartTimeUs(1_720_000_000_000_000L) + .setWindowPositionInFirstPeriodUs(0) + .setDefaultPositionUs(30_000_000) + .build(); + Timeline initialContentTimeline = new FakeTimeline(liveWindowDefinition); + // p1 is entirely covered by a server-side inserted ad. + AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId); + AdPlaybackState adPeriodAdPlaybackState = + addAdGroupToAdPlaybackState( + contentOnlyAdPlaybackState, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 20_000_000, + /* adDurationsUs...= */ 20_000_000); + // ServerSideAdInsertionMediaSource requires an AdPlaybackState for every period. + ImmutableMap adPlaybackStates = + ImmutableMap.of( + initialContentTimeline.getUidOfPeriod(/* periodIndex= */ 0), + contentOnlyAdPlaybackState, + initialContentTimeline.getUidOfPeriod(/* periodIndex= */ 1), + adPeriodAdPlaybackState, + initialContentTimeline.getUidOfPeriod(/* periodIndex= */ 2), + contentOnlyAdPlaybackState); + FakeMediaSource contentMediaSource = new FakeMediaSource(initialContentTimeline); + ServerSideAdInsertionMediaSource mediaSource = + new ServerSideAdInsertionMediaSource( + contentMediaSource, /* adPlaybackStateUpdater= */ contentTimeline -> false); + mediaSource.setAdPlaybackStates(adPlaybackStates, initialContentTimeline); + ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + + // Join the live stream while the ad in p1 is on air. + player.setMediaSource(mediaSource); + player.prepare(); + advance(player).untilState(Player.STATE_READY); + boolean isPlayingAdAfterJoining = player.isPlayingAd(); + // Refresh the live timeline with a default position that moved past the ad, into p2. + contentMediaSource.setNewSourceInfo( + new FakeTimeline( + liveWindowDefinition.buildUpon().setDefaultPositionUs(45_000_000).build())); + advance(player).untilPendingCommandsAreFullyHandled(); + boolean isPlayingAdAfterRefresh = player.isPlayingAd(); + @Nullable PlaybackException error = player.getPlayerError(); + player.release(); + + assertThat(isPlayingAdAfterJoining).isTrue(); + assertThat(error).isNull(); + assertThat(isPlayingAdAfterRefresh).isTrue(); + } + @Test public void addMediaSource_whilePlayingAd_correctMasking() throws Exception { long contentDurationMs = 10_000; From 29ffafd8c0fedba8055afc27ca02243eea70c1a2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 14 Aug 2026 17:04:06 +0100 Subject: [PATCH 2/2] Revert comment change in ExoPlayerImplInternal --- .../java/androidx/media3/exoplayer/ExoPlayerImplInternal.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index d1841abd735..e9d95dbe7b4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -4096,9 +4096,7 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( && isOldAdGroupWithinNewPeriod && earliestAdGroupIsUnchangedOrLater; // Drop update if the change is from/to server-side inserted ads at the same content position to - // avoid any unintentional renderer reset. Note that the resolved period may be a period - // preceding newPeriodUid, because resolveMediaPeriodIdForAdsAfterPeriodPositionChange rolls - // back to unplayed ad periods, so the period has to be looked up by the resolved period uid. + // avoid any unintentional renderer reset. boolean isInStreamAdChange = isIgnorableServerSideAdInsertionPeriodChange( isUsingPlaceholderPeriod,