Add playback tests (work in progress).
All valid Android devices should pass these tests.
This commit is contained in:
parent
e6a93a08de
commit
0c968703c8
38
playbacktests/build.gradle
Normal file
38
playbacktests/build.gradle
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 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')
|
||||
}
|
42
playbacktests/src/main/AndroidManifest.xml
Normal file
42
playbacktests/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer.playbacktests"
|
||||
android:versionCode="1401"
|
||||
android:versionName="1.4.1">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<uses-library android:name="android.test.runner"/>
|
||||
|
||||
<activity android:name="com.google.android.exoplayer.playbacktests.util.HostActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="ExoPlayerTest"/>
|
||||
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer.playbacktests"
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
@ -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<HostActivity> {
|
||||
|
||||
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<Representation> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Called on the main thread.
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* Called periodically to check whether the test has finished.
|
||||
* <p>
|
||||
* Called on the main thread.
|
||||
*
|
||||
* @return True if the test has finished. False otherwise.
|
||||
*/
|
||||
boolean isFinished();
|
||||
|
||||
/**
|
||||
* Asserts that the test passed.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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> T loadManifest(Context context, String url, UriLoadable.Parser<T> parser)
|
||||
throws IOException {
|
||||
String userAgent = getUserAgent(context);
|
||||
DefaultUriDataSource manifestDataSource = new DefaultUriDataSource(context, userAgent);
|
||||
ManifestFetcher<T> manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
|
||||
SyncManifestCallback<T> 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 <T> The type of the manifest.
|
||||
*/
|
||||
private static final class SyncManifestCallback<T> implements ManifestCallback<T> {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
13
playbacktests/src/main/project.properties
Normal file
13
playbacktests/src/main/project.properties
Normal file
@ -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
|
28
playbacktests/src/main/res/layout/host_activity.xml
Normal file
28
playbacktests/src/main/res/layout/host_activity.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:focusable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<SurfaceView android:id="@+id/surface_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
include ':library'
|
||||
include ':demo'
|
||||
include ':playbacktests'
|
||||
include ':opus-extension'
|
||||
include ':vp9-extension'
|
||||
include ':webm-sw-demo'
|
||||
|
Loading…
x
Reference in New Issue
Block a user