mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
703fb777c4
commit
e7fd6a0e01
@ -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
|
||||
|
@ -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();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
@ -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,52 +50,47 @@ import java.util.concurrent.TimeoutException;
|
||||
/** Helper class to run an ExoPlayer test. */
|
||||
public final class ExoPlayerTestRunner implements Player.EventListener, ActionSchedule.Callback {
|
||||
|
||||
/** 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)
|
||||
.setAverageBitrate(800_000)
|
||||
.setWidth(1280)
|
||||
.setHeight(720)
|
||||
.build();
|
||||
|
||||
/** 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)
|
||||
.setAverageBitrate(100_000)
|
||||
.setChannelCount(2)
|
||||
.setSampleRate(44100)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
public static final Format VIDEO_FORMAT =
|
||||
new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_H264)
|
||||
.setAverageBitrate(800_000)
|
||||
.setWidth(1280)
|
||||
.setHeight(720)
|
||||
.build();
|
||||
|
||||
/** A generic audio {@link Format} which can be used to set up media sources and renderers. */
|
||||
public static final Format AUDIO_FORMAT =
|
||||
new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.AUDIO_AAC)
|
||||
.setAverageBitrate(100_000)
|
||||
.setChannelCount(2)
|
||||
.setSampleRate(44100)
|
||||
.build();
|
||||
|
||||
private Clock clock;
|
||||
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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user