From 0c968703c829db38855c480c2ae760cae87dfd70 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 1 Sep 2015 14:27:01 +0100 Subject: [PATCH] Add playback tests (work in progress). All valid Android devices should pass these tests. --- playbacktests/build.gradle | 38 ++ playbacktests/src/main/AndroidManifest.xml | 42 +++ .../playbacktests/gts/H264DashTest.java | 331 ++++++++++++++++++ .../exoplayer/playbacktests/util/Action.java | 147 ++++++++ .../playbacktests/util/ActionSchedule.java | 226 ++++++++++++ .../playbacktests/util/CodecCountersUtil.java | 80 +++++ .../playbacktests/util/ExoHostedTest.java | 177 ++++++++++ .../playbacktests/util/HostActivity.java | 213 +++++++++++ .../playbacktests/util/LogcatLogger.java | 169 +++++++++ .../playbacktests/util/TestUtil.java | 110 ++++++ playbacktests/src/main/project.properties | 13 + .../src/main/res/layout/host_activity.xml | 28 ++ settings.gradle | 1 + 13 files changed, 1575 insertions(+) create mode 100644 playbacktests/build.gradle create mode 100644 playbacktests/src/main/AndroidManifest.xml create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java create mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java create mode 100644 playbacktests/src/main/project.properties create mode 100644 playbacktests/src/main/res/layout/host_activity.xml diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle new file mode 100644 index 0000000000..5a95cfff41 --- /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 22 + buildToolsVersion "22.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 + } + 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..31b5016cef --- /dev/null +++ b/playbacktests/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java new file mode 100644 index 0000000000..37255590e6 --- /dev/null +++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java @@ -0,0 +1,331 @@ +/* + * 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.gts; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.FormatEvaluator; +import com.google.android.exoplayer.dash.DashChunkSource; +import com.google.android.exoplayer.dash.DashTrackSelector; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.dash.mpd.Representation; +import com.google.android.exoplayer.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer.playbacktests.util.CodecCountersUtil; +import com.google.android.exoplayer.playbacktests.util.ExoHostedTest; +import com.google.android.exoplayer.playbacktests.util.HostActivity; +import com.google.android.exoplayer.playbacktests.util.LogcatLogger; +import com.google.android.exoplayer.playbacktests.util.TestUtil; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultUriDataSource; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.os.Handler; +import android.test.ActivityInstrumentationTestCase2; +import android.view.Surface; + +import java.io.IOException; +import java.util.List; + +/** + * Tests H264 DASH playbacks using {@link ExoPlayer}. + */ +public final class H264DashTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "H264DashTest"; + + private static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000; + private static final long MAX_ADDITIONAL_TIME_MS = 60000; + private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; + + private static final String SOURCE_URL = "https://storage.googleapis.com/exoplayer-test-media-1" + + "/gen/screens/dash-vod-single-segment/manifest-baseline.mpd"; + private static final int SOURCE_VIDEO_FRAME_COUNT = 3840; + private static final int SOURCE_AUDIO_FRAME_COUNT = 5524; + private static final String AUDIO_REPRESENTATION_ID = "141"; + private static final String VIDEO_REPRESENTATION_ID_240 = "avc-baseline-240"; + private static final String VIDEO_REPRESENTATION_ID_480 = "avc-baseline-480"; + + public H264DashTest() { + super(HostActivity.class); + } + + public void testBaseline480() throws IOException { + if (Util.SDK_INT < 16) { + // Pass. + return; + } + MediaPresentationDescription mpd = TestUtil.loadManifest(getActivity(), SOURCE_URL, + new MediaPresentationDescriptionParser()); + H264DashHostedTest test = new H264DashHostedTest(mpd, true, AUDIO_REPRESENTATION_ID, + VIDEO_REPRESENTATION_ID_480); + getActivity().runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); + } + + public void testBaselineAdaptive() throws IOException { + if (Util.SDK_INT < 16) { + // Pass. + return; + } + MediaPresentationDescription mpd = TestUtil.loadManifest(getActivity(), SOURCE_URL, + new MediaPresentationDescriptionParser()); + H264DashHostedTest test = new H264DashHostedTest(mpd, true, AUDIO_REPRESENTATION_ID, + VIDEO_REPRESENTATION_ID_240, VIDEO_REPRESENTATION_ID_480); + getActivity().runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); + } + + public void testBaselineAdaptiveWithSeeking() throws IOException { + if (Util.SDK_INT < 16) { + // Pass. + return; + } + MediaPresentationDescription mpd = TestUtil.loadManifest(getActivity(), SOURCE_URL, + new MediaPresentationDescriptionParser()); + H264DashHostedTest test = new H264DashHostedTest(mpd, false, AUDIO_REPRESENTATION_ID, + VIDEO_REPRESENTATION_ID_240, VIDEO_REPRESENTATION_ID_480); + test.setSchedule(new ActionSchedule.Builder(TAG) + .delay(10000).seek(15000) + .delay(10000).seek(30000).seek(31000).seek(32000).seek(33000).seek(34000) + .delay(1000).pause().delay(1000).play() + .delay(1000).pause().seek(100000).delay(1000).play() + .build()); + getActivity().runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); + } + + public void testBaselineAdaptiveWithRendererDisabling() throws IOException { + if (Util.SDK_INT < 16) { + // Pass. + return; + } + MediaPresentationDescription mpd = TestUtil.loadManifest(getActivity(), SOURCE_URL, + new MediaPresentationDescriptionParser()); + H264DashHostedTest test = new H264DashHostedTest(mpd, false, AUDIO_REPRESENTATION_ID, + VIDEO_REPRESENTATION_ID_240, VIDEO_REPRESENTATION_ID_480); + test.setSchedule(new ActionSchedule.Builder(TAG) + // Wait 10 seconds, disable the video renderer, wait another 5 seconds and enable it again. + .delay(10000).disableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .delay(10000).enableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + // Ditto for the audio renderer. + .delay(10000).disableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .delay(10000).enableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + // Wait 10 seconds, then disable and enable the video renderer 5 times in quick succession. + .delay(10000).disableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.VIDEO_RENDERER_INDEX) + // Ditto for the audio renderer. + .delay(10000).disableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .disableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .enableRenderer(H264DashHostedTest.AUDIO_RENDERER_INDEX) + .build()); + getActivity().runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); + } + + @TargetApi(16) + private static class H264DashHostedTest extends ExoHostedTest { + + private static final int RENDERER_COUNT = 2; + private static final int VIDEO_RENDERER_INDEX = 0; + private static final int AUDIO_RENDERER_INDEX = 1; + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 60; + + private static final String VIDEO_TAG = "Video"; + private static final String AUDIO_TAG = "Audio"; + private static final int VIDEO_EVENT_ID = 0; + private static final int AUDIO_EVENT_ID = 1; + + private final MediaPresentationDescription mpd; + private final boolean fullPlaybackNoSeeking; + private String[] audioFormats; + private String[] videoFormats; + + private CodecCounters videoCounters; + private CodecCounters audioCounters; + + /** + * @param mpd The manifest. + * @param fullPlaybackNoSeeking True if the test will play the entire source with no seeking. + * False otherwise. + * @param audioFormat The audio format. + * @param videoFormats The video formats. + */ + public H264DashHostedTest(MediaPresentationDescription mpd, boolean fullPlaybackNoSeeking, + String audioFormat, String... videoFormats) { + super(RENDERER_COUNT); + this.mpd = Assertions.checkNotNull(mpd); + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.audioFormats = new String[] {audioFormat}; + this.videoFormats = videoFormats; + } + + @Override + public TrackRenderer[] buildRenderers(HostActivity host, ExoPlayer player, Surface surface) { + Handler handler = new Handler(); + LogcatLogger logger = new LogcatLogger(TAG, player); + LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); + String userAgent = TestUtil.getUserAgent(host); + + // Build the video renderer. + DataSource videoDataSource = new DefaultUriDataSource(host, null, userAgent); + TrackSelector videoTrackSelector = new TrackSelector(AdaptationSet.TYPE_VIDEO, videoFormats); + ChunkSource videoChunkSource = new DashChunkSource(mpd, videoTrackSelector, videoDataSource, + new FormatEvaluator.RandomEvaluator(0)); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, handler, logger, VIDEO_EVENT_ID); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer( + videoSampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, handler, logger, 50); + videoCounters = videoRenderer.codecCounters; + player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + + // Build the audio renderer. + DataSource audioDataSource = new DefaultUriDataSource(host, null, userAgent); + TrackSelector audioTrackSelector = new TrackSelector(AdaptationSet.TYPE_AUDIO, audioFormats); + ChunkSource audioChunkSource = new DashChunkSource(mpd, audioTrackSelector, audioDataSource, + null); + ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, handler, logger, AUDIO_EVENT_ID); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( + audioSampleSource, handler, logger); + audioCounters = audioRenderer.codecCounters; + + TrackRenderer[] renderers = new TrackRenderer[RENDERER_COUNT]; + renderers[VIDEO_RENDERER_INDEX] = videoRenderer; + renderers[AUDIO_RENDERER_INDEX] = audioRenderer; + return renderers; + } + + @Override + protected void assertPassedInternal() { + if (fullPlaybackNoSeeking) { + // Audio is not adaptive and we didn't seek (which can re-instantiate the audio decoder + // in ExoPlayer), so the decoder output format should have changed exactly once. The output + // buffers should have changed 0 or 1 times. + CodecCountersUtil.assertOutputFormatChangedCount(AUDIO_TAG, audioCounters, 1); + CodecCountersUtil.assertOutputBuffersChangedLimit(AUDIO_TAG, audioCounters, 1); + + if (videoFormats.length == 1) { + // Video is not adaptive, so the decoder output format should have changed exactly once. + // The output buffers should have changed 0 or 1 times. + CodecCountersUtil.assertOutputFormatChangedCount(VIDEO_TAG, audioCounters, 1); + CodecCountersUtil.assertOutputBuffersChangedLimit(VIDEO_TAG, audioCounters, 1); + } + + // We shouldn't have skipped any output buffers. + CodecCountersUtil.assertSkippedOutputBufferCount(AUDIO_TAG, audioCounters, 0); + CodecCountersUtil.assertSkippedOutputBufferCount(VIDEO_TAG, videoCounters, 0); + + // We allow one fewer output buffer due to the way that MediaCodecTrackRenderer and the + // underlying decoders handle the end of stream. This should be tightened up in the future. + CodecCountersUtil.assertTotalOutputBufferCount(VIDEO_TAG, videoCounters, + SOURCE_VIDEO_FRAME_COUNT - 1, SOURCE_VIDEO_FRAME_COUNT); + CodecCountersUtil.assertTotalOutputBufferCount(AUDIO_TAG, audioCounters, + SOURCE_AUDIO_FRAME_COUNT - 1, SOURCE_AUDIO_FRAME_COUNT); + + // The total playing time should match the source duration. + long sourceDuration = mpd.duration; + long minAllowedActualPlayingTime = sourceDuration - MAX_PLAYING_TIME_DISCREPANCY_MS; + long maxAllowedActualPlayingTime = sourceDuration + MAX_PLAYING_TIME_DISCREPANCY_MS; + long actualPlayingTime = getTotalPlayingTimeMs(); + assertTrue("Total playing time: " + actualPlayingTime + ". Actual media duration: " + + sourceDuration, minAllowedActualPlayingTime <= actualPlayingTime + && actualPlayingTime <= maxAllowedActualPlayingTime); + } + + // Assert that the level of performance was acceptable. + int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION + * CodecCountersUtil.getTotalOutputBuffers(videoCounters)); + CodecCountersUtil.assertDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, droppedFrameLimit); + } + + private static final class TrackSelector implements DashTrackSelector { + + private final int adaptationSetType; + private final String[] representationIds; + + private TrackSelector(int adaptationSetType, String[] representationIds) { + this.adaptationSetType = adaptationSetType; + this.representationIds = representationIds; + } + + @Override + public void selectTracks(MediaPresentationDescription manifest, int periodIndex, + Output output) throws IOException { + Period period = manifest.getPeriod(periodIndex); + int adaptationSetIndex = period.getAdaptationSetIndex(adaptationSetType); + AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + int[] representationIndices = getRepresentationIndices(representationIds, adaptationSet); + if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { + output.adaptiveTrack(manifest, periodIndex, adaptationSetIndex, representationIndices); + } + for (int i = 0; i < representationIndices.length; i++) { + output.fixedTrack(manifest, periodIndex, adaptationSetIndex, representationIndices[i]); + } + } + + private static int[] getRepresentationIndices(String[] representationIds, + AdaptationSet adaptationSet) { + List 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'