From f2b1d0cfa3e5885fb2c8c7869f19dd69fa76cc49 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2023 02:11:26 -0700 Subject: [PATCH] Move TimeOffsetMediaPeriod to its own class This makes it reusable for other MediaSource/Periods in the same package. Issue: google/ExoPlayer#11226 PiperOrigin-RevId: 563687935 --- .../media3/exoplayer/LoadingInfo.java | 21 ++ .../exoplayer/source/MergingMediaPeriod.java | 180 +----------- .../source/TimeOffsetMediaPeriod.java | 217 ++++++++++++++ .../source/TimeOffsetMediaPeriodTest.java | 264 ++++++++++++++++++ 4 files changed, 503 insertions(+), 179 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriod.java create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriodTest.java diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java index ebbd8f2b9b..0907ef9266 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadingInfo.java @@ -18,8 +18,10 @@ package androidx.media3.exoplayer; import static androidx.media3.common.util.Assertions.checkArgument; import android.os.SystemClock; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.util.UnstableApi; +import com.google.common.base.Objects; import com.google.errorprone.annotations.CanIgnoreReturnValue; /** Information about the player state when loading is started or continued. */ @@ -133,4 +135,23 @@ public final class LoadingInfo { && realtimeMs != C.TIME_UNSET && lastRebufferRealtimeMs >= realtimeMs; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof LoadingInfo)) { + return false; + } + LoadingInfo that = (LoadingInfo) o; + return playbackPositionUs == that.playbackPositionUs + && playbackSpeed == that.playbackSpeed + && lastRebufferRealtimeMs == that.lastRebufferRealtimeMs; + } + + @Override + public int hashCode() { + return Objects.hashCode(playbackPositionUs, playbackSpeed, lastRebufferRealtimeMs); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java index 858b412034..6e910e4b4a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java @@ -20,12 +20,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; -import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.NullableType; -import androidx.media3.decoder.DecoderInputBuffer; -import androidx.media3.exoplayer.FormatHolder; import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.SeekParameters; import androidx.media3.exoplayer.source.chunk.Chunk; @@ -38,7 +35,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Merges multiple {@link MediaPeriod}s. */ /* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { @@ -80,7 +76,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public MediaPeriod getChildPeriod(int index) { return periods[index] instanceof TimeOffsetMediaPeriod - ? ((TimeOffsetMediaPeriod) periods[index]).mediaPeriod + ? ((TimeOffsetMediaPeriod) periods[index]).getWrappedMediaPeriod() : periods[index]; } @@ -302,180 +298,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } - private static final class TimeOffsetMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - private final MediaPeriod mediaPeriod; - private final long timeOffsetUs; - - private @MonotonicNonNull Callback callback; - - public TimeOffsetMediaPeriod(MediaPeriod mediaPeriod, long timeOffsetUs) { - this.mediaPeriod = mediaPeriod; - this.timeOffsetUs = timeOffsetUs; - } - - @Override - public void prepare(Callback callback, long positionUs) { - this.callback = callback; - mediaPeriod.prepare(/* callback= */ this, positionUs - timeOffsetUs); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - mediaPeriod.maybeThrowPrepareError(); - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public List getStreamKeys(List trackSelections) { - return mediaPeriod.getStreamKeys(trackSelections); - } - - @Override - public long selectTracks( - @NullableType ExoTrackSelection[] selections, - boolean[] mayRetainStreamFlags, - @NullableType SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs) { - @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; - for (int i = 0; i < streams.length; i++) { - TimeOffsetSampleStream sampleStream = (TimeOffsetSampleStream) streams[i]; - childStreams[i] = sampleStream != null ? sampleStream.getChildStream() : null; - } - long startPositionUs = - mediaPeriod.selectTracks( - selections, - mayRetainStreamFlags, - childStreams, - streamResetFlags, - positionUs - timeOffsetUs); - for (int i = 0; i < streams.length; i++) { - @Nullable SampleStream childStream = childStreams[i]; - if (childStream == null) { - streams[i] = null; - } else if (streams[i] == null - || ((TimeOffsetSampleStream) streams[i]).getChildStream() != childStream) { - streams[i] = new TimeOffsetSampleStream(childStream, timeOffsetUs); - } - } - return startPositionUs + timeOffsetUs; - } - - @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs - timeOffsetUs, toKeyframe); - } - - @Override - public long readDiscontinuity() { - long discontinuityPositionUs = mediaPeriod.readDiscontinuity(); - return discontinuityPositionUs == C.TIME_UNSET - ? C.TIME_UNSET - : discontinuityPositionUs + timeOffsetUs; - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs - timeOffsetUs) + timeOffsetUs; - } - - @Override - public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs(positionUs - timeOffsetUs, seekParameters) - + timeOffsetUs; - } - - @Override - public long getBufferedPositionUs() { - long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); - return bufferedPositionUs == C.TIME_END_OF_SOURCE - ? C.TIME_END_OF_SOURCE - : bufferedPositionUs + timeOffsetUs; - } - - @Override - public long getNextLoadPositionUs() { - long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); - return nextLoadPositionUs == C.TIME_END_OF_SOURCE - ? C.TIME_END_OF_SOURCE - : nextLoadPositionUs + timeOffsetUs; - } - - @Override - public boolean continueLoading(LoadingInfo loadingInfo) { - return mediaPeriod.continueLoading( - loadingInfo - .buildUpon() - .setPlaybackPositionUs(loadingInfo.playbackPositionUs - timeOffsetUs) - .build()); - } - - @Override - public boolean isLoading() { - return mediaPeriod.isLoading(); - } - - @Override - public void reevaluateBuffer(long positionUs) { - mediaPeriod.reevaluateBuffer(positionUs - timeOffsetUs); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - Assertions.checkNotNull(callback).onPrepared(/* mediaPeriod= */ this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - Assertions.checkNotNull(callback).onContinueLoadingRequested(/* source= */ this); - } - } - - private static final class TimeOffsetSampleStream implements SampleStream { - - private final SampleStream sampleStream; - private final long timeOffsetUs; - - public TimeOffsetSampleStream(SampleStream sampleStream, long timeOffsetUs) { - this.sampleStream = sampleStream; - this.timeOffsetUs = timeOffsetUs; - } - - public SampleStream getChildStream() { - return sampleStream; - } - - @Override - public boolean isReady() { - return sampleStream.isReady(); - } - - @Override - public void maybeThrowError() throws IOException { - sampleStream.maybeThrowError(); - } - - @Override - public int readData( - FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) { - int readResult = sampleStream.readData(formatHolder, buffer, readFlags); - if (readResult == C.RESULT_BUFFER_READ) { - buffer.timeUs = buffer.timeUs + timeOffsetUs; - } - return readResult; - } - - @Override - public int skipData(long positionUs) { - return sampleStream.skipData(positionUs - timeOffsetUs); - } - } - private static final class ForwardingTrackSelection implements ExoTrackSelection { private final ExoTrackSelection trackSelection; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriod.java new file mode 100644 index 0000000000..5be71f9722 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriod.java @@ -0,0 +1,217 @@ +/* + * Copyright 2023 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 androidx.media3.exoplayer.source; + +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.StreamKey; +import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.NullableType; +import androidx.media3.decoder.DecoderInputBuffer; +import androidx.media3.exoplayer.FormatHolder; +import androidx.media3.exoplayer.LoadingInfo; +import androidx.media3.exoplayer.SeekParameters; +import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import java.io.IOException; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link MediaPeriod} that applies a fixed time offset to all timestamps */ +/* package */ final class TimeOffsetMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + private final MediaPeriod mediaPeriod; + private final long timeOffsetUs; + + private @MonotonicNonNull Callback callback; + + /** + * Create the time offset period. + * + * @param mediaPeriod The wrapped {@link MediaPeriod}. + * @param timeOffsetUs The offset to apply to all timestamps coming from the wrapped period, in + * microseconds. + */ + public TimeOffsetMediaPeriod(MediaPeriod mediaPeriod, long timeOffsetUs) { + this.mediaPeriod = mediaPeriod; + this.timeOffsetUs = timeOffsetUs; + } + + /** Returns the wrapped {@link MediaPeriod}. */ + public MediaPeriod getWrappedMediaPeriod() { + return mediaPeriod; + } + + @Override + public void prepare(Callback callback, long positionUs) { + this.callback = callback; + mediaPeriod.prepare(/* callback= */ this, positionUs - timeOffsetUs); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + mediaPeriod.maybeThrowPrepareError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public List getStreamKeys(List trackSelections) { + return mediaPeriod.getStreamKeys(trackSelections); + } + + @Override + public long selectTracks( + @NullableType ExoTrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; + for (int i = 0; i < streams.length; i++) { + TimeOffsetSampleStream sampleStream = (TimeOffsetSampleStream) streams[i]; + childStreams[i] = sampleStream != null ? sampleStream.getChildStream() : null; + } + long startPositionUs = + mediaPeriod.selectTracks( + selections, + mayRetainStreamFlags, + childStreams, + streamResetFlags, + positionUs - timeOffsetUs); + for (int i = 0; i < streams.length; i++) { + @Nullable SampleStream childStream = childStreams[i]; + if (childStream == null) { + streams[i] = null; + } else if (streams[i] == null + || ((TimeOffsetSampleStream) streams[i]).getChildStream() != childStream) { + streams[i] = new TimeOffsetSampleStream(childStream, timeOffsetUs); + } + } + return startPositionUs + timeOffsetUs; + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs - timeOffsetUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + long discontinuityPositionUs = mediaPeriod.readDiscontinuity(); + return discontinuityPositionUs == C.TIME_UNSET + ? C.TIME_UNSET + : discontinuityPositionUs + timeOffsetUs; + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs - timeOffsetUs) + timeOffsetUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs(positionUs - timeOffsetUs, seekParameters) + + timeOffsetUs; + } + + @Override + public long getBufferedPositionUs() { + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + return bufferedPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : bufferedPositionUs + timeOffsetUs; + } + + @Override + public long getNextLoadPositionUs() { + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + return nextLoadPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : nextLoadPositionUs + timeOffsetUs; + } + + @Override + public boolean continueLoading(LoadingInfo loadingInfo) { + return mediaPeriod.continueLoading( + loadingInfo + .buildUpon() + .setPlaybackPositionUs(loadingInfo.playbackPositionUs - timeOffsetUs) + .build()); + } + + @Override + public boolean isLoading() { + return mediaPeriod.isLoading(); + } + + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs - timeOffsetUs); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + Assertions.checkNotNull(callback).onPrepared(/* mediaPeriod= */ this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + Assertions.checkNotNull(callback).onContinueLoadingRequested(/* source= */ this); + } + + private static final class TimeOffsetSampleStream implements SampleStream { + + private final SampleStream sampleStream; + private final long timeOffsetUs; + + public TimeOffsetSampleStream(SampleStream sampleStream, long timeOffsetUs) { + this.sampleStream = sampleStream; + this.timeOffsetUs = timeOffsetUs; + } + + public SampleStream getChildStream() { + return sampleStream; + } + + @Override + public boolean isReady() { + return sampleStream.isReady(); + } + + @Override + public void maybeThrowError() throws IOException { + sampleStream.maybeThrowError(); + } + + @Override + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) { + int readResult = sampleStream.readData(formatHolder, buffer, readFlags); + if (readResult == C.RESULT_BUFFER_READ) { + buffer.timeUs = buffer.timeUs + timeOffsetUs; + } + return readResult; + } + + @Override + public int skipData(long positionUs) { + return sampleStream.skipData(positionUs - timeOffsetUs); + } + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriodTest.java new file mode 100644 index 0000000000..0168cda562 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TimeOffsetMediaPeriodTest.java @@ -0,0 +1,264 @@ +/* + * Copyright 2023 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 androidx.media3.exoplayer.source; + +import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT; +import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; +import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.TrackGroup; +import androidx.media3.decoder.DecoderInputBuffer; +import androidx.media3.exoplayer.FormatHolder; +import androidx.media3.exoplayer.LoadingInfo; +import androidx.media3.exoplayer.SeekParameters; +import androidx.media3.exoplayer.drm.DrmSessionEventListener; +import androidx.media3.exoplayer.drm.DrmSessionManager; +import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; +import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import androidx.media3.exoplayer.trackselection.FixedTrackSelection; +import androidx.media3.exoplayer.upstream.DefaultAllocator; +import androidx.media3.test.utils.FakeMediaPeriod; +import androidx.media3.test.utils.FakeSampleStream; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.CountDownLatch; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TimeOffsetMediaPeriod}. */ +@RunWith(AndroidJUnit4.class) +public final class TimeOffsetMediaPeriodTest { + + @Test + public void selectTracks_createsSampleStreamCorrectingOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(fakeMediaPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 0); + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer inputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + ImmutableList.Builder readResults = ImmutableList.builder(); + + SampleStream sampleStream = selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + readResults.add(sampleStream.readData(formatHolder, inputBuffer, FLAG_REQUIRE_FORMAT)); + readResults.add(sampleStream.readData(formatHolder, inputBuffer, /* readFlags= */ 0)); + long readBufferTimeUs = inputBuffer.timeUs; + readResults.add(sampleStream.readData(formatHolder, inputBuffer, /* readFlags= */ 0)); + boolean readEndOfStreamBuffer = inputBuffer.isEndOfStream(); + + assertThat(readResults.build()) + .containsExactly(C.RESULT_FORMAT_READ, C.RESULT_BUFFER_READ, C.RESULT_BUFFER_READ); + assertThat(readBufferTimeUs).isEqualTo(5000); + assertThat(readEndOfStreamBuffer).isTrue(); + } + + @Test + public void getBufferedPositionUs_returnsPositionWithOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 12000, C.BUFFER_FLAG_KEY_FRAME))); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(fakeMediaPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 0); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + assertThat(timeOffsetMediaPeriod.getBufferedPositionUs()).isEqualTo(9000); + } + + @Test + public void getNextLoadPositionUs_returnsPositionWithOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 12000, C.BUFFER_FLAG_KEY_FRAME))); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(fakeMediaPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 0); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + assertThat(timeOffsetMediaPeriod.getNextLoadPositionUs()).isEqualTo(9000); + } + + @Test + public void prepare_isForwardedWithTimeOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + MediaPeriod spyPeriod = spy(fakeMediaPeriod); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000); + + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000); + + verify(spyPeriod).prepare(any(), eq(4000L)); + } + + @Test + public void discardBuffer_isForwardedWithTimeOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + MediaPeriod spyPeriod = spy(fakeMediaPeriod); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + timeOffsetMediaPeriod.discardBuffer(/* positionUs= */ 1000, /* toKeyframe= */ true); + + verify(spyPeriod).discardBuffer(4000, true); + } + + @Test + public void seekTo_isForwardedWithTimeOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + MediaPeriod spyPeriod = spy(fakeMediaPeriod); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + long seekResultTimeUs = timeOffsetMediaPeriod.seekToUs(/* positionUs= */ 1000); + + verify(spyPeriod).seekToUs(4000); + assertThat(seekResultTimeUs).isEqualTo(1000); + } + + @Test + public void getAdjustedSeekPosition_isForwardedWithTimeOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + fakeMediaPeriod.setSeekToUsOffset(2000); + MediaPeriod spyPeriod = spy(fakeMediaPeriod); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + long adjustedSeekPositionUs = + timeOffsetMediaPeriod.getAdjustedSeekPositionUs( + /* positionUs= */ 1000, SeekParameters.DEFAULT); + + verify(spyPeriod).getAdjustedSeekPositionUs(4000, SeekParameters.DEFAULT); + assertThat(adjustedSeekPositionUs).isEqualTo(3000); // = 4000 + 2000 - 3000 + } + + @Test + public void continueLoading_isForwardedWithTimeOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + MediaPeriod spyPeriod = spy(fakeMediaPeriod); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + timeOffsetMediaPeriod.continueLoading( + new LoadingInfo.Builder().setPlaybackPositionUs(1000).build()); + + verify(spyPeriod) + .continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(4000).build()); + } + + @Test + public void reevaluateBuffer_isForwardedWithTimeOffset() throws Exception { + FakeMediaPeriod fakeMediaPeriod = + createFakeMediaPeriod( + ImmutableList.of( + oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + MediaPeriod spyPeriod = spy(fakeMediaPeriod); + TimeOffsetMediaPeriod timeOffsetMediaPeriod = + new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000); + prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000); + selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod); + + timeOffsetMediaPeriod.reevaluateBuffer(/* positionUs= */ 1000); + + verify(spyPeriod).reevaluateBuffer(4000); + } + + private static FakeMediaPeriod createFakeMediaPeriod( + ImmutableList sampleStreamItems) { + EventDispatcher eventDispatcher = + new EventDispatcher() + .withParameters(/* windowIndex= */ 0, new MediaPeriodId(/* periodUid= */ new Object())); + return new FakeMediaPeriod( + new TrackGroupArray(new TrackGroup(new Format.Builder().build())), + new DefaultAllocator(/* trimOnReset= */ false, /* individualAllocationSize= */ 1024), + (unusedFormat, unusedMediaPeriodId) -> sampleStreamItems, + eventDispatcher, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* deferOnPrepared= */ false); + } + + private static void prepareMediaPeriodSync(MediaPeriod mediaPeriod, long positionUs) + throws Exception { + CountDownLatch prepareCountDown = new CountDownLatch(1); + mediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + prepareCountDown.countDown(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + mediaPeriod.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build()); + } + }, + positionUs); + prepareCountDown.await(); + } + + private static SampleStream selectTracksOnMediaPeriodAndTriggerLoading(MediaPeriod mediaPeriod) { + ExoTrackSelection selection = + new FixedTrackSelection(mediaPeriod.getTrackGroups().get(0), /* track= */ 0); + SampleStream[] streams = new SampleStream[1]; + mediaPeriod.selectTracks( + /* selections= */ new ExoTrackSelection[] {selection}, + /* mayRetainStreamFlags= */ new boolean[] {false}, + streams, + /* streamResetFlags= */ new boolean[] {false}, + /* positionUs= */ 0); + mediaPeriod.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build()); + return streams[0]; + } +}