Add option to MergingMediaSource to adjust for period time diffs

Without this option it's impossible to merge periods covering
different timestamps (at least not without playback issues).

Issue:issue:#6103
PiperOrigin-RevId: 299817540
This commit is contained in:
tonihei 2020-03-09 12:20:02 +00:00 committed by Oliver Woodman
parent 041a0696b7
commit 0a89d0e8c3
4 changed files with 482 additions and 35 deletions

View File

@ -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 `<ruby>` and `<rt>` tags in WebVTT subtitles (rendering is coming
later).

View File

@ -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<SampleStream, Integer> streamPeriodIndices;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final ArrayList<MediaPeriod> 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<StreamKey> getStreamKeys(List<TrackSelection> 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);
}
}
}

View File

@ -66,34 +66,59 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
private static final int PERIOD_COUNT_UNSET = -1;
private final boolean adjustPeriodTimeOffsets;
private final MediaSource[] mediaSources;
private final Timeline[] timelines;
private final ArrayList<MediaSource> pendingTimelineSources;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private int periodCount;
private long[][] periodTimeOffsetsUs;
@Nullable private IllegalMergeException mergeError;
/**
* Creates a merging media source.
*
* <p>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<Integer> {
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<Integer> {
@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<Integer> {
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;
}
}

View File

@ -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;
}
}
}