From d38c6c84a6f66d79e84622c0ab7fe488fe95929f Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 23 Mar 2020 08:46:48 +0000 Subject: [PATCH] Fix bug in MaskingMediaSource caused by removed periods. If MaskingMediaSource masks a multi-window media source, it may be that a period is removed while we are using an initial unprepared masking MediaPeriod. That means it's not guaranteed that a timeline update still contains our unpreparedMaskingMediaPeriod and we should ignore timeline updates where the period is no longer present because the it will be removed anyway. PiperOrigin-RevId: 302383787 --- .../exoplayer2/source/MaskingMediaSource.java | 10 +++++- .../android/exoplayer2/ExoPlayerTest.java | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) 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);