blob: 61cc61ed42189332f1378381b39f23b64189c119 [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.Format;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.ForwardingTimeline;
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
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.qual.MonotonicNonNull;
/**
* Fake {@link MediaSource} that provides a given timeline. Creating the period will return a {@link
* FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s.
*/
public class FakeMediaSource extends BaseMediaSource {
/** A forwarding timeline to provide an initial timeline for fake multi window sources. */
public static class InitialTimeline extends ForwardingTimeline {
public InitialTimeline(Timeline timeline) {
super(timeline);
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
Window childWindow = timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
childWindow.isDynamic = true;
childWindow.isSeekable = false;
return childWindow;
}
}
private static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://manifest.uri"));
private static final int MANIFEST_LOAD_BYTES = 100;
private final TrackGroupArray trackGroupArray;
private final ArrayList<FakeMediaPeriod> activeMediaPeriods;
private final ArrayList<MediaPeriodId> createdMediaPeriods;
private @MonotonicNonNull Timeline timeline;
private boolean preparedSource;
private boolean releasedSource;
@Nullable private Handler sourceInfoRefreshHandler;
@Nullable private TransferListener transferListener;
/**
* Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a
* {@link TrackGroupArray} using the given {@link Format}s. The provided {@link Timeline} may be
* null to prevent an immediate source info refresh message when preparing the media source. It
* can be manually set later using {@link #setNewSourceInfo(Timeline)}.
*/
public FakeMediaSource(@Nullable Timeline timeline, Format... formats) {
this(timeline, buildTrackGroupArray(formats));
}
/**
* Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with the
* given {@link TrackGroupArray}. The provided {@link Timeline} may be null to prevent an
* immediate source info refresh message when preparing the media source. It can be manually set
* later using {@link #setNewSourceInfo(Timeline)}.
*/
public FakeMediaSource(@Nullable Timeline timeline, TrackGroupArray trackGroupArray) {
if (timeline != null) {
this.timeline = timeline;
}
this.trackGroupArray = trackGroupArray;
this.activeMediaPeriods = new ArrayList<>();
this.createdMediaPeriods = new ArrayList<>();
}
@Nullable
protected Timeline getTimeline() {
return timeline;
}
@Override
@Nullable
public Object getTag() {
if (timeline == null || timeline.isEmpty()) {
return null;
}
return timeline.getWindow(0, new Timeline.Window()).tag;
}
@Override
@Nullable
public Timeline getInitialTimeline() {
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1
? null
: new InitialTimeline(timeline);
}
@Override
public boolean isSingleWindow() {
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1;
}
@Override
public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
assertThat(preparedSource).isFalse();
transferListener = mediaTransferListener;
preparedSource = true;
releasedSource = false;
sourceInfoRefreshHandler = Util.createHandler();
if (timeline != null) {
finishSourcePreparation();
}
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
assertThat(preparedSource).isTrue();
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
assertThat(preparedSource).isTrue();
assertThat(releasedSource).isFalse();
int periodIndex = Util.castNonNull(timeline).getIndexOfPeriod(id.periodUid);
Assertions.checkArgument(periodIndex != C.INDEX_UNSET);
Period period = timeline.getPeriod(periodIndex, new Period());
EventDispatcher eventDispatcher =
createEventDispatcher(period.windowIndex, id, period.getPositionInWindowMs());
FakeMediaPeriod mediaPeriod =
createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher, transferListener);
activeMediaPeriods.add(mediaPeriod);
createdMediaPeriods.add(id);
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
assertThat(preparedSource).isTrue();
assertThat(releasedSource).isFalse();
FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
assertThat(activeMediaPeriods.remove(fakeMediaPeriod)).isTrue();
fakeMediaPeriod.release();
}
@Override
protected void releaseSourceInternal() {
assertThat(preparedSource).isTrue();
assertThat(releasedSource).isFalse();
assertThat(activeMediaPeriods.isEmpty()).isTrue();
releasedSource = true;
preparedSource = false;
Util.castNonNull(sourceInfoRefreshHandler).removeCallbacksAndMessages(null);
sourceInfoRefreshHandler = null;
}
/**
* Sets a new timeline. If the source is already prepared, this triggers a source info refresh
* message being sent to the listener.
*/
public synchronized void setNewSourceInfo(final Timeline newTimeline) {
if (sourceInfoRefreshHandler != null) {
sourceInfoRefreshHandler.post(
() -> {
assertThat(releasedSource).isFalse();
assertThat(preparedSource).isTrue();
timeline = newTimeline;
finishSourcePreparation();
});
} else {
timeline = newTimeline;
}
}
/** Returns whether the source is currently prepared. */
public boolean isPrepared() {
return preparedSource;
}
/**
* Assert that the source and all periods have been released.
*/
public void assertReleased() {
assertThat(releasedSource || !preparedSource).isTrue();
}
/**
* Assert that a media period for the given id has been created.
*/
public void assertMediaPeriodCreated(MediaPeriodId mediaPeriodId) {
assertThat(createdMediaPeriods).contains(mediaPeriodId);
}
/** Returns a list of {@link MediaPeriodId}s, with one element for each created media period. */
public List<MediaPeriodId> getCreatedMediaPeriods() {
return createdMediaPeriods;
}
/**
* Creates a {@link FakeMediaPeriod} for this media source.
*
* @param id The identifier of the period.
* @param trackGroupArray The {@link TrackGroupArray} supported by the media period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param eventDispatcher An {@link EventDispatcher} to dispatch media source events.
* @param transferListener The transfer listener which should be informed of any data transfers.
* May be null if no listener is available.
* @return A new {@link FakeMediaPeriod}.
*/
protected FakeMediaPeriod createFakeMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
EventDispatcher eventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(trackGroupArray, eventDispatcher);
}
private void finishSourcePreparation() {
refreshSourceInfo(Assertions.checkStateNotNull(timeline));
if (!timeline.isEmpty()) {
MediaLoadData mediaLoadData =
new MediaLoadData(
C.DATA_TYPE_MANIFEST,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeMs= */ C.TIME_UNSET,
/* mediaEndTimeMs = */ C.TIME_UNSET);
long elapsedRealTimeMs = SystemClock.elapsedRealtime();
EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
eventDispatcher.loadStarted(
new LoadEventInfo(
FAKE_DATA_SPEC,
FAKE_DATA_SPEC.uri,
/* responseHeaders= */ Collections.emptyMap(),
elapsedRealTimeMs,
/* loadDurationMs= */ 0,
/* bytesLoaded= */ 0),
mediaLoadData);
eventDispatcher.loadCompleted(
new LoadEventInfo(
FAKE_DATA_SPEC,
FAKE_DATA_SPEC.uri,
/* responseHeaders= */ Collections.emptyMap(),
elapsedRealTimeMs,
/* loadDurationMs= */ 0,
/* bytesLoaded= */ MANIFEST_LOAD_BYTES),
mediaLoadData);
}
}
private static TrackGroupArray buildTrackGroupArray(Format... formats) {
TrackGroup[] trackGroups = new TrackGroup[formats.length];
for (int i = 0; i < formats.length; i++) {
trackGroups[i] = new TrackGroup(formats[i]);
}
return new TrackGroupArray(trackGroups);
}
}