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'