representations = adaptationSet.representations;
+ int[] representationIndices = new int[representationIds.length];
+ for (int i = 0; i < representationIds.length; i++) {
+ String representationId = representationIds[i];
+ boolean foundIndex = false;
+ for (int j = 0; j < representations.size() && !foundIndex; j++) {
+ if (representations.get(j).format.id.equals(representationId)) {
+ representationIndices[i] = j;
+ foundIndex = true;
+ }
+ }
+ if (!foundIndex) {
+ throw new IllegalStateException("Representation " + representationId + " not found.");
+ }
+ }
+ return representationIndices;
+ }
+
+ }
+
+ }
+
+}
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..307380332f
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java
@@ -0,0 +1,147 @@
+/*
+ * 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.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 An {@link ExoPlayer} on which the action is executed.
+ */
+ public final void doAction(ExoPlayer player) {
+ Log.i(tag, description);
+ doActionImpl(player);
+ }
+
+ /**
+ * Called by {@link #doAction(ExoPlayer)} do actually perform the action.
+ *
+ * @param player An {@link ExoPlayer} on which the action is executed.
+ */
+ protected abstract void doActionImpl(ExoPlayer player);
+
+ /**
+ * 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) {
+ 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) {
+ 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) {
+ player.setPlayWhenReady(playWhenReady);
+ }
+
+ }
+
+ /**
+ * Calls {@link ExoPlayer#setSelectedTrack(int, int)}.
+ */
+ public static final class SetSelectedTrack extends Action {
+
+ private final int rendererIndex;
+ private final int trackIndex;
+
+ /**
+ * @param tag A tag to use for logging.
+ * @param rendererIndex The index of the renderer.
+ * @param trackIndex The index of the track.
+ */
+ public SetSelectedTrack(String tag, int rendererIndex, int trackIndex) {
+ super(tag, "SelectedTrack:" + rendererIndex + ":" + trackIndex);
+ this.rendererIndex = rendererIndex;
+ this.trackIndex = trackIndex;
+ }
+
+ @Override
+ protected void doActionImpl(ExoPlayer player) {
+ player.setSelectedTrack(rendererIndex, trackIndex);
+ }
+
+ }
+
+}
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..b78057aa39
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java
@@ -0,0 +1,226 @@
+/*
+ * 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.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.SetSelectedTrack;
+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 each {@link Action} should be applied.
+ * @param mainHandler A handler associated with the main thread of the host activity.
+ */
+ /* package */ void start(ExoPlayer player, Handler mainHandler) {
+ rootNode.schedule(player, 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 SetSelectedTrack(tag, index, ExoPlayer.TRACK_DEFAULT));
+ }
+
+ /**
+ * Schedules a renderer disable action to be executed.
+ *
+ * @return The builder, for convenience.
+ */
+ public Builder disableRenderer(int index) {
+ return apply(new SetSelectedTrack(tag, index, ExoPlayer.TRACK_DISABLED));
+ }
+
+ 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 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 each {@link Action} should be applied.
+ * @param mainHandler A handler associated with the main thread of the host activity.
+ */
+ public void schedule(ExoPlayer player, Handler mainHandler) {
+ this.player = player;
+ this.mainHandler = mainHandler;
+ mainHandler.postDelayed(this, delayMs);
+ }
+
+ @Override
+ public void run() {
+ action.doAction(player);
+ if (next != null) {
+ next.schedule(player, 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) {
+ // 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..949b63d5c6
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java
@@ -0,0 +1,80 @@
+/*
+ * 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 assertOutputFormatChangedCount(String name, CodecCounters counters,
+ int expected) {
+ counters.ensureUpdated();
+ int actual = counters.outputFormatChangedCount;
+ TestCase.assertEquals("Codec(" + name + ") output format changed " + actual + " times. "
+ + "Expected " + expected + " times.", expected, actual);
+ }
+
+ public static void assertOutputBuffersChangedLimit(String name, CodecCounters counters,
+ int limit) {
+ counters.ensureUpdated();
+ int actual = counters.outputBuffersChangedCount;
+ TestCase.assertTrue("Codec(" + name + ") output buffers changed " + actual + " times. "
+ + "Limit: " + limit + ".", actual <= limit);
+ }
+
+ 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);
+ }
+
+}
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..a21ffffa30
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.audio.AudioTrack;
+import com.google.android.exoplayer.playbacktests.util.HostActivity.HostedTest;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.Surface;
+
+/**
+ * A {@link HostedTest} for {@link ExoPlayer} playback tests.
+ */
+public abstract class ExoHostedTest implements HostedTest, ExoPlayer.Listener {
+
+ 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 int rendererCount;
+ private final boolean failOnPlayerError;
+
+ private ActionSchedule pendingSchedule;
+ private Handler actionHandler;
+ private ExoPlayer player;
+ private ExoPlaybackException playerError;
+ private boolean playerWasPrepared;
+ private boolean playerFinished;
+ private boolean playing;
+ private long totalPlayingTimeMs;
+ private long lastPlayingStartTimeMs;
+
+ /**
+ * Constructs a test that fails if a player error occurs.
+ *
+ * @param rendererCount The number of renderers that will be injected into the player.
+ */
+ public ExoHostedTest(int rendererCount) {
+ this(rendererCount, true);
+ }
+
+ /**
+ * @param rendererCount The number of renderers that will be injected into the player.
+ * @param failOnPlayerError True if a player error should be considered a test failure. False
+ * otherwise.
+ */
+ public ExoHostedTest(int rendererCount, boolean failOnPlayerError) {
+ this.rendererCount = rendererCount;
+ 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, actionHandler);
+ }
+ }
+
+ // HostedTest implementation
+
+ @Override
+ public final void initialize(HostActivity host, Surface surface) {
+ // Build the player.
+ player = ExoPlayer.Factory.newInstance(rendererCount);
+ player.addListener(this);
+ player.prepare(buildRenderers(host, player, surface));
+ player.setPlayWhenReady(true);
+ actionHandler = new Handler();
+ // Schedule any pending actions.
+ if (pendingSchedule != null) {
+ pendingSchedule.start(player, actionHandler);
+ pendingSchedule = null;
+ }
+ }
+
+ @Override
+ public final void release() {
+ actionHandler.removeCallbacksAndMessages(null);
+ player.release();
+ player = null;
+ }
+
+ @Override
+ public final boolean isFinished() {
+ return playerFinished;
+ }
+
+ @Override
+ public final void assertPassed() {
+ if (failOnPlayerError && playerError != null) {
+ throw new Error(playerError);
+ }
+ assertPassedInternal();
+ }
+
+ // ExoPlayer.Listener
+
+ @Override
+ public final void onPlayerStateChanged(boolean playWhenReady, int 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.
+ }
+
+ // Internal logic
+
+ @SuppressWarnings("unused")
+ protected abstract TrackRenderer[] buildRenderers(HostActivity host, ExoPlayer player,
+ Surface surface) throws IllegalStateException;
+
+ @SuppressWarnings("unused")
+ protected void onPlayerErrorInternal(ExoPlaybackException error) {
+ // Do nothing. Interested subclasses may override.
+ }
+
+ protected void assertPassedInternal() {
+ // Do nothing. Subclasses may override to add additional assertions.
+ }
+
+ // Utility methods and actions for subclasses.
+
+ protected final long getTotalPlayingTimeMs() {
+ return totalPlayingTimeMs;
+ }
+
+ protected final ExoPlaybackException getError() {
+ return playerError;
+ }
+
+}
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..bd88280aa5
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java
@@ -0,0 +1,213 @@
+/*
+ * 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 android.app.Activity;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+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 once the activity has been resumed and its surface has been created.
+ *
+ * Called on the main thread.
+ *
+ * @param host The host in which the test is being run.
+ * @param surface The created surface.
+ */
+ void initialize(HostActivity host, Surface surface);
+
+ /**
+ * Called when the test has finished, or if the activity is paused or its surface is destroyed.
+ *
+ * Called on the main thread.
+ */
+ void release();
+
+ /**
+ * Called periodically to check whether the test has finished.
+ *
+ * Called on the main thread.
+ *
+ * @return True if the test has finished. False otherwise.
+ */
+ boolean isFinished();
+
+ /**
+ * Asserts that the test passed.
+ *
+ * Called on the test thread once the test has reported that it's finished and after the test
+ * has been released.
+ */
+ void assertPassed();
+
+ }
+
+ private static final String TAG = "HostActivity";
+
+ private SurfaceView surfaceView;
+ private Handler mainHandler;
+ private CheckFinishedRunnable checkFinishedRunnable;
+
+ private HostedTest hostedTest;
+ private ConditionVariable hostedTestReleasedCondition;
+ private boolean hostedTestInitialized;
+ private boolean hostedTestFinished;
+
+ /**
+ * Executes a {@link HostedTest} inside the host.
+ *
+ * Must only be called once on each instance. Must be called from the test thread.
+ *
+ * @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());
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Assertions.checkState(HostActivity.this.hostedTest == null);
+ HostActivity.this.hostedTest = Assertions.checkNotNull(hostedTest);
+ maybeInitializeHostedTest();
+ }
+ });
+ if (hostedTestReleasedCondition.block(timeoutMs)) {
+ if (hostedTestFinished) {
+ Log.d(TAG, "Test finished. Checking pass conditions.");
+ hostedTest.assertPassed();
+ Log.d(TAG, "Pass conditions checked.");
+ } else {
+ Log.e(TAG, "Test released before it finished. Activity may have been paused whilst test "
+ + "was in progress.");
+ fail();
+ }
+ } else {
+ Log.e(TAG, "Test timed out after " + timeoutMs + " ms.");
+ fail();
+ }
+ }
+
+ // 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();
+ hostedTestReleasedCondition = new ConditionVariable();
+ checkFinishedRunnable = new CheckFinishedRunnable();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ maybeInitializeHostedTest();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ maybeReleaseHostedTest();
+ }
+
+ // SurfaceHolder.Callback
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ maybeInitializeHostedTest();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ maybeReleaseHostedTest();
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ // Do nothing.
+ }
+
+ // Internal logic
+
+ private void maybeInitializeHostedTest() {
+ if (hostedTest == null || hostedTestInitialized) {
+ return;
+ }
+ Surface surface = surfaceView.getHolder().getSurface();
+ if (surface != null && surface.isValid()) {
+ hostedTestInitialized = true;
+ Log.d(TAG, "Initializing test.");
+ hostedTest.initialize(this, surface);
+ checkFinishedRunnable.startChecking();
+ }
+ }
+
+ private void maybeReleaseHostedTest() {
+ if (hostedTest != null && hostedTestInitialized) {
+ hostedTest.release();
+ hostedTest = null;
+ mainHandler.removeCallbacks(checkFinishedRunnable);
+ hostedTestReleasedCondition.open();
+ }
+ }
+
+ 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;
+ finish();
+ } else {
+ mainHandler.postDelayed(this, CHECK_INTERVAL_MS);
+ }
+ }
+
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java
new file mode 100644
index 0000000000..f205c321c6
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java
@@ -0,0 +1,169 @@
+/*
+ * 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.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.audio.AudioTrack.InitializationException;
+import com.google.android.exoplayer.audio.AudioTrack.WriteException;
+import com.google.android.exoplayer.chunk.ChunkSampleSource;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.hls.HlsSampleSource;
+
+import android.media.MediaCodec.CryptoException;
+import android.util.Log;
+import android.view.Surface;
+
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Logs information reported by an {@link ExoPlayer} instance and various player components.
+ */
+public final class LogcatLogger implements ExoPlayer.Listener,
+ MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
+ ChunkSampleSource.EventListener, HlsSampleSource.EventListener {
+
+ private static final NumberFormat TIME_FORMAT;
+ static {
+ TIME_FORMAT = NumberFormat.getInstance(Locale.US);
+ TIME_FORMAT.setMinimumFractionDigits(2);
+ TIME_FORMAT.setMaximumFractionDigits(2);
+ }
+
+ private final String tag;
+ private final ExoPlayer player;
+
+ /**
+ * @param tag A tag to use for logging.
+ * @param player The player.
+ */
+ public LogcatLogger(String tag, ExoPlayer player) {
+ this.tag = tag;
+ this.player = player;
+ player.addListener(this);
+ }
+
+ // ExoPlayer.Listener.
+
+ @Override
+ public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ Log.i(tag, "Player state: " + getTimeString(player.getCurrentPosition()) + ", "
+ + playWhenReady + ", " + getStateString(playbackState));
+ }
+
+ @Override
+ public final void onPlayerError(ExoPlaybackException e) {
+ Log.e(tag, "Player failed", e);
+ }
+
+ @Override
+ public void onPlayWhenReadyCommitted() {}
+
+ // Component listeners.
+
+ @Override
+ public void onDecoderInitializationError(DecoderInitializationException e) {
+ Log.e(tag, "Decoder initialization error", e);
+ }
+
+ @Override
+ public void onCryptoError(CryptoException e) {
+ Log.e(tag, "Crypto error", e);
+ }
+
+ @Override
+ public void onLoadError(int sourceId, IOException e) {
+ Log.e(tag, "Load error (" + sourceId + ")", e);
+ }
+
+ @Override
+ public void onAudioTrackInitializationError(InitializationException e) {
+ Log.e(tag, "Audio track initialization error", e);
+ }
+
+ @Override
+ public void onAudioTrackWriteError(WriteException e) {
+ Log.e(tag, "Audio track write error", e);
+ }
+
+ @Override
+ public void onDroppedFrames(int count, long elapsed) {
+ Log.w(tag, "Dropped frames (" + count + ")");
+ }
+
+ @Override
+ public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
+ long initializationDurationMs) {
+ Log.i(tag, "Initialized decoder: " + decoderName);
+ }
+
+ @Override
+ public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
+ int mediaTimeMs) {
+ Log.i(tag, "Downstream format changed (" + sourceId + "): " + format.id);
+ }
+
+ @Override
+ public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+ Log.i(tag, "Video size changed: " + width + "x" + height);
+ }
+
+ @Override
+ public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
+ int mediaStartTimeMs, int mediaEndTimeMs) {}
+
+ @Override
+ public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger,
+ Format format, int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs,
+ long loadDurationMs) {}
+
+ @Override
+ public void onLoadCanceled(int sourceId, long bytesLoaded) {}
+
+ @Override
+ public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs) {}
+
+ @Override
+ public void onDrawnToSurface(Surface surface) {}
+
+ private static String getStateString(int state) {
+ switch (state) {
+ case ExoPlayer.STATE_BUFFERING:
+ return "B";
+ case ExoPlayer.STATE_ENDED:
+ return "E";
+ case ExoPlayer.STATE_IDLE:
+ return "I";
+ case ExoPlayer.STATE_PREPARING:
+ return "P";
+ case ExoPlayer.STATE_READY:
+ return "R";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getTimeString(long timeMs) {
+ return TIME_FORMAT.format((timeMs) / 1000f);
+ }
+
+}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java
new file mode 100644
index 0000000000..b5027b9309
--- /dev/null
+++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java
@@ -0,0 +1,110 @@
+/*
+ * 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.upstream.DefaultUriDataSource;
+import com.google.android.exoplayer.upstream.UriLoadable;
+import com.google.android.exoplayer.util.ManifestFetcher;
+import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
+import com.google.android.exoplayer.util.Util;
+
+import android.content.Context;
+import android.os.ConditionVariable;
+
+import java.io.IOException;
+
+/**
+ * Utility methods for ExoPlayer playback tests.
+ */
+public final class TestUtil {
+
+ private TestUtil() {}
+
+ /**
+ * Gets a suitable user agent string for ExoPlayer playback tests.
+ *
+ * @param context A context.
+ * @return The user agent.
+ */
+ public static String getUserAgent(Context context) {
+ return Util.getUserAgent(context, "ExoPlayerPlaybackTests");
+ }
+
+ /**
+ * Loads a manifest.
+ *
+ * @param context A context.
+ * @param url The manifest url.
+ * @param parser A suitable parser for the manifest.
+ * @return The parser manifest.
+ * @throws IOException If an error occurs loading the manifest.
+ */
+ public static T loadManifest(Context context, String url, UriLoadable.Parser parser)
+ throws IOException {
+ String userAgent = getUserAgent(context);
+ DefaultUriDataSource manifestDataSource = new DefaultUriDataSource(context, userAgent);
+ ManifestFetcher manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
+ SyncManifestCallback callback = new SyncManifestCallback<>();
+ manifestFetcher.singleLoad(context.getMainLooper(), callback);
+ return callback.getResult();
+ }
+
+ /**
+ * A {@link ManifestCallback} that provides a blocking {@link #getResult()} method for retrieving
+ * the result.
+ *
+ * @param The type of the manifest.
+ */
+ private static final class SyncManifestCallback implements ManifestCallback {
+
+ private final ConditionVariable haveResultCondition;
+
+ private T result;
+ private IOException error;
+
+ public SyncManifestCallback() {
+ haveResultCondition = new ConditionVariable();
+ }
+
+ @Override
+ public void onSingleManifest(T manifest) {
+ result = manifest;
+ haveResultCondition.open();
+
+ }
+ @Override
+ public void onSingleManifestError(IOException e) {
+ error = e;
+ haveResultCondition.open();
+ }
+
+ /**
+ * Blocks for the result.
+ *
+ * @return The loaded manifest.
+ * @throws IOException If an error occurred loading the manifest.
+ */
+ public T getResult() throws IOException {
+ haveResultCondition.block();
+ if (error != null) {
+ throw error;
+ }
+ return result;
+ }
+
+ }
+
+}
diff --git a/playbacktests/src/main/project.properties b/playbacktests/src/main/project.properties
new file mode 100644
index 0000000000..4fdc858b92
--- /dev/null
+++ b/playbacktests/src/main/project.properties
@@ -0,0 +1,13 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-22
+android.library=false
+android.library.reference.1=../../../library/src/main
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 7441d135d4..70fb45ca11 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -13,6 +13,7 @@
// limitations under the License.
include ':library'
include ':demo'
+include ':playbacktests'
include ':opus-extension'
include ':vp9-extension'
include ':webm-sw-demo'