mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
041a0696b7
commit
0a89d0e8c3
@ -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).
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user