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 ea3080a49ee..56808244c6e 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 @@ -9373,6 +9373,54 @@ public void noTargetLiveOffsetInMedia_doesNotAdjustLiveOffset() throws Exception assertThat(liveOffsetAtEnd).isIn(Range.closed(11_900L, 12_100L)); } + @Test + public void seekTo_toPositionZero_overridesLiveStartPosition() + throws Exception { + ExoPlayer player = + new TestExoPlayerBuilder(context) + .setClock( + new FakeClock(/* initialTimeMs= */ 20_000, /* isAutoAdvancing= */ true)) + .setUseLazyPreparation(true) // This is default for ExoPlayer.Builder() + .build(); + + // TODO - if you set expectedInitialPositionUs to 0, the test will fail... And it shouldn't + int expectedInitialPositionUs = 10_000; + player.seekTo(expectedInitialPositionUs); + FakeMediaSource mediaSource = new FakeMediaSource(); + player.setMediaSource(mediaSource, false); + player.prepare(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_BUFFERING); + + MediaItem mediaItem = + new MediaItem.Builder().setUri(Uri.EMPTY).build(); + Timeline liveTimeline = + new SinglePeriodTimeline( + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + /* periodDurationUs= */ 1000 * C.MICROS_PER_SECOND, + /* windowDurationUs= */ 1000 * C.MICROS_PER_SECOND, + /* windowPositionInPeriodUs= */ 0, + /* windowDefaultStartPositionUs= */ 20 * C.MICROS_PER_SECOND, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* manifest= */ null, + mediaItem, + mediaItem.liveConfiguration); + mediaSource.setNewSourceInfo(liveTimeline, true); + + runUntilTimelineChanged(player); + + // Trigger EPII to copy current PlaybackInfo to EP - TODO I'll use + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f)); + runUntilPendingCommandsAreFullyHandled(player); + long contentPosition = player.getCurrentPosition(); + player.release(); + + assertThat(contentPosition).isEqualTo(expectedInitialPositionUs); + + } + @Test public void onEvents_correspondToListenerCalls() throws Exception { ExoPlayer player = new TestExoPlayerBuilder(context).build(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MaskingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MaskingMediaSourceTest.java new file mode 100644 index 00000000000..096bd79ec36 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MaskingMediaSourceTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +/** Unit tests for {@link MaskingMediaSource}. */ +@RunWith(AndroidJUnit4.class) +public class MaskingMediaSourceTest { + private static final MediaItem EMPTY_MEDIA_ITEM = + new MediaItem.Builder().setUri(Uri.EMPTY).build(); + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private MediaPeriod mockMediaPeriod; + + + class FakeDynamicTimelineMediaSource extends BaseMediaSource { + + @Override + protected void prepareSourceInternal(TransferListener mediaTransferListener) { + } + + @Override + protected void releaseSourceInternal() { + } + + @Override + public MediaItem getMediaItem() { + return EMPTY_MEDIA_ITEM; + } + + @Override + public void maybeThrowSourceInfoRefreshError() { + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return mockMediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + + } + + public void setNewSourceInfo(Timeline liveTimeline) { + refreshSourceInfo(liveTimeline); + } + } + + @Before + public void setupMocks() { + + } + + @Test + public void onChildSourceInfoRefreshed_withLiveTimeline_initialSeek() throws IOException { + MediaItem mediaItem = + new MediaItem.Builder().setUri(Uri.EMPTY).build(); + Timeline liveTimeline = + new SinglePeriodTimeline( + /* presentationStartTimeMs= */ 0, + /* windowStartTimeMs= */ 0, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + /* periodDurationUs= */ 1000 * C.MICROS_PER_SECOND, + /* windowDurationUs= */ 1000 * C.MICROS_PER_SECOND, + /* windowPositionInPeriodUs= */ 0, + /* windowDefaultStartPositionUs= */ 20 * C.MICROS_PER_SECOND, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* manifest= */ null, + mediaItem, + mediaItem.liveConfiguration); + Object periodId = liveTimeline.getUidOfPeriod(0); + FakeDynamicTimelineMediaSource mediaSource = new FakeDynamicTimelineMediaSource(); + MaskingMediaSource testedMediaSource = new MaskingMediaSource(mediaSource, true); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + testRunner.runOnPlaybackThread(() -> { + testedMediaSource.prepareSourceInternal(null); + }); + + // This is the sequence of calls when EPII: + // - lazy prepares the initial masked media source + // - updatePeriods() creates the first period + // - the Timeline update occurs with a live timeline with start position and duration + + testRunner.prepareSourceLazy(); + + testRunner.runOnPlaybackThread(() -> { + int startPositionUs = 20; // TODO - if this value is 0, the test will fail, of course it should not. + MaskingMediaPeriod period = testedMediaSource.createPeriod(new MediaSource.MediaPeriodId(periodId), null, startPositionUs); + mediaSource.setNewSourceInfo(liveTimeline); + assertThat(period.getPreparePositionOverrideUs()).isEqualTo(startPositionUs); + }); + + + testRunner.releaseSource(); + } finally { + testRunner.release(); + } + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 3bb4e0562d4..480ca0e4a48 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -131,6 +132,17 @@ public Timeline prepareSource() throws IOException { return assertTimelineChangeBlocking(); } + /** + * Prepares source on playback thread without expecting a timeline change. Use for "lazy" prepare + * MediaSource, e.g. {@link MaskingMediaSource} + */ + public void prepareSourceLazy() { + runOnPlaybackThread( + () -> { + mediaSource.prepareSource(mediaSourceListener, /* mediaTransferListener= */ null); + }); + } + /** * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} with a zero * start position on the playback thread, asserting that a non-null {@link MediaPeriod} is