diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 28d9369c83..35b3e1848e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -234,7 +234,15 @@ public final class MaskingMediaSource extends CompositeMediaSource { @RequiresNonNull("unpreparedMaskingMediaPeriod") private void setPreparePositionOverrideToUnpreparedMaskingPeriod(long preparePositionOverrideUs) { MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; - long periodDurationUs = timeline.getPeriodByUid(maskingPeriod.id.periodUid, period).durationUs; + int maskingPeriodIndex = timeline.getIndexOfPeriod(maskingPeriod.id.periodUid); + if (maskingPeriodIndex == C.INDEX_UNSET) { + // The new timeline doesn't contain this period anymore. This can happen if the media source + // has multiple periods and removed the first period with a timeline update. Ignore the + // update, as the non-existing period will be released anyway as soon as the player receives + // this new timeline. + return; + } + long periodDurationUs = timeline.getPeriod(maskingPeriodIndex, period).durationUs; if (periodDurationUs != C.TIME_UNSET) { // Ensure the overridden position doesn't exceed the period duration. if (preparePositionOverrideUs >= periodDurationUs) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index dd5df83278..5b17299a53 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -4276,6 +4276,40 @@ public final class ExoPlayerTest { assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions); } + @Test + public void + timelineUpdateInMultiWindowMediaSource_removingPeriod_withUnpreparedMaskingMediaPeriod_doesNotThrow() + throws Exception { + TimelineWindowDefinition window1 = + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1); + TimelineWindowDefinition window2 = + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + // Do something and wait so that the player can create its unprepared MaskingMediaPeriod + .seek(/* positionMs= */ 0) + .waitForSeekProcessed() + // Let the player assign the unprepared period to window1. + .executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window1, window2))) + .waitForTimelineChanged() + // Remove window1 and assume the update is handled without throwing. + .executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window2))) + .waitForTimelineChanged() + .stop() + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + // Assertion is to not throw while running the action schedule above. + } + @Test public void setPlayWhenReady_keepsCurrentPosition() throws Exception { AtomicLong positionAfterSetPlayWhenReady = new AtomicLong(C.TIME_UNSET);