SimpleExoplayer Builder for testing

Create a Builder that creates SimpleExoPlayer instances with fake
components, suitable for testing.

Basically extracts the Builder from ExoPlayerTestRunner to a standalone
class that can be re-used.

PiperOrigin-RevId: 305458419
This commit is contained in:
christosts 2020-04-08 13:37:19 +01:00 committed by Oliver Woodman
parent 703fb777c4
commit e7fd6a0e01
8 changed files with 1191 additions and 716 deletions

View File

@ -98,6 +98,8 @@
* MP4: Store the Android capture frame rate only in `Format.metadata`.
`Format.frameRate` now stores the calculated frame rate.
* Testing
* Add `TestExoPlayer`, a utility class with APIs to create
`SimpleExoPlayer` instances with fake components for testing.
* Upgrade Truth dependency from 0.44 to 1.0.
* Upgrade to JUnit 4.13-rc-2.
* UI

View File

@ -0,0 +1,28 @@
/*
* 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.util;
/**
* A functional interface representing a supplier of results.
*
* @param <T> The type of results supplied by this supplier.
*/
public interface Supplier<T> {
/** Gets a result. */
T get();
}

View File

@ -17,8 +17,6 @@ package com.google.android.exoplayer2.analytics;
import static com.google.common.truth.Truth.assertThat;
import android.os.Handler;
import android.os.SystemClock;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
@ -32,7 +30,6 @@ import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
@ -45,13 +42,13 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.testutil.ActionSchedule;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
import com.google.android.exoplayer2.testutil.FakeAudioRenderer;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeVideoRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
@ -131,9 +128,7 @@ public final class AnalyticsCollectorTest {
public void emptyTimeline() throws Exception {
FakeMediaSource mediaSource =
new FakeMediaSource(
Timeline.EMPTY,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
Timeline.EMPTY, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
@ -149,8 +144,8 @@ public final class AnalyticsCollectorTest {
FakeMediaSource mediaSource =
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline);
@ -193,12 +188,12 @@ public final class AnalyticsCollectorTest {
new ConcatenatingMediaSource(
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline);
@ -252,8 +247,8 @@ public final class AnalyticsCollectorTest {
public void periodTransitionWithRendererChange() throws Exception {
MediaSource mediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline);
@ -307,9 +302,9 @@ public final class AnalyticsCollectorTest {
new ConcatenatingMediaSource(
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT));
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
@ -378,11 +373,11 @@ public final class AnalyticsCollectorTest {
public void seekBackAfterReadingAhead() throws Exception {
MediaSource mediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT));
long periodDurationMs =
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
ActionSchedule actionSchedule =
@ -462,9 +457,9 @@ public final class AnalyticsCollectorTest {
@Test
public void prepareNewSource() throws Exception {
MediaSource mediaSource1 =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
MediaSource mediaSource2 =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
@ -543,7 +538,7 @@ public final class AnalyticsCollectorTest {
@Test
public void reprepareAfterError() throws Exception {
MediaSource mediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
@ -616,7 +611,7 @@ public final class AnalyticsCollectorTest {
@Test
public void dynamicTimelineChange() throws Exception {
MediaSource childMediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
final ConcatenatingMediaSource concatenatedMediaSource =
new ConcatenatingMediaSource(childMediaSource, childMediaSource);
long periodDurationMs =
@ -697,7 +692,7 @@ public final class AnalyticsCollectorTest {
@Test
public void playlistOperations() throws Exception {
MediaSource fakeMediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
@ -792,7 +787,7 @@ public final class AnalyticsCollectorTest {
contentDurationsUs,
adPlaybackState.get()));
FakeMediaSource fakeMediaSource =
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
@ -1031,7 +1026,7 @@ public final class AnalyticsCollectorTest {
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ 5 * C.MICROS_PER_SECOND)));
FakeMediaSource fakeMediaSource =
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
@ -1223,12 +1218,12 @@ public final class AnalyticsCollectorTest {
};
TestAnalyticsListener listener = new TestAnalyticsListener();
try {
new ExoPlayerTestRunner.Builder()
new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext())
.setMediaSources(mediaSource)
.setRenderersFactory(renderersFactory)
.setAnalyticsListener(listener)
.setActionSchedule(actionSchedule)
.build(ApplicationProvider.getApplicationContext())
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
@ -1238,140 +1233,6 @@ public final class AnalyticsCollectorTest {
return listener;
}
private static final class FakeVideoRenderer extends FakeRenderer {
private final VideoRendererEventListener.EventDispatcher eventDispatcher;
private final DecoderCounters decoderCounters;
private Format format;
private long streamOffsetUs;
private boolean renderedFirstFrameAfterReset;
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(C.TRACK_TYPE_VIDEO);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters();
}
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false;
}
@Override
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
super.onStreamChanged(formats, offsetUs);
streamOffsetUs = offsetUs;
if (renderedFirstFrameAfterReset) {
renderedFirstFrameAfterReset = false;
}
}
@Override
protected void onStopped() throws ExoPlaybackException {
super.onStopped();
eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0);
eventDispatcher.reportVideoFrameProcessingOffset(
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10, this.format);
}
@Override
protected void onDisabled() {
super.onDisabled();
eventDispatcher.disabled(decoderCounters);
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);
renderedFirstFrameAfterReset = false;
}
@Override
protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format);
eventDispatcher.decoderInitialized(
/* decoderName= */ "fake.video.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0);
this.format = format;
}
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterEnable
? (getState() == Renderer.STATE_STARTED || mayRenderFirstFrameAfterEnableIfNotStarted)
: !renderedFirstFrameAfterReset;
shouldProcess |= shouldRenderFirstFrame && playbackPositionUs >= streamOffsetUs;
if (shouldProcess && !renderedFirstFrameAfterReset) {
eventDispatcher.videoSizeChanged(
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio);
eventDispatcher.renderedFirstFrame(/* surface= */ null);
renderedFirstFrameAfterReset = true;
renderedFirstFrameAfterEnable = true;
}
return shouldProcess;
}
}
private static final class FakeAudioRenderer extends FakeRenderer {
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
private final DecoderCounters decoderCounters;
private boolean notifiedAudioSessionId;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters();
}
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
notifiedAudioSessionId = false;
}
@Override
protected void onDisabled() {
super.onDisabled();
eventDispatcher.disabled(decoderCounters);
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);
}
@Override
protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format);
eventDispatcher.decoderInitialized(
/* decoderName= */ "fake.audio.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0);
}
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
if (shouldProcess && !notifiedAudioSessionId) {
eventDispatcher.audioSessionId(/* audioSessionId= */ 1);
notifiedAudioSessionId = true;
}
return shouldProcess;
}
}
private static final class EventWindowAndPeriodId {
private final int windowIndex;

View File

@ -17,14 +17,12 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
import android.content.Context;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.LoadControl;
@ -33,22 +31,18 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -56,13 +50,7 @@ import java.util.concurrent.TimeoutException;
/** Helper class to run an ExoPlayer test. */
public final class ExoPlayerTestRunner implements Player.EventListener, ActionSchedule.Callback {
/**
* Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for
* unset test properties.
*/
public static final class Builder {
/** A generic video {@link Format} which can be used to set up media sources and renderers. */
/** A generic video {@link Format} which can be used to set up a {@link FakeMediaSource}. */
public static final Format VIDEO_FORMAT =
new Format.Builder()
.setSampleMimeType(MimeTypes.VIDEO_H264)
@ -71,7 +59,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
.setHeight(720)
.build();
/** A generic audio {@link Format} which can be used to set up media sources and renderers. */
/** A generic audio {@link Format} which can be used to set up a {@link FakeMediaSource}. */
public static final Format AUDIO_FORMAT =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AAC)
@ -80,28 +68,29 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
.setSampleRate(44100)
.build();
private Clock clock;
/**
* Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for
* unset test properties.
*/
public static final class Builder {
private final TestExoPlayer.Builder testPlayerBuilder;
private Timeline timeline;
private List<MediaSource> mediaSources;
private Object manifest;
private DefaultTrackSelector trackSelector;
private LoadControl loadControl;
private BandwidthMeter bandwidthMeter;
private Format[] supportedFormats;
private Renderer[] renderers;
private RenderersFactory renderersFactory;
private Object manifest;
private ActionSchedule actionSchedule;
private Player.EventListener eventListener;
private AnalyticsListener analyticsListener;
private Integer expectedPlayerEndedCount;
private boolean useLazyPreparation;
private boolean pauseAtEndOfMediaItems;
private int initialWindowIndex;
private long initialPositionMs;
private boolean skipSettingMediaSources;
public Builder() {
public Builder(Context context) {
testPlayerBuilder = new TestExoPlayer.Builder(context);
mediaSources = new ArrayList<>();
supportedFormats = new Format[] {VIDEO_FORMAT};
initialWindowIndex = C.INDEX_UNSET;
initialPositionMs = C.TIME_UNSET;
}
@ -170,6 +159,20 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
return this;
}
/**
* Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media
* periods. The default value is a single {@link #VIDEO_FORMAT}. Note that this parameter
* doesn't have any influence if a media source with {@link #setMediaSources(MediaSource...)} is
* set.
*
* @param supportedFormats A list of supported {@link Format}s.
* @return This builder.
*/
public Builder setSupportedFormats(Format... supportedFormats) {
this.supportedFormats = supportedFormats;
return this;
}
/**
* Skips calling {@link com.google.android.exoplayer2.ExoPlayer#setMediaSources(List)} before
* preparing. Calling this method is not allowed after calls to {@link
@ -181,19 +184,17 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
public Builder skipSettingMediaSources() {
assertThat(timeline).isNull();
assertThat(manifest).isNull();
assertTrue(mediaSources.isEmpty());
assertThat(mediaSources).isEmpty();
skipSettingMediaSources = true;
return this;
}
/**
* Sets whether to use lazy preparation.
*
* @param useLazyPreparation Whether to use lazy preparation.
* @see TestExoPlayer.Builder#setUseLazyPreparation(boolean)
* @return This builder.
*/
public Builder setUseLazyPreparation(boolean useLazyPreparation) {
this.useLazyPreparation = useLazyPreparation;
testPlayerBuilder.setUseLazyPreparation(useLazyPreparation);
return this;
}
@ -209,94 +210,56 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
}
/**
* Sets a {@link DefaultTrackSelector} to be used by the test runner. The default value is a
* {@link DefaultTrackSelector} in its initial configuration.
*
* @param trackSelector A {@link DefaultTrackSelector} to be used by the test runner.
* @see TestExoPlayer.Builder#setTrackSelector(DefaultTrackSelector)
* @return This builder.
*/
public Builder setTrackSelector(DefaultTrackSelector trackSelector) {
this.trackSelector = trackSelector;
testPlayerBuilder.setTrackSelector(trackSelector);
return this;
}
/**
* Sets a {@link LoadControl} to be used by the test runner. The default value is a
* {@link DefaultLoadControl}.
*
* @param loadControl A {@link LoadControl} to be used by the test runner.
* @see TestExoPlayer.Builder#setLoadControl(LoadControl)
* @return This builder.
*/
public Builder setLoadControl(LoadControl loadControl) {
this.loadControl = loadControl;
testPlayerBuilder.setLoadControl(loadControl);
return this;
}
/**
* Sets the {@link BandwidthMeter} to be used by the test runner. The default value is a {@link
* DefaultBandwidthMeter} in its default configuration.
*
* @param bandwidthMeter The {@link BandwidthMeter} to be used by the test runner.
* @see TestExoPlayer.Builder#setBandwidthMeter(BandwidthMeter)
* @return This builder.
*/
public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) {
this.bandwidthMeter = bandwidthMeter;
this.testPlayerBuilder.setBandwidthMeter(bandwidthMeter);
return this;
}
/**
* Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media
* periods and for setting up a {@link FakeRenderer}. The default value is a single {@link
* #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source
* with {@link #setMediaSources(MediaSource...)} and renderers with {@link
* #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set.
*
* @param supportedFormats A list of supported {@link Format}s.
* @return This builder.
*/
public Builder setSupportedFormats(Format... supportedFormats) {
this.supportedFormats = supportedFormats;
return this;
}
/**
* Sets the {@link Renderer}s to be used by the test runner. The default value is a single
* {@link FakeRenderer} supporting the formats set by {@link #setSupportedFormats(Format...)}.
* Setting the renderers is not allowed after a call to
* {@link #setRenderersFactory(RenderersFactory)}.
*
* @param renderers A list of {@link Renderer}s to be used by the test runner.
* @see TestExoPlayer.Builder#setRenderers(Renderer...)
* @return This builder.
*/
public Builder setRenderers(Renderer... renderers) {
assertThat(renderersFactory).isNull();
this.renderers = renderers;
testPlayerBuilder.setRenderers(renderers);
return this;
}
/**
* Sets the {@link RenderersFactory} to be used by the test runner. The default factory creates
* all renderers set by {@link #setRenderers(Renderer...)}. Setting the renderer factory is not
* allowed after a call to {@link #setRenderers(Renderer...)}.
*
* @param renderersFactory A {@link RenderersFactory} to be used by the test runner.
* @see TestExoPlayer.Builder#setRenderersFactory(RenderersFactory)
* @return This builder.
*/
public Builder setRenderersFactory(RenderersFactory renderersFactory) {
assertThat(renderers).isNull();
this.renderersFactory = renderersFactory;
testPlayerBuilder.setRenderersFactory(renderersFactory);
return this;
}
/**
* Sets the {@link Clock} to be used by the test runner. The default value is a {@link
* AutoAdvancingFakeClock}.
*
* @param clock A {@link Clock} to be used by the test runner.
* @see TestExoPlayer.Builder#setClock(Clock)
* @return This builder.
*/
public Builder setClock(Clock clock) {
this.clock = clock;
testPlayerBuilder.setClock(clock);
return this;
}
@ -352,45 +315,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
/**
* Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults.
*
* @param context The context.
* @return The built {@link ExoPlayerTestRunner}.
*/
public ExoPlayerTestRunner build(Context context) {
if (supportedFormats == null) {
supportedFormats = new Format[] {VIDEO_FORMAT};
}
if (trackSelector == null) {
trackSelector = new DefaultTrackSelector(context);
}
if (bandwidthMeter == null) {
bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
}
if (renderersFactory == null) {
if (renderers == null) {
Set<Integer> trackTypes = new HashSet<>();
for (Format format : supportedFormats) {
trackTypes.add(MimeTypes.getTrackType(format.sampleMimeType));
}
renderers = new Renderer[trackTypes.size()];
int i = 0;
for (int trackType : trackTypes) {
renderers[i] = new FakeRenderer(trackType);
i++;
}
}
renderersFactory =
(eventHandler,
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput) -> renderers;
}
if (loadControl == null) {
loadControl = new DefaultLoadControl();
}
if (clock == null) {
clock = new AutoAdvancingFakeClock();
}
public ExoPlayerTestRunner build() {
if (mediaSources.isEmpty() && !skipSettingMediaSources) {
if (timeline == null) {
timeline = new FakeTimeline(/* windowCount= */ 1, manifest);
@ -401,34 +328,24 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
expectedPlayerEndedCount = 1;
}
return new ExoPlayerTestRunner(
context,
clock,
initialWindowIndex,
initialPositionMs,
testPlayerBuilder,
mediaSources,
skipSettingMediaSources,
useLazyPreparation,
pauseAtEndOfMediaItems,
renderersFactory,
trackSelector,
loadControl,
bandwidthMeter,
initialWindowIndex,
initialPositionMs,
actionSchedule,
eventListener,
analyticsListener,
expectedPlayerEndedCount);
expectedPlayerEndedCount,
pauseAtEndOfMediaItems);
}
}
private final Context context;
private final Clock clock;
private final TestExoPlayer.Builder playerBuilder;
private final List<MediaSource> mediaSources;
private final boolean skipSettingMediaSources;
private final int initialWindowIndex;
private final long initialPositionMs;
private final List<MediaSource> mediaSources;
private final RenderersFactory renderersFactory;
private final DefaultTrackSelector trackSelector;
private final LoadControl loadControl;
private final BandwidthMeter bandwidthMeter;
@Nullable private final ActionSchedule actionSchedule;
@Nullable private final Player.EventListener eventListener;
@Nullable private final AnalyticsListener analyticsListener;
@ -442,8 +359,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final ArrayList<Integer> periodIndices;
private final ArrayList<Integer> discontinuityReasons;
private final ArrayList<Integer> playbackStates;
private final boolean skipSettingMediaSources;
private final boolean useLazyPreparation;
private final boolean pauseAtEndOfMediaItems;
private SimpleExoPlayer player;
@ -452,47 +367,36 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private boolean playerWasPrepared;
private ExoPlayerTestRunner(
Context context,
Clock clock,
int initialWindowIndex,
long initialPositionMs,
TestExoPlayer.Builder playerBuilder,
List<MediaSource> mediaSources,
boolean skipSettingMediaSources,
boolean useLazyPreparation,
boolean pauseAtEndOfMediaItems,
RenderersFactory renderersFactory,
DefaultTrackSelector trackSelector,
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
int initialWindowIndex,
long initialPositionMs,
@Nullable ActionSchedule actionSchedule,
@Nullable Player.EventListener eventListener,
@Nullable AnalyticsListener analyticsListener,
int expectedPlayerEndedCount) {
this.context = context;
this.clock = clock;
this.initialWindowIndex = initialWindowIndex;
this.initialPositionMs = initialPositionMs;
int expectedPlayerEndedCount,
boolean pauseAtEndOfMediaItems) {
this.playerBuilder = playerBuilder;
this.mediaSources = mediaSources;
this.skipSettingMediaSources = skipSettingMediaSources;
this.useLazyPreparation = useLazyPreparation;
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
this.renderersFactory = renderersFactory;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
this.bandwidthMeter = bandwidthMeter;
this.initialWindowIndex = initialWindowIndex;
this.initialPositionMs = initialPositionMs;
this.actionSchedule = actionSchedule;
this.eventListener = eventListener;
this.analyticsListener = analyticsListener;
this.timelines = new ArrayList<>();
this.timelineChangeReasons = new ArrayList<>();
this.periodIndices = new ArrayList<>();
this.discontinuityReasons = new ArrayList<>();
this.playbackStates = new ArrayList<>();
this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);
this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
this.playerThread = new HandlerThread("ExoPlayerTest thread");
timelines = new ArrayList<>();
timelineChangeReasons = new ArrayList<>();
periodIndices = new ArrayList<>();
discontinuityReasons = new ArrayList<>();
playbackStates = new ArrayList<>();
endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);
actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
playerThread = new HandlerThread("ExoPlayerTest thread");
playerThread.start();
this.handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null);
handler =
playerBuilder.getClock().createHandler(playerThread.getLooper(), /* callback= */ null);
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
}
// Called on the test thread to run the test.
@ -519,16 +423,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
handler.post(
() -> {
try {
player =
new SimpleExoPlayer.Builder(context, renderersFactory)
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.setBandwidthMeter(bandwidthMeter)
.setAnalyticsCollector(new AnalyticsCollector(clock))
.setClock(clock)
.setUseLazyPreparation(useLazyPreparation)
.setLooper(Looper.myLooper())
.build();
player = playerBuilder.setLooper(Looper.myLooper()).build();
player.addListener(ExoPlayerTestRunner.this);
if (eventListener != null) {
player.addListener(eventListener);
@ -541,7 +436,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
}
player.play();
if (actionSchedule != null) {
actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);
actionSchedule.start(
player,
playerBuilder.getTrackSelector(),
/* surface= */ null,
handler,
/* callback= */ ExoPlayerTestRunner.this);
}
if (initialWindowIndex != C.INDEX_UNSET) {
player.seekTo(initialWindowIndex, initialPositionMs);

View File

@ -0,0 +1,72 @@
/*
* 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.testutil;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
/** A {@link FakeRenderer} that supports {@link C#TRACK_TYPE_AUDIO}. */
public class FakeAudioRenderer extends FakeRenderer {
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
private final DecoderCounters decoderCounters;
private boolean notifiedAudioSessionId;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters();
}
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
notifiedAudioSessionId = false;
}
@Override
protected void onDisabled() {
super.onDisabled();
eventDispatcher.disabled(decoderCounters);
}
@Override
protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format);
eventDispatcher.decoderInitialized(
/* decoderName= */ "fake.audio.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0);
}
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
if (shouldProcess && !notifiedAudioSessionId) {
eventDispatcher.audioSessionId(/* audioSessionId= */ 1);
notifiedAudioSessionId = true;
}
return shouldProcess;
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.testutil;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link FakeRenderer} that supports {@link C#TRACK_TYPE_VIDEO}. */
public class FakeVideoRenderer extends FakeRenderer {
private final VideoRendererEventListener.EventDispatcher eventDispatcher;
private final DecoderCounters decoderCounters;
private @MonotonicNonNull Format format;
private long streamOffsetUs;
private boolean renderedFirstFrameAfterReset;
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(C.TRACK_TYPE_VIDEO);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters();
}
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false;
}
@Override
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
super.onStreamChanged(formats, offsetUs);
streamOffsetUs = offsetUs;
if (renderedFirstFrameAfterReset) {
renderedFirstFrameAfterReset = false;
}
}
@Override
protected void onStopped() throws ExoPlaybackException {
super.onStopped();
eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0);
eventDispatcher.reportVideoFrameProcessingOffset(
/* totalProcessingOffsetUs= */ 400000,
/* frameCount= */ 10,
Assertions.checkNotNull(format));
}
@Override
protected void onDisabled() {
super.onDisabled();
eventDispatcher.disabled(decoderCounters);
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);
renderedFirstFrameAfterReset = false;
}
@Override
protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format);
eventDispatcher.decoderInitialized(
/* decoderName= */ "fake.video.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0);
this.format = format;
}
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterEnable
? (getState() == Renderer.STATE_STARTED || mayRenderFirstFrameAfterEnableIfNotStarted)
: !renderedFirstFrameAfterReset;
shouldProcess |= shouldRenderFirstFrame && playbackPositionUs >= streamOffsetUs;
if (shouldProcess && !renderedFirstFrameAfterReset) {
@MonotonicNonNull Format format = Assertions.checkNotNull(this.format);
eventDispatcher.videoSizeChanged(
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio);
eventDispatcher.renderedFirstFrame(/* surface= */ null);
renderedFirstFrameAfterReset = true;
renderedFirstFrameAfterEnable = true;
}
return shouldProcess;
}
}

View File

@ -0,0 +1,456 @@
/*
* 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.testutil;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Supplier;
import com.google.android.exoplayer2.video.VideoListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Utilities to write unit/integration tests with a SimpleExoPlayer instance that uses fake
* components.
*/
public class TestExoPlayer {
/** Reflectively call Robolectric ShadowLooper#runOneTask. */
private static final Object shadowLooper;
private static final Method runOneTaskMethod;
static {
try {
Class<?> clazz = Class.forName("org.robolectric.Shadows");
Method shadowOfMethod =
Assertions.checkNotNull(clazz.getDeclaredMethod("shadowOf", Looper.class));
shadowLooper =
Assertions.checkNotNull(shadowOfMethod.invoke(new Object(), Looper.getMainLooper()));
runOneTaskMethod = shadowLooper.getClass().getDeclaredMethod("runOneTask");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** A builder of {@link SimpleExoPlayer} instances for testing. */
public static class Builder {
private final Context context;
private Clock clock;
private DefaultTrackSelector trackSelector;
private LoadControl loadControl;
private BandwidthMeter bandwidthMeter;
@Nullable private Renderer[] renderers;
@Nullable private RenderersFactory renderersFactory;
private boolean useLazyPreparation;
private Looper looper;
public Builder(Context context) {
this.context = context;
clock = new AutoAdvancingFakeClock();
trackSelector = new DefaultTrackSelector(context);
loadControl = new DefaultLoadControl();
bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
looper = Assertions.checkNotNull(Looper.myLooper());
}
/**
* Sets whether to use lazy preparation.
*
* @param useLazyPreparation Whether to use lazy preparation.
* @return This builder.
*/
public Builder setUseLazyPreparation(boolean useLazyPreparation) {
this.useLazyPreparation = useLazyPreparation;
return this;
}
/** Returns whether the player will use lazy preparation. */
public boolean getUseLazyPreparation() {
return useLazyPreparation;
}
/**
* Sets a {@link DefaultTrackSelector}. The default value is a {@link DefaultTrackSelector} in
* its initial configuration.
*
* @param trackSelector The {@link DefaultTrackSelector} to be used by the player.
* @return This builder.
*/
public Builder setTrackSelector(DefaultTrackSelector trackSelector) {
Assertions.checkNotNull(trackSelector);
this.trackSelector = trackSelector;
return this;
}
/** Returns the track selector used by the player. */
public DefaultTrackSelector getTrackSelector() {
return trackSelector;
}
/**
* Sets a {@link LoadControl} to be used by the player. The default value is a {@link
* DefaultLoadControl}.
*
* @param loadControl The {@link LoadControl} to be used by the player.
* @return This builder.
*/
public Builder setLoadControl(LoadControl loadControl) {
this.loadControl = loadControl;
return this;
}
/** Returns the {@link LoadControl} that will be used by the player. */
public LoadControl getLoadControl() {
return loadControl;
}
/**
* Sets the {@link BandwidthMeter}. The default value is a {@link DefaultBandwidthMeter} in its
* default configuration.
*
* @param bandwidthMeter The {@link BandwidthMeter} to be used by the player.
* @return This builder.
*/
public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) {
Assertions.checkNotNull(bandwidthMeter);
this.bandwidthMeter = bandwidthMeter;
return this;
}
/** Returns the bandwidth meter used by the player. */
public BandwidthMeter getBandwidthMeter() {
return bandwidthMeter;
}
/**
* Sets the {@link Renderer}s. If not set, the player will use a {@link FakeVideoRenderer} and a
* {@link FakeAudioRenderer}. Setting the renderers is not allowed after a call to {@link
* #setRenderersFactory(RenderersFactory)}.
*
* @param renderers A list of {@link Renderer}s to be used by the player.
* @return This builder.
*/
public Builder setRenderers(Renderer... renderers) {
assertThat(renderersFactory).isNull();
this.renderers = renderers;
return this;
}
/**
* Returns the {@link Renderer Renderers} that have been set with {@link #setRenderers} or null
* if no {@link Renderer Renderers} have been explicitly set. Note that these renderers may not
* be the ones used by the built player, for example if a {@link #setRenderersFactory Renderer
* factory} has been set.
*/
@Nullable
public Renderer[] getRenderers() {
return renderers;
}
/**
* Sets the {@link RenderersFactory}. The default factory creates all renderers set by {@link
* #setRenderers(Renderer...)}. Setting the renderer factory is not allowed after a call to
* {@link #setRenderers(Renderer...)}.
*
* @param renderersFactory A {@link RenderersFactory} to be used by the player.
* @return This builder.
*/
public Builder setRenderersFactory(RenderersFactory renderersFactory) {
assertThat(renderers).isNull();
this.renderersFactory = renderersFactory;
return this;
}
/**
* Returns the {@link RenderersFactory} that has been set with {@link #setRenderersFactory} or
* null if no factory has been explicitly set.
*/
@Nullable
public RenderersFactory getRenderersFactory() {
return renderersFactory;
}
/**
* Sets the {@link Clock} to be used by the player. The default value is a {@link
* AutoAdvancingFakeClock}.
*
* @param clock A {@link Clock} to be used by the player.
* @return This builder.
*/
public Builder setClock(Clock clock) {
assertThat(clock).isNotNull();
this.clock = clock;
return this;
}
/** Returns the clock used by the player. */
public Clock getClock() {
return clock;
}
/**
* Sets the {@link Looper} to be used by the player.
*
* @param looper The {@link Looper} to be used by the player.
* @return This builder.
*/
public Builder setLooper(Looper looper) {
this.looper = looper;
return this;
}
/** Returns the {@link Looper} that will be used by the player. */
public Looper getLooper() {
return looper;
}
/**
* Builds an {@link SimpleExoPlayer} using the provided values or their defaults.
*
* @return The built {@link ExoPlayerTestRunner}.
*/
public SimpleExoPlayer build() {
// Do not update renderersFactory and renderers here, otherwise their getters may
// return different values before and after build() is called, making them confusing.
RenderersFactory playerRenderersFactory = renderersFactory;
if (playerRenderersFactory == null) {
playerRenderersFactory =
(eventHandler,
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput) ->
renderers != null
? renderers
: new Renderer[] {
new FakeVideoRenderer(eventHandler, videoRendererEventListener),
new FakeAudioRenderer(eventHandler, audioRendererEventListener)
};
}
return new SimpleExoPlayer.Builder(context, playerRenderersFactory)
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.setBandwidthMeter(bandwidthMeter)
.setAnalyticsCollector(new AnalyticsCollector(clock))
.setClock(clock)
.setUseLazyPreparation(useLazyPreparation)
.setLooper(looper)
.build();
}
}
private TestExoPlayer() {}
/**
* Run tasks of the main {@link Looper} until the {@code player}'s state reaches the {@code
* expectedState}.
*/
public static void runUntilPlaybackState(
SimpleExoPlayer player, @Player.State int expectedState) {
verifyMainTestThread(player);
if (player.getPlaybackState() == expectedState) {
return;
}
AtomicBoolean receivedExpectedState = new AtomicBoolean(false);
Player.EventListener listener =
new Player.EventListener() {
@Override
public void onPlaybackStateChanged(int state) {
if (state == expectedState) {
receivedExpectedState.set(true);
}
}
};
player.addListener(listener);
runUntil(() -> receivedExpectedState.get());
player.removeListener(listener);
}
/**
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* Player.EventListener#onPlaybackSpeedChanged} callback with that matches {@code
* expectedPlayWhenReady}.
*/
public static void runUntilPlayWhenReady(SimpleExoPlayer player, boolean expectedPlayWhenReady) {
verifyMainTestThread(player);
if (player.getPlayWhenReady() == expectedPlayWhenReady) {
return;
}
AtomicBoolean receivedExpectedPlayWhenReady = new AtomicBoolean(false);
Player.EventListener listener =
new Player.EventListener() {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
if (playWhenReady == expectedPlayWhenReady) {
receivedExpectedPlayWhenReady.set(true);
}
player.removeListener(this);
}
};
player.addListener(listener);
runUntil(() -> receivedExpectedPlayWhenReady.get());
}
/**
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* Player.EventListener#onTimelineChanged} callback.
*
* @param player The {@link SimpleExoPlayer}.
* @param expectedTimeline A specific {@link Timeline} to wait for, or null if any timeline is
* accepted.
* @return The received {@link Timeline}.
*/
public static Timeline runUntilTimelineChanged(
SimpleExoPlayer player, @Nullable Timeline expectedTimeline) {
verifyMainTestThread(player);
if (expectedTimeline != null && expectedTimeline.equals(player.getCurrentTimeline())) {
return expectedTimeline;
}
AtomicReference<Timeline> receivedTimeline = new AtomicReference<>();
Player.EventListener listener =
new Player.EventListener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
if (expectedTimeline == null || expectedTimeline.equals(timeline)) {
receivedTimeline.set(timeline);
}
player.removeListener(this);
}
};
player.addListener(listener);
runUntil(() -> receivedTimeline.get() != null);
return receivedTimeline.get();
}
/**
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* Player.EventListener#onPositionDiscontinuity} callback with the specified {@link
* Player.DiscontinuityReason}.
*/
public static void runUntilPositionDiscontinuity(
SimpleExoPlayer player, @Player.DiscontinuityReason int expectedReason) {
AtomicBoolean receivedCallback = new AtomicBoolean(false);
Player.EventListener listener =
new Player.EventListener() {
@Override
public void onPositionDiscontinuity(int reason) {
if (reason == expectedReason) {
receivedCallback.set(true);
player.removeListener(this);
}
}
};
player.addListener(listener);
runUntil(() -> receivedCallback.get());
}
/**
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* Player.EventListener#onPlayerError} callback.
*
* @param player The {@link SimpleExoPlayer}.
* @return The raised error.
*/
public static ExoPlaybackException runUntilError(SimpleExoPlayer player) {
verifyMainTestThread(player);
AtomicReference<ExoPlaybackException> receivedError = new AtomicReference<>();
Player.EventListener listener =
new Player.EventListener() {
@Override
public void onPlayerError(ExoPlaybackException error) {
receivedError.set(error);
player.removeListener(this);
}
};
player.addListener(listener);
runUntil(() -> receivedError.get() != null);
return receivedError.get();
}
/**
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* com.google.android.exoplayer2.video.VideoRendererEventListener#onRenderedFirstFrame} callback.
*/
public static void runUntilRenderedFirstFrame(SimpleExoPlayer player) {
verifyMainTestThread(player);
AtomicBoolean receivedCallback = new AtomicBoolean(false);
VideoListener listener =
new VideoListener() {
@Override
public void onRenderedFirstFrame() {
receivedCallback.set(true);
player.removeVideoListener(this);
}
};
player.addVideoListener(listener);
runUntil(() -> receivedCallback.get());
}
/** Run tasks of the main {@link Looper} until the {@code condition} returns {@code true}. */
public static void runUntil(Supplier<Boolean> condition) {
verifyMainTestThread();
try {
while (!condition.get()) {
runOneTaskMethod.invoke(shadowLooper);
}
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
private static void verifyMainTestThread(SimpleExoPlayer player) {
if (Looper.myLooper() != Looper.getMainLooper()
|| player.getApplicationLooper() != Looper.getMainLooper()) {
throw new IllegalStateException();
}
}
private static void verifyMainTestThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException();
}
}
}