Replace FakeExoPlayer with real player running with fake clock.

This ensures that simulated playbacks always use the current player implementation.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=179929911
This commit is contained in:
tonihei 2017-12-22 08:39:21 -08:00 committed by Oliver Woodman
parent 410e614cfd
commit f279f3c843
3 changed files with 16 additions and 594 deletions

View File

@ -19,7 +19,6 @@ import android.os.HandlerThread;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
@ -32,11 +31,11 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.MimeTypes;
@ -59,26 +58,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
*/
public static final class Builder {
/**
* Factory to create an {@link SimpleExoPlayer} instance. The player will be created on its own
* {@link HandlerThread}.
*/
public interface PlayerFactory {
/**
* Creates a new {@link SimpleExoPlayer} using the provided renderers factory, track selector,
* and load control.
*
* @param renderersFactory A {@link RenderersFactory} to be used for the new player.
* @param trackSelector A {@link MappingTrackSelector} to be used for the new player.
* @param loadControl A {@link LoadControl} to be used for the new player.
* @return A new {@link SimpleExoPlayer}.
*/
SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory,
MappingTrackSelector trackSelector, LoadControl loadControl);
}
/**
* A generic video {@link Format} which can be used to set up media sources and renderers.
*/
@ -93,7 +72,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
private Clock clock;
private PlayerFactory playerFactory;
private Timeline timeline;
private Object manifest;
private MediaSource mediaSource;
@ -223,21 +201,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
return this;
}
/**
* Sets the {@link PlayerFactory} which creates the {@link SimpleExoPlayer} to be used by the
* test runner. The default value is a {@link SimpleExoPlayer} with the renderers provided by
* {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)}, the
* track selector provided by {@link #setTrackSelector(MappingTrackSelector)} and the load
* control provided by {@link #setLoadControl(LoadControl)}.
*
* @param playerFactory A {@link PlayerFactory} to create the player.
* @return This builder.
*/
public Builder setExoPlayer(PlayerFactory playerFactory) {
this.playerFactory = playerFactory;
return this;
}
/**
* Sets the {@link Clock} to be used by the test runner. The default value is {@link
* Clock#DEFAULT}.
@ -345,15 +308,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
if (clock == null) {
clock = Clock.DEFAULT;
}
if (playerFactory == null) {
playerFactory = new PlayerFactory() {
@Override
public SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory,
MappingTrackSelector trackSelector, LoadControl loadControl) {
return ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl);
}
};
}
if (mediaSource == null) {
if (timeline == null) {
timeline = new FakeTimeline(1);
@ -365,7 +319,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
}
return new ExoPlayerTestRunner(
clock,
playerFactory,
mediaSource,
renderersFactory,
trackSelector,
@ -378,7 +331,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
}
}
private final PlayerFactory playerFactory;
private final Clock clock;
private final MediaSource mediaSource;
private final RenderersFactory renderersFactory;
private final MappingTrackSelector trackSelector;
@ -405,7 +358,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
private ExoPlayerTestRunner(
Clock clock,
PlayerFactory playerFactory,
MediaSource mediaSource,
RenderersFactory renderersFactory,
MappingTrackSelector trackSelector,
@ -415,7 +367,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
@Nullable VideoRendererEventListener videoRendererEventListener,
@Nullable AudioRendererEventListener audioRendererEventListener,
int expectedPlayerEndedCount) {
this.playerFactory = playerFactory;
this.clock = clock;
this.mediaSource = mediaSource;
this.renderersFactory = renderersFactory;
this.trackSelector = trackSelector;
@ -451,7 +403,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
@Override
public void run() {
try {
player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl);
player = new TestSimpleExoPlayer(renderersFactory, trackSelector, loadControl, clock);
player.addListener(ExoPlayerTestRunner.this);
if (eventListener != null) {
player.addListener(eventListener);
@ -685,4 +637,15 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
actionScheduleFinishedCountDownLatch.countDown();
}
/** SimpleExoPlayer implementation using a custom Clock. */
private static final class TestSimpleExoPlayer extends SimpleExoPlayer {
public TestSimpleExoPlayer(
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
Clock clock) {
super(renderersFactory, trackSelector, loadControl, clock);
}
}
}

View File

@ -24,7 +24,7 @@ import java.util.ArrayList;
import java.util.List;
/** Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. */
public final class FakeClock implements Clock {
public class FakeClock implements Clock {
private final List<Long> wakeUpTimes;
private final List<HandlerMessageData> handlerMessages;

View File

@ -1,541 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.testutil;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Fake {@link SimpleExoPlayer} which runs a simplified copy of the playback loop as fast as
* possible without waiting. It does only support single period timelines and does not support
* updates during playback (like seek, timeline changes, repeat mode changes).
*/
public class FakeSimpleExoPlayer extends SimpleExoPlayer {
private FakeExoPlayer player;
public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
LoadControl loadControl, FakeClock clock) {
super(renderersFactory, trackSelector, loadControl, clock);
player.setFakeClock(clock);
}
@Override
protected ExoPlayer createExoPlayerImpl(
Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
this.player = new FakeExoPlayer(renderers, trackSelector, loadControl);
return player;
}
private static class FakeExoPlayer extends StubExoPlayer implements MediaSource.Listener,
MediaPeriod.Callback, Runnable {
private final Renderer[] renderers;
private final TrackSelector trackSelector;
private final LoadControl loadControl;
private final CopyOnWriteArraySet<Player.EventListener> eventListeners;
private final HandlerThread playbackThread;
private final Handler playbackHandler;
private final Handler eventListenerHandler;
private FakeClock clock;
private MediaSource mediaSource;
private Timeline timeline;
private Object manifest;
private MediaPeriod mediaPeriod;
private TrackSelectorResult selectorResult;
private boolean isStartingUp;
private boolean isLoading;
private int playbackState;
private long rendererPositionUs;
private long durationUs;
private volatile long currentPositionMs;
private volatile long bufferedPositionMs;
public FakeExoPlayer(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
this.renderers = renderers;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
this.eventListeners = new CopyOnWriteArraySet<>();
Looper eventListenerLooper = Looper.myLooper();
this.eventListenerHandler = new Handler(eventListenerLooper != null ? eventListenerLooper
: Looper.getMainLooper());
this.playbackThread = new HandlerThread("FakeExoPlayer Thread");
playbackThread.start();
this.playbackHandler = new Handler(playbackThread.getLooper());
this.isStartingUp = true;
this.isLoading = false;
this.playbackState = Player.STATE_IDLE;
this.durationUs = C.TIME_UNSET;
}
public void setFakeClock(FakeClock clock) {
this.clock = clock;
}
@Override
public void addListener(Player.EventListener listener) {
eventListeners.add(listener);
}
@Override
public void removeListener(Player.EventListener listener) {
eventListeners.remove(listener);
}
@Override
public int getPlaybackState() {
return playbackState;
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
if (!playWhenReady) {
throw new UnsupportedOperationException();
}
}
@Override
public boolean getPlayWhenReady() {
return true;
}
@Override
public int getRepeatMode() {
return Player.REPEAT_MODE_OFF;
}
@Override
public boolean getShuffleModeEnabled() {
return false;
}
@Override
public boolean isLoading() {
return isLoading;
}
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@Override
public void stop() {
stop(/* reset= */ false);
}
@Override
public void stop(boolean reset) {
stopPlayback(/* quitPlaybackThread= */ false);
}
@Override
@SuppressWarnings("ThreadJoinLoop")
public void release() {
stopPlayback(/* quitPlaybackThread= */ true);
while (playbackThread.isAlive()) {
try {
playbackThread.join();
} catch (InterruptedException e) {
// Ignore interrupt.
}
}
}
@Override
public int getRendererCount() {
return renderers.length;
}
@Override
public int getRendererType(int index) {
return renderers[index].getTrackType();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return selectorResult != null ? selectorResult.groups : null;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return selectorResult != null ? selectorResult.selections : null;
}
@Nullable
@Override
public Object getCurrentManifest() {
return manifest;
}
@Override
public Timeline getCurrentTimeline() {
return timeline;
}
@Override
public int getCurrentPeriodIndex() {
return 0;
}
@Override
public int getCurrentWindowIndex() {
return 0;
}
@Override
public int getNextWindowIndex() {
return C.INDEX_UNSET;
}
@Override
public int getPreviousWindowIndex() {
return C.INDEX_UNSET;
}
@Override
public long getDuration() {
return C.usToMs(durationUs);
}
@Override
public long getCurrentPosition() {
return currentPositionMs;
}
@Override
public long getBufferedPosition() {
return bufferedPositionMs == C.TIME_END_OF_SOURCE ? getDuration() : bufferedPositionMs;
}
@Override
public int getBufferedPercentage() {
long duration = getDuration();
return duration == C.TIME_UNSET ? 0 : (int) (getBufferedPosition() * 100 / duration);
}
@Override
public boolean isCurrentWindowDynamic() {
return false;
}
@Override
public boolean isCurrentWindowSeekable() {
return false;
}
@Override
public boolean isPlayingAd() {
return false;
}
@Override
public int getCurrentAdGroupIndex() {
return 0;
}
@Override
public int getCurrentAdIndexInAdGroup() {
return 0;
}
@Override
public long getContentPosition() {
return getCurrentPosition();
}
@Override
public Looper getPlaybackLooper() {
return playbackThread.getLooper();
}
@Override
public void prepare(MediaSource mediaSource) {
prepare(mediaSource, true, true);
}
@Override
public void prepare(final MediaSource mediaSource, boolean resetPosition, boolean resetState) {
if (!resetPosition || !resetState) {
throw new UnsupportedOperationException();
}
this.mediaSource = mediaSource;
playbackHandler.post(new Runnable() {
@Override
public void run() {
mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this);
}
});
}
// MediaSource.Listener
@Override
public void onSourceInfoRefreshed(MediaSource source, final Timeline timeline,
final @Nullable Object manifest) {
if (this.timeline != null) {
throw new UnsupportedOperationException();
}
Assertions.checkArgument(timeline.getPeriodCount() == 1);
Assertions.checkArgument(timeline.getWindowCount() == 1);
final ConditionVariable waitForNotification = new ConditionVariable();
eventListenerHandler.post(new Runnable() {
@Override
public void run() {
for (Player.EventListener eventListener : eventListeners) {
FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs;
FakeExoPlayer.this.timeline = timeline;
FakeExoPlayer.this.manifest = manifest;
eventListener.onTimelineChanged(timeline, manifest,
Player.TIMELINE_CHANGE_REASON_PREPARED);
waitForNotification.open();
}
}
});
waitForNotification.block();
this.mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), loadControl.getAllocator());
mediaPeriod.prepare(this, 0);
}
// MediaPeriod.Callback
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
maybeContinueLoading();
}
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
try {
initializePlaybackLoop();
} catch (ExoPlaybackException e) {
handlePlayerError(e);
}
}
// Runnable (Playback loop).
@Override
public void run() {
try {
maybeContinueLoading();
mediaPeriod.discardBuffer(rendererPositionUs, /* toKeyframe= */ false);
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
if (playbackState == Player.STATE_READY) {
for (Renderer renderer : renderers) {
renderer.render(rendererPositionUs, C.msToUs(clock.elapsedRealtime()));
if (!renderer.isEnded()) {
allRenderersEnded = false;
}
if (!(renderer.isReady() || renderer.isEnded())) {
allRenderersReadyOrEnded = false;
}
}
}
if (rendererPositionUs >= durationUs && allRenderersEnded) {
changePlaybackState(Player.STATE_ENDED);
return;
}
long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
if (playbackState == Player.STATE_BUFFERING && allRenderersReadyOrEnded
&& haveSufficientBuffer(!isStartingUp, rendererPositionUs, bufferedPositionUs)) {
changePlaybackState(Player.STATE_READY);
isStartingUp = false;
} else if (playbackState == Player.STATE_READY && !allRenderersReadyOrEnded) {
changePlaybackState(Player.STATE_BUFFERING);
}
// Advance simulated time by 10ms.
clock.advanceTime(10);
if (playbackState == Player.STATE_READY) {
rendererPositionUs += 10000;
}
this.currentPositionMs = C.usToMs(rendererPositionUs);
this.bufferedPositionMs = C.usToMs(bufferedPositionUs);
playbackHandler.post(this);
} catch (ExoPlaybackException e) {
handlePlayerError(e);
}
}
// Internal logic
private void initializePlaybackLoop() throws ExoPlaybackException {
Assertions.checkNotNull(clock);
trackSelector.init(new InvalidationListener() {
@Override
public void onTrackSelectionsInvalidated() {
throw new IllegalStateException();
}
});
RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
rendererCapabilities[i] = renderers[i].getCapabilities();
}
selectorResult = trackSelector.selectTracks(rendererCapabilities,
mediaPeriod.getTrackGroups());
SampleStream[] sampleStreams = new SampleStream[renderers.length];
boolean[] mayRetainStreamFlags = new boolean[renderers.length];
Arrays.fill(mayRetainStreamFlags, true);
mediaPeriod.selectTracks(
selectorResult.selections.getAll(),
mayRetainStreamFlags,
sampleStreams,
new boolean[renderers.length],
/* positionUs = */ 0);
eventListenerHandler.post(new Runnable() {
@Override
public void run() {
for (Player.EventListener eventListener : eventListeners) {
eventListener.onTracksChanged(selectorResult.groups, selectorResult.selections);
}
}
});
loadControl.onPrepared();
loadControl.onTracksSelected(renderers, selectorResult.groups, selectorResult.selections);
for (int i = 0; i < renderers.length; i++) {
TrackSelection selection = selectorResult.selections.get(i);
Format[] formats = new Format[selection.length()];
for (int j = 0; j < formats.length; j++) {
formats[j] = selection.getFormat(j);
}
renderers[i].enable(selectorResult.rendererConfigurations[i], formats, sampleStreams[i], 0,
false, 0);
renderers[i].setCurrentStreamFinal();
}
rendererPositionUs = 0;
changePlaybackState(Player.STATE_BUFFERING);
playbackHandler.post(this);
}
private void maybeContinueLoading() {
boolean newIsLoading = false;
long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) {
long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs;
if (loadControl.shouldContinueLoading(bufferedDurationUs, 1f)) {
newIsLoading = true;
mediaPeriod.continueLoading(rendererPositionUs);
}
}
if (newIsLoading != isLoading) {
isLoading = newIsLoading;
eventListenerHandler.post(new Runnable() {
@Override
public void run() {
for (Player.EventListener eventListener : eventListeners) {
eventListener.onLoadingChanged(isLoading);
}
}
});
}
}
private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs,
long bufferedPositionUs) {
return bufferedPositionUs == C.TIME_END_OF_SOURCE
|| loadControl.shouldStartPlayback(
bufferedPositionUs - rendererPositionUs, 1f, rebuffering);
}
private void handlePlayerError(final ExoPlaybackException e) {
eventListenerHandler.post(new Runnable() {
@Override
public void run() {
for (Player.EventListener listener : eventListeners) {
listener.onPlayerError(e);
}
}
});
changePlaybackState(Player.STATE_ENDED);
}
private void changePlaybackState(final int playbackState) {
this.playbackState = playbackState;
eventListenerHandler.post(new Runnable() {
@Override
public void run() {
for (Player.EventListener listener : eventListeners) {
listener.onPlayerStateChanged(true, playbackState);
}
}
});
}
private void releaseMedia() {
if (mediaSource != null) {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
mediaPeriod = null;
}
mediaSource.releaseSource();
mediaSource = null;
}
}
private void stopPlayback(final boolean quitPlaybackThread) {
playbackHandler.post(new Runnable() {
@Override
public void run () {
playbackHandler.removeCallbacksAndMessages(null);
releaseMedia();
changePlaybackState(Player.STATE_IDLE);
if (quitPlaybackThread) {
playbackThread.quit();
}
}
});
}
}
}