diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1824847eca..7e93115260 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,9 @@ ([#2863](https://github.com/google/ExoPlayer/issues/2863)). * Add optional automatic `WifiLock` handling to `SimpleExoPlayer` ([#6914](https://github.com/google/ExoPlayer/issues/6914)). + * Add option to `MergingMediaSource` to adjust the time offsets between + the merged sources + ([#6103](https://github.com/google/ExoPlayer/issues/6103)). * Text: * Parse `` and `` tags in WebVTT subtitles (rendering is coming later). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index afa25d6fce..c2e0c478ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -17,22 +17,26 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; +import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Merges multiple {@link MediaPeriod}s. */ /* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - public final MediaPeriod[] periods; - + private final MediaPeriod[] periods; private final IdentityHashMap streamPeriodIndices; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final ArrayList childrenPendingPreparation; @@ -42,7 +46,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private MediaPeriod[] enabledPeriods; private SequenceableLoader compositeSequenceableLoader; - public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + public MergingMediaPeriod( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + long[] periodTimeOffsetsUs, MediaPeriod... periods) { this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; @@ -51,6 +57,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); enabledPeriods = new MediaPeriod[0]; + for (int i = 0; i < periods.length; i++) { + if (periodTimeOffsetsUs[i] != 0) { + this.periods[i] = new TimeOffsetMediaPeriod(periods[i], periodTimeOffsetsUs[i]); + } + } + } + + /** + * Returns the child period passed to {@link + * #MergingMediaPeriod(CompositeSequenceableLoaderFactory, long[], MediaPeriod...)} at the + * specified index. + */ + public MediaPeriod getChildPeriod(int index) { + return periods[index] instanceof TimeOffsetMediaPeriod + ? ((TimeOffsetMediaPeriod) periods[index]).mediaPeriod + : periods[index]; } @Override @@ -181,23 +203,32 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public long readDiscontinuity() { - long positionUs = periods[0].readDiscontinuity(); - // Periods other than the first one are not allowed to report discontinuities. - for (int i = 1; i < periods.length; i++) { - if (periods[i].readDiscontinuity() != C.TIME_UNSET) { - throw new IllegalStateException("Child reported discontinuity."); - } - } - // It must be possible to seek enabled periods to the new position, if there is one. - if (positionUs != C.TIME_UNSET) { - for (MediaPeriod enabledPeriod : enabledPeriods) { - if (enabledPeriod != periods[0] - && enabledPeriod.seekToUs(positionUs) != positionUs) { + long discontinuityUs = C.TIME_UNSET; + for (MediaPeriod period : enabledPeriods) { + long otherDiscontinuityUs = period.readDiscontinuity(); + if (otherDiscontinuityUs != C.TIME_UNSET) { + if (discontinuityUs == C.TIME_UNSET) { + discontinuityUs = otherDiscontinuityUs; + // First reported discontinuity. Seek all previous periods to the new position. + for (MediaPeriod previousPeriod : enabledPeriods) { + if (previousPeriod == period) { + break; + } + if (previousPeriod.seekToUs(discontinuityUs) != discontinuityUs) { + throw new IllegalStateException("Unexpected child seekToUs result."); + } + } + } else if (otherDiscontinuityUs != discontinuityUs) { + throw new IllegalStateException("Conflicting discontinuities."); + } + } else if (discontinuityUs != C.TIME_UNSET) { + // We already have a discontinuity, seek this period to the new position. + if (period.seekToUs(discontinuityUs) != discontinuityUs) { throw new IllegalStateException("Unexpected child seekToUs result."); } } } - return positionUs; + return discontinuityUs; } @Override @@ -253,4 +284,173 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 TrackSelection[] 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(long positionUs) { + return mediaPeriod.continueLoading(positionUs - timeOffsetUs); + } + + @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, boolean formatRequired) { + int readResult = sampleStream.readData(formatHolder, buffer, formatRequired); + if (readResult == C.RESULT_BUFFER_READ) { + buffer.timeUs = Math.max(0, buffer.timeUs + timeOffsetUs); + } + return readResult; + } + + @Override + public int skipData(long positionUs) { + return sampleStream.skipData(positionUs - timeOffsetUs); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index dd7675f3d4..d69c037a5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -66,34 +66,59 @@ public final class MergingMediaSource extends CompositeMediaSource { private static final int PERIOD_COUNT_UNSET = -1; + private final boolean adjustPeriodTimeOffsets; private final MediaSource[] mediaSources; private final Timeline[] timelines; private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int periodCount; + private long[][] periodTimeOffsetsUs; @Nullable private IllegalMergeException mergeError; /** + * Creates a merging media source. + * + *

Offsets between the timestamps in the media sources will not be adjusted. + * * @param mediaSources The {@link MediaSource}s to merge. */ public MergingMediaSource(MediaSource... mediaSources) { - this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + this(/* adjustPeriodTimeOffsets= */ false, mediaSources); } /** - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). + * Creates a merging media source. + * + * @param adjustPeriodTimeOffsets Whether to adjust timestamps of the merged media sources to all + * start at the same time. * @param mediaSources The {@link MediaSource}s to merge. */ - public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + public MergingMediaSource(boolean adjustPeriodTimeOffsets, MediaSource... mediaSources) { + this(adjustPeriodTimeOffsets, new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + } + + /** + * Creates a merging media source. + * + * @param adjustPeriodTimeOffsets Whether to adjust timestamps of the merged media sources to all + * start at the same time. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @param mediaSources The {@link MediaSource}s to merge. + */ + public MergingMediaSource( + boolean adjustPeriodTimeOffsets, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, MediaSource... mediaSources) { + this.adjustPeriodTimeOffsets = adjustPeriodTimeOffsets; this.mediaSources = mediaSources; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); periodCount = PERIOD_COUNT_UNSET; timelines = new Timeline[mediaSources.length]; + periodTimeOffsetsUs = new long[0][]; } @Override @@ -125,16 +150,19 @@ public final class MergingMediaSource extends CompositeMediaSource { for (int i = 0; i < periods.length; i++) { MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); - periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs); + periods[i] = + mediaSources[i].createPeriod( + childMediaPeriodId, allocator, startPositionUs - periodTimeOffsetsUs[periodIndex][i]); } - return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); + return new MergingMediaPeriod( + compositeSequenceableLoaderFactory, periodTimeOffsetsUs[periodIndex], periods); } @Override public void releasePeriod(MediaPeriod mediaPeriod) { MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod; for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i].releasePeriod(mergingPeriod.periods[i]); + mediaSources[i].releasePeriod(mergingPeriod.getChildPeriod(i)); } } @@ -151,15 +179,24 @@ public final class MergingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( Integer id, MediaSource mediaSource, Timeline timeline) { - if (mergeError == null) { - mergeError = checkTimelineMerges(timeline); - } if (mergeError != null) { return; } + if (periodCount == PERIOD_COUNT_UNSET) { + periodCount = timeline.getPeriodCount(); + } else if (timeline.getPeriodCount() != periodCount) { + mergeError = new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH); + return; + } + if (periodTimeOffsetsUs.length == 0) { + periodTimeOffsetsUs = new long[periodCount][timelines.length]; + } pendingTimelineSources.remove(mediaSource); timelines[id] = timeline; if (pendingTimelineSources.isEmpty()) { + if (adjustPeriodTimeOffsets) { + computePeriodTimeOffsets(); + } refreshSourceInfo(timelines[0]); } } @@ -171,14 +208,17 @@ public final class MergingMediaSource extends CompositeMediaSource { return id == 0 ? mediaPeriodId : null; } - @Nullable - private IllegalMergeException checkTimelineMerges(Timeline timeline) { - if (periodCount == PERIOD_COUNT_UNSET) { - periodCount = timeline.getPeriodCount(); - } else if (timeline.getPeriodCount() != periodCount) { - return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH); + private void computePeriodTimeOffsets() { + Timeline.Period period = new Timeline.Period(); + for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + long primaryWindowOffsetUs = + -timelines[0].getPeriod(periodIndex, period).getPositionInWindowUs(); + for (int timelineIndex = 1; timelineIndex < timelines.length; timelineIndex++) { + long secondaryWindowOffsetUs = + -timelines[timelineIndex].getPeriod(periodIndex, period).getPositionInWindowUs(); + periodTimeOffsetsUs[periodIndex][timelineIndex] = + primaryWindowOffsetUs - secondaryWindowOffsetUs; + } } - return null; } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java new file mode 100644 index 0000000000..d201782b53 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2020 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 static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.concurrent.CountDownLatch; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.LooperMode; + +/** Unit test for {@link MergingMediaPeriod}. */ +@RunWith(AndroidJUnit4.class) +@LooperMode(LooperMode.Mode.PAUSED) +public final class MergingMediaPeriodTest { + + private static final Format childFormat11 = new Format.Builder().setId("1_1").build(); + private static final Format childFormat12 = new Format.Builder().setId("1_2").build(); + private static final Format childFormat21 = new Format.Builder().setId("2_1").build(); + private static final Format childFormat22 = new Format.Builder().setId("2_2").build(); + + @Test + public void getTrackGroups_returnsAllChildTrackGroups() throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat11, childFormat12), + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat21, childFormat22)); + + assertThat(mergingMediaPeriod.getTrackGroups().length).isEqualTo(4); + assertThat(mergingMediaPeriod.getTrackGroups().get(0).getFormat(0)).isEqualTo(childFormat11); + assertThat(mergingMediaPeriod.getTrackGroups().get(1).getFormat(0)).isEqualTo(childFormat12); + assertThat(mergingMediaPeriod.getTrackGroups().get(2).getFormat(0)).isEqualTo(childFormat21); + assertThat(mergingMediaPeriod.getTrackGroups().get(3).getFormat(0)).isEqualTo(childFormat22); + } + + @Test + public void selectTracks_createsSampleStreamsFromChildPeriods() throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat11, childFormat12), + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat21, childFormat22)); + + TrackSelection selectionForChild1 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(1), /* track= */ 0); + TrackSelection selectionForChild2 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(2), /* track= */ 0); + SampleStream[] streams = new SampleStream[4]; + mergingMediaPeriod.selectTracks( + /* selections= */ new TrackSelection[] {null, selectionForChild1, selectionForChild2, null}, + /* mayRetainStreamFlags= */ new boolean[] {false, false, false, false}, + streams, + /* streamResetFlags= */ new boolean[] {false, false, false, false}, + /* positionUs= */ 0); + + assertThat(streams[0]).isNull(); + assertThat(streams[3]).isNull(); + + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer inputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + assertThat(streams[1].readData(formatHolder, inputBuffer, /* formatRequired= */ true)) + .isEqualTo(C.RESULT_FORMAT_READ); + assertThat(formatHolder.format).isEqualTo(childFormat12); + + assertThat(streams[2].readData(formatHolder, inputBuffer, /* formatRequired= */ true)) + .isEqualTo(C.RESULT_FORMAT_READ); + assertThat(formatHolder.format).isEqualTo(childFormat21); + } + + @Test + public void + selectTracks_withPeriodOffsets_selectTracksWithOffset_andCreatesSampleStreamsCorrectingOffset() + throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat11, childFormat12), + new MergingPeriodDefinition(/* timeOffsetUs= */ -3000, childFormat21, childFormat22)); + + TrackSelection selectionForChild1 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(0), /* track= */ 0); + TrackSelection selectionForChild2 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(2), /* track= */ 0); + SampleStream[] streams = new SampleStream[2]; + mergingMediaPeriod.selectTracks( + /* selections= */ new TrackSelection[] {selectionForChild1, selectionForChild2}, + /* mayRetainStreamFlags= */ new boolean[] {false, false}, + streams, + /* streamResetFlags= */ new boolean[] {false, false}, + /* positionUs= */ 0); + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer inputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + streams[0].readData(formatHolder, inputBuffer, /* formatRequired= */ true); + streams[1].readData(formatHolder, inputBuffer, /* formatRequired= */ true); + + FakeMediaPeriodWithSelectTracksPosition childMediaPeriod1 = + (FakeMediaPeriodWithSelectTracksPosition) mergingMediaPeriod.getChildPeriod(0); + assertThat(childMediaPeriod1.selectTracksPositionUs).isEqualTo(0); + assertThat(streams[0].readData(formatHolder, inputBuffer, /* formatRequired= */ false)) + .isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.timeUs).isEqualTo(0L); + + FakeMediaPeriodWithSelectTracksPosition childMediaPeriod2 = + (FakeMediaPeriodWithSelectTracksPosition) mergingMediaPeriod.getChildPeriod(1); + assertThat(childMediaPeriod2.selectTracksPositionUs).isEqualTo(3000L); + assertThat(streams[1].readData(formatHolder, inputBuffer, /* formatRequired= */ false)) + .isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.timeUs).isEqualTo(0L); + } + + private MergingMediaPeriod prepareMergingPeriod(MergingPeriodDefinition... definitions) + throws Exception { + MediaPeriod[] mediaPeriods = new MediaPeriod[definitions.length]; + long[] timeOffsetsUs = new long[definitions.length]; + for (int i = 0; i < definitions.length; i++) { + timeOffsetsUs[i] = definitions[i].timeOffsetUs; + TrackGroup[] trackGroups = new TrackGroup[definitions[i].formats.length]; + for (int j = 0; j < definitions[i].formats.length; j++) { + trackGroups[j] = new TrackGroup(definitions[i].formats[j]); + } + mediaPeriods[i] = + new FakeMediaPeriodWithSelectTracksPosition( + new TrackGroupArray(trackGroups), new EventDispatcher()); + } + MergingMediaPeriod mergingMediaPeriod = + new MergingMediaPeriod( + new DefaultCompositeSequenceableLoaderFactory(), timeOffsetsUs, mediaPeriods); + + CountDownLatch prepareCountDown = new CountDownLatch(1); + mergingMediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + prepareCountDown.countDown(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + mergingMediaPeriod.continueLoading(/* positionUs= */ 0); + } + }, + /* positionUs= */ 0); + prepareCountDown.await(); + + return mergingMediaPeriod; + } + + private static final class FakeMediaPeriodWithSelectTracksPosition extends FakeMediaPeriod { + + public long selectTracksPositionUs; + + public FakeMediaPeriodWithSelectTracksPosition( + TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher) { + super(trackGroupArray, eventDispatcher); + selectTracksPositionUs = C.TIME_UNSET; + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + selectTracksPositionUs = positionUs; + return super.selectTracks( + selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + } + } + + private static final class MergingPeriodDefinition { + + public long timeOffsetUs; + public Format[] formats; + + public MergingPeriodDefinition(long timeOffsetUs, Format... formats) { + this.timeOffsetUs = timeOffsetUs; + this.formats = formats; + } + } +}