diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
index 73be219bef..ae7d4c9382 100644
--- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
@@ -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;
diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle
new file mode 100644
index 0000000000..ff294ad0b5
--- /dev/null
+++ b/playbacktests/build.gradle
@@ -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')
+}
diff --git a/playbacktests/src/main/AndroidManifest.xml b/playbacktests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..7ce85ab729
--- /dev/null
+++ b/playbacktests/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java
new file mode 100644
index 0000000000..8a6d720a47
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java
@@ -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);
+ }
+
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java
new file mode 100644
index 0000000000..a34b09ba85
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java
@@ -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.
+ }
+
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java
new file mode 100644
index 0000000000..ee76fd35f8
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java
@@ -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);
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/DebugMediaCodecVideoTrackRenderer.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/DebugMediaCodecVideoTrackRenderer.java
new file mode 100644
index 0000000000..d9ac230bfe
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/DebugMediaCodecVideoTrackRenderer.java
@@ -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];
+ }
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java
new file mode 100644
index 0000000000..1ed8655d61
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java
@@ -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;
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java
new file mode 100644
index 0000000000..4bf9de1db1
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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);
+ }
+ }
+
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java
new file mode 100644
index 0000000000..8de3a197f2
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java
@@ -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.
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java
new file mode 100644
index 0000000000..09f557442b
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java
@@ -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);
+ }
+ }
+
+}
diff --git a/playbacktests/src/main/res/layout/host_activity.xml b/playbacktests/src/main/res/layout/host_activity.xml
new file mode 100644
index 0000000000..75a88b823e
--- /dev/null
+++ b/playbacktests/src/main/res/layout/host_activity.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
index 15c9e057cb..c04f5cdab9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -14,6 +14,7 @@
include ':library'
include ':testutils'
include ':demo'
+include ':playbacktests'
include ':extension-opus'
include ':extension-vp9'
include ':extension-okhttp'