mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add playback tests (work in progress).
All valid Android devices should pass these tests.
This commit is contained in:
parent
e6a93a08de
commit
0c968703c8
13 changed files with 1575 additions and 0 deletions
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.
|
// limitations under the License.
|
||||||
include ':library'
|
include ':library'
|
||||||
include ':demo'
|
include ':demo'
|
||||||
|
include ':playbacktests'
|
||||||
include ':opus-extension'
|
include ':opus-extension'
|
||||||
include ':vp9-extension'
|
include ':vp9-extension'
|
||||||
include ':webm-sw-demo'
|
include ':webm-sw-demo'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue