Add playback tests (work in progress).

All valid Android devices should pass these tests.
This commit is contained in:
Oliver Woodman 2015-09-01 14:27:01 +01:00
parent e6a93a08de
commit 0c968703c8
13 changed files with 1575 additions and 0 deletions

View 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')
}

View 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>

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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.
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View 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

View 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>

View File

@ -13,6 +13,7 @@
// limitations under the License.
include ':library'
include ':demo'
include ':playbacktests'
include ':opus-extension'
include ':vp9-extension'
include ':webm-sw-demo'