Add fake simple exo player.

This implementation runs as fast as possible by triggering a simplified
playback loop continuously without waiting. The class only supports a basic
use case with a single-period timeline, no timeline update, no seeks or other
user-initiated actions.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=164120420
This commit is contained in:
tonihei 2017-08-03 07:22:15 -07:00 committed by Oliver Woodman
parent 6f7dc974c9
commit ccd05cbd04
2 changed files with 535 additions and 1 deletions

View File

@ -141,7 +141,7 @@ public class SimpleExoPlayer implements ExoPlayer {
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects. // Build the player and associated objects.
player = new ExoPlayerImpl(renderers, trackSelector, loadControl); player = createExoPlayerImpl(renderers, trackSelector, loadControl);
} }
/** /**
@ -722,6 +722,19 @@ public class SimpleExoPlayer implements ExoPlayer {
// Internal methods. // Internal methods.
/**
* Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}.
*
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @return A new {@link ExoPlayer} instance.
*/
protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl);
}
private void removeSurfaceCallbacks() { private void removeSurfaceCallbacks() {
if (textureView != null) { if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) { if (textureView.getSurfaceTextureListener() != componentListener) {

View File

@ -0,0 +1,521 @@
/*
* 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 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);
player.setFakeClock(clock);
}
@Override
protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
this.player = new FakeExoPlayer(renderers, trackSelector, loadControl);
return player;
}
private class FakeExoPlayer implements ExoPlayer, 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 != true) {
throw new UnsupportedOperationException();
}
}
@Override
public boolean getPlayWhenReady() {
return true;
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
throw new UnsupportedOperationException();
}
@Override
public int getRepeatMode() {
return Player.REPEAT_MODE_OFF;
}
@Override
public boolean isLoading() {
return isLoading;
}
@Override
public void seekToDefaultPosition() {
throw new UnsupportedOperationException();
}
@Override
public void seekToDefaultPosition(int windowIndex) {
throw new UnsupportedOperationException();
}
@Override
public void seekTo(long positionMs) {
throw new UnsupportedOperationException();
}
@Override
public void seekTo(int windowIndex, long positionMs) {
throw new UnsupportedOperationException();
}
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
throw new UnsupportedOperationException();
}
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@Override
public void stop() {
throw new UnsupportedOperationException();
}
@Override
public void release() {
playbackThread.quit();
}
@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 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 != true || resetState != true) {
throw new UnsupportedOperationException();
}
this.mediaSource = mediaSource;
playbackHandler.post(new Runnable() {
@Override
public void run() {
mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this);
}
});
}
@Override
public void sendMessages(ExoPlayerMessage... messages) {
throw new UnsupportedOperationException();
}
@Override
public void blockingSendMessages(ExoPlayerMessage... messages) {
throw new UnsupportedOperationException();
}
// MediaSource.Listener
@Override
public void onSourceInfoRefreshed(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);
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();
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], 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)) {
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) {
if (bufferedPositionUs == C.TIME_END_OF_SOURCE) {
return true;
}
return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, 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);
}
}
});
}
}
}