Add playback tests to V2.

DashTest will be migrated separately, since it's a little
more work.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=124722462
This commit is contained in:
olly 2016-06-13 07:06:44 -07:00 committed by Oliver Woodman
parent 0978227a84
commit 5cd7deffe7
13 changed files with 1355 additions and 1 deletions

View File

@ -447,7 +447,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
}
private void flushCodec() throws ExoPlaybackException {
protected void flushCodec() throws ExoPlaybackException {
codecHotswapDeadlineMs = -1;
inputIndex = -1;
outputIndex = -1;

View File

@ -0,0 +1,38 @@
// Copyright (C) 2014 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.
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
compile project(':library')
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.playbacktests"
android:versionCode="1508"
android:versionName="1.5.8">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/>
<activity android:name="com.google.android.exoplayer.playbacktests.util.HostActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="ExoPlayerTest"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer.playbacktests"
android:name="android.test.InstrumentationTestRunner"/>
</manifest>

View File

@ -0,0 +1,150 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import com.google.android.exoplayer.DefaultTrackSelector;
import com.google.android.exoplayer.ExoPlayer;
import android.util.Log;
/**
* Base class for actions to perform during playback tests.
*/
public abstract class Action {
private final String tag;
private final String description;
/**
* @param tag A tag to use for logging.
* @param description A description to be logged when the action is executed.
*/
public Action(String tag, String description) {
this.tag = tag;
this.description = description;
}
/**
* Executes the action.
*
* @param player The player to which the action should be applied.
* @param trackSelector The track selector to which the action should be applied.
*/
public final void doAction(ExoPlayer player, DefaultTrackSelector trackSelector) {
Log.i(tag, description);
doActionImpl(player, trackSelector);
}
/**
* Called by {@link #doAction(ExoPlayer, DefaultTrackSelector)} do actually perform the action.
*
* @param player The player to which the action should be applied.
* @param trackSelector The track selector to which the action should be applied.
*/
protected abstract void doActionImpl(ExoPlayer player, DefaultTrackSelector trackSelector);
/**
* Calls {@link ExoPlayer#seekTo(long)}.
*/
public static final class Seek extends Action {
private final long positionMs;
/**
* @param tag A tag to use for logging.
* @param positionMs The seek position.
*/
public Seek(String tag, long positionMs) {
super(tag, "Seek:" + positionMs);
this.positionMs = positionMs;
}
@Override
protected void doActionImpl(ExoPlayer player, DefaultTrackSelector trackSelector) {
player.seekTo(positionMs);
}
}
/**
* Calls {@link ExoPlayer#stop()}.
*/
public static final class Stop extends Action {
/**
* @param tag A tag to use for logging.
*/
public Stop(String tag) {
super(tag, "Stop");
}
@Override
protected void doActionImpl(ExoPlayer player, DefaultTrackSelector trackSelector) {
player.stop();
}
}
/**
* Calls {@link ExoPlayer#setPlayWhenReady(boolean)}.
*/
public static final class SetPlayWhenReady extends Action {
private final boolean playWhenReady;
/**
* @param tag A tag to use for logging.
* @param playWhenReady The value to pass.
*/
public SetPlayWhenReady(String tag, boolean playWhenReady) {
super(tag, playWhenReady ? "Play" : "Pause");
this.playWhenReady = playWhenReady;
}
@Override
protected void doActionImpl(ExoPlayer player, DefaultTrackSelector trackSelector) {
player.setPlayWhenReady(playWhenReady);
}
}
/**
* Calls {@link DefaultTrackSelector#setRendererDisabled(int, boolean)}.
*/
public static final class SetRendererDisabled extends Action {
private final int rendererIndex;
private final boolean disabled;
/**
* @param tag A tag to use for logging.
* @param rendererIndex The index of the renderer.
* @param disabled Whether the renderer should be disabled.
*/
public SetRendererDisabled(String tag, int rendererIndex, boolean disabled) {
super(tag, "SetRendererDisabled:" + rendererIndex + ":" + disabled);
this.rendererIndex = rendererIndex;
this.disabled = disabled;
}
@Override
protected void doActionImpl(ExoPlayer player, DefaultTrackSelector trackSelector) {
trackSelector.setRendererDisabled(rendererIndex, disabled);
}
}
}

View File

@ -0,0 +1,233 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import com.google.android.exoplayer.DefaultTrackSelector;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.playbacktests.util.Action.Seek;
import com.google.android.exoplayer.playbacktests.util.Action.SetPlayWhenReady;
import com.google.android.exoplayer.playbacktests.util.Action.SetRendererDisabled;
import com.google.android.exoplayer.playbacktests.util.Action.Stop;
import android.os.Handler;
/**
* Schedules a sequence of {@link Action}s for execution during a test.
*/
public final class ActionSchedule {
private final ActionNode rootNode;
/**
* @param rootNode The first node in the sequence.
*/
private ActionSchedule(ActionNode rootNode) {
this.rootNode = rootNode;
}
/**
* Starts execution of the schedule.
*
* @param player The player to which actions should be applied.
* @param trackSelector The track selector to which actions should be applied.
* @param mainHandler A handler associated with the main thread of the host activity.
*/
/* package */ void start(ExoPlayer player, DefaultTrackSelector trackSelector,
Handler mainHandler) {
rootNode.schedule(player, trackSelector, mainHandler);
}
/**
* A builder for {@link ActionSchedule} instances.
*/
public static final class Builder {
private final String tag;
private final ActionNode rootNode;
private long currentDelayMs;
private ActionNode previousNode;
/**
* @param tag A tag to use for logging.
*/
public Builder(String tag) {
this.tag = tag;
rootNode = new ActionNode(new RootAction(tag), 0);
previousNode = rootNode;
}
/**
* Schedules a delay between executing any previous actions and any subsequent ones.
*
* @param delayMs The delay in milliseconds.
* @return The builder, for convenience.
*/
public Builder delay(long delayMs) {
currentDelayMs += delayMs;
return this;
}
/**
* Schedules an action to be executed.
*
* @param action The action to schedule.
* @return The builder, for convenience.
*/
public Builder apply(Action action) {
ActionNode next = new ActionNode(action, currentDelayMs);
previousNode.setNext(next);
previousNode = next;
currentDelayMs = 0;
return this;
}
/**
* Schedules a seek action to be executed.
*
* @param positionMs The seek position.
* @return The builder, for convenience.
*/
public Builder seek(long positionMs) {
return apply(new Seek(tag, positionMs));
}
/**
* Schedules a stop action to be executed.
*
* @return The builder, for convenience.
*/
public Builder stop() {
return apply(new Stop(tag));
}
/**
* Schedules a play action to be executed.
*
* @return The builder, for convenience.
*/
public Builder play() {
return apply(new SetPlayWhenReady(tag, true));
}
/**
* Schedules a pause action to be executed.
*
* @return The builder, for convenience.
*/
public Builder pause() {
return apply(new SetPlayWhenReady(tag, false));
}
/**
* Schedules a renderer enable action to be executed.
*
* @return The builder, for convenience.
*/
public Builder enableRenderer(int index) {
return apply(new SetRendererDisabled(tag, index, false));
}
/**
* Schedules a renderer disable action to be executed.
*
* @return The builder, for convenience.
*/
public Builder disableRenderer(int index) {
return apply(new SetRendererDisabled(tag, index, true));
}
public ActionSchedule build() {
return new ActionSchedule(rootNode);
}
}
/**
* Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified.
*/
private static final class ActionNode implements Runnable {
private final Action action;
private final long delayMs;
private ActionNode next;
private ExoPlayer player;
private DefaultTrackSelector trackSelector;
private Handler mainHandler;
/**
* @param action The wrapped action.
* @param delayMs The delay between the node being scheduled and the action being executed.
*/
public ActionNode(Action action, long delayMs) {
this.action = action;
this.delayMs = delayMs;
}
/**
* Sets the next action.
*
* @param next The next {@link Action}.
*/
public void setNext(ActionNode next) {
this.next = next;
}
/**
* Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node
* will be scheduled immediately after {@link #action} is executed.
*
* @param player The player to which actions should be applied.
* @param trackSelector The track selector to which actions should be applied.
* @param mainHandler A handler associated with the main thread of the host activity.
*/
public void schedule(ExoPlayer player, DefaultTrackSelector trackSelector,
Handler mainHandler) {
this.player = player;
this.trackSelector = trackSelector;
this.mainHandler = mainHandler;
mainHandler.postDelayed(this, delayMs);
}
@Override
public void run() {
action.doAction(player, trackSelector);
if (next != null) {
next.schedule(player, trackSelector, mainHandler);
}
}
}
/**
* A no-op root action.
*/
private static final class RootAction extends Action {
public RootAction(String tag) {
super(tag, "Root");
}
@Override
protected void doActionImpl(ExoPlayer player, DefaultTrackSelector trackSelector) {
// Do nothing.
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import com.google.android.exoplayer.CodecCounters;
import junit.framework.TestCase;
/**
* Assertions for {@link CodecCounters}.
*/
public final class CodecCountersUtil {
private CodecCountersUtil() {}
/**
* Returns the sum of the skipped, dropped and rendered buffers.
*
* @param counters The counters for which the total should be calculated.
* @return The sum of the skipped, dropped and rendered buffers.
*/
public static int getTotalOutputBuffers(CodecCounters counters) {
return counters.skippedOutputBufferCount + counters.droppedOutputBufferCount
+ counters.renderedOutputBufferCount;
}
public static void assertSkippedOutputBufferCount(String name, CodecCounters counters,
int expected) {
counters.ensureUpdated();
int actual = counters.skippedOutputBufferCount;
TestCase.assertEquals("Codec(" + name + ") skipped " + actual + " buffers. Expected "
+ expected + ".", expected, actual);
}
public static void assertTotalOutputBufferCount(String name, CodecCounters counters,
int minCount, int maxCount) {
counters.ensureUpdated();
int actual = getTotalOutputBuffers(counters);
TestCase.assertTrue("Codec(" + name + ") output " + actual + " buffers. Expected in range ["
+ minCount + ", " + maxCount + "].", minCount <= actual && actual <= maxCount);
}
public static void assertDroppedOutputBufferLimit(String name, CodecCounters counters,
int limit) {
counters.ensureUpdated();
int actual = counters.droppedOutputBufferCount;
TestCase.assertTrue("Codec(" + name + ") was late decoding: " + actual + " buffers. "
+ "Limit: " + limit + ".", actual <= limit);
}
public static void assertConsecutiveDroppedOutputBufferLimit(String name, CodecCounters counters,
int limit) {
counters.ensureUpdated();
int actual = counters.maxConsecutiveDroppedOutputBufferCount;
TestCase.assertTrue("Codec(" + name + ") was late decoding: " + actual
+ " buffers consecutively. " + "Limit: " + limit + ".", actual <= limit);
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import com.google.android.exoplayer.DecoderInputBuffer;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.VideoTrackRendererEventListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Handler;
/**
* Decodes and renders video using {@link MediaCodecVideoTrackRenderer}. Provides buffer timestamp
* assertions.
*/
@TargetApi(16)
public class DebugMediaCodecVideoTrackRenderer extends MediaCodecVideoTrackRenderer {
private static final int ARRAY_SIZE = 1000;
private final long[] timestampsList = new long[ARRAY_SIZE];
private int startIndex;
private int queueSize;
private int bufferCount;
public DebugMediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector,
int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler,
VideoTrackRendererEventListener eventListener, int maxDroppedFrameCountToNotify) {
super(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false,
eventHandler, eventListener, maxDroppedFrameCountToNotify);
startIndex = 0;
queueSize = 0;
}
@Override
protected void releaseCodec() {
super.releaseCodec();
clearTimestamps();
}
@Override
protected void flushCodec() throws ExoPlaybackException {
super.flushCodec();
clearTimestamps();
}
@Override
protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
insertTimestamp(buffer.timeUs);
maybeShiftTimestampsList();
}
@Override
protected void onProcessedOutputBuffer(long presentationTimeUs) {
bufferCount++;
long expectedTimestampUs = dequeueTimestamp();
if (expectedTimestampUs != presentationTimeUs) {
throw new IllegalStateException("Expected to dequeue video buffer with presentation "
+ "timestamp: " + expectedTimestampUs + ". Instead got: " + presentationTimeUs
+ " (Processed buffers since last flush: " + bufferCount + ").");
}
}
private void clearTimestamps() {
startIndex = 0;
queueSize = 0;
bufferCount = 0;
}
private void insertTimestamp(long presentationTimeUs) {
for (int i = startIndex + queueSize - 1; i >= startIndex; i--) {
if (presentationTimeUs >= timestampsList[i]) {
timestampsList[i + 1] = presentationTimeUs;
queueSize++;
return;
}
timestampsList[i + 1] = timestampsList[i];
}
timestampsList[startIndex] = presentationTimeUs;
queueSize++;
}
private void maybeShiftTimestampsList() {
if (startIndex + queueSize == ARRAY_SIZE) {
System.arraycopy(timestampsList, startIndex, timestampsList, 0, queueSize);
startIndex = 0;
}
}
private long dequeueTimestamp() {
startIndex++;
queueSize--;
return timestampsList[startIndex - 1];
}
}

View File

@ -0,0 +1,293 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.DefaultTrackSelectionPolicy;
import com.google.android.exoplayer.DefaultTrackSelector;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.ExoPlayerFactory;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.SimpleExoPlayer;
import com.google.android.exoplayer.TrackSelectionPolicy;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.playbacktests.util.HostActivity.HostedTest;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSourceFactory;
import com.google.android.exoplayer.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer.util.Util;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;
/**
* A {@link HostedTest} for {@link ExoPlayer} playback tests.
*/
public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListener,
SimpleExoPlayer.DebugListener {
static {
// ExoPlayer's AudioTrack class is able to work around spurious timestamps reported by the
// platform (by ignoring them). Disable this workaround, since we're interested in testing
// that the underlying platform is behaving correctly.
AudioTrack.failOnSpuriousAudioTimestamp = true;
}
private final String tag;
private final boolean failOnPlayerError;
private ActionSchedule pendingSchedule;
private Handler actionHandler;
private DefaultTrackSelector trackSelector;
private SimpleExoPlayer player;
private ExoPlaybackException playerError;
private boolean playerWasPrepared;
private boolean playerFinished;
private boolean playing;
private long totalPlayingTimeMs;
private long lastPlayingStartTimeMs;
private CodecCounters videoCodecCounters;
private CodecCounters audioCodecCounters;
/**
* Constructs a test that fails if a player error occurs.
*
* @param tag A tag to use for logging.
*/
public ExoHostedTest(String tag) {
this(tag, true);
}
/**
* @param tag A tag to use for logging.
* @param failOnPlayerError True if a player error should be considered a test failure. False
* otherwise.
*/
public ExoHostedTest(String tag, boolean failOnPlayerError) {
this.tag = tag;
this.failOnPlayerError = failOnPlayerError;
}
/**
* Sets a schedule to be applied during the test.
*
* @param schedule The schedule.
*/
public final void setSchedule(ActionSchedule schedule) {
if (player == null) {
pendingSchedule = schedule;
} else {
schedule.start(player, trackSelector, actionHandler);
}
}
// HostedTest implementation
@Override
public final void onStart(HostActivity host, Surface surface) {
// Build the player.
TrackSelectionPolicy trackSelectionPolicy = buildTrackSelectionPolicy(host);
trackSelector = new DefaultTrackSelector(trackSelectionPolicy, null);
player = buildExoPlayer(host, surface, trackSelector);
DataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(host, Util
.getUserAgent(host, "ExoPlayerPlaybackTests"));
player.setSource(buildSource(host, dataSourceFactory, player.getBandwidthMeter()));
player.addListener(this);
player.setDebugListener(this);
player.setPlayWhenReady(true);
actionHandler = new Handler();
// Schedule any pending actions.
if (pendingSchedule != null) {
pendingSchedule.start(player, trackSelector, actionHandler);
pendingSchedule = null;
}
}
@Override
public final void onStop() {
actionHandler.removeCallbacksAndMessages(null);
player.release();
player = null;
}
@Override
public final boolean isFinished() {
return playerFinished;
}
@Override
public final void onFinished() {
if (failOnPlayerError && playerError != null) {
throw new Error(playerError);
}
logMetrics();
assertPassed();
}
// ExoPlayer.Listener
@Override
public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]");
playerWasPrepared |= playbackState != ExoPlayer.STATE_IDLE;
if (playbackState == ExoPlayer.STATE_ENDED
|| (playbackState == ExoPlayer.STATE_IDLE && playerWasPrepared)) {
playerFinished = true;
}
boolean playing = playWhenReady && playbackState == ExoPlayer.STATE_READY;
if (!this.playing && playing) {
lastPlayingStartTimeMs = SystemClock.elapsedRealtime();
} else if (this.playing && !playing) {
totalPlayingTimeMs += SystemClock.elapsedRealtime() - lastPlayingStartTimeMs;
}
this.playing = playing;
}
@Override
public final void onPlayerError(ExoPlaybackException error) {
playerWasPrepared = true;
playerError = error;
onPlayerErrorInternal(error);
}
@Override
public final void onPlayWhenReadyCommitted() {
// Do nothing.
}
// SimpleExoPlayer.DebugListener
@Override
public void onAudioEnabled(CodecCounters counters) {
Log.d(tag, "audioEnabled");
}
@Override
public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(tag, "audioDecoderInitialized [" + decoderName + "]");
}
@Override
public void onAudioFormatChanged(Format format) {
Log.d(tag, "audioFormatChanged [" + format.id + "]");
if (format != null) {
audioCodecCounters = player.getVideoCodecCounters();
}
}
@Override
public void onAudioDisabled(CodecCounters counters) {
Log.d(tag, "audioDisabled");
}
@Override
public void onVideoEnabled(CodecCounters counters) {
Log.d(tag, "videoEnabled");
}
@Override
public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(tag, "videoDecoderInitialized [" + decoderName + "]");
}
@Override
public void onVideoFormatChanged(Format format) {
Log.d(tag, "videoFormatChanged [" + format.id + "]");
if (format != null) {
videoCodecCounters = player.getVideoCodecCounters();
}
}
@Override
public void onVideoDisabled(CodecCounters counters) {
Log.d(tag, "videoDisabled");
}
@Override
public void onDroppedFrames(int count, long elapsed) {
Log.d(tag, "droppedFrames [" + count + "]");
}
@Override
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
Log.e(tag, "audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
+ elapsedSinceLastFeedMs + "]", null);
}
// Internal logic
@SuppressWarnings("unused")
protected TrackSelectionPolicy buildTrackSelectionPolicy(HostActivity host) {
return new DefaultTrackSelectionPolicy();
}
@SuppressWarnings("unused")
protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface,
DefaultTrackSelector trackSelector) {
SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(host, trackSelector);
player.setSurface(surface);
return player;
}
@SuppressWarnings("unused")
protected abstract SampleSource buildSource(HostActivity host,
DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter);
@SuppressWarnings("unused")
protected void onPlayerErrorInternal(ExoPlaybackException error) {
// Do nothing. Interested subclasses may override.
}
protected void assertPassed() {
// Do nothing. Subclasses may override to add additional assertions.
}
protected void logMetrics() {
// Do nothing. Subclasses may override to log metrics.
}
// Utility methods and actions for subclasses.
protected final long getTotalPlayingTimeMs() {
return totalPlayingTimeMs;
}
protected final ExoPlaybackException getError() {
return playerError;
}
protected final CodecCounters getLastVideoCodecCounters() {
if (videoCodecCounters != null) {
videoCodecCounters.ensureUpdated();
}
return videoCodecCounters;
}
protected final CodecCounters getLastAudioCodecCounters() {
if (audioCodecCounters != null) {
audioCodecCounters.ensureUpdated();
}
return audioCodecCounters;
}
}

View File

@ -0,0 +1,252 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import static junit.framework.Assert.fail;
import com.google.android.exoplayer.playbacktests.R;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
/**
* A host activity for performing playback tests.
*/
public final class HostActivity extends Activity implements SurfaceHolder.Callback {
/**
* Interface for tests that run inside of a {@link HostActivity}.
*/
public interface HostedTest {
/**
* Called on the main thread when the test is started.
* <p>
* The test will not be started until the {@link HostActivity} has been resumed and its
* {@link Surface} has been created.
*
* @param host The {@link HostActivity} in which the test is being run.
* @param surface The {@link Surface}.
*/
void onStart(HostActivity host, Surface surface);
/**
* Called on the main thread when the test is stopped.
* <p>
* The test will be stopped if it has finished, if the {@link HostActivity} has been paused, or
* if the {@link HostActivity}'s {@link Surface} has been destroyed.
*/
void onStop();
/**
* Called on the main thread to check whether the test has finished.
*
* @return True if the test has finished. False otherwise.
*/
boolean isFinished();
/**
* Called on the main thread after the test has finished and been stopped.
* <p>
* Implementations may use this method to assert that test criteria were met.
*/
void onFinished();
}
private static final String TAG = "HostActivity";
private WakeLock wakeLock;
private WifiLock wifiLock;
private SurfaceView surfaceView;
private Handler mainHandler;
private CheckFinishedRunnable checkFinishedRunnable;
private HostedTest hostedTest;
private ConditionVariable hostedTestStoppedCondition;
private boolean hostedTestStarted;
private boolean hostedTestFinished;
/**
* Executes a {@link HostedTest} inside the host.
*
* @param hostedTest The test to execute.
* @param timeoutMs The number of milliseconds to wait for the test to finish. If the timeout
* is exceeded then the test will fail.
*/
public void runTest(final HostedTest hostedTest, long timeoutMs) {
Assertions.checkArgument(timeoutMs > 0);
Assertions.checkState(Thread.currentThread() != getMainLooper().getThread());
Assertions.checkState(this.hostedTest == null);
this.hostedTest = Assertions.checkNotNull(hostedTest);
hostedTestStoppedCondition = new ConditionVariable();
hostedTestStarted = false;
hostedTestFinished = false;
runOnUiThread(new Runnable() {
@Override
public void run() {
maybeStartHostedTest();
}
});
if (hostedTestStoppedCondition.block(timeoutMs)) {
if (hostedTestFinished) {
Log.d(TAG, "Test finished. Checking pass conditions.");
hostedTest.onFinished();
Log.d(TAG, "Pass conditions checked.");
} else {
String message = "Test released before it finished. Activity may have been paused whilst "
+ "test was in progress.";
Log.e(TAG, message);
fail(message);
}
} else {
String message = "Test timed out after " + timeoutMs + " ms.";
Log.e(TAG, message);
fail(message);
}
}
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.host_activity);
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
surfaceView.getHolder().addCallback(this);
mainHandler = new Handler();
checkFinishedRunnable = new CheckFinishedRunnable();
}
@Override
public void onStart() {
Context appContext = getApplicationContext();
WifiManager wifiManager = (WifiManager) appContext.getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiManager.createWifiLock(getWifiLockMode(), TAG);
wifiLock.acquire();
PowerManager powerManager = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wakeLock.acquire();
super.onStart();
}
@Override
public void onResume() {
super.onResume();
maybeStartHostedTest();
}
@Override
public void onPause() {
super.onPause();
maybeStopHostedTest();
}
@Override
public void onStop() {
super.onStop();
wakeLock.release();
wakeLock = null;
wifiLock.release();
wifiLock = null;
}
// SurfaceHolder.Callback
@Override
public void surfaceCreated(SurfaceHolder holder) {
maybeStartHostedTest();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
maybeStopHostedTest();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Do nothing.
}
// Internal logic
private void maybeStartHostedTest() {
if (hostedTest == null || hostedTestStarted) {
return;
}
Surface surface = surfaceView.getHolder().getSurface();
if (surface != null && surface.isValid()) {
hostedTestStarted = true;
Log.d(TAG, "Starting test.");
hostedTest.onStart(this, surface);
checkFinishedRunnable.startChecking();
}
}
private void maybeStopHostedTest() {
if (hostedTest != null && hostedTestStarted) {
hostedTest.onStop();
hostedTest = null;
mainHandler.removeCallbacks(checkFinishedRunnable);
hostedTestStoppedCondition.open();
}
}
@SuppressLint("InlinedApi")
private static final int getWifiLockMode() {
return Util.SDK_INT < 12 ? WifiManager.WIFI_MODE_FULL : WifiManager.WIFI_MODE_FULL_HIGH_PERF;
}
private final class CheckFinishedRunnable implements Runnable {
private static final long CHECK_INTERVAL_MS = 1000;
private void startChecking() {
mainHandler.post(this);
}
@Override
public void run() {
if (hostedTest.isFinished()) {
hostedTestFinished = true;
maybeStopHostedTest();
} else {
mainHandler.postDelayed(this, CHECK_INTERVAL_MS);
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import android.util.Log;
/**
* Implementation of {@link MetricsLogger} that prints the metrics to logcat.
*/
public final class LogcatMetricsLogger implements MetricsLogger {
private final String tag;
public LogcatMetricsLogger(String tag) {
this.tag = tag;
}
@Override
public void logMetric(String key, int value) {
Log.d(tag, key + ": " + value);
}
@Override
public void logMetric(String key, double value) {
Log.d(tag, key + ": " + value);
}
@Override
public void logMetric(String key, String value) {
Log.d(tag, key + ": " + value);
}
@Override
public void close() {
// Do nothing.
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (C) 2014 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.exoplayer.playbacktests.util;
import android.app.Instrumentation;
/**
* Metric Logging interface for ExoPlayer playback tests.
*/
public interface MetricsLogger {
String KEY_FRAMES_DROPPED_COUNT = "frames_dropped_count";
String KEY_FRAMES_RENDERED_COUNT = "frames_rendered_count";
String KEY_FRAMES_SKIPPED_COUNT = "frames_skipped_count";
String KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT = "maximum_consecutive_frames_dropped_count";
String KEY_TEST_NAME = "test_name";
/**
* Logs an int metric provided from a test.
*
* @param key The key of the metric to be logged.
* @param value The value of the metric to be logged.
*/
void logMetric(String key, int value);
/**
* Logs a double metric provided from a test.
*
* @param key The key of the metric to be logged.
* @param value The value of the metric to be logged.
*/
void logMetric(String key, double value);
/**
* Logs a string metric provided from a test.
*
* @param key The key of the metric to be logged.
* @param value The value of the metric to be logged.
*/
void logMetric(String key, String value);
/**
* Closes the logger.
*/
void close();
/**
* A factory for instantiating {@link MetricsLogger} instances.
*/
final class Factory {
private Factory() {}
/**
* Obtains a new instance of {@link MetricsLogger}.
*
* @param instrumentation The test instrumentation.
* @param tag The tag to be used for logcat logs.
* @param reportName The name of the report log.
* @param streamName The name of the stream of metrics.
*/
public static MetricsLogger createDefault(Instrumentation instrumentation, String tag,
String reportName, String streamName) {
return new LogcatMetricsLogger(tag);
}
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (C) 2014 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<SurfaceView android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</FrameLayout>

View File

@ -14,6 +14,7 @@
include ':library'
include ':testutils'
include ':demo'
include ':playbacktests'
include ':extension-opus'
include ':extension-vp9'
include ':extension-okhttp'