blob: 03a52cf51487587618b28a671f4fa476473dd0af [file] [log] [blame]
/*
* Copyright (C) 2017 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.testutil;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting
* tracks will give the player {@link FakeSampleStream}s. Loading data completes immediately after
* the period has finished preparing.
*/
public class FakeMediaPeriod implements MediaPeriod {
public static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://fake.uri"));
private final TrackGroupArray trackGroupArray;
private final List<SampleStream> sampleStreams;
private final EventDispatcher eventDispatcher;
@Nullable private Handler playerHandler;
@Nullable private Callback prepareCallback;
private boolean deferOnPrepared;
private boolean notifiedReadingStarted;
private boolean prepared;
private long seekOffsetUs;
private long discontinuityPositionUs;
/**
* @param trackGroupArray The track group array.
* @param eventDispatcher A dispatcher for media source events.
*/
public FakeMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher) {
this(trackGroupArray, eventDispatcher, /* deferOnPrepared */ false);
}
/**
* @param trackGroupArray The track group array.
* @param eventDispatcher A dispatcher for media source events.
* @param deferOnPrepared Whether {@link MediaPeriod.Callback#onPrepared(MediaPeriod)} should be
* called only after {@link #setPreparationComplete()} has been called. If {@code false}
* preparation completes immediately.
*/
public FakeMediaPeriod(
TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, boolean deferOnPrepared) {
this.trackGroupArray = trackGroupArray;
this.eventDispatcher = eventDispatcher;
this.deferOnPrepared = deferOnPrepared;
discontinuityPositionUs = C.TIME_UNSET;
sampleStreams = new ArrayList<>();
eventDispatcher.mediaPeriodCreated();
}
/**
* Sets a discontinuity position to be returned from the next call to
* {@link #readDiscontinuity()}.
*
* @param discontinuityPositionUs The position to be returned, in microseconds.
*/
public void setDiscontinuityPositionUs(long discontinuityPositionUs) {
this.discontinuityPositionUs = discontinuityPositionUs;
}
/**
* Allows the fake media period to complete preparation. May be called on any thread.
*/
public synchronized void setPreparationComplete() {
deferOnPrepared = false;
if (playerHandler != null && prepareCallback != null) {
playerHandler.post(this::finishPreparation);
}
}
/**
* Sets an offset to be applied to positions returned by {@link #seekToUs(long)}.
*
* @param seekOffsetUs The offset to be applied, in microseconds.
*/
public void setSeekToUsOffset(long seekOffsetUs) {
this.seekOffsetUs = seekOffsetUs;
}
public void release() {
prepared = false;
eventDispatcher.mediaPeriodReleased();
}
@Override
public synchronized void prepare(Callback callback, long positionUs) {
eventDispatcher.loadStarted(
FAKE_DATA_SPEC,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
/* mediaEndTimeUs = */ C.TIME_UNSET,
SystemClock.elapsedRealtime());
prepareCallback = callback;
if (deferOnPrepared) {
playerHandler = Util.createHandler();
} else {
finishPreparation();
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
// Do nothing.
}
@Override
public TrackGroupArray getTrackGroups() {
assertThat(prepared).isTrue();
return trackGroupArray;
}
@Override
public long selectTracks(
@NullableType TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs) {
assertThat(prepared).isTrue();
sampleStreams.clear();
int rendererCount = selections.length;
for (int i = 0; i < rendererCount; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
streams[i] = null;
}
if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i];
assertThat(selection.length()).isAtLeast(1);
TrackGroup trackGroup = selection.getTrackGroup();
assertThat(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET).isTrue();
int indexInTrackGroup = selection.getIndexInTrackGroup(selection.getSelectedIndex());
assertThat(indexInTrackGroup).isAtLeast(0);
assertThat(indexInTrackGroup).isLessThan(trackGroup.length);
streams[i] = createSampleStream(positionUs, selection, eventDispatcher);
sampleStreams.add(streams[i]);
streamResetFlags[i] = true;
}
}
return positionUs;
}
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) {
// Do nothing.
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
assertThat(prepared).isTrue();
if (!notifiedReadingStarted) {
eventDispatcher.readingStarted();
notifiedReadingStarted = true;
}
long positionDiscontinuityUs = this.discontinuityPositionUs;
this.discontinuityPositionUs = C.TIME_UNSET;
return positionDiscontinuityUs;
}
@Override
public long getBufferedPositionUs() {
assertThat(prepared).isTrue();
return C.TIME_END_OF_SOURCE;
}
@Override
public long seekToUs(long positionUs) {
assertThat(prepared).isTrue();
long seekPositionUs = positionUs + seekOffsetUs;
for (SampleStream sampleStream : sampleStreams) {
seekSampleStream(sampleStream, seekPositionUs);
}
return seekPositionUs;
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return positionUs + seekOffsetUs;
}
@Override
public long getNextLoadPositionUs() {
assertThat(prepared).isTrue();
return C.TIME_END_OF_SOURCE;
}
@Override
public boolean continueLoading(long positionUs) {
return false;
}
@Override
public boolean isLoading() {
return false;
}
/**
* Creates a sample stream for the provided selection.
*
* @param positionUs The position at which the tracks were selected, in microseconds.
* @param selection A selection of tracks.
* @param eventDispatcher A dispatcher for events that should be used by the sample stream.
* @return A {@link SampleStream} for this selection.
*/
protected SampleStream createSampleStream(
long positionUs, TrackSelection selection, EventDispatcher eventDispatcher) {
return new FakeSampleStream(
selection.getSelectedFormat(),
eventDispatcher,
positionUs,
/* timeUsIncrement= */ 0,
FakeSampleStream.SINGLE_SAMPLE_THEN_END_OF_STREAM);
}
/**
* Seeks inside the given sample stream.
*
* @param sampleStream A sample stream that was created by a call to {@link
* #createSampleStream(long, TrackSelection, EventDispatcher)}.
* @param positionUs The position to seek to, in microseconds.
*/
protected void seekSampleStream(SampleStream sampleStream, long positionUs) {
// Queue a single sample from the seek position again.
((FakeSampleStream) sampleStream)
.resetSampleStreamItems(positionUs, FakeSampleStream.SINGLE_SAMPLE_THEN_END_OF_STREAM);
}
private void finishPreparation() {
prepared = true;
Util.castNonNull(prepareCallback).onPrepared(this);
eventDispatcher.loadCompleted(
FAKE_DATA_SPEC,
FAKE_DATA_SPEC.uri,
/* responseHeaders= */ Collections.emptyMap(),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
/* mediaEndTimeUs = */ C.TIME_UNSET,
SystemClock.elapsedRealtime(),
/* loadDurationMs= */ 0,
/* bytesLoaded= */ 100);
}
}