Commit b6155d2e by ojw28 Committed by GitHub

Merge pull request #3983 from google/dev-v2-r2.7.1

r2.7.1
parents 052de3c6 ff43df1e
Showing with 2590 additions and 1000 deletions
...@@ -42,7 +42,7 @@ Next add a gradle compile dependency to the `build.gradle` file of your app ...@@ -42,7 +42,7 @@ Next add a gradle compile dependency to the `build.gradle` file of your app
module. The following will add a dependency to the full library: module. The following will add a dependency to the full library:
```gradle ```gradle
compile 'com.google.android.exoplayer:exoplayer:2.X.X' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
``` ```
where `2.X.X` is your preferred version. Alternatively, you can depend on only where `2.X.X` is your preferred version. Alternatively, you can depend on only
...@@ -51,9 +51,9 @@ dependencies on the Core, DASH and UI library modules, as might be required for ...@@ -51,9 +51,9 @@ dependencies on the Core, DASH and UI library modules, as might be required for
an app that plays DASH content: an app that plays DASH content:
```gradle ```gradle
compile 'com.google.android.exoplayer:exoplayer-core:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
compile 'com.google.android.exoplayer:exoplayer-dash:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
compile 'com.google.android.exoplayer:exoplayer-ui:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
``` ```
The available library modules are listed below. Adding a dependency to the full The available library modules are listed below. Adding a dependency to the full
...@@ -105,9 +105,9 @@ You should now see the ExoPlayer modules appear as part of your project. You can ...@@ -105,9 +105,9 @@ You should now see the ExoPlayer modules appear as part of your project. You can
depend on them as you would on any other local module, for example: depend on them as you would on any other local module, for example:
```gradle ```gradle
compile project(':exoplayer-library-core') implementation project(':exoplayer-library-core')
compile project(':exoplayer-library-dash') implementation project(':exoplayer-library-dash')
compile project(':exoplayer-library-ui') implementation project(':exoplayer-library-ui')
``` ```
## Developing ExoPlayer ## ## Developing ExoPlayer ##
......
# Release notes # # Release notes #
### 2.7.1 ###
* Gradle: Replaced 'compile' (deprecated) with 'implementation' and
'api'. This may lead to build breakage for applications upgrading from
previous version that rely on indirect dependencies of certain modules. In
such cases, application developers need to add the missing dependency to
their gradle file. You can read more about the new dependency configurations
[here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).
* HlsMediaSource: Make HLS periods start at zero instead of the epoch.
Applications that rely on HLS timelines having a period starting at
the epoch will need to update their handling of HLS timelines. The program
date time is still available via the informational
`Timeline.Window.windowStartTimeMs` field
([#3865](https://github.com/google/ExoPlayer/issues/3865),
[#3888](https://github.com/google/ExoPlayer/issues/3888)).
* Enable seeking in MP4 streams where duration is set incorrectly in the track
header ([#3926](https://github.com/google/ExoPlayer/issues/3926)).
* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and
`LibvpxVideoRenderer`, even if it is late.
### 2.7.0 ### ### 2.7.0 ###
* Player interface: * Player interface:
......
...@@ -12,13 +12,16 @@ ...@@ -12,13 +12,16 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.7.1'
releaseVersionCode = 2701
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater. // by the library requires API level 16 or greater.
minSdkVersion = 14 minSdkVersion = 14
compileSdkVersion = 27
targetSdkVersion = 27 targetSdkVersion = 27
compileSdkVersion = 27
buildToolsVersion = '26.0.2' buildToolsVersion = '26.0.2'
testSupportLibraryVersion = '0.5' testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.0.0' supportLibraryVersion = '27.0.0'
...@@ -28,7 +31,6 @@ project.ext { ...@@ -28,7 +31,6 @@ project.ext {
junitVersion = '4.12' junitVersion = '4.12'
truthVersion = '0.39' truthVersion = '0.39'
robolectricVersion = '3.7.1' robolectricVersion = '3.7.1'
releaseVersion = '2.7.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -24,6 +24,7 @@ include modulePrefix + 'library-hls' ...@@ -24,6 +24,7 @@ include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui' include modulePrefix + 'library-ui'
include modulePrefix + 'testutils' include modulePrefix + 'testutils'
include modulePrefix + 'testutils-robolectric'
include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac' include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-gvr'
...@@ -43,6 +44,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl ...@@ -43,6 +44,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric')
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
......
...@@ -19,6 +19,8 @@ android { ...@@ -19,6 +19,8 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion 16
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
...@@ -42,11 +44,13 @@ android { ...@@ -42,11 +44,13 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
compile project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compile project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
} }
...@@ -14,12 +14,10 @@ ...@@ -14,12 +14,10 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.castdemo" package="com.google.android.exoplayer2.castdemo">
android:versionCode="2700"
android:versionName="2.7.0">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/> <uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
...@@ -281,8 +282,12 @@ import java.util.ArrayList; ...@@ -281,8 +282,12 @@ import java.util.ArrayList;
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(
Timeline timeline, Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex(); updateCurrentItemIndex();
if (timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
} }
// CastPlayer.SessionAvailabilityListener implementation. // CastPlayer.SessionAvailabilityListener implementation.
......
...@@ -19,6 +19,8 @@ android { ...@@ -19,6 +19,8 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion 16
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
...@@ -41,10 +43,11 @@ android { ...@@ -41,10 +43,11 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compile project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
compile project(modulePrefix + 'extension-ima') implementation project(modulePrefix + 'extension-ima')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
} }
...@@ -14,12 +14,10 @@ ...@@ -14,12 +14,10 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo" package="com.google.android.exoplayer2.imademo">
android:versionCode="2700"
android:versionName="2.7.0">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/> <uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
......
...@@ -19,6 +19,8 @@ android { ...@@ -19,6 +19,8 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion 16
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
...@@ -55,15 +57,16 @@ android { ...@@ -55,15 +57,16 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compile project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-smoothstreaming')
withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg') implementation project(modulePrefix + 'library-ui')
withExtensionsCompile project(path: modulePrefix + 'extension-flac') withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsCompile project(path: modulePrefix + 'extension-ima') withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
withExtensionsCompile project(path: modulePrefix + 'extension-opus') withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
withExtensionsCompile project(path: modulePrefix + 'extension-vp9') withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
withExtensionsCompile project(path: modulePrefix + 'extension-rtmp') withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
} }
...@@ -15,15 +15,13 @@ ...@@ -15,15 +15,13 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo">
android:versionCode="2700"
android:versionName="2.7.0">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/> <uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/> <uses-sdk/>
<application <application
android:label="@string/application_name" android:label="@string/application_name"
......
...@@ -12,10 +12,10 @@ Cast receiver app. ...@@ -12,10 +12,10 @@ Cast receiver app.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-cast:rX.X.X' implementation 'com.google.android.exoplayer:extension-cast:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -26,22 +26,24 @@ android { ...@@ -26,22 +26,24 @@ android {
} }
dependencies { dependencies {
// This dependency is necessary to force the supportLibraryVersion of // These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4 to be used. Else an older version (25.2.0) // com.android.support:support-v4, com.android.support:appcompat-v7 and
// is included via: // com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example:
// com.google.android.gms:play-services-cast-framework:11.4.2 // com.google.android.gms:play-services-cast-framework:11.4.2
// |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.google.android.gms:play-services-basement:11.4.2
// |-- com.android.support:support-v4:25.2.0 // |-- com.android.support:support-v4:25.2.0
compile 'com.android.support:support-v4:' + supportLibraryVersion api 'com.android.support:support-v4:' + supportLibraryVersion
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion api 'com.android.support:appcompat-v7:' + supportLibraryVersion
compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
testCompile project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testCompile 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testCompile 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testCompile 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
ext { ext {
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.exoplayer2.ext.cast.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
</manifest>
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cast; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
...@@ -25,11 +26,9 @@ import org.junit.Test; ...@@ -25,11 +26,9 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Tests for {@link CastTimelineTracker}. */ /** Tests for {@link CastTimelineTracker}. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
public class CastTimelineTrackerTest { public class CastTimelineTrackerTest {
private static final long DURATION_1_MS = 1000; private static final long DURATION_1_MS = 1000;
...@@ -49,12 +48,12 @@ public class CastTimelineTrackerTest { ...@@ -49,12 +48,12 @@ public class CastTimelineTrackerTest {
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION}); new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
CastTimelineTracker tracker = new CastTimelineTracker(); CastTimelineTracker tracker = new CastTimelineTracker();
mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS); mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET); tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS); mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), tracker.getCastTimeline(status),
...@@ -62,7 +61,7 @@ public class CastTimelineTrackerTest { ...@@ -62,7 +61,7 @@ public class CastTimelineTrackerTest {
C.TIME_UNSET, C.TIME_UNSET,
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_3_MS));
mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS); mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), tracker.getCastTimeline(status),
...@@ -80,7 +79,7 @@ public class CastTimelineTrackerTest { ...@@ -80,7 +79,7 @@ public class CastTimelineTrackerTest {
DURATION_5_MS, DURATION_5_MS,
MediaInfo.UNKNOWN_DURATION MediaInfo.UNKNOWN_DURATION
}); });
mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS); mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(newStatus),
...@@ -89,7 +88,7 @@ public class CastTimelineTrackerTest { ...@@ -89,7 +88,7 @@ public class CastTimelineTrackerTest {
C.msToUs(DURATION_5_MS), C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_3_MS));
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS); mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(newStatus),
...@@ -98,7 +97,7 @@ public class CastTimelineTrackerTest { ...@@ -98,7 +97,7 @@ public class CastTimelineTrackerTest {
C.msToUs(DURATION_5_MS), C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_3_MS));
mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS); mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(newStatus),
...@@ -112,7 +111,7 @@ public class CastTimelineTrackerTest { ...@@ -112,7 +111,7 @@ public class CastTimelineTrackerTest {
int[] itemIds, String[] contentIds, long[] durationsMs) { int[] itemIds, String[] contentIds, long[] durationsMs) {
ArrayList<MediaQueueItem> items = new ArrayList<>(); ArrayList<MediaQueueItem> items = new ArrayList<>();
for (int i = 0; i < contentIds.length; i++) { for (int i = 0; i < contentIds.length; i++) {
MediaInfo mediaInfo = mockMediaInfo(contentIds[i], durationsMs[i]); MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
MediaQueueItem item = Mockito.mock(MediaQueueItem.class); MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
Mockito.when(item.getMedia()).thenReturn(mediaInfo); Mockito.when(item.getMedia()).thenReturn(mediaInfo);
Mockito.when(item.getItemId()).thenReturn(itemIds[i]); Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
...@@ -123,10 +122,11 @@ public class CastTimelineTrackerTest { ...@@ -123,10 +122,11 @@ public class CastTimelineTrackerTest {
return status; return status;
} }
private static MediaInfo mockMediaInfo(String contentId, long durationMs) { private static MediaInfo getMediaInfo(String contentId, long durationMs) {
MediaInfo mediaInfo = Mockito.mock(MediaInfo.class); return new MediaInfo.Builder(contentId)
Mockito.when(mediaInfo.getContentId()).thenReturn(contentId); .setStreamDuration(durationMs)
Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs); .setContentType(MimeTypes.APPLICATION_MP4)
return mediaInfo; .setStreamType(MediaInfo.STREAM_TYPE_NONE)
.build();
} }
} }
...@@ -35,16 +35,13 @@ android { ...@@ -35,16 +35,13 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') api files('libs/cronet_api.jar')
compile files('libs/cronet_api.jar') implementation files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_common_java.jar') implementation files('libs/cronet_impl_native_java.jar')
compile files('libs/cronet_impl_native_java.jar') implementation project(modulePrefix + 'library-core')
androidTestCompile project(modulePrefix + 'library') implementation 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestCompile project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'library')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion
} }
ext { ext {
......
...@@ -18,16 +18,6 @@ ...@@ -18,16 +18,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.cronet"> package="com.google.android.exoplayer2.ext.cronet">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.google.android.exoplayer2.ext.cronet"/>
</manifest> </manifest>
...@@ -19,9 +19,6 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,9 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
...@@ -30,11 +27,11 @@ import org.junit.Before; ...@@ -30,11 +27,11 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** /** Tests for {@link ByteArrayUploadDataProvider}. */
* Tests for {@link ByteArrayUploadDataProvider}. @RunWith(RobolectricTestRunner.class)
*/
@RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest { public final class ByteArrayUploadDataProviderTest {
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
...@@ -45,7 +42,7 @@ public final class ByteArrayUploadDataProviderTest { ...@@ -45,7 +42,7 @@ public final class ByteArrayUploadDataProviderTest {
@Before @Before
public void setUp() { public void setUp() {
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); MockitoAnnotations.initMocks(this);
byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);
} }
...@@ -90,5 +87,4 @@ public final class ByteArrayUploadDataProviderTest { ...@@ -90,5 +87,4 @@ public final class ByteArrayUploadDataProviderTest {
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA); assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
verify(mockUploadDataSink).onRewindSucceeded(); verify(mockUploadDataSink).onRewindSucceeded();
} }
} }
...@@ -31,10 +31,8 @@ import static org.mockito.Mockito.when; ...@@ -31,10 +31,8 @@ import static org.mockito.Mockito.when;
import android.net.Uri; import android.net.Uri;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.support.test.InstrumentationRegistry; import android.os.SystemClock;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
...@@ -50,6 +48,7 @@ import java.util.Arrays; ...@@ -50,6 +48,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.chromium.net.CronetEngine; import org.chromium.net.CronetEngine;
...@@ -61,13 +60,14 @@ import org.junit.Before; ...@@ -61,13 +60,14 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowSystemClock;
/** /** Tests for {@link CronetDataSource}. */
* Tests for {@link CronetDataSource}. @RunWith(RobolectricTestRunner.class)
*/
@RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest { public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100; private static final int TEST_CONNECT_TIMEOUT_MS = 100;
...@@ -85,18 +85,11 @@ public final class CronetDataSourceTest { ...@@ -85,18 +85,11 @@ public final class CronetDataSourceTest {
private UrlResponseInfo testUrlResponseInfo; private UrlResponseInfo testUrlResponseInfo;
@Mock private UrlRequest.Builder mockUrlRequestBuilder; @Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock @Mock private UrlRequest mockUrlRequest;
private UrlRequest mockUrlRequest; @Mock private Predicate<String> mockContentTypePredicate;
@Mock @Mock private TransferListener<CronetDataSource> mockTransferListener;
private Predicate<String> mockContentTypePredicate; @Mock private Executor mockExecutor;
@Mock @Mock private NetworkException mockNetworkException;
private TransferListener<CronetDataSource> mockTransferListener;
@Mock
private Clock mockClock;
@Mock
private Executor mockExecutor;
@Mock
private NetworkException mockNetworkException;
@Mock private CronetEngine mockCronetEngine; @Mock private CronetEngine mockCronetEngine;
private CronetDataSource dataSourceUnderTest; private CronetDataSource dataSourceUnderTest;
...@@ -104,8 +97,9 @@ public final class CronetDataSourceTest { ...@@ -104,8 +97,9 @@ public final class CronetDataSourceTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); MockitoAnnotations.initMocks(this);
dataSourceUnderTest = spy( dataSourceUnderTest =
spy(
new CronetDataSource( new CronetDataSource(
mockCronetEngine, mockCronetEngine,
mockExecutor, mockExecutor,
...@@ -114,7 +108,7 @@ public final class CronetDataSourceTest { ...@@ -114,7 +108,7 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS, TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS, TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects true, // resetTimeoutOnRedirects
mockClock, Clock.DEFAULT,
null, null,
false)); false));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
...@@ -126,8 +120,8 @@ public final class CronetDataSourceTest { ...@@ -126,8 +120,8 @@ public final class CronetDataSourceTest {
mockStatusResponse(); mockStatusResponse();
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
testPostDataSpec = new DataSpec( testPostDataSpec =
Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0); new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
testResponseHeader = new HashMap<>(); testResponseHeader = new HashMap<>();
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE); testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
// This value can be anything since the DataSpec is unset. // This value can be anything since the DataSpec is unset.
...@@ -173,20 +167,19 @@ public final class CronetDataSourceTest { ...@@ -173,20 +167,19 @@ public final class CronetDataSourceTest {
// Prepare a mock UrlRequest to be used in the second open() call. // Prepare a mock UrlRequest to be used in the second open() call.
final UrlRequest mockUrlRequest2 = mock(UrlRequest.class); final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2); when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
// Invoke the callback for the previous request. // Invoke the callback for the previous request.
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
mockUrlRequest, mockUrlRequest, testUrlResponseInfo, mockNetworkException);
testUrlResponseInfo, dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo);
mockNetworkException);
dataSourceUnderTest.onResponseStarted(
mockUrlRequest2,
testUrlResponseInfo);
return null; return null;
} }
}).when(mockUrlRequest2).start(); })
.when(mockUrlRequest2)
.start();
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
} }
...@@ -253,8 +246,8 @@ public final class CronetDataSourceTest { ...@@ -253,8 +246,8 @@ public final class CronetDataSourceTest {
@Test @Test
public void testRequestOpenFailDueToDnsFailure() { public void testRequestOpenFailDueToDnsFailure() {
mockResponseStartFailure(); mockResponseStartFailure();
when(mockNetworkException.getErrorCode()).thenReturn( when(mockNetworkException.getErrorCode())
NetworkException.ERROR_HOSTNAME_NOT_RESOLVED); .thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
try { try {
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
...@@ -524,8 +517,8 @@ public final class CronetDataSourceTest { ...@@ -524,8 +517,8 @@ public final class CronetDataSourceTest {
assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT); assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
assertThat(returnedBuffer).isEqualTo(new byte[16]); assertThat(returnedBuffer).isEqualTo(new byte[16]);
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener. // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest, verify(mockTransferListener, never())
C.RESULT_END_OF_INPUT); .onBytesTransferred(dataSourceUnderTest, C.RESULT_END_OF_INPUT);
// There should still be only one call to read on cronet. // There should still be only one call to read on cronet.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
// Check for connection not automatically closed. // Check for connection not automatically closed.
...@@ -534,10 +527,10 @@ public final class CronetDataSourceTest { ...@@ -534,10 +527,10 @@ public final class CronetDataSourceTest {
} }
@Test @Test
public void testConnectTimeout() { public void testConnectTimeout() throws InterruptedException {
when(mockClock.elapsedRealtime()).thenReturn(0L); long startTimeMs = SystemClock.elapsedRealtime();
final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable timedOutCondition = new ConditionVariable(); final CountDownLatch timedOutLatch = new CountDownLatch(1);
new Thread() { new Thread() {
@Override @Override
...@@ -551,29 +544,29 @@ public final class CronetDataSourceTest { ...@@ -551,29 +544,29 @@ public final class CronetDataSourceTest {
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
.isEqualTo(TEST_CONNECTION_STATUS); .isEqualTo(TEST_CONNECTION_STATUS);
timedOutCondition.open(); timedOutLatch.countDown();
} }
} }
}.start(); }.start();
startCondition.block(); startCondition.block();
// We should still be trying to open. // We should still be trying to open.
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// Now we timeout. // Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
timedOutCondition.block(); timedOutLatch.await();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
@Test @Test
public void testConnectInterrupted() { public void testConnectInterrupted() throws InterruptedException {
when(mockClock.elapsedRealtime()).thenReturn(0L); long startTimeMs = SystemClock.elapsedRealtime();
final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable timedOutCondition = new ConditionVariable(); final CountDownLatch timedOutLatch = new CountDownLatch(1);
Thread thread = Thread thread =
new Thread() { new Thread() {
...@@ -588,7 +581,7 @@ public final class CronetDataSourceTest { ...@@ -588,7 +581,7 @@ public final class CronetDataSourceTest {
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
.isEqualTo(TEST_INVALID_CONNECTION_STATUS); .isEqualTo(TEST_INVALID_CONNECTION_STATUS);
timedOutCondition.open(); timedOutLatch.countDown();
} }
} }
}; };
...@@ -596,29 +589,29 @@ public final class CronetDataSourceTest { ...@@ -596,29 +589,29 @@ public final class CronetDataSourceTest {
startCondition.block(); startCondition.block();
// We should still be trying to open. // We should still be trying to open.
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// Now we interrupt. // Now we interrupt.
thread.interrupt(); thread.interrupt();
timedOutCondition.block(); timedOutLatch.await();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
@Test @Test
public void testConnectResponseBeforeTimeout() { public void testConnectResponseBeforeTimeout() throws InterruptedException {
when(mockClock.elapsedRealtime()).thenReturn(0L); long startTimeMs = SystemClock.elapsedRealtime();
final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable openCondition = new ConditionVariable(); final CountDownLatch openLatch = new CountDownLatch(1);
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
try { try {
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
openCondition.open(); openLatch.countDown();
} catch (HttpDataSourceException e) { } catch (HttpDataSourceException e) {
fail(); fail();
} }
...@@ -627,20 +620,20 @@ public final class CronetDataSourceTest { ...@@ -627,20 +620,20 @@ public final class CronetDataSourceTest {
startCondition.block(); startCondition.block();
// We should still be trying to open. // We should still be trying to open.
assertThat(openCondition.block(50)).isFalse(); assertNotCountedDown(openLatch);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
assertThat(openCondition.block(50)).isFalse(); assertNotCountedDown(openLatch);
// The response arrives just in time. // The response arrives just in time.
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
openCondition.block(); openLatch.await();
} }
@Test @Test
public void testRedirectIncreasesConnectionTimeout() throws InterruptedException { public void testRedirectIncreasesConnectionTimeout() throws InterruptedException {
when(mockClock.elapsedRealtime()).thenReturn(0L); long startTimeMs = SystemClock.elapsedRealtime();
final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable timedOutCondition = new ConditionVariable(); final CountDownLatch timedOutLatch = new CountDownLatch(1);
final AtomicInteger openExceptions = new AtomicInteger(0); final AtomicInteger openExceptions = new AtomicInteger(0);
new Thread() { new Thread() {
...@@ -654,40 +647,36 @@ public final class CronetDataSourceTest { ...@@ -654,40 +647,36 @@ public final class CronetDataSourceTest {
assertThat(e instanceof CronetDataSource.OpenException).isTrue(); assertThat(e instanceof CronetDataSource.OpenException).isTrue();
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
openExceptions.getAndIncrement(); openExceptions.getAndIncrement();
timedOutCondition.open(); timedOutLatch.countDown();
} }
} }
}.start(); }.start();
startCondition.block(); startCondition.block();
// We should still be trying to open. // We should still be trying to open.
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// A redirect arrives just in time. // A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo, dataSourceUnderTest.onRedirectReceived(
"RandomRedirectedUrl1"); mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1; long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);
// Give the thread some time to run.
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
// We should still be trying to open as we approach the new timeout. // We should still be trying to open as we approach the new timeout.
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// A redirect arrives just in time. // A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo, dataSourceUnderTest.onRedirectReceived(
"RandomRedirectedUrl2"); mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2; newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);
// Give the thread some time to run.
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
// We should still be trying to open as we approach the new timeout. // We should still be trying to open as we approach the new timeout.
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// Now we timeout. // Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs); ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
timedOutCondition.block(); timedOutLatch.await();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
assertThat(openExceptions.get()).isEqualTo(1); assertThat(openExceptions.get()).isEqualTo(1);
...@@ -707,9 +696,11 @@ public final class CronetDataSourceTest { ...@@ -707,9 +696,11 @@ public final class CronetDataSourceTest {
} }
@Test @Test
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders() public void
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
throws HttpDataSourceException { throws HttpDataSourceException {
dataSourceUnderTest = spy( dataSourceUnderTest =
spy(
new CronetDataSource( new CronetDataSource(
mockCronetEngine, mockCronetEngine,
mockExecutor, mockExecutor,
...@@ -718,7 +709,7 @@ public final class CronetDataSourceTest { ...@@ -718,7 +709,7 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS, TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS, TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects true, // resetTimeoutOnRedirects
mockClock, Clock.DEFAULT,
null, null,
true)); true));
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
...@@ -736,10 +727,12 @@ public final class CronetDataSourceTest { ...@@ -736,10 +727,12 @@ public final class CronetDataSourceTest {
} }
@Test @Test
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader() public void
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
throws HttpDataSourceException { throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
dataSourceUnderTest = spy( dataSourceUnderTest =
spy(
new CronetDataSource( new CronetDataSource(
mockCronetEngine, mockCronetEngine,
mockExecutor, mockExecutor,
...@@ -748,7 +741,7 @@ public final class CronetDataSourceTest { ...@@ -748,7 +741,7 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS, TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS, TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects true, // resetTimeoutOnRedirects
mockClock, Clock.DEFAULT,
null, null,
true)); true));
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
...@@ -778,7 +771,8 @@ public final class CronetDataSourceTest { ...@@ -778,7 +771,8 @@ public final class CronetDataSourceTest {
@Test @Test
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
throws HttpDataSourceException { throws HttpDataSourceException {
dataSourceUnderTest = spy( dataSourceUnderTest =
spy(
new CronetDataSource( new CronetDataSource(
mockCronetEngine, mockCronetEngine,
mockExecutor, mockExecutor,
...@@ -787,7 +781,7 @@ public final class CronetDataSourceTest { ...@@ -787,7 +781,7 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS, TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS, TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects true, // resetTimeoutOnRedirects
mockClock, Clock.DEFAULT,
null, null,
true)); true));
mockSingleRedirectSuccess(); mockSingleRedirectSuccess();
...@@ -804,8 +798,9 @@ public final class CronetDataSourceTest { ...@@ -804,8 +798,9 @@ public final class CronetDataSourceTest {
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that // Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
// the subsequent open() call succeeds. // the subsequent open() call succeeds.
doThrow(new NullPointerException()).when(mockTransferListener).onTransferEnd( doThrow(new NullPointerException())
dataSourceUnderTest); .when(mockTransferListener)
.onTransferEnd(dataSourceUnderTest);
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
try { try {
dataSourceUnderTest.close(); dataSourceUnderTest.close();
...@@ -833,13 +828,12 @@ public final class CronetDataSourceTest { ...@@ -833,13 +828,12 @@ public final class CronetDataSourceTest {
} }
@Test @Test
public void testReadInterrupted() throws HttpDataSourceException { public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
when(mockClock.elapsedRealtime()).thenReturn(0L);
mockResponseStartSuccess(); mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
final ConditionVariable startCondition = buildReadStartedCondition(); final ConditionVariable startCondition = buildReadStartedCondition();
final ConditionVariable timedOutCondition = new ConditionVariable(); final CountDownLatch timedOutLatch = new CountDownLatch(1);
byte[] returnedBuffer = new byte[8]; byte[] returnedBuffer = new byte[8];
Thread thread = Thread thread =
new Thread() { new Thread() {
...@@ -851,17 +845,17 @@ public final class CronetDataSourceTest { ...@@ -851,17 +845,17 @@ public final class CronetDataSourceTest {
} catch (HttpDataSourceException e) { } catch (HttpDataSourceException e) {
// Expected. // Expected.
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
timedOutCondition.open(); timedOutLatch.countDown();
} }
} }
}; };
thread.start(); thread.start();
startCondition.block(); startCondition.block();
assertThat(timedOutCondition.block(50)).isFalse(); assertNotCountedDown(timedOutLatch);
// Now we interrupt. // Now we interrupt.
thread.interrupt(); thread.interrupt();
timedOutCondition.block(); timedOutLatch.await();
} }
@Test @Test
...@@ -876,7 +870,8 @@ public final class CronetDataSourceTest { ...@@ -876,7 +870,8 @@ public final class CronetDataSourceTest {
// Helper methods. // Helper methods.
private void mockStatusResponse() { private void mockStatusResponse() {
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
UrlRequest.StatusListener statusListener = UrlRequest.StatusListener statusListener =
...@@ -884,23 +879,27 @@ public final class CronetDataSourceTest { ...@@ -884,23 +879,27 @@ public final class CronetDataSourceTest {
statusListener.onStatus(TEST_CONNECTION_STATUS); statusListener.onStatus(TEST_CONNECTION_STATUS);
return null; return null;
} }
}).when(mockUrlRequest).getStatus(any(UrlRequest.StatusListener.class)); })
.when(mockUrlRequest)
.getStatus(any(UrlRequest.StatusListener.class));
} }
private void mockResponseStartSuccess() { private void mockResponseStartSuccess() {
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onResponseStarted( dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
mockUrlRequest,
testUrlResponseInfo);
return null; return null;
} }
}).when(mockUrlRequest).start(); })
.when(mockUrlRequest)
.start();
} }
private void mockResponseStartRedirect() { private void mockResponseStartRedirect() {
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onRedirectReceived( dataSourceUnderTest.onRedirectReceived(
...@@ -909,11 +908,14 @@ public final class CronetDataSourceTest { ...@@ -909,11 +908,14 @@ public final class CronetDataSourceTest {
"http://redirect.location.com"); "http://redirect.location.com");
return null; return null;
} }
}).when(mockUrlRequest).start(); })
.when(mockUrlRequest)
.start();
} }
private void mockSingleRedirectSuccess() { private void mockSingleRedirectSuccess() {
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
if (!redirectCalled) { if (!redirectCalled) {
...@@ -923,29 +925,31 @@ public final class CronetDataSourceTest { ...@@ -923,29 +925,31 @@ public final class CronetDataSourceTest {
createUrlResponseInfoWithUrl("http://example.com/video", 300), createUrlResponseInfoWithUrl("http://example.com/video", 300),
"http://example.com/video/redirect"); "http://example.com/video/redirect");
} else { } else {
dataSourceUnderTest.onResponseStarted( dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
mockUrlRequest,
testUrlResponseInfo);
} }
return null; return null;
} }
}).when(mockUrlRequest).start(); })
.when(mockUrlRequest)
.start();
} }
private void mockFollowRedirectSuccess() { private void mockFollowRedirectSuccess() {
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onResponseStarted( dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
mockUrlRequest,
testUrlResponseInfo);
return null; return null;
} }
}).when(mockUrlRequest).followRedirect(); })
.when(mockUrlRequest)
.followRedirect();
} }
private void mockResponseStartFailure() { private void mockResponseStartFailure() {
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
...@@ -954,12 +958,15 @@ public final class CronetDataSourceTest { ...@@ -954,12 +958,15 @@ public final class CronetDataSourceTest {
mockNetworkException); mockNetworkException);
return null; return null;
} }
}).when(mockUrlRequest).start(); })
.when(mockUrlRequest)
.start();
} }
private void mockReadSuccess(int position, int length) { private void mockReadSuccess(int position, int length) {
final int[] positionAndRemaining = new int[] {position, length}; final int[] positionAndRemaining = new int[] {position, length};
doAnswer(new Answer<Void>() { doAnswer(
new Answer<Void>() {
@Override @Override
public Void answer(InvocationOnMock invocation) throws Throwable { public Void answer(InvocationOnMock invocation) throws Throwable {
if (positionAndRemaining[1] == 0) { if (positionAndRemaining[1] == 0) {
...@@ -971,13 +978,13 @@ public final class CronetDataSourceTest { ...@@ -971,13 +978,13 @@ public final class CronetDataSourceTest {
positionAndRemaining[0] += readLength; positionAndRemaining[0] += readLength;
positionAndRemaining[1] -= readLength; positionAndRemaining[1] -= readLength;
dataSourceUnderTest.onReadCompleted( dataSourceUnderTest.onReadCompleted(
mockUrlRequest, mockUrlRequest, testUrlResponseInfo, inputBuffer);
testUrlResponseInfo,
inputBuffer);
} }
return null; return null;
} }
}).when(mockUrlRequest).read(any(ByteBuffer.class)); })
.when(mockUrlRequest)
.read(any(ByteBuffer.class));
} }
private void mockReadFailure() { private void mockReadFailure() {
...@@ -1013,16 +1020,26 @@ public final class CronetDataSourceTest { ...@@ -1013,16 +1020,26 @@ public final class CronetDataSourceTest {
private ConditionVariable buildUrlRequestStartedCondition() { private ConditionVariable buildUrlRequestStartedCondition() {
final ConditionVariable startedCondition = new ConditionVariable(); final ConditionVariable startedCondition = new ConditionVariable();
doAnswer(new Answer<Object>() { doAnswer(
new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
startedCondition.open(); startedCondition.open();
return null; return null;
} }
}).when(mockUrlRequest).start(); })
.when(mockUrlRequest)
.start();
return startedCondition; return startedCondition;
} }
private void assertNotCountedDown(CountDownLatch countDownLatch) throws InterruptedException {
// We are asserting that another thread does not count down the latch. We therefore sleep some
// time to give the other thread the chance to fail this test.
Thread.sleep(50);
assertThat(countDownLatch.getCount()).isGreaterThan(0L);
}
private static byte[] buildTestDataArray(int position, int length) { private static byte[] buildTestDataArray(int position, int length) {
return buildTestDataBuffer(position, length).array(); return buildTestDataBuffer(position, length).array();
} }
...@@ -1045,5 +1062,4 @@ public final class CronetDataSourceTest { ...@@ -1045,5 +1062,4 @@ public final class CronetDataSourceTest {
testBuffer.flip(); testBuffer.flip();
return testBuffer; return testBuffer;
} }
} }
...@@ -31,7 +31,7 @@ android { ...@@ -31,7 +31,7 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
} }
ext { ext {
......
...@@ -31,8 +31,8 @@ android { ...@@ -31,8 +31,8 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
androidTestCompile project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
} }
ext { ext {
......
...@@ -12,10 +12,10 @@ of surround sound and ambisonic soundfields. ...@@ -12,10 +12,10 @@ of surround sound and ambisonic soundfields.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' implementation 'com.google.android.exoplayer:extension-gvr:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -25,8 +25,8 @@ android { ...@@ -25,8 +25,8 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile 'com.google.vr:sdk-audio:1.80.0' implementation 'com.google.vr:sdk-audio:1.80.0'
} }
ext { ext {
......
...@@ -12,10 +12,10 @@ alongside content. ...@@ -12,10 +12,10 @@ alongside content.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-ima:rX.X.X' implementation 'com.google.android.exoplayer:extension-ima:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -26,7 +26,6 @@ android { ...@@ -26,7 +26,6 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core')
// This dependency is necessary to force the supportLibraryVersion of // This dependency is necessary to force the supportLibraryVersion of
// com.android.support:support-v4 to be used. Else an older version (25.2.0) // com.android.support:support-v4 to be used. Else an older version (25.2.0)
// is included via: // is included via:
...@@ -34,14 +33,10 @@ dependencies { ...@@ -34,14 +33,10 @@ dependencies {
// |-- com.google.android.gms:play-services-ads-lite:11.4.2 // |-- com.google.android.gms:play-services-ads-lite:11.4.2
// |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.google.android.gms:play-services-basement:11.4.2
// |-- com.android.support:support-v4:25.2.0 // |-- com.android.support:support-v4:25.2.0
compile 'com.android.support:support-v4:' + supportLibraryVersion api 'com.android.support:support-v4:' + supportLibraryVersion
compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
compile 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion implementation project(modulePrefix + 'library-core')
androidTestCompile project(modulePrefix + 'library') implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion
} }
ext { ext {
......
...@@ -20,12 +20,12 @@ import android.support.annotation.Nullable; ...@@ -20,12 +20,12 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import java.io.IOException;
/** /**
* A {@link MediaSource} that inserts ads linearly with a provided content media source. * A {@link MediaSource} that inserts ads linearly with a provided content media source.
...@@ -33,10 +33,9 @@ import com.google.android.exoplayer2.upstream.DataSource; ...@@ -33,10 +33,9 @@ import com.google.android.exoplayer2.upstream.DataSource;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/ */
@Deprecated @Deprecated
public final class ImaAdsMediaSource extends CompositeMediaSource<Void> { public final class ImaAdsMediaSource implements MediaSource {
private final AdsMediaSource adsMediaSource; private final AdsMediaSource adsMediaSource;
private Listener listener;
/** /**
* Constructs a new source that inserts ads linearly with the content specified by * Constructs a new source that inserts ads linearly with the content specified by
...@@ -75,10 +74,23 @@ public final class ImaAdsMediaSource extends CompositeMediaSource<Void> { ...@@ -75,10 +74,23 @@ public final class ImaAdsMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { public void prepareSource(
super.prepareSource(player, isTopLevelSource, listener); final ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
this.listener = listener; adsMediaSource.prepareSource(
prepareChildSource(/* id= */ null, adsMediaSource); player,
isTopLevelSource,
new Listener() {
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
}
});
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
adsMediaSource.maybeThrowSourceInfoRefreshError();
} }
@Override @Override
...@@ -92,8 +104,7 @@ public final class ImaAdsMediaSource extends CompositeMediaSource<Void> { ...@@ -92,8 +104,7 @@ public final class ImaAdsMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
protected void onChildSourceInfoRefreshed( public void releaseSource() {
Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { adsMediaSource.releaseSource();
listener.onSourceInfoRefreshed(this, timeline, manifest);
} }
} }
...@@ -11,10 +11,10 @@ ExoPlayer. ...@@ -11,10 +11,10 @@ ExoPlayer.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-leanback:rX.X.X' implementation 'com.google.android.exoplayer:extension-leanback:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -25,8 +25,8 @@ android { ...@@ -25,8 +25,8 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile('com.android.support:leanback-v17:' + supportLibraryVersion) implementation('com.android.support:leanback-v17:' + supportLibraryVersion)
} }
ext { ext {
......
...@@ -12,10 +12,10 @@ behaviour can be extended to support other playback and custom actions. ...@@ -12,10 +12,10 @@ behaviour can be extended to support other playback and custom actions.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-mediasession:rX.X.X' implementation 'com.google.android.exoplayer:extension-mediasession:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -25,8 +25,8 @@ android { ...@@ -25,8 +25,8 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
} }
ext { ext {
......
...@@ -105,7 +105,8 @@ public final class MediaSessionConnector { ...@@ -105,7 +105,8 @@ public final class MediaSessionConnector {
*/ */
public interface PlaybackPreparer extends CommandReceiver { public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_PREPARE long ACTIONS =
PlaybackStateCompat.ACTION_PREPARE
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
| PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
| PlaybackStateCompat.ACTION_PREPARE_FROM_URI | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
...@@ -115,13 +116,13 @@ public final class MediaSessionConnector { ...@@ -115,13 +116,13 @@ public final class MediaSessionConnector {
/** /**
* Returns the actions which are supported by the preparer. The supported actions must be a * Returns the actions which are supported by the preparer. The supported actions must be a
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, {@link
* {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, * PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, {@link
* {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, * PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, {@link
* {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, * PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, {@link
* {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, * PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, {@link
* {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and * PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and {@link
* {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}. * PlaybackStateCompat#ACTION_PLAY_FROM_URI}.
* *
* @return The bitmask of the supported media actions. * @return The bitmask of the supported media actions.
*/ */
...@@ -264,15 +265,6 @@ public final class MediaSessionConnector { ...@@ -264,15 +265,6 @@ public final class MediaSessionConnector {
*/ */
public interface QueueEditor extends CommandReceiver { public interface QueueEditor extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
/**
* Returns {@link PlaybackStateCompat#ACTION_SET_RATING} or {@code 0}. The Media API does
* not declare action constants for adding and removing queue items.
*
* @param player The {@link Player}.
*/
long getSupportedQueueEditorActions(@Nullable Player player);
/** /**
* See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}. * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}.
*/ */
...@@ -291,9 +283,14 @@ public final class MediaSessionConnector { ...@@ -291,9 +283,14 @@ public final class MediaSessionConnector {
* See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}.
*/ */
void onRemoveQueueItemAt(Player player, int index); void onRemoveQueueItemAt(Player player, int index);
/** }
* See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}.
*/ /** Callback receiving a user rating for the active media item. */
public interface RatingCallback extends CommandReceiver {
long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */
void onSetRating(Player player, RatingCompat rating); void onSetRating(Player player, RatingCompat rating);
} }
...@@ -341,6 +338,7 @@ public final class MediaSessionConnector { ...@@ -341,6 +338,7 @@ public final class MediaSessionConnector {
private PlaybackPreparer playbackPreparer; private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator; private QueueNavigator queueNavigator;
private QueueEditor queueEditor; private QueueEditor queueEditor;
private RatingCallback ratingCallback;
private ExoPlaybackException playbackException; private ExoPlaybackException playbackException;
/** /**
...@@ -471,6 +469,17 @@ public final class MediaSessionConnector { ...@@ -471,6 +469,17 @@ public final class MediaSessionConnector {
: EDITOR_MEDIA_SESSION_FLAGS); : EDITOR_MEDIA_SESSION_FLAGS);
} }
/**
* Sets the {@link RatingCallback} to handle user ratings.
*
* @param ratingCallback The rating callback.
*/
public void setRatingCallback(RatingCallback ratingCallback) {
unregisterCommandReceiver(this.ratingCallback);
this.ratingCallback = ratingCallback;
registerCommandReceiver(this.ratingCallback);
}
private void registerCommandReceiver(CommandReceiver commandReceiver) { private void registerCommandReceiver(CommandReceiver commandReceiver) {
if (commandReceiver != null && commandReceiver.getCommands() != null) { if (commandReceiver != null && commandReceiver.getCommands() != null) {
for (String command : commandReceiver.getCommands()) { for (String command : commandReceiver.getCommands()) {
...@@ -539,8 +548,8 @@ public final class MediaSessionConnector { ...@@ -539,8 +548,8 @@ public final class MediaSessionConnector {
actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions( actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(
player)); player));
} }
if (queueEditor != null) { if (ratingCallback != null) {
actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player)); actions |= RatingCallback.ACTIONS;
} }
return actions; return actions;
} }
...@@ -634,6 +643,10 @@ public final class MediaSessionConnector { ...@@ -634,6 +643,10 @@ public final class MediaSessionConnector {
& PlaybackPreparer.ACTIONS & action) != 0; & PlaybackPreparer.ACTIONS & action) != 0;
} }
private boolean canDispatchToRatingCallback(long action) {
return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
}
private boolean canDispatchToPlaybackController(long action) { private boolean canDispatchToPlaybackController(long action) {
return (playbackController.getSupportedPlaybackActions(player) return (playbackController.getSupportedPlaybackActions(player)
& PlaybackController.ACTIONS & action) != 0; & PlaybackController.ACTIONS & action) != 0;
...@@ -644,11 +657,6 @@ public final class MediaSessionConnector { ...@@ -644,11 +657,6 @@ public final class MediaSessionConnector {
& QueueNavigator.ACTIONS & action) != 0; & QueueNavigator.ACTIONS & action) != 0;
} }
private boolean canDispatchToQueueEditor(long action) {
return queueEditor != null && (queueEditor.getSupportedQueueEditorActions(player)
& QueueEditor.ACTIONS & action) != 0;
}
private class ExoPlayerEventListener extends Player.DefaultEventListener { private class ExoPlayerEventListener extends Player.DefaultEventListener {
private int currentWindowIndex; private int currentWindowIndex;
...@@ -880,6 +888,13 @@ public final class MediaSessionConnector { ...@@ -880,6 +888,13 @@ public final class MediaSessionConnector {
} }
@Override @Override
public void onSetRating(RatingCompat rating) {
if (canDispatchToRatingCallback(PlaybackStateCompat.ACTION_SET_RATING)) {
ratingCallback.onSetRating(player, rating);
}
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description) { public void onAddQueueItem(MediaDescriptionCompat description) {
if (queueEditor != null) { if (queueEditor != null) {
queueEditor.onAddQueueItem(player, description); queueEditor.onAddQueueItem(player, description);
...@@ -907,13 +922,6 @@ public final class MediaSessionConnector { ...@@ -907,13 +922,6 @@ public final class MediaSessionConnector {
} }
} }
@Override
public void onSetRating(RatingCompat rating) {
if (canDispatchToQueueEditor(PlaybackStateCompat.ACTION_SET_RATING)) {
queueEditor.onSetRating(player, rating);
}
}
} }
} }
...@@ -20,7 +20,6 @@ import android.os.ResultReceiver; ...@@ -20,7 +20,6 @@ import android.os.ResultReceiver;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -165,11 +164,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi ...@@ -165,11 +164,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
} }
@Override @Override
public long getSupportedQueueEditorActions(@Nullable Player player) {
return 0;
}
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description) { public void onAddQueueItem(Player player, MediaDescriptionCompat description) {
onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount()); onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount());
} }
...@@ -200,11 +194,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi ...@@ -200,11 +194,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
queueMediaSource.removeMediaSource(index); queueMediaSource.removeMediaSource(index);
} }
@Override
public void onSetRating(Player player, RatingCompat rating) {
// Do nothing.
}
// CommandReceiver implementation. // CommandReceiver implementation.
@NonNull @NonNull
......
...@@ -19,10 +19,10 @@ licensed separately. ...@@ -19,10 +19,10 @@ licensed separately.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' implementation 'com.google.android.exoplayer:extension-okhttp:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -30,8 +30,9 @@ android { ...@@ -30,8 +30,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile('com.squareup.okhttp3:okhttp:3.9.0') { implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation('com.squareup.okhttp3:okhttp:3.9.0') {
exclude group: 'org.json' exclude group: 'org.json'
} }
} }
......
...@@ -31,7 +31,7 @@ android { ...@@ -31,7 +31,7 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
} }
ext { ext {
......
...@@ -20,10 +20,10 @@ Android, which is licensed separately. ...@@ -20,10 +20,10 @@ Android, which is licensed separately.
The easiest way to use the extension is to add it as a gradle dependency: The easiest way to use the extension is to add it as a gradle dependency:
```gradle ```gradle
compile 'com.google.android.exoplayer:extension-rtmp:rX.X.X' implementation 'com.google.android.exoplayer:extension-rtmp:2.X.X'
``` ```
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module Alternatively, you can clone the ExoPlayer repository and depend on the module
......
...@@ -25,8 +25,9 @@ android { ...@@ -25,8 +25,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile 'net.butterflytv.utils:rtmp-client:3.0.1' implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
} }
ext { ext {
......
...@@ -31,8 +31,9 @@ android { ...@@ -31,8 +31,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
androidTestCompile 'com.google.truth:truth:' + truthVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
} }
ext { ext {
......
...@@ -119,7 +119,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -119,7 +119,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private VpxDecoder decoder; private VpxDecoder decoder;
private VpxInputBuffer inputBuffer; private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer; private VpxOutputBuffer outputBuffer;
private VpxOutputBuffer nextOutputBuffer;
private DrmSession<ExoMediaCrypto> drmSession; private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; private DrmSession<ExoMediaCrypto> pendingDrmSession;
...@@ -128,7 +127,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -128,7 +127,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private Bitmap bitmap; private Bitmap bitmap;
private boolean renderedFirstFrame; private boolean renderedFirstFrame;
private boolean forceRenderFrame;
private long joiningDeadlineMs; private long joiningDeadlineMs;
private Surface surface; private Surface surface;
private VpxOutputBufferRenderer outputBufferRenderer; private VpxOutputBufferRenderer outputBufferRenderer;
...@@ -144,6 +142,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -144,6 +142,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private int droppedFrames; private int droppedFrames;
private int consecutiveDroppedFrameCount; private int consecutiveDroppedFrameCount;
private int buffersInCodecCount; private int buffersInCodecCount;
private long lastRenderTimeUs;
protected DecoderCounters decoderCounters; protected DecoderCounters decoderCounters;
...@@ -254,7 +253,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -254,7 +253,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
try { try {
// Rendering loop. // Rendering loop.
TraceUtil.beginSection("drainAndFeed"); TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs)) {} while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
while (feedInputBuffer()) {} while (feedInputBuffer()) {}
TraceUtil.endSection(); TraceUtil.endSection();
} catch (VpxDecoderException e) { } catch (VpxDecoderException e) {
...@@ -319,6 +318,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -319,6 +318,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
protected void onStarted() { protected void onStarted() {
droppedFrames = 0; droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
} }
@Override @Override
...@@ -379,7 +379,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -379,7 +379,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
@CallSuper @CallSuper
protected void flushDecoder() throws ExoPlaybackException { protected void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false; waitingForKeys = false;
forceRenderFrame = false;
buffersInCodecCount = 0; buffersInCodecCount = 0;
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
releaseDecoder(); releaseDecoder();
...@@ -390,10 +389,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -390,10 +389,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
outputBuffer.release(); outputBuffer.release();
outputBuffer = null; outputBuffer = null;
} }
if (nextOutputBuffer != null) {
nextOutputBuffer.release();
nextOutputBuffer = null;
}
decoder.flush(); decoder.flush();
decoderReceivedBuffers = false; decoderReceivedBuffers = false;
} }
...@@ -408,13 +403,11 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -408,13 +403,11 @@ public class LibvpxVideoRenderer extends BaseRenderer {
inputBuffer = null; inputBuffer = null;
outputBuffer = null; outputBuffer = null;
nextOutputBuffer = null;
decoder.release(); decoder.release();
decoder = null; decoder = null;
decoderCounters.decoderReleaseCount++; decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false; decoderReceivedBuffers = false;
forceRenderFrame = false;
buffersInCodecCount = 0; buffersInCodecCount = 0;
} }
...@@ -482,22 +475,15 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -482,22 +475,15 @@ public class LibvpxVideoRenderer extends BaseRenderer {
} }
/** /**
* Returns whether the current frame should be dropped. * Returns whether the buffer being processed should be dropped.
* *
* @param outputBufferTimeUs The timestamp of the current output buffer. * @param earlyUs The time until the buffer should be presented in microseconds. A negative value
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET} * indicates that the buffer is late.
* if the next output buffer is unavailable. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* @param positionUs The current playback position. * measured at the start of the current iteration of the rendering loop.
* @param joiningDeadlineMs The joining deadline.
* @return Returns whether to drop the current output buffer.
*/ */
protected boolean shouldDropOutputBuffer( protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
long outputBufferTimeUs, return isBufferLate(earlyUs);
long nextOutputBufferTimeUs,
long positionUs,
long joiningDeadlineMs) {
return isBufferLate(outputBufferTimeUs - positionUs)
&& (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
} }
/** /**
...@@ -506,12 +492,27 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -506,12 +492,27 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* *
* @param earlyUs The time until the current buffer should be presented in microseconds. A * @param earlyUs The time until the current buffer should be presented in microseconds. A
* negative value indicates that the buffer is late. * negative value indicates that the buffer is late.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
*/ */
protected boolean shouldDropBuffersToKeyframe(long earlyUs) { protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) {
return isBufferVeryLate(earlyUs); return isBufferVeryLate(earlyUs);
} }
/** /**
* Returns whether to force rendering an output buffer.
*
* @param earlyUs The time until the current buffer should be presented in microseconds. A
* negative value indicates that the buffer is late.
* @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in
* microseconds.
* @return Returns whether to force rendering an output buffer.
*/
protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {
return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000;
}
/**
* Skips the specified output buffer and releases it. * Skips the specified output buffer and releases it.
* *
* @param outputBuffer The output buffer to skip. * @param outputBuffer The output buffer to skip.
...@@ -543,6 +544,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -543,6 +544,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
int bufferMode = outputBuffer.mode; int bufferMode = outputBuffer.mode;
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
if (!renderRgb && !renderYuv) { if (!renderRgb && !renderYuv) {
dropOutputBuffer(outputBuffer); dropOutputBuffer(outputBuffer);
} else { } else {
...@@ -755,22 +757,18 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -755,22 +757,18 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** /**
* Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link
* #processOutputBuffer(long)}. * #processOutputBuffer(long, long)}.
* *
* @param positionUs The player's current position. * @param positionUs The player's current position.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @return Whether it may be possible to drain more output data. * @return Whether it may be possible to drain more output data.
* @throws ExoPlaybackException If an error occurs draining the output buffer. * @throws ExoPlaybackException If an error occurs draining the output buffer.
*/ */
private boolean drainOutputBuffer(long positionUs) private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException, VpxDecoderException { throws ExoPlaybackException, VpxDecoderException {
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
if (outputBuffer == null) { if (outputBuffer == null) {
if (nextOutputBuffer != null) {
outputBuffer = nextOutputBuffer;
nextOutputBuffer = null;
} else {
outputBuffer = decoder.dequeueOutputBuffer(); outputBuffer = decoder.dequeueOutputBuffer();
}
if (outputBuffer == null) { if (outputBuffer == null) {
return false; return false;
} }
...@@ -778,10 +776,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -778,10 +776,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
} }
if (nextOutputBuffer == null) {
nextOutputBuffer = decoder.dequeueOutputBuffer();
}
if (outputBuffer.isEndOfStream()) { if (outputBuffer.isEndOfStream()) {
if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
// We're waiting to re-initialize the decoder, and have now processed all final buffers. // We're waiting to re-initialize the decoder, and have now processed all final buffers.
...@@ -795,7 +789,12 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -795,7 +789,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return false; return false;
} }
return processOutputBuffer(positionUs); boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs);
if (processedOutputBuffer) {
onProcessedOutputBuffer(outputBuffer.timeUs);
outputBuffer = null;
}
return processedOutputBuffer;
} }
/** /**
...@@ -803,53 +802,47 @@ public class LibvpxVideoRenderer extends BaseRenderer { ...@@ -803,53 +802,47 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* whether it may be possible to process another output buffer. * whether it may be possible to process another output buffer.
* *
* @param positionUs The player's current position. * @param positionUs The player's current position.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @return Whether it may be possible to drain another output buffer. * @return Whether it may be possible to drain another output buffer.
* @throws ExoPlaybackException If an error occurs processing the output buffer. * @throws ExoPlaybackException If an error occurs processing the output buffer.
*/ */
private boolean processOutputBuffer(long positionUs) throws ExoPlaybackException { private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
long earlyUs = outputBuffer.timeUs - positionUs;
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes. // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(outputBuffer.timeUs - positionUs)) { if (isBufferLate(earlyUs)) {
forceRenderFrame = false;
skipOutputBuffer(outputBuffer); skipOutputBuffer(outputBuffer);
onProcessedOutputBuffer(outputBuffer.timeUs);
outputBuffer = null;
return true; return true;
} }
return false; return false;
} }
if (forceRenderFrame) { long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
forceRenderFrame = false; boolean isStarted = getState() == STATE_STARTED;
if (!renderedFirstFrame
|| (isStarted
&& shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
renderOutputBuffer(outputBuffer); renderOutputBuffer(outputBuffer);
onProcessedOutputBuffer(outputBuffer.timeUs);
outputBuffer = null;
return true; return true;
} }
long nextOutputBufferTimeUs = if (!isStarted) {
nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() return false;
? nextOutputBuffer.timeUs }
: C.TIME_UNSET;
long earlyUs = outputBuffer.timeUs - positionUs; if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) { && maybeDropBuffersToKeyframe(positionUs)) {
forceRenderFrame = true;
return false; return false;
} else if (shouldDropOutputBuffer( } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) {
dropOutputBuffer(outputBuffer); dropOutputBuffer(outputBuffer);
onProcessedOutputBuffer(outputBuffer.timeUs);
outputBuffer = null;
return true; return true;
} }
// If we have yet to render a frame to the current output (either initially or immediately if (earlyUs < 30000) {
// following a seek), render one irrespective of the state or current position.
if (!renderedFirstFrame || (getState() == STATE_STARTED && earlyUs <= 30000)) {
renderOutputBuffer(outputBuffer); renderOutputBuffer(outputBuffer);
onProcessedOutputBuffer(outputBuffer.timeUs); return true;
outputBuffer = null;
} }
return false; return false;
......
...@@ -25,11 +25,11 @@ android { ...@@ -25,11 +25,11 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') api project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-dash') api project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls') api project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'library-smoothstreaming') api project(modulePrefix + 'library-smoothstreaming')
compile project(modulePrefix + 'library-ui') api project(modulePrefix + 'library-ui')
} }
ext { ext {
......
...@@ -31,6 +31,7 @@ android { ...@@ -31,6 +31,7 @@ android {
} }
test { test {
java.srcDirs += "../../testutils/src/main/java/" java.srcDirs += "../../testutils/src/main/java/"
java.srcDirs += "../../testutils_robolectric/src/main/java/"
} }
} }
...@@ -44,15 +45,15 @@ android { ...@@ -44,15 +45,15 @@ android {
} }
dependencies { dependencies {
compile 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'com.google.truth:truth:' + truthVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
testCompile 'com.google.truth:truth:' + truthVersion testImplementation 'com.google.truth:truth:' + truthVersion
testCompile 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testCompile 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testCompile 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -89,7 +89,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ...@@ -89,7 +89,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
try { try {
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
byte[] completeData = TestUtil.getByteArray(instrumentation, DATA_PATH); byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH);
byte[] expectedData = Arrays.copyOfRange(completeData, offset, byte[] expectedData = Arrays.copyOfRange(completeData, offset,
length == C.LENGTH_UNSET ? completeData.length : offset + length); length == C.LENGTH_UNSET ? completeData.length : offset + length);
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
......
...@@ -170,7 +170,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -170,7 +170,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
// because it uses a callback. // because it uses a callback.
hasPendingPrepare = true; hasPendingPrepare = true;
pendingOperationAcks++; pendingOperationAcks++;
internalPlayer.prepare(mediaSource, resetPosition); internalPlayer.prepare(mediaSource, resetPosition, resetState);
updatePlaybackInfo( updatePlaybackInfo(
playbackInfo, playbackInfo,
/* positionDiscontinuity= */ false, /* positionDiscontinuity= */ false,
...@@ -567,10 +567,6 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -567,10 +567,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
@DiscontinuityReason int positionDiscontinuityReason) { @DiscontinuityReason int positionDiscontinuityReason) {
pendingOperationAcks -= operationAcks; pendingOperationAcks -= operationAcks;
if (pendingOperationAcks == 0) { if (pendingOperationAcks == 0) {
if (playbackInfo.timeline == null) {
// Replace internal null timeline with externally visible empty timeline.
playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest);
}
if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (playbackInfo.startPositionUs == C.TIME_UNSET) {
// Replace internal unset start position with externally visible start position of zero. // Replace internal unset start position with externally visible start position of zero.
playbackInfo = playbackInfo =
......
...@@ -154,7 +154,7 @@ import java.util.Collections; ...@@ -154,7 +154,7 @@ import java.util.Collections;
seekParameters = SeekParameters.DEFAULT; seekParameters = SeekParameters.DEFAULT;
playbackInfo = playbackInfo =
new PlaybackInfo( new PlaybackInfo(
/* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); Timeline.EMPTY, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
playbackInfoUpdate = new PlaybackInfoUpdate(); playbackInfoUpdate = new PlaybackInfoUpdate();
rendererCapabilities = new RendererCapabilities[renderers.length]; rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
...@@ -176,8 +176,9 @@ import java.util.Collections; ...@@ -176,8 +176,9 @@ import java.util.Collections;
handler = clock.createHandler(internalPlaybackThread.getLooper(), this); handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
} }
public void prepare(MediaSource mediaSource, boolean resetPosition) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource) handler
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
.sendToTarget(); .sendToTarget();
} }
...@@ -286,7 +287,10 @@ import java.util.Collections; ...@@ -286,7 +287,10 @@ import java.util.Collections;
try { try {
switch (msg.what) { switch (msg.what) {
case MSG_PREPARE: case MSG_PREPARE:
prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); prepareInternal(
(MediaSource) msg.obj,
/* resetPosition= */ msg.arg1 != 0,
/* resetState= */ msg.arg2 != 0);
break; break;
case MSG_SET_PLAY_WHEN_READY: case MSG_SET_PLAY_WHEN_READY:
setPlayWhenReadyInternal(msg.arg1 != 0); setPlayWhenReadyInternal(msg.arg1 != 0);
...@@ -339,7 +343,7 @@ import java.util.Collections; ...@@ -339,7 +343,7 @@ import java.util.Collections;
} }
maybeNotifyPlaybackInfoChanged(); maybeNotifyPlaybackInfoChanged();
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
Log.e(TAG, "Renderer error.", e); Log.e(TAG, "Playback error.", e);
stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
maybeNotifyPlaybackInfoChanged(); maybeNotifyPlaybackInfoChanged();
...@@ -387,9 +391,9 @@ import java.util.Collections; ...@@ -387,9 +391,9 @@ import java.util.Collections;
} }
} }
private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
pendingPrepareCount++; pendingPrepareCount++;
resetInternal(/* releaseMediaSource= */ true, resetPosition, /* resetState= */ true); resetInternal(/* releaseMediaSource= */ true, resetPosition, resetState);
loadControl.onPrepared(); loadControl.onPrepared();
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING); setState(Player.STATE_BUFFERING);
...@@ -576,7 +580,6 @@ import java.util.Collections; ...@@ -576,7 +580,6 @@ import java.util.Collections;
} }
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
Timeline timeline = playbackInfo.timeline;
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
MediaPeriodId periodId; MediaPeriodId periodId;
...@@ -607,7 +610,7 @@ import java.util.Collections; ...@@ -607,7 +610,7 @@ import java.util.Collections;
} }
try { try {
if (mediaSource == null || timeline == null) { if (mediaSource == null || pendingPrepareCount > 0) {
// Save seek position for later, as we are still waiting for a prepared source. // Save seek position for later, as we are still waiting for a prepared source.
pendingInitialSeekPosition = seekPosition; pendingInitialSeekPosition = seekPosition;
} else if (periodPositionUs == C.TIME_UNSET) { } else if (periodPositionUs == C.TIME_UNSET) {
...@@ -752,7 +755,7 @@ import java.util.Collections; ...@@ -752,7 +755,7 @@ import java.util.Collections;
private int getFirstPeriodIndex() { private int getFirstPeriodIndex() {
Timeline timeline = playbackInfo.timeline; Timeline timeline = playbackInfo.timeline;
return timeline == null || timeline.isEmpty() return timeline.isEmpty()
? 0 ? 0
: timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
.firstPeriodIndex; .firstPeriodIndex;
...@@ -779,7 +782,7 @@ import java.util.Collections; ...@@ -779,7 +782,7 @@ import java.util.Collections;
pendingInitialSeekPosition = null; pendingInitialSeekPosition = null;
} }
if (resetState) { if (resetState) {
queue.setTimeline(null); queue.setTimeline(Timeline.EMPTY);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) { for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
} }
...@@ -788,11 +791,11 @@ import java.util.Collections; ...@@ -788,11 +791,11 @@ import java.util.Collections;
} }
playbackInfo = playbackInfo =
new PlaybackInfo( new PlaybackInfo(
resetState ? null : playbackInfo.timeline, resetState ? Timeline.EMPTY : playbackInfo.timeline,
resetState ? null : playbackInfo.manifest, resetState ? null : playbackInfo.manifest,
resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId, resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId,
// Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
resetPosition ? C.TIME_UNSET : playbackInfo.startPositionUs, resetPosition ? C.TIME_UNSET : playbackInfo.positionUs,
resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs, resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs,
playbackInfo.playbackState, playbackInfo.playbackState,
/* isLoading= */ false, /* isLoading= */ false,
...@@ -805,11 +808,11 @@ import java.util.Collections; ...@@ -805,11 +808,11 @@ import java.util.Collections;
} }
} }
private void sendMessageInternal(PlayerMessage message) { private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException {
if (message.getPositionMs() == C.TIME_UNSET) { if (message.getPositionMs() == C.TIME_UNSET) {
// If no delivery time is specified, trigger immediate message delivery. // If no delivery time is specified, trigger immediate message delivery.
sendMessageToTarget(message); sendMessageToTarget(message);
} else if (playbackInfo.timeline == null) { } else if (mediaSource == null || pendingPrepareCount > 0) {
// Still waiting for initial timeline to resolve position. // Still waiting for initial timeline to resolve position.
pendingMessages.add(new PendingMessageInfo(message)); pendingMessages.add(new PendingMessageInfo(message));
} else { } else {
...@@ -824,7 +827,7 @@ import java.util.Collections; ...@@ -824,7 +827,7 @@ import java.util.Collections;
} }
} }
private void sendMessageToTarget(PlayerMessage message) { private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
if (message.getHandler().getLooper() == handler.getLooper()) { if (message.getHandler().getLooper() == handler.getLooper()) {
deliverMessage(message); deliverMessage(message);
if (playbackInfo.playbackState == Player.STATE_READY if (playbackInfo.playbackState == Player.STATE_READY
...@@ -838,22 +841,24 @@ import java.util.Collections; ...@@ -838,22 +841,24 @@ import java.util.Collections;
} }
private void sendMessageToTargetThread(final PlayerMessage message) { private void sendMessageToTargetThread(final PlayerMessage message) {
message Handler handler = message.getHandler();
.getHandler() handler.post(
.post(
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
try {
deliverMessage(message); deliverMessage(message);
} catch (ExoPlaybackException e) {
Log.e(TAG, "Unexpected error delivering message on external thread.", e);
throw new RuntimeException(e);
}
} }
}); });
} }
private void deliverMessage(PlayerMessage message) { private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
try { try {
message.getTarget().handleMessage(message.getType(), message.getPayload()); message.getTarget().handleMessage(message.getType(), message.getPayload());
} catch (ExoPlaybackException e) {
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
} finally { } finally {
message.markAsProcessed(/* isDelivered= */ true); message.markAsProcessed(/* isDelivered= */ true);
} }
...@@ -899,7 +904,8 @@ import java.util.Collections; ...@@ -899,7 +904,8 @@ import java.util.Collections;
return true; return true;
} }
private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)
throws ExoPlaybackException {
if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) { if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {
return; return;
} }
...@@ -1130,7 +1136,7 @@ import java.util.Collections; ...@@ -1130,7 +1136,7 @@ import java.util.Collections;
playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
resolvePendingMessagePositions(); resolvePendingMessagePositions();
if (oldTimeline == null) { if (pendingPrepareCount > 0) {
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
pendingPrepareCount = 0; pendingPrepareCount = 0;
if (pendingInitialSeekPosition != null) { if (pendingInitialSeekPosition != null) {
...@@ -1292,8 +1298,8 @@ import java.util.Collections; ...@@ -1292,8 +1298,8 @@ import java.util.Collections;
SeekPosition seekPosition, boolean trySubsequentPeriods) { SeekPosition seekPosition, boolean trySubsequentPeriods) {
Timeline timeline = playbackInfo.timeline; Timeline timeline = playbackInfo.timeline;
Timeline seekTimeline = seekPosition.timeline; Timeline seekTimeline = seekPosition.timeline;
if (timeline == null) { if (timeline.isEmpty()) {
// We don't have a timeline yet, so we can't resolve the position. // We don't have a valid timeline yet, so we can't resolve the position.
return null; return null;
} }
if (seekTimeline.isEmpty()) { if (seekTimeline.isEmpty()) {
...@@ -1349,7 +1355,7 @@ import java.util.Collections; ...@@ -1349,7 +1355,7 @@ import java.util.Collections;
// The player has no media source yet. // The player has no media source yet.
return; return;
} }
if (playbackInfo.timeline == null) { if (pendingPrepareCount > 0) {
// We're waiting to get information about periods. // We're waiting to get information about periods.
mediaSource.maybeThrowSourceInfoRefreshError(); mediaSource.maybeThrowSourceInfoRefreshError();
return; return;
......
...@@ -27,27 +27,23 @@ public final class ExoPlayerLibraryInfo { ...@@ -27,27 +27,23 @@ public final class ExoPlayerLibraryInfo {
*/ */
public static final String TAG = "ExoPlayer"; public static final String TAG = "ExoPlayer";
/** /** The version of the library expressed as a string, for example "1.2.3". */
* The version of the library expressed as a string, for example "1.2.3".
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.7.0"; public static final String VERSION = "2.7.1";
/** /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.0"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.1";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
* <p> *
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * <p>Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2007000; public static final int VERSION_INT = 2007001;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; ...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
*/ */
/* package */ final class PlaybackInfo { /* package */ final class PlaybackInfo {
public final @Nullable Timeline timeline; public final Timeline timeline;
public final @Nullable Object manifest; public final @Nullable Object manifest;
public final MediaPeriodId periodId; public final MediaPeriodId periodId;
public final long startPositionUs; public final long startPositionUs;
...@@ -37,7 +37,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; ...@@ -37,7 +37,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public volatile long bufferedPositionUs; public volatile long bufferedPositionUs;
public PlaybackInfo( public PlaybackInfo(
@Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) { Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
this( this(
timeline, timeline,
/* manifest= */ null, /* manifest= */ null,
...@@ -50,7 +50,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; ...@@ -50,7 +50,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
} }
public PlaybackInfo( public PlaybackInfo(
@Nullable Timeline timeline, Timeline timeline,
@Nullable Object manifest, @Nullable Object manifest,
MediaPeriodId periodId, MediaPeriodId periodId,
long startPositionUs, long startPositionUs,
......
...@@ -33,7 +33,8 @@ public final class PlayerMessage { ...@@ -33,7 +33,8 @@ public final class PlayerMessage {
* *
* @param messageType The message type. * @param messageType The message type.
* @param payload The message payload. * @param payload The message payload.
* @throws ExoPlaybackException If an error occurred whilst handling the message. * @throws ExoPlaybackException If an error occurred whilst handling the message. Should only be
* thrown by targets that handle messages on the playback thread.
*/ */
void handleMessage(int messageType, Object payload) throws ExoPlaybackException; void handleMessage(int messageType, Object payload) throws ExoPlaybackException;
} }
......
...@@ -1443,7 +1443,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1443,7 +1443,7 @@ public final class DefaultAudioSink implements AudioSink {
rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
} }
if (Util.SDK_INT <= 26) { if (Util.SDK_INT <= 28) {
if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0 if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0
&& state == PLAYSTATE_PLAYING) { && state == PLAYSTATE_PLAYING) {
// If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state
......
...@@ -49,7 +49,6 @@ import java.util.List; ...@@ -49,7 +49,6 @@ import java.util.List;
private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc");
private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
/** /**
...@@ -128,7 +127,8 @@ import java.util.List; ...@@ -128,7 +127,8 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount(); int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) { if (sampleCount == 0) {
return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]); return new TrackSampleTable(
new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET);
} }
// Entries are byte offsets of chunks. // Entries are byte offsets of chunks.
...@@ -193,6 +193,7 @@ import java.util.List; ...@@ -193,6 +193,7 @@ import java.util.List;
long[] timestamps; long[] timestamps;
int[] flags; int[] flags;
long timestampTimeUnits = 0; long timestampTimeUnits = 0;
long duration;
if (!isRechunkable) { if (!isRechunkable) {
offsets = new long[sampleCount]; offsets = new long[sampleCount];
...@@ -260,6 +261,7 @@ import java.util.List; ...@@ -260,6 +261,7 @@ import java.util.List;
offset += sizes[i]; offset += sizes[i];
remainingSamplesInChunk--; remainingSamplesInChunk--;
} }
duration = timestampTimeUnits + timestampOffset;
Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0); Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
// Remove trailing ctts entries with 0-valued sample counts. // Remove trailing ctts entries with 0-valued sample counts.
...@@ -294,13 +296,15 @@ import java.util.List; ...@@ -294,13 +296,15 @@ import java.util.List;
maximumSize = rechunkedResults.maximumSize; maximumSize = rechunkedResults.maximumSize;
timestamps = rechunkedResults.timestamps; timestamps = rechunkedResults.timestamps;
flags = rechunkedResults.flags; flags = rechunkedResults.flags;
duration = rechunkedResults.duration;
} }
long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);
if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
// This implementation does not support applying both gapless metadata and an edit list. // This implementation does not support applying both gapless metadata and an edit list.
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
...@@ -317,10 +321,11 @@ import java.util.List; ...@@ -317,10 +321,11 @@ import java.util.List;
long editStartTime = track.editListMediaTimes[0]; long editStartTime = track.editListMediaTimes[0];
long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
track.timescale, track.movieTimescale); track.timescale, track.movieTimescale);
long lastSampleEndTime = timestampTimeUnits; if (timestamps[0] <= editStartTime
if (timestamps[0] <= editStartTime && editStartTime < timestamps[1] && editStartTime < timestamps[1]
&& timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) { && timestamps[timestamps.length - 1] < editEndTime
long paddingTimeUnits = lastSampleEndTime - editEndTime; && editEndTime <= duration) {
long paddingTimeUnits = duration - editEndTime;
long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],
track.format.sampleRate, track.timescale); track.format.sampleRate, track.timescale);
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
...@@ -330,7 +335,7 @@ import java.util.List; ...@@ -330,7 +335,7 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding; gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
} }
} }
...@@ -339,11 +344,15 @@ import java.util.List; ...@@ -339,11 +344,15 @@ import java.util.List;
// The current version of the spec leaves handling of an edit with zero segment_duration in // The current version of the spec leaves handling of an edit with zero segment_duration in
// unfragmented files open to interpretation. We handle this as a special case and include all // unfragmented files open to interpretation. We handle this as a special case and include all
// samples in the edit. // samples in the edit.
long editStartTime = track.editListMediaTimes[0];
for (int i = 0; i < timestamps.length; i++) { for (int i = 0; i < timestamps.length; i++) {
timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0], timestamps[i] =
C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestamp(
timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale);
} }
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); durationUs =
Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
// Omit any sample at the end point of an edit for audio tracks. // Omit any sample at the end point of an edit for audio tracks.
...@@ -354,13 +363,15 @@ import java.util.List; ...@@ -354,13 +363,15 @@ import java.util.List;
int nextSampleIndex = 0; int nextSampleIndex = 0;
boolean copyMetadata = false; boolean copyMetadata = false;
for (int i = 0; i < track.editListDurations.length; i++) { for (int i = 0; i < track.editListDurations.length; i++) {
long mediaTime = track.editListMediaTimes[i]; long editMediaTime = track.editListMediaTimes[i];
if (mediaTime != -1) { if (editMediaTime != -1) {
long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, long editDuration =
track.movieTimescale); Util.scaleLargeTimestamp(
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); track.editListDurations[i], track.timescale, track.movieTimescale);
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true);
false); int endIndex =
Util.binarySearchCeil(
timestamps, editMediaTime + editDuration, omitClippedSample, false);
editedSampleCount += endIndex - startIndex; editedSampleCount += endIndex - startIndex;
copyMetadata |= nextSampleIndex != startIndex; copyMetadata |= nextSampleIndex != startIndex;
nextSampleIndex = endIndex; nextSampleIndex = endIndex;
...@@ -377,12 +388,13 @@ import java.util.List; ...@@ -377,12 +388,13 @@ import java.util.List;
long pts = 0; long pts = 0;
int sampleIndex = 0; int sampleIndex = 0;
for (int i = 0; i < track.editListDurations.length; i++) { for (int i = 0; i < track.editListDurations.length; i++) {
long mediaTime = track.editListMediaTimes[i]; long editMediaTime = track.editListMediaTimes[i];
long duration = track.editListDurations[i]; long editDuration = track.editListDurations[i];
if (mediaTime != -1) { if (editMediaTime != -1) {
long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, long endMediaTime =
track.movieTimescale); editMediaTime
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); + Util.scaleLargeTimestamp(editDuration, track.timescale, track.movieTimescale);
int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true);
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
if (copyMetadata) { if (copyMetadata) {
int count = endIndex - startIndex; int count = endIndex - startIndex;
...@@ -392,8 +404,9 @@ import java.util.List; ...@@ -392,8 +404,9 @@ import java.util.List;
} }
for (int j = startIndex; j < endIndex; j++) { for (int j = startIndex; j < endIndex; j++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime, long timeInSegmentUs =
C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestamp(
timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale);
editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
editedMaximumSize = sizes[j]; editedMaximumSize = sizes[j];
...@@ -401,8 +414,9 @@ import java.util.List; ...@@ -401,8 +414,9 @@ import java.util.List;
sampleIndex++; sampleIndex++;
} }
} }
pts += duration; pts += editDuration;
} }
long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale);
boolean hasSyncSample = false; boolean hasSyncSample = false;
for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) { for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
...@@ -413,11 +427,16 @@ import java.util.List; ...@@ -413,11 +427,16 @@ import java.util.List;
// Such edit lists are often (although not always) broken, so we ignore it and continue. // Such edit lists are often (although not always) broken, so we ignore it and continue.
Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample."); Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, return new TrackSampleTable(
editedFlags); editedOffsets,
editedSizes,
editedMaximumSize,
editedTimestamps,
editedFlags,
editedDurationUs);
} }
/** /**
......
...@@ -33,13 +33,21 @@ import com.google.android.exoplayer2.util.Util; ...@@ -33,13 +33,21 @@ import com.google.android.exoplayer2.util.Util;
public final int maximumSize; public final int maximumSize;
public final long[] timestamps; public final long[] timestamps;
public final int[] flags; public final int[] flags;
public final long duration;
private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) {
private Results(
long[] offsets,
int[] sizes,
int maximumSize,
long[] timestamps,
int[] flags,
long duration) {
this.offsets = offsets; this.offsets = offsets;
this.sizes = sizes; this.sizes = sizes;
this.maximumSize = maximumSize; this.maximumSize = maximumSize;
this.timestamps = timestamps; this.timestamps = timestamps;
this.flags = flags; this.flags = flags;
this.duration = duration;
} }
} }
...@@ -95,8 +103,9 @@ import com.google.android.exoplayer2.util.Util; ...@@ -95,8 +103,9 @@ import com.google.android.exoplayer2.util.Util;
newSampleIndex++; newSampleIndex++;
} }
} }
long duration = timestampDeltaInTimeUnits * originalSampleIndex;
return new Results(offsets, sizes, maximumSize, timestamps, flags); return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
} }
} }
...@@ -427,7 +427,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -427,7 +427,10 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} }
mp4Track.trackOutput.format(format); mp4Track.trackOutput.format(format);
durationUs = Math.max(durationUs, track.durationUs); durationUs =
Math.max(
durationUs,
track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs);
if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
firstVideoTrackIndex = tracks.size(); firstVideoTrackIndex = tracks.size();
} }
......
...@@ -48,9 +48,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -48,9 +48,19 @@ import com.google.android.exoplayer2.util.Util;
* Sample flags. * Sample flags.
*/ */
public final int[] flags; public final int[] flags;
/**
* The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
* table is empty.
*/
public final long durationUs;
public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs, public TrackSampleTable(
int[] flags) { long[] offsets,
int[] sizes,
int maximumSize,
long[] timestampsUs,
int[] flags,
long durationUs) {
Assertions.checkArgument(sizes.length == timestampsUs.length); Assertions.checkArgument(sizes.length == timestampsUs.length);
Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length);
...@@ -60,6 +70,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -60,6 +70,7 @@ import com.google.android.exoplayer2.util.Util;
this.maximumSize = maximumSize; this.maximumSize = maximumSize;
this.timestampsUs = timestampsUs; this.timestampsUs = timestampsUs;
this.flags = flags; this.flags = flags;
this.durationUs = durationUs;
sampleCount = offsets.length; sampleCount = offsets.length;
} }
......
...@@ -100,12 +100,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -100,12 +100,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@C.VideoScalingMode @C.VideoScalingMode
private int scalingMode; private int scalingMode;
private boolean renderedFirstFrame; private boolean renderedFirstFrame;
private boolean forceRenderFrame;
private long joiningDeadlineMs; private long joiningDeadlineMs;
private long droppedFrameAccumulationStartTimeMs; private long droppedFrameAccumulationStartTimeMs;
private int droppedFrames; private int droppedFrames;
private int consecutiveDroppedFrameCount; private int consecutiveDroppedFrameCount;
private int buffersInCodecCount; private int buffersInCodecCount;
private long lastRenderTimeUs;
private int pendingRotationDegrees; private int pendingRotationDegrees;
private float pendingPixelWidthHeightRatio; private float pendingPixelWidthHeightRatio;
...@@ -314,6 +314,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -314,6 +314,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.onStarted(); super.onStarted();
droppedFrames = 0; droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
} }
@Override @Override
...@@ -438,7 +439,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -438,7 +439,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.releaseCodec(); super.releaseCodec();
} finally { } finally {
buffersInCodecCount = 0; buffersInCodecCount = 0;
forceRenderFrame = false;
if (dummySurface != null) { if (dummySurface != null) {
if (surface == dummySurface) { if (surface == dummySurface) {
surface = null; surface = null;
...@@ -454,7 +454,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -454,7 +454,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
protected void flushCodec() throws ExoPlaybackException { protected void flushCodec() throws ExoPlaybackException {
super.flushCodec(); super.flushCodec();
buffersInCodecCount = 0; buffersInCodecCount = 0;
forceRenderFrame = false;
} }
@Override @Override
...@@ -546,15 +545,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -546,15 +545,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (surface == dummySurface) { if (surface == dummySurface) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes. // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(earlyUs)) { if (isBufferLate(earlyUs)) {
forceRenderFrame = false;
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true; return true;
} }
return false; return false;
} }
if (!renderedFirstFrame || forceRenderFrame) { long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
forceRenderFrame = false; boolean isStarted = getState() == STATE_STARTED;
if (!renderedFirstFrame
|| (isStarted
&& shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
if (Util.SDK_INT >= 21) { if (Util.SDK_INT >= 21) {
renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime()); renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
} else { } else {
...@@ -563,13 +564,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -563,13 +564,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return true; return true;
} }
if (getState() != STATE_STARTED) { if (!isStarted) {
return false; return false;
} }
// Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current
// iteration of the rendering loop. // iteration of the rendering loop.
long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs;
earlyUs -= elapsedSinceStartOfLoopUs; earlyUs -= elapsedSinceStartOfLoopUs;
// Compute the buffer's desired release time in nanoseconds. // Compute the buffer's desired release time in nanoseconds.
...@@ -583,7 +584,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -583,7 +584,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
&& maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) {
forceRenderFrame = true;
return false; return false;
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
dropOutputBuffer(codec, bufferIndex, presentationTimeUs); dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
...@@ -607,6 +607,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -607,6 +607,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Thread.sleep((earlyUs - 10000) / 1000); Thread.sleep((earlyUs - 10000) / 1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return false;
} }
} }
renderOutputBuffer(codec, bufferIndex, presentationTimeUs); renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
...@@ -655,6 +656,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -655,6 +656,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
/** /**
* Returns whether to force rendering an output buffer.
*
* @param earlyUs The time until the current buffer should be presented in microseconds. A
* negative value indicates that the buffer is late.
* @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in
* microseconds.
* @return Returns whether to force rendering an output buffer.
*/
protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {
return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000;
}
/**
* Skips the output buffer with the specified index. * Skips the output buffer with the specified index.
* *
* @param codec The codec that owns the output buffer. * @param codec The codec that owns the output buffer.
...@@ -738,6 +752,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -738,6 +752,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
TraceUtil.beginSection("releaseOutputBuffer"); TraceUtil.beginSection("releaseOutputBuffer");
codec.releaseOutputBuffer(index, true); codec.releaseOutputBuffer(index, true);
TraceUtil.endSection(); TraceUtil.endSection();
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
decoderCounters.renderedOutputBufferCount++; decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0; consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame(); maybeNotifyRenderedFirstFrame();
...@@ -753,12 +768,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -753,12 +768,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.
*/ */
@TargetApi(21) @TargetApi(21)
protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs, protected void renderOutputBufferV21(
long releaseTimeNs) { MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {
maybeNotifyVideoSizeChanged(); maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer"); TraceUtil.beginSection("releaseOutputBuffer");
codec.releaseOutputBuffer(index, releaseTimeNs); codec.releaseOutputBuffer(index, releaseTimeNs);
TraceUtil.endSection(); TraceUtil.endSection();
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
decoderCounters.renderedOutputBufferCount++; decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0; consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame(); maybeNotifyRenderedFirstFrame();
......
...@@ -42,7 +42,9 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; ...@@ -42,7 +42,9 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.testutil.FakeTrackSelector;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
...@@ -1169,10 +1171,8 @@ public final class ExoPlayerTest { ...@@ -1169,10 +1171,8 @@ public final class ExoPlayerTest {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testReprepareAfterPlaybackError") new ActionSchedule.Builder("testReprepareAfterPlaybackError")
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_READY)
// Cause an internal exception by seeking to an invalid position while the media source .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
// is still being prepared and the player doesn't immediately know it will fail.
.seek(/* windowIndex= */ 100, /* positionMs= */ 0)
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.prepareSource( .prepareSource(
new FakeMediaSource(timeline, /* manifest= */ null), new FakeMediaSource(timeline, /* manifest= */ null),
...@@ -1203,11 +1203,8 @@ public final class ExoPlayerTest { ...@@ -1203,11 +1203,8 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testReprepareAfterPlaybackError") new ActionSchedule.Builder("testReprepareAfterPlaybackError")
.pause() .pause()
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_READY)
// Cause an internal exception by seeking to an invalid position while the media source .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
// is still being prepared and the player doesn't immediately know it will fail.
.seek(/* windowIndex= */ 100, /* positionMs= */ 0)
.waitForSeekProcessed()
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 50) .seek(/* positionMs= */ 50)
.waitForSeekProcessed() .waitForSeekProcessed()
...@@ -1246,8 +1243,7 @@ public final class ExoPlayerTest { ...@@ -1246,8 +1243,7 @@ public final class ExoPlayerTest {
testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelinesEqual(timeline, timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertPositionDiscontinuityReasonsEqual( testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK);
assertThat(positionHolder[0]).isEqualTo(50); assertThat(positionHolder[0]).isEqualTo(50);
assertThat(positionHolder[1]).isEqualTo(50); assertThat(positionHolder[1]).isEqualTo(50);
} }
...@@ -1289,6 +1285,104 @@ public final class ExoPlayerTest { ...@@ -1289,6 +1285,104 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
final long[] positionHolder = new long[3];
final int[] windowIndexHolder = new int[3];
final FakeMediaSource secondMediaSource =
new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition")
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 500)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
// Position while in error state
positionHolder[0] = player.getCurrentPosition();
windowIndexHolder[0] = player.getCurrentWindowIndex();
}
})
.prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
// Position while repreparing.
positionHolder[1] = player.getCurrentPosition();
windowIndexHolder[1] = player.getCurrentWindowIndex();
secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null);
}
})
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
// Position after repreparation finished.
positionHolder[2] = player.getCurrentPosition();
windowIndexHolder[2] = player.getCurrentWindowIndex();
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build();
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
// Expected exception.
}
assertThat(positionHolder[0]).isAtLeast(500L);
assertThat(positionHolder[1]).isEqualTo(positionHolder[0]);
assertThat(positionHolder[2]).isEqualTo(positionHolder[0]);
assertThat(windowIndexHolder[0]).isEqualTo(1);
assertThat(windowIndexHolder[1]).isEqualTo(1);
assertThat(windowIndexHolder[2]).isEqualTo(1);
}
@Test
public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final FakeMediaSource mediaSource2 =
new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition")
.pause()
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false)
.waitForPlaybackState(Player.STATE_BUFFERING)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build();
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
// Expected exception.
}
testRunner.assertTimelinesEqual(timeline, timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
}
@Test
public void testSendMessagesDuringPreparation() throws Exception { public void testSendMessagesDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
...@@ -1421,7 +1515,8 @@ public final class ExoPlayerTest { ...@@ -1421,7 +1515,8 @@ public final class ExoPlayerTest {
new FakeMediaSource(timeline, null), new FakeMediaSource(timeline, null),
/* resetPosition= */ false, /* resetPosition= */ false,
/* resetState= */ true) /* resetState= */ true)
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_ENDED)
.build(); .build();
new Builder() new Builder()
.setTimeline(timeline) .setTimeline(timeline)
......
...@@ -22,8 +22,8 @@ import static org.mockito.Mockito.when; ...@@ -22,8 +22,8 @@ import static org.mockito.Mockito.when;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import java.util.HashMap; import java.util.HashMap;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
......
...@@ -20,7 +20,6 @@ import static org.junit.Assert.fail; ...@@ -20,7 +20,6 @@ import static org.junit.Assert.fail;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
...@@ -29,6 +28,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; ...@@ -29,6 +28,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException; import java.io.IOException;
import org.junit.Before; import org.junit.Before;
......
...@@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
...@@ -27,6 +26,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; ...@@ -27,6 +26,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException; import java.io.IOException;
import org.junit.Test; import org.junit.Test;
......
...@@ -24,7 +24,6 @@ import android.os.Handler; ...@@ -24,7 +24,6 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
...@@ -32,6 +31,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; ...@@ -32,6 +31,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
......
...@@ -17,12 +17,12 @@ package com.google.android.exoplayer2.source; ...@@ -17,12 +17,12 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException; import java.io.IOException;
import org.junit.Before; import org.junit.Before;
......
...@@ -19,13 +19,13 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,13 +19,13 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException; import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import java.io.IOException; import java.io.IOException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
......
...@@ -33,17 +33,9 @@ android { ...@@ -33,17 +33,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestCompile project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
testCompile project(modulePrefix + 'testutils')
testCompile 'com.google.truth:truth:' + truthVersion
testCompile 'junit:junit:' + junitVersion
testCompile 'org.mockito:mockito-core:' + mockitoVersion
testCompile 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -50,6 +50,7 @@ import java.io.BufferedReader; ...@@ -50,6 +50,7 @@ import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Locale; import java.util.Locale;
...@@ -1113,7 +1114,9 @@ public final class DashMediaSource implements MediaSource { ...@@ -1113,7 +1114,9 @@ public final class DashMediaSource implements MediaSource {
@Override @Override
public Long parse(Uri uri, InputStream inputStream) throws IOException { public Long parse(Uri uri, InputStream inputStream) throws IOException {
String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); String firstLine =
new BufferedReader(new InputStreamReader(inputStream, Charset.forName(C.UTF8_NAME)))
.readLine();
try { try {
Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine);
if (!matcher.matches()) { if (!matcher.matches()) {
......
...@@ -18,16 +18,6 @@ ...@@ -18,16 +18,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.source.dash.test"> package="com.google.android.exoplayer2.source.dash.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="25"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer2.source.dash.test"
android:name="android.test.InstrumentationTestRunner"/>
</manifest> </manifest>
/*
* Copyright (C) 2017 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.exoplayer2.source.dash;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link DashMediaSource}. */
@RunWith(RobolectricTestRunner.class)
public final class DashMediaSourceTest {
@Test
public void testIso8601ParserParse() throws IOException {
DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();
// UTC.
assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37Z");
assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37+00:00");
assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37+0000");
assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37+00");
// Positive timezone offsets.
assertParseStringToLong(1512381697000L - 4980000L, parser, "2017-12-04T10:01:37+01:23");
assertParseStringToLong(1512381697000L - 4980000L, parser, "2017-12-04T10:01:37+0123");
assertParseStringToLong(1512381697000L - 3600000L, parser, "2017-12-04T10:01:37+01");
// Negative timezone offsets with minus character.
assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37-01:23");
assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37-0123");
assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37-01:00");
assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37-0100");
assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37-01");
// Negative timezone offsets with hyphen character.
assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37−01:23");
assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37−0123");
assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37−01:00");
assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37−0100");
assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37−01");
}
@Test
public void testIso8601ParserParseMissingTimezone() throws IOException {
DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();
try {
assertParseStringToLong(0, parser, "2017-12-04T10:01:37");
fail();
} catch (ParserException e) {
// Expected.
}
}
private static void assertParseStringToLong(
long expected, ParsingLoadable.Parser<Long> parser, String data) throws IOException {
long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data)));
assertThat(actual).isEqualTo(expected);
}
}
...@@ -28,33 +28,38 @@ import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegm ...@@ -28,33 +28,38 @@ import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegm
import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays; import java.util.Arrays;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Unit tests for {@link DashUtil}. */
* Unit tests for {@link DashUtil}. @RunWith(RobolectricTestRunner.class)
*/ public final class DashUtilTest {
public final class DashUtilTest extends TestCase {
@Test
public void testLoadDrmInitDataFromManifest() throws Exception { public void testLoadDrmInitDataFromManifest() throws Exception {
Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData()))); Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData())));
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
assertThat(drmInitData).isEqualTo(newDrmInitData()); assertThat(drmInitData).isEqualTo(newDrmInitData());
} }
@Test
public void testLoadDrmInitDataMissing() throws Exception { public void testLoadDrmInitDataMissing() throws Exception {
Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */))); Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */)));
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
assertThat(drmInitData).isNull(); assertThat(drmInitData).isNull();
} }
@Test
public void testLoadDrmInitDataNoRepresentations() throws Exception { public void testLoadDrmInitDataNoRepresentations() throws Exception {
Period period = newPeriod(newAdaptationSets(/* no representation */)); Period period = newPeriod(newAdaptationSets(/* no representation */ ));
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
assertThat(drmInitData).isNull(); assertThat(drmInitData).isNull();
} }
@Test
public void testLoadDrmInitDataNoAdaptationSets() throws Exception { public void testLoadDrmInitDataNoAdaptationSets() throws Exception {
Period period = newPeriod(/* no adaptation set */); Period period = newPeriod(/* no adaptation set */ );
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
assertThat(drmInitData).isNull(); assertThat(drmInitData).isNull();
} }
...@@ -68,8 +73,18 @@ public final class DashUtilTest extends TestCase { ...@@ -68,8 +73,18 @@ public final class DashUtilTest extends TestCase {
} }
private static Representation newRepresentations(DrmInitData drmInitData) { private static Representation newRepresentations(DrmInitData drmInitData) {
Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, Format format =
MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); Format.createVideoContainerFormat(
"id",
MimeTypes.VIDEO_MP4,
MimeTypes.VIDEO_H264,
"",
Format.NO_VALUE,
1024,
768,
Format.NO_VALUE,
null,
0);
if (drmInitData != null) { if (drmInitData != null) {
format = format.copyWithDrmInitData(drmInitData); format = format.copyWithDrmInitData(drmInitData);
} }
...@@ -77,8 +92,7 @@ public final class DashUtilTest extends TestCase { ...@@ -77,8 +92,7 @@ public final class DashUtilTest extends TestCase {
} }
private static DrmInitData newDrmInitData() { private static DrmInitData newDrmInitData() {
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", return new DrmInitData(
new byte[] {1, 4, 7, 0, 3, 6})); new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[] {1, 4, 7, 0, 3, 6}));
} }
} }
/*
* Copyright (C) 2017 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.exoplayer2.source.dash;
import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;
import com.google.android.exoplayer2.source.dash.manifest.EventStream;
import com.google.android.exoplayer2.util.MimeTypes;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/**
* Unit test for {@link EventSampleStream}.
*/
@RunWith(RobolectricTestRunner.class)
public final class EventSampleStreamTest {
private static final String SCHEME_ID = "urn:test";
private static final String VALUE = "123";
private static final Format FORMAT = Format.createSampleFormat("urn:test/123",
MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);
private static final byte[] MESSAGE_DATA = new byte[] {1, 2, 3, 4};
private static final long DURATION_MS = 3000;
private static final long TIME_SCALE = 1000;
private FormatHolder formatHolder;
private MetadataInputBuffer inputBuffer;
private EventMessageEncoder eventMessageEncoder;
@Before
public void setUp() {
formatHolder = new FormatHolder();
inputBuffer = new MetadataInputBuffer();
eventMessageEncoder = new EventMessageEncoder();
}
/**
* Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} will
* return format for the first call.
*/
@Test
public void testReadDataReturnFormatForFirstRead() {
EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[0], new EventMessage[0]);
EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_FORMAT_READ);
assertThat(formatHolder.format).isEqualTo(FORMAT);
}
/**
* Tests that a non-dynamic {@link EventSampleStream} will return a buffer with
* {@link C#BUFFER_FLAG_END_OF_STREAM} when trying to read sample out-of-bound.
*/
@Test
public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForNonDynamicEventSampleStream() {
EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[0], new EventMessage[0]);
EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
// first read - read format
readData(sampleStream);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.isEndOfStream()).isTrue();
}
/**
* Tests that a dynamic {@link EventSampleStream} will return {@link C#RESULT_NOTHING_READ}
* when trying to read sample out-of-bound.
*/
@Test
public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForDynamicEventSampleStream() {
EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[0], new EventMessage[0]);
EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, true);
// first read - read format
readData(sampleStream);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_NOTHING_READ);
}
/**
* Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} will
* return sample data after the first call.
*/
@Test
public void testReadDataReturnDataAfterFormat() {
long presentationTimeUs = 1000000;
EventMessage eventMessage = newEventMessageWithIdAndTime(1, presentationTimeUs);
EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs}, new EventMessage[] {eventMessage});
EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
// first read - read format
readData(sampleStream);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage));
}
/**
* Tests that {@link EventSampleStream#skipData(long)} will skip until the given position, and
* the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from that position.
*/
@Test
public void testSkipDataThenReadDataReturnDataFromSkippedPosition() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2},
new EventMessage[] {eventMessage1, eventMessage2});
EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
// first read - read format
readData(sampleStream);
int skipped = sampleStream.skipData(presentationTimeUs2);
int result = readData(sampleStream);
assertThat(skipped).isEqualTo(1);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage2));
}
/**
* Tests that {@link EventSampleStream#seekToUs(long)} (long)} will seek to the given position,
* and the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from that position.
*/
@Test
public void testSeekToUsThenReadDataReturnDataFromSeekPosition() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2},
new EventMessage[] {eventMessage1, eventMessage2});
EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
// first read - read format
readData(sampleStream);
sampleStream.seekToUs(presentationTimeUs2);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage2));
}
/**
* Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
* underlying event stream, but keep the read timestamp, so the next
* {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from after the last read sample timestamp.
*/
@Test
public void testUpdateEventStreamContinueToReadAfterLastReadSamplePresentationTime() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
long presentationTimeUs3 = 3000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3);
EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2},
new EventMessage[] {eventMessage1, eventMessage2});
EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
// first read - read format
readData(sampleStream);
// read first and second sample.
readData(sampleStream);
readData(sampleStream);
sampleStream.updateEventStream(eventStream2, true);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage3));
}
/**
* Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
* underlying event stream, but keep the timestamp the stream has skipped to, so the next
* {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from the skipped position.
*/
@Test
public void testSkipDataThenUpdateStreamContinueToReadFromSkippedPosition() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
long presentationTimeUs3 = 3000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3);
EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2},
new EventMessage[] {eventMessage1, eventMessage2});
EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
// first read - read format
readData(sampleStream);
sampleStream.skipData(presentationTimeUs2 + 1);
sampleStream.updateEventStream(eventStream2, true);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage3));
}
/**
* Tests that {@link EventSampleStream#skipData(long)} will only skip to the point right after
* it last event. A following {@link EventSampleStream#updateEventStream(EventStream, boolean)}
* will update the underlying event stream and keep the timestamp the stream has skipped to, so
* the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from the skipped position.
*/
@Test
public void testSkipDataThenUpdateStreamContinueToReadDoNotSkippedMoreThanAvailable() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
long presentationTimeUs3 = 3000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3);
EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1},
new EventMessage[] {eventMessage1});
EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
// first read - read format
readData(sampleStream);
// even though the skip call is to 2000001, since eventStream1 only contains sample until
// 1000000, it will only skip to 1000001.
sampleStream.skipData(presentationTimeUs2 + 1);
sampleStream.updateEventStream(eventStream2, true);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage2));
}
/**
* Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
* underlying event stream, but keep the timestamp the stream has seek to, so the next
* {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from the seek position.
*/
@Test
public void testSeekToUsThenUpdateStreamContinueToReadFromSeekPosition() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
long presentationTimeUs3 = 3000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3);
EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2},
new EventMessage[] {eventMessage1, eventMessage2});
EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
// first read - read format
readData(sampleStream);
sampleStream.seekToUs(presentationTimeUs2);
sampleStream.updateEventStream(eventStream2, true);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage2));
}
/**
* Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
* underlying event stream, but keep the timestamp the stream has seek to, so the next
* {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call
* will return sample data from the seek position.
*/
@Test
public void testSeekToThenUpdateStreamContinueToReadFromSeekPositionEvenSeekMoreThanAvailable() {
long presentationTimeUs1 = 1000000;
long presentationTimeUs2 = 2000000;
long presentationTimeUs3 = 3000000;
EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1);
EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2);
EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3);
EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1},
new EventMessage[] {eventMessage1});
EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,
new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
// first read - read format
readData(sampleStream);
sampleStream.seekToUs(presentationTimeUs2 + 1);
sampleStream.updateEventStream(eventStream2, true);
int result = readData(sampleStream);
assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
assertThat(inputBuffer.data.array())
.isEqualTo(getEncodedMessage(eventMessage3));
}
private int readData(EventSampleStream sampleStream) {
inputBuffer.clear();
return sampleStream.readData(formatHolder, inputBuffer, false);
}
private EventMessage newEventMessageWithIdAndTime(int id, long presentationTimeUs) {
return new EventMessage(SCHEME_ID, VALUE, DURATION_MS, id, MESSAGE_DATA, presentationTimeUs);
}
private byte[] getEncodedMessage(EventMessage eventMessage) {
return eventMessageEncoder.encode(eventMessage, TIME_SCALE);
}
}
...@@ -18,39 +18,47 @@ package com.google.android.exoplayer2.source.dash.manifest; ...@@ -18,39 +18,47 @@ package com.google.android.exoplayer2.source.dash.manifest;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** /** Unit tests for {@link DashManifestParser}. */
* Unit tests for {@link DashManifestParser}. @RunWith(RobolectricTestRunner.class)
*/ public class DashManifestParserTest {
public class DashManifestParserTest extends InstrumentationTestCase {
private static final String SAMPLE_MPD_1 = "sample_mpd_1"; private static final String SAMPLE_MPD_1 = "sample_mpd_1";
private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type"; private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type";
private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template"; private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template";
private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream"; private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream";
/** /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */
* Simple test to ensure the sample manifests parse without any exceptions being thrown. @Test
*/
public void testParseMediaPresentationDescription() throws IOException { public void testParseMediaPresentationDescription() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
parser.parse(Uri.parse("https://example.com/test.mpd"), parser.parse(
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_1)); Uri.parse("https://example.com/test.mpd"),
parser.parse(Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_1));
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); parser.parse(
Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_2_UNKNOWN_MIME_TYPE));
} }
@Test
public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException { public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"), DashManifest mpd =
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_3_SEGMENT_TEMPLATE)); parser.parse(
Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_3_SEGMENT_TEMPLATE));
assertThat(mpd.getPeriodCount()).isEqualTo(1); assertThat(mpd.getPeriodCount()).isEqualTo(1);
...@@ -75,11 +83,13 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -75,11 +83,13 @@ public class DashManifestParserTest extends InstrumentationTestCase {
} }
} }
public void testParseMediaPresentationDescriptionCanParseEventStream() @Test
throws IOException { public void testParseMediaPresentationDescriptionCanParseEventStream() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"), DashManifest mpd =
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_4_EVENT_STREAM)); parser.parse(
Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_4_EVENT_STREAM));
Period period = mpd.getPeriod(0); Period period = mpd.getPeriod(0);
assertThat(period.eventStreams).hasSize(3); assertThat(period.eventStreams).hasSize(3);
...@@ -87,8 +97,14 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -87,8 +97,14 @@ public class DashManifestParserTest extends InstrumentationTestCase {
// assert text-only event stream // assert text-only event stream
EventStream eventStream1 = period.eventStreams.get(0); EventStream eventStream1 = period.eventStreams.get(0);
assertThat(eventStream1.events.length).isEqualTo(1); assertThat(eventStream1.events.length).isEqualTo(1);
EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0, EventMessage expectedEvent1 =
"+ 1 800 10101010".getBytes(), 0); new EventMessage(
"urn:uuid:XYZY",
"call",
10000,
0,
"+ 1 800 10101010".getBytes(Charset.forName(C.UTF8_NAME)),
0);
assertThat(eventStream1.events[0]).isEqualTo(expectedEvent1); assertThat(eventStream1.events[0]).isEqualTo(expectedEvent1);
// assert CData-structured event stream // assert CData-structured event stream
...@@ -135,6 +151,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -135,6 +151,7 @@ public class DashManifestParserTest extends InstrumentationTestCase {
1000000000)); 1000000000));
} }
@Test
public void testParseCea608AccessibilityChannel() { public void testParseCea608AccessibilityChannel() {
assertThat( assertThat(
DashManifestParser.parseCea608AccessibilityChannel( DashManifestParser.parseCea608AccessibilityChannel(
...@@ -175,6 +192,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -175,6 +192,7 @@ public class DashManifestParserTest extends InstrumentationTestCase {
.isEqualTo(Format.NO_VALUE); .isEqualTo(Format.NO_VALUE);
} }
@Test
public void testParseCea708AccessibilityChannel() { public void testParseCea708AccessibilityChannel() {
assertThat( assertThat(
DashManifestParser.parseCea708AccessibilityChannel( DashManifestParser.parseCea708AccessibilityChannel(
...@@ -226,5 +244,4 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -226,5 +244,4 @@ public class DashManifestParserTest extends InstrumentationTestCase {
private static List<Descriptor> buildCea708AccessibilityDescriptors(String value) { private static List<Descriptor> buildCea708AccessibilityDescriptors(String value) {
return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null)); return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null));
} }
} }
...@@ -24,39 +24,48 @@ import java.util.Arrays; ...@@ -24,39 +24,48 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Unit tests for {@link DashManifest}. */
* Unit tests for {@link DashManifest}. @RunWith(RobolectricTestRunner.class)
*/ public class DashManifestTest {
public class DashManifestTest extends TestCase {
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
@Test
public void testCopy() throws Exception { public void testCopy() throws Exception {
Representation[][][] representations = newRepresentations(3, 2, 3); Representation[][][] representations = newRepresentations(3, 2, 3);
DashManifest sourceManifest = newDashManifest(10, DashManifest sourceManifest =
newPeriod("1", 1, newDashManifest(
10,
newPeriod(
"1",
1,
newAdaptationSet(2, representations[0][0]), newAdaptationSet(2, representations[0][0]),
newAdaptationSet(3, representations[0][1])), newAdaptationSet(3, representations[0][1])),
newPeriod("4", 4, newPeriod(
"4",
4,
newAdaptationSet(5, representations[1][0]), newAdaptationSet(5, representations[1][0]),
newAdaptationSet(6, representations[1][1])), newAdaptationSet(6, representations[1][1])),
newPeriod("7", 7, newPeriod(
"7",
7,
newAdaptationSet(8, representations[2][0]), newAdaptationSet(8, representations[2][0]),
newAdaptationSet(9, representations[2][1]))); newAdaptationSet(9, representations[2][1])));
List<RepresentationKey> keys = Arrays.asList( List<RepresentationKey> keys =
Arrays.asList(
new RepresentationKey(0, 0, 0), new RepresentationKey(0, 0, 0),
new RepresentationKey(0, 0, 1), new RepresentationKey(0, 0, 1),
new RepresentationKey(0, 1, 2), new RepresentationKey(0, 1, 2),
new RepresentationKey(1, 0, 1), new RepresentationKey(1, 0, 1),
new RepresentationKey(1, 1, 0), new RepresentationKey(1, 1, 0),
new RepresentationKey(1, 1, 2), new RepresentationKey(1, 1, 2),
new RepresentationKey(2, 0, 1), new RepresentationKey(2, 0, 1),
new RepresentationKey(2, 0, 2), new RepresentationKey(2, 0, 2),
new RepresentationKey(2, 1, 0)); new RepresentationKey(2, 1, 0));
...@@ -65,66 +74,91 @@ public class DashManifestTest extends TestCase { ...@@ -65,66 +74,91 @@ public class DashManifestTest extends TestCase {
DashManifest copyManifest = sourceManifest.copy(keys); DashManifest copyManifest = sourceManifest.copy(keys);
DashManifest expectedManifest = newDashManifest(10, DashManifest expectedManifest =
newPeriod("1", 1, newDashManifest(
10,
newPeriod(
"1",
1,
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
newAdaptationSet(3, representations[0][1][2])), newAdaptationSet(3, representations[0][1][2])),
newPeriod("4", 4, newPeriod(
"4",
4,
newAdaptationSet(5, representations[1][0][1]), newAdaptationSet(5, representations[1][0][1]),
newAdaptationSet(6, representations[1][1][0], representations[1][1][2])), newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),
newPeriod("7", 7, newPeriod(
"7",
7,
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
newAdaptationSet(9, representations[2][1][0]))); newAdaptationSet(9, representations[2][1][0])));
assertManifestEquals(expectedManifest, copyManifest); assertManifestEquals(expectedManifest, copyManifest);
} }
@Test
public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception { public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception {
Representation[][][] representations = newRepresentations(2, 1, 1); Representation[][][] representations = newRepresentations(2, 1, 1);
DashManifest sourceManifest = newDashManifest(10, DashManifest sourceManifest =
newPeriod("1", 1, newDashManifest(
newAdaptationSet(2, representations[0][0])), 10,
newPeriod("4", 4, newPeriod("1", 1, newAdaptationSet(2, representations[0][0])),
newAdaptationSet(5, representations[1][0]))); newPeriod("4", 4, newAdaptationSet(5, representations[1][0])));
DashManifest copyManifest = sourceManifest.copy(Arrays.asList( DashManifest copyManifest =
new RepresentationKey(0, 0, 0), sourceManifest.copy(
new RepresentationKey(1, 0, 0))); Arrays.asList(new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)));
DashManifest expectedManifest = newDashManifest(10, DashManifest expectedManifest =
newPeriod("1", 1, newDashManifest(
newAdaptationSet(2, representations[0][0])), 10,
newPeriod("4", 4, newPeriod("1", 1, newAdaptationSet(2, representations[0][0])),
newAdaptationSet(5, representations[1][0]))); newPeriod("4", 4, newAdaptationSet(5, representations[1][0])));
assertManifestEquals(expectedManifest, copyManifest); assertManifestEquals(expectedManifest, copyManifest);
} }
@Test
public void testCopySkipPeriod() throws Exception { public void testCopySkipPeriod() throws Exception {
Representation[][][] representations = newRepresentations(3, 2, 3); Representation[][][] representations = newRepresentations(3, 2, 3);
DashManifest sourceManifest = newDashManifest(10, DashManifest sourceManifest =
newPeriod("1", 1, newDashManifest(
10,
newPeriod(
"1",
1,
newAdaptationSet(2, representations[0][0]), newAdaptationSet(2, representations[0][0]),
newAdaptationSet(3, representations[0][1])), newAdaptationSet(3, representations[0][1])),
newPeriod("4", 4, newPeriod(
"4",
4,
newAdaptationSet(5, representations[1][0]), newAdaptationSet(5, representations[1][0]),
newAdaptationSet(6, representations[1][1])), newAdaptationSet(6, representations[1][1])),
newPeriod("7", 7, newPeriod(
"7",
7,
newAdaptationSet(8, representations[2][0]), newAdaptationSet(8, representations[2][0]),
newAdaptationSet(9, representations[2][1]))); newAdaptationSet(9, representations[2][1])));
DashManifest copyManifest = sourceManifest.copy(Arrays.asList( DashManifest copyManifest =
sourceManifest.copy(
Arrays.asList(
new RepresentationKey(0, 0, 0), new RepresentationKey(0, 0, 0),
new RepresentationKey(0, 0, 1), new RepresentationKey(0, 0, 1),
new RepresentationKey(0, 1, 2), new RepresentationKey(0, 1, 2),
new RepresentationKey(2, 0, 1), new RepresentationKey(2, 0, 1),
new RepresentationKey(2, 0, 2), new RepresentationKey(2, 0, 2),
new RepresentationKey(2, 1, 0))); new RepresentationKey(2, 1, 0)));
DashManifest expectedManifest = newDashManifest(7, DashManifest expectedManifest =
newPeriod("1", 1, newDashManifest(
7,
newPeriod(
"1",
1,
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
newAdaptationSet(3, representations[0][1][2])), newAdaptationSet(3, representations[0][1][2])),
newPeriod("7", 4, newPeriod(
"7",
4,
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
newAdaptationSet(9, representations[2][1][0]))); newAdaptationSet(9, representations[2][1][0])));
assertManifestEquals(expectedManifest, copyManifest); assertManifestEquals(expectedManifest, copyManifest);
...@@ -164,8 +198,8 @@ public class DashManifestTest extends TestCase { ...@@ -164,8 +198,8 @@ public class DashManifestTest extends TestCase {
} }
} }
private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts, private static Representation[][][] newRepresentations(
int representationCounts) { int periodCount, int adaptationSetCounts, int representationCounts) {
Representation[][][] representations = new Representation[periodCount][][]; Representation[][][] representations = new Representation[periodCount][][];
for (int i = 0; i < periodCount; i++) { for (int i = 0; i < periodCount; i++) {
representations[i] = new Representation[adaptationSetCounts][]; representations[i] = new Representation[adaptationSetCounts][];
...@@ -184,8 +218,8 @@ public class DashManifestTest extends TestCase { ...@@ -184,8 +218,8 @@ public class DashManifestTest extends TestCase {
} }
private static DashManifest newDashManifest(int duration, Period... periods) { private static DashManifest newDashManifest(int duration, Period... periods) {
return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, return new DashManifest(
Arrays.asList(periods)); 0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, Arrays.asList(periods));
} }
private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) { private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) {
...@@ -195,5 +229,4 @@ public class DashManifestTest extends TestCase { ...@@ -195,5 +229,4 @@ public class DashManifestTest extends TestCase {
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null); return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null);
} }
} }
...@@ -18,17 +18,19 @@ package com.google.android.exoplayer2.source.dash.manifest; ...@@ -18,17 +18,19 @@ package com.google.android.exoplayer2.source.dash.manifest;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Unit test for {@link RangedUri}. */
* Unit test for {@link RangedUri}. @RunWith(RobolectricTestRunner.class)
*/ public class RangedUriTest {
public class RangedUriTest extends TestCase {
private static final String BASE_URI = "http://www.test.com/"; private static final String BASE_URI = "http://www.test.com/";
private static final String PARTIAL_URI = "path/file.ext"; private static final String PARTIAL_URI = "path/file.ext";
private static final String FULL_URI = BASE_URI + PARTIAL_URI; private static final String FULL_URI = BASE_URI + PARTIAL_URI;
@Test
public void testMerge() { public void testMerge() {
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
...@@ -36,6 +38,7 @@ public class RangedUriTest extends TestCase { ...@@ -36,6 +38,7 @@ public class RangedUriTest extends TestCase {
assertMerge(rangeA, rangeB, expected, null); assertMerge(rangeA, rangeB, expected, null);
} }
@Test
public void testMergeUnbounded() { public void testMergeUnbounded() {
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET); RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);
...@@ -43,6 +46,7 @@ public class RangedUriTest extends TestCase { ...@@ -43,6 +46,7 @@ public class RangedUriTest extends TestCase {
assertMerge(rangeA, rangeB, expected, null); assertMerge(rangeA, rangeB, expected, null);
} }
@Test
public void testNonMerge() { public void testNonMerge() {
// A and B do not overlap, so should not merge // A and B do not overlap, so should not merge
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
...@@ -65,6 +69,7 @@ public class RangedUriTest extends TestCase { ...@@ -65,6 +69,7 @@ public class RangedUriTest extends TestCase {
assertNonMerge(rangeA, rangeB, null); assertNonMerge(rangeA, rangeB, null);
} }
@Test
public void testMergeWithBaseUri() { public void testMergeWithBaseUri() {
RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10); RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
...@@ -85,5 +90,4 @@ public class RangedUriTest extends TestCase { ...@@ -85,5 +90,4 @@ public class RangedUriTest extends TestCase {
merged = rangeB.attemptMerge(rangeA, baseUrl); merged = rangeB.attemptMerge(rangeA, baseUrl);
assertThat(merged).isNull(); assertThat(merged).isNull();
} }
} }
...@@ -20,27 +20,49 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -20,27 +20,49 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Unit test for {@link Representation}. */
* Unit test for {@link Representation}. @RunWith(RobolectricTestRunner.class)
*/ public class RepresentationTest {
public class RepresentationTest extends TestCase {
@Test
public void testGetCacheKey() { public void testGetCacheKey() {
String uri = "http://www.google.com"; String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1); SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null, Format format =
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0); Format.createVideoContainerFormat(
Representation representation = Representation.newInstance("test_stream_1", 3, format, uri, "0",
base); MimeTypes.APPLICATION_MP4,
null,
MimeTypes.VIDEO_H264,
2500000,
1920,
1080,
Format.NO_VALUE,
null,
0);
Representation representation =
Representation.newInstance("test_stream_1", 3, format, uri, base);
assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.0.3"); assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.0.3");
format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null, format =
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0); Format.createVideoContainerFormat(
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT, "150",
format, uri, base); MimeTypes.APPLICATION_MP4,
null,
MimeTypes.VIDEO_H264,
2500000,
1920,
1080,
Format.NO_VALUE,
null,
0);
representation =
Representation.newInstance(
"test_stream_1", Representation.REVISION_ID_DEFAULT, format, uri, base);
assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.150.-1"); assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.150.-1");
} }
} }
...@@ -16,14 +16,17 @@ ...@@ -16,14 +16,17 @@
package com.google.android.exoplayer2.source.dash.manifest; package com.google.android.exoplayer2.source.dash.manifest;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Unit test for {@link UrlTemplate}. */
* Unit test for {@link UrlTemplate}. @RunWith(RobolectricTestRunner.class)
*/ public class UrlTemplateTest {
public class UrlTemplateTest extends TestCase {
@Test
public void testRealExamples() { public void testRealExamples() {
String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)"; String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)";
UrlTemplate urlTemplate = UrlTemplate.compile(template); UrlTemplate urlTemplate = UrlTemplate.compile(template);
...@@ -41,6 +44,7 @@ public class UrlTemplateTest extends TestCase { ...@@ -41,6 +44,7 @@ public class UrlTemplateTest extends TestCase {
assertThat(url).isEqualTo("chunk_ctvideo_cfm4s_ridabc1_cn10_w2073857842_mpd.m4s"); assertThat(url).isEqualTo("chunk_ctvideo_cfm4s_ridabc1_cn10_w2073857842_mpd.m4s");
} }
@Test
public void testFull() { public void testFull() {
String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$"; String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$";
UrlTemplate urlTemplate = UrlTemplate.compile(template); UrlTemplate urlTemplate = UrlTemplate.compile(template);
...@@ -48,6 +52,7 @@ public class UrlTemplateTest extends TestCase { ...@@ -48,6 +52,7 @@ public class UrlTemplateTest extends TestCase {
assertThat(url).isEqualTo("650000_a_abc1_b_5000_c_10"); assertThat(url).isEqualTo("650000_a_abc1_b_5000_c_10");
} }
@Test
public void testFullWithDollarEscaping() { public void testFullWithDollarEscaping() {
String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$"; String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$";
UrlTemplate urlTemplate = UrlTemplate.compile(template); UrlTemplate urlTemplate = UrlTemplate.compile(template);
...@@ -55,6 +60,7 @@ public class UrlTemplateTest extends TestCase { ...@@ -55,6 +60,7 @@ public class UrlTemplateTest extends TestCase {
assertThat(url).isEqualTo("$650000$_a$_abc1_b_5000_c_10$"); assertThat(url).isEqualTo("$650000$_a$_abc1_b_5000_c_10$");
} }
@Test
public void testInvalidSubstitution() { public void testInvalidSubstitution() {
String template = "$IllegalId$"; String template = "$IllegalId$";
try { try {
...@@ -64,5 +70,4 @@ public class UrlTemplateTest extends TestCase { ...@@ -64,5 +70,4 @@ public class UrlTemplateTest extends TestCase {
// Expected. // Expected.
} }
} }
} }
...@@ -16,10 +16,10 @@ ...@@ -16,10 +16,10 @@
package com.google.android.exoplayer2.source.dash.offline; package com.google.android.exoplayer2.source.dash.offline;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C;
import java.nio.charset.Charset;
/** /** Data for DASH downloading tests. */
* Data for DASH downloading tests.
*/
/* package */ interface DashDownloadTestData { /* package */ interface DashDownloadTestData {
Uri TEST_MPD_URI = Uri.parse("test.mpd"); Uri TEST_MPD_URI = Uri.parse("test.mpd");
...@@ -40,7 +40,8 @@ import android.net.Uri; ...@@ -40,7 +40,8 @@ import android.net.Uri;
+ " <Representation>\n" + " <Representation>\n"
+ " <SegmentList>\n" + " <SegmentList>\n"
// Bounded range data // Bounded range data
+ " <Initialization range=\"0-9\" sourceURL=\"audio_init_data\" />\n" + " <Initialization\n"
+ " range=\"0-9\" sourceURL=\"audio_init_data\" />\n"
// Unbounded range data // Unbounded range data
+ " <SegmentURL media=\"audio_segment_1\" />\n" + " <SegmentURL media=\"audio_segment_1\" />\n"
+ " <SegmentURL media=\"audio_segment_2\" />\n" + " <SegmentURL media=\"audio_segment_2\" />\n"
...@@ -84,7 +85,8 @@ import android.net.Uri; ...@@ -84,7 +85,8 @@ import android.net.Uri;
+ " </Representation>\n" + " </Representation>\n"
+ " </AdaptationSet>\n" + " </AdaptationSet>\n"
+ " </Period>\n" + " </Period>\n"
+ "</MPD>").getBytes(); + "</MPD>")
.getBytes(Charset.forName(C.UTF8_NAME));
byte[] TEST_MPD_NO_INDEX = byte[] TEST_MPD_NO_INDEX =
("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
...@@ -96,5 +98,6 @@ import android.net.Uri; ...@@ -96,5 +98,6 @@ import android.net.Uri;
+ " </Representation>\n" + " </Representation>\n"
+ " </AdaptationSet>\n" + " </AdaptationSet>\n"
+ " </Period>\n" + " </Period>\n"
+ "</MPD>").getBytes(); + "</MPD>")
.getBytes(Charset.forName(C.UTF8_NAME));
} }
...@@ -22,10 +22,10 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmp ...@@ -22,10 +22,10 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmp
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.DownloadException;
import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.Downloader.ProgressListener;
...@@ -35,7 +35,6 @@ import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; ...@@ -35,7 +35,6 @@ import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
...@@ -44,34 +43,38 @@ import com.google.android.exoplayer2.util.Util; ...@@ -44,34 +43,38 @@ import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** /** Unit tests for {@link DashDownloader}. */
* Unit tests for {@link DashDownloader}. @RunWith(RobolectricTestRunner.class)
*/ public class DashDownloaderTest {
public class DashDownloaderTest extends InstrumentationTestCase {
private SimpleCache cache; private SimpleCache cache;
private File tempFolder; private File tempFolder;
@Override @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); MockitoAnnotations.initMocks(this);
MockitoUtil.setUpMockito(this); tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
} }
@Override @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
Util.recursiveDelete(tempFolder); Util.recursiveDelete(tempFolder);
super.tearDown();
} }
@Test
public void testGetManifest() throws Exception { public void testGetManifest() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet = new FakeDataSet().setData(TEST_MPD_URI, TEST_MPD);
.setData(TEST_MPD_URI, TEST_MPD);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet); DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
DashManifest manifest = dashDownloader.getManifest(); DashManifest manifest = dashDownloader.getManifest();
...@@ -80,10 +83,12 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -80,10 +83,12 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testDownloadManifestFailure() throws Exception { public void testDownloadManifestFailure() throws Exception {
byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10); byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10);
byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length); byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length);
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.newData(TEST_MPD_URI) .newData(TEST_MPD_URI)
.appendReadData(testMpdFirstPart) .appendReadData(testMpdFirstPart)
.appendReadError(new IOException()) .appendReadError(new IOException())
...@@ -108,8 +113,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -108,8 +113,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testDownloadRepresentation() throws Exception { public void testDownloadRepresentation() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -123,8 +130,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -123,8 +130,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testDownloadRepresentationInSmallParts() throws Exception { public void testDownloadRepresentationInSmallParts() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.newData("audio_segment_1") .newData("audio_segment_1")
...@@ -142,8 +151,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -142,8 +151,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testDownloadRepresentations() throws Exception { public void testDownloadRepresentations() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -161,8 +172,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -161,8 +172,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testDownloadAllRepresentations() throws Exception { public void testDownloadAllRepresentations() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -195,8 +208,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -195,8 +208,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
dashDownloader.remove(); dashDownloader.remove();
} }
@Test
public void testProgressiveDownload() throws Exception { public void testProgressiveDownload() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -208,8 +223,8 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -208,8 +223,8 @@ public class DashDownloaderTest extends InstrumentationTestCase {
FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
Factory factory = mock(Factory.class); Factory factory = mock(Factory.class);
when(factory.createDataSource()).thenReturn(fakeDataSource); when(factory.createDataSource()).thenReturn(fakeDataSource);
DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, DashDownloader dashDownloader =
new DownloaderConstructorHelper(cache, factory)); new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
dashDownloader.selectRepresentations( dashDownloader.selectRepresentations(
new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)});
...@@ -227,8 +242,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -227,8 +242,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo("text_segment_3"); assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo("text_segment_3");
} }
@Test
public void testProgressiveDownloadSeparatePeriods() throws Exception { public void testProgressiveDownloadSeparatePeriods() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -240,8 +257,8 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -240,8 +257,8 @@ public class DashDownloaderTest extends InstrumentationTestCase {
FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
Factory factory = mock(Factory.class); Factory factory = mock(Factory.class);
when(factory.createDataSource()).thenReturn(fakeDataSource); when(factory.createDataSource()).thenReturn(fakeDataSource);
DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, DashDownloader dashDownloader =
new DownloaderConstructorHelper(cache, factory)); new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
dashDownloader.selectRepresentations( dashDownloader.selectRepresentations(
new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)}); new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)});
...@@ -259,8 +276,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -259,8 +276,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo("period_2_segment_3"); assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo("period_2_segment_3");
} }
@Test
public void testDownloadRepresentationFailure() throws Exception { public void testDownloadRepresentationFailure() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -285,8 +304,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -285,8 +304,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testCounters() throws Exception { public void testCounters() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -319,8 +340,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -319,8 +340,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6); assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6);
} }
@Test
public void testListener() throws Exception { public void testListener() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -340,8 +363,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -340,8 +363,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
inOrder.verifyNoMoreInteractions(); inOrder.verifyNoMoreInteractions();
} }
@Test
public void testRemoveAll() throws Exception { public void testRemoveAll() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -360,8 +385,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -360,8 +385,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCacheEmpty(cache); assertCacheEmpty(cache);
} }
@Test
public void testRepresentationWithoutIndex() throws Exception { public void testRepresentationWithoutIndex() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)
.setRandomData("test_segment_1", 4); .setRandomData("test_segment_1", 4);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet); DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
...@@ -379,8 +406,10 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -379,8 +406,10 @@ public class DashDownloaderTest extends InstrumentationTestCase {
assertCacheEmpty(cache); assertCacheEmpty(cache);
} }
@Test
public void testSelectRepresentationsClearsPreviousSelection() throws Exception { public void testSelectRepresentationsClearsPreviousSelection() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet() FakeDataSet fakeDataSet =
new FakeDataSet()
.setData(TEST_MPD_URI, TEST_MPD) .setData(TEST_MPD_URI, TEST_MPD)
.setRandomData("audio_init_data", 10) .setRandomData("audio_init_data", 10)
.setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_1", 4)
...@@ -401,11 +430,13 @@ public class DashDownloaderTest extends InstrumentationTestCase { ...@@ -401,11 +430,13 @@ public class DashDownloaderTest extends InstrumentationTestCase {
return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
} }
private static void assertCounters(DashDownloader dashDownloader, int totalSegments, private static void assertCounters(
int downloadedSegments, int downloadedBytes) { DashDownloader dashDownloader,
int totalSegments,
int downloadedSegments,
int downloadedBytes) {
assertThat(dashDownloader.getTotalSegments()).isEqualTo(totalSegments); assertThat(dashDownloader.getTotalSegments()).isEqualTo(totalSegments);
assertThat(dashDownloader.getDownloadedSegments()).isEqualTo(downloadedSegments); assertThat(dashDownloader.getDownloadedSegments()).isEqualTo(downloadedSegments);
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(downloadedBytes); assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(downloadedBytes);
} }
} }
...@@ -33,12 +33,9 @@ android { ...@@ -33,12 +33,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compile 'com.android.support:support-annotations:' + supportLibraryVersion implementation project(modulePrefix + 'library-core')
androidTestCompile project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
} }
ext { ext {
......
...@@ -261,9 +261,13 @@ import java.util.List; ...@@ -261,9 +261,13 @@ import java.util.List;
// If the playlist is too old to contain the chunk, we need to refresh it. // If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
} else { } else {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, chunkMediaSequence =
targetPositionUs - mediaPlaylist.startTimeUs, true, Util.binarySearchFloor(
!playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; mediaPlaylist.segments,
targetPositionUs,
/* inclusive= */ true,
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ mediaPlaylist.mediaSequence;
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
// We try getting the next chunk without adapting in case that's the reason for falling // We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window. // behind the live window.
...@@ -320,7 +324,9 @@ import java.util.List; ...@@ -320,7 +324,9 @@ import java.util.List;
} }
// Compute start time of the next chunk. // Compute start time of the next chunk.
long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; long offsetFromInitialStartTimeUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs;
int discontinuitySequence = mediaPlaylist.discontinuitySequence int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ segment.relativeDiscontinuitySequence; + segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
......
...@@ -366,28 +366,50 @@ public final class HlsMediaSource implements MediaSource, ...@@ -366,28 +366,50 @@ public final class HlsMediaSource implements MediaSource,
@Override @Override
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
SinglePeriodTimeline timeline; SinglePeriodTimeline timeline;
long presentationStartTimeMs = playlist.hasProgramDateTime ? 0 : C.TIME_UNSET;
long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs)
: C.TIME_UNSET; : C.TIME_UNSET;
// For playlist types EVENT and VOD we know segments are never removed, so the presentation
// started at the same time as the window. Otherwise, we don't know the presentation start time.
long presentationStartTimeMs =
playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|| playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
? windowStartTimeMs
: C.TIME_UNSET;
long windowDefaultStartPositionUs = playlist.startOffsetUs; long windowDefaultStartPositionUs = playlist.startOffsetUs;
if (playlistTracker.isLive()) { if (playlistTracker.isLive()) {
long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs) long offsetFromInitialStartTimeUs =
: C.TIME_UNSET; playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long periodDurationUs =
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
List<HlsMediaPlaylist.Segment> segments = playlist.segments; List<HlsMediaPlaylist.Segment> segments = playlist.segments;
if (windowDefaultStartPositionUs == C.TIME_UNSET) { if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = segments.isEmpty() ? 0 windowDefaultStartPositionUs = segments.isEmpty() ? 0
: segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;
} }
timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs, timeline =
periodDurationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, new SinglePeriodTimeline(
true, !playlist.hasEndTag); presentationStartTimeMs,
windowStartTimeMs,
periodDurationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ !playlist.hasEndTag);
} else /* not live */ { } else /* not live */ {
if (windowDefaultStartPositionUs == C.TIME_UNSET) { if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0; windowDefaultStartPositionUs = 0;
} }
timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs, timeline =
playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs, new SinglePeriodTimeline(
windowDefaultStartPositionUs, true, false); presentationStartTimeMs,
windowStartTimeMs,
/* periodDurationUs= */ playlist.durationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ 0,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ false);
} }
sourceListener.onSourceInfoRefreshed(this, timeline, sourceListener.onSourceInfoRefreshed(this, timeline,
new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); new HlsManifest(playlistTracker.getMasterPlaylist(), playlist));
......
...@@ -83,7 +83,6 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -83,7 +83,6 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
* @param mediaPlaylist The primary playlist new snapshot. * @param mediaPlaylist The primary playlist new snapshot.
*/ */
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist); void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
} }
/** /**
...@@ -128,6 +127,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -128,6 +127,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
private HlsUrl primaryHlsUrl; private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot; private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive; private boolean isLive;
private long initialStartTimeUs;
/** /**
* @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media
...@@ -153,6 +153,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -153,6 +153,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
playlistBundles = new IdentityHashMap<>(); playlistBundles = new IdentityHashMap<>();
playlistRefreshHandler = new Handler(); playlistRefreshHandler = new Handler();
initialStartTimeUs = C.TIME_UNSET;
} }
/** /**
...@@ -208,6 +209,11 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -208,6 +209,11 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return snapshot; return snapshot;
} }
/** Returns the start time of the first loaded primary playlist. */
public long getInitialStartTimeUs() {
return initialStartTimeUs;
}
/** /**
* Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
* valid, meaning all the segments referenced by the playlist are expected to be available. If the * valid, meaning all the segments referenced by the playlist are expected to be available. If the
...@@ -371,6 +377,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -371,6 +377,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
if (primaryUrlSnapshot == null) { if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot. // This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag; isLive = !newSnapshot.hasEndTag;
initialStartTimeUs = newSnapshot.startTimeUs;
} }
primaryUrlSnapshot = newSnapshot; primaryUrlSnapshot = newSnapshot;
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
......
...@@ -18,16 +18,6 @@ ...@@ -18,16 +18,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.source.hls.test"> package="com.google.android.exoplayer2.source.hls.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer2.source.hls.test"
android:name="android.test.InstrumentationTestRunner"/>
</manifest> </manifest>
...@@ -15,9 +15,10 @@ ...@@ -15,9 +15,10 @@
*/ */
package com.google.android.exoplayer2.source.hls.offline; package com.google.android.exoplayer2.source.hls.offline;
/** import com.google.android.exoplayer2.C;
* Data for HLS downloading tests. import java.nio.charset.Charset;
*/
/** Data for HLS downloading tests. */
/* package */ interface HlsDownloadTestData { /* package */ interface HlsDownloadTestData {
String MASTER_PLAYLIST_URI = "test.m3u8"; String MASTER_PLAYLIST_URI = "test.m3u8";
...@@ -34,13 +35,17 @@ package com.google.android.exoplayer2.source.hls.offline; ...@@ -34,13 +35,17 @@ package com.google.android.exoplayer2.source.hls.offline;
byte[] MASTER_PLAYLIST_DATA = byte[] MASTER_PLAYLIST_DATA =
("#EXTM3U\n" ("#EXTM3U\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n"
+ MEDIA_PLAYLIST_1_URI + "\n" + MEDIA_PLAYLIST_1_URI
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
+ MEDIA_PLAYLIST_2_URI + "\n" + MEDIA_PLAYLIST_2_URI
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
+ MEDIA_PLAYLIST_3_URI + "\n" + MEDIA_PLAYLIST_3_URI
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n"
+ MEDIA_PLAYLIST_0_URI).getBytes(); + MEDIA_PLAYLIST_0_URI)
.getBytes(Charset.forName(C.UTF8_NAME));
byte[] MEDIA_PLAYLIST_DATA = byte[] MEDIA_PLAYLIST_DATA =
("#EXTM3U\n" ("#EXTM3U\n"
...@@ -54,7 +59,8 @@ package com.google.android.exoplayer2.source.hls.offline; ...@@ -54,7 +59,8 @@ package com.google.android.exoplayer2.source.hls.offline;
+ "fileSequence1.ts\n" + "fileSequence1.ts\n"
+ "#EXTINF:9.97667,\n" + "#EXTINF:9.97667,\n"
+ "fileSequence2.ts\n" + "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST").getBytes(); + "#EXT-X-ENDLIST")
.getBytes(Charset.forName(C.UTF8_NAME));
String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8"; String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8";
...@@ -72,6 +78,6 @@ package com.google.android.exoplayer2.source.hls.offline; ...@@ -72,6 +78,6 @@ package com.google.android.exoplayer2.source.hls.offline;
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n" + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n"
+ "#EXTINF:9.97667,\n" + "#EXTINF:9.97667,\n"
+ "fileSequence2.ts\n" + "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST").getBytes(); + "#EXT-X-ENDLIST")
.getBytes(Charset.forName(C.UTF8_NAME));
} }
...@@ -33,7 +33,6 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa ...@@ -33,7 +33,6 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSet;
...@@ -42,22 +41,29 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; ...@@ -42,22 +41,29 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Unit tests for {@link HlsDownloader}. */ /** Unit tests for {@link HlsDownloader}. */
public class HlsDownloaderTest extends InstrumentationTestCase { @RunWith(RobolectricTestRunner.class)
public class HlsDownloaderTest {
private SimpleCache cache; private SimpleCache cache;
private File tempFolder; private File tempFolder;
private FakeDataSet fakeDataSet; private FakeDataSet fakeDataSet;
private HlsDownloader hlsDownloader; private HlsDownloader hlsDownloader;
@Override @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
fakeDataSet = new FakeDataSet() fakeDataSet =
new FakeDataSet()
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
...@@ -70,12 +76,12 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -70,12 +76,12 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI);
} }
@Override @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
Util.recursiveDelete(tempFolder); Util.recursiveDelete(tempFolder);
super.tearDown();
} }
@Test
public void testDownloadManifest() throws Exception { public void testDownloadManifest() throws Exception {
HlsMasterPlaylist manifest = hlsDownloader.getManifest(); HlsMasterPlaylist manifest = hlsDownloader.getManifest();
...@@ -83,17 +89,23 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -83,17 +89,23 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI); assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI);
} }
@Test
public void testSelectRepresentationsClearsPreviousSelection() throws Exception { public void testSelectRepresentationsClearsPreviousSelection() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_2_URI, assertCachedData(
cache,
fakeDataSet,
MASTER_PLAYLIST_URI,
MEDIA_PLAYLIST_2_URI,
MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts"); MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts");
} }
@Test
public void testCounterMethods() throws Exception { public void testCounterMethods() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
...@@ -104,12 +116,12 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -104,12 +116,12 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
} }
@Test
public void testInitStatus() throws Exception { public void testInitStatus() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
HlsDownloader newHlsDownloader = HlsDownloader newHlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI);
getHlsDownloader(MASTER_PLAYLIST_URI);
newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
newHlsDownloader.init(); newHlsDownloader.init();
...@@ -119,16 +131,22 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -119,16 +131,22 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
} }
@Test
public void testDownloadRepresentation() throws Exception { public void testDownloadRepresentation() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_1_URI, assertCachedData(
cache,
fakeDataSet,
MASTER_PLAYLIST_URI,
MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
} }
@Test
public void testDownloadMultipleRepresentations() throws Exception { public void testDownloadMultipleRepresentations() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
...@@ -136,9 +154,11 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -136,9 +154,11 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Test
public void testDownloadAllRepresentations() throws Exception { public void testDownloadAllRepresentations() throws Exception {
// Add data for the rest of the playlists // Add data for the rest of the playlists
fakeDataSet.setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA) fakeDataSet
.setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence0.ts", 10) .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence0.ts", 10)
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence1.ts", 11) .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence1.ts", 11)
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12) .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12)
...@@ -167,6 +187,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -167,6 +187,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
hlsDownloader.remove(); hlsDownloader.remove();
} }
@Test
public void testRemoveAll() throws Exception { public void testRemoveAll() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
...@@ -175,27 +196,32 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -175,27 +196,32 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
assertCacheEmpty(cache); assertCacheEmpty(cache);
} }
@Test
public void testDownloadMediaPlaylist() throws Exception { public void testDownloadMediaPlaylist() throws Exception {
hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI); hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI);
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet, MEDIA_PLAYLIST_1_URI, assertCachedData(
cache,
fakeDataSet,
MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
} }
@Test
public void testDownloadEncMediaPlaylist() throws Exception { public void testDownloadEncMediaPlaylist() throws Exception {
fakeDataSet = new FakeDataSet() fakeDataSet =
new FakeDataSet()
.setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA) .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)
.setRandomData("enc.key", 8) .setRandomData("enc.key", 8)
.setRandomData("enc2.key", 9) .setRandomData("enc2.key", 9)
.setRandomData("fileSequence0.ts", 10) .setRandomData("fileSequence0.ts", 10)
.setRandomData("fileSequence1.ts", 11) .setRandomData("fileSequence1.ts", 11)
.setRandomData("fileSequence2.ts", 12); .setRandomData("fileSequence2.ts", 12);
hlsDownloader = hlsDownloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI);
getHlsDownloader(ENC_MEDIA_PLAYLIST_URI);
hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI}); hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI});
hlsDownloader.download(null); hlsDownloader.download(null);
...@@ -204,8 +230,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase { ...@@ -204,8 +230,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
private HlsDownloader getHlsDownloader(String mediaPlaylistUri) { private HlsDownloader getHlsDownloader(String mediaPlaylistUri) {
Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); Factory factory = new Factory(null).setFakeDataSet(fakeDataSet);
return new HlsDownloader(Uri.parse(mediaPlaylistUri), return new HlsDownloader(
new DownloaderConstructorHelper(cache, factory)); Uri.parse(mediaPlaylistUri), new DownloaderConstructorHelper(cache, factory));
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -26,18 +27,21 @@ import java.io.ByteArrayInputStream; ...@@ -26,18 +27,21 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Test for {@link HlsMasterPlaylistParserTest}. */
* Test for {@link HlsMasterPlaylistParserTest}. @RunWith(RobolectricTestRunner.class)
*/ public class HlsMasterPlaylistParserTest {
public class HlsMasterPlaylistParserTest extends TestCase {
private static final String PLAYLIST_URI = "https://example.com/test.m3u8"; private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
private static final String PLAYLIST_SIMPLE = " #EXTM3U \n" private static final String PLAYLIST_SIMPLE =
" #EXTM3U \n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n" + "http://example.com/low.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
...@@ -52,31 +56,42 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -52,31 +56,42 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8"; + "http://example.com/audio-only.m3u8";
private static final String PLAYLIST_WITH_AVG_BANDWIDTH = " #EXTM3U \n" private static final String PLAYLIST_WITH_AVG_BANDWIDTH =
" #EXTM3U \n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n" + "http://example.com/low.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000," + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000,"
+ "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n"; + "http://example.com/spaces_in_codecs.m3u8\n";
private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n" private static final String PLAYLIST_WITH_INVALID_HEADER =
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" "#EXTMU3\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"; + "http://example.com/low.m3u8\n";
private static final String PLAYLIST_WITH_CC = " #EXTM3U \n" private static final String PLAYLIST_WITH_CC =
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" " #EXTM3U \n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,"
+ "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"; + "http://example.com/low.m3u8\n";
private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n" private static final String PLAYLIST_WITHOUT_CC =
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" " #EXTM3U \n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128," + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,"
+ "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
+ "CLOSED-CAPTIONS=NONE\n" + "CLOSED-CAPTIONS=NONE\n"
+ "http://example.com/low.m3u8\n"; + "http://example.com/low.m3u8\n";
private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG = "#EXTM3U\n" private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG =
"#EXTM3U\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n"
+ "uri1.m3u8\n" + "uri1.m3u8\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n"
...@@ -90,6 +105,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -90,6 +105,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\"," + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\","
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n"; + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n";
@Test
public void testParseMasterPlaylist() throws IOException { public void testParseMasterPlaylist() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
...@@ -129,9 +145,10 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -129,9 +145,10 @@ public class HlsMasterPlaylistParserTest extends TestCase {
assertThat(variants.get(4).url).isEqualTo("http://example.com/audio-only.m3u8"); assertThat(variants.get(4).url).isEqualTo("http://example.com/audio-only.m3u8");
} }
@Test
public void testMasterPlaylistWithBandwdithAverage() throws IOException { public void testMasterPlaylistWithBandwdithAverage() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, HlsMasterPlaylist masterPlaylist =
PLAYLIST_WITH_AVG_BANDWIDTH); parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants; List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
...@@ -139,6 +156,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -139,6 +156,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
assertThat(variants.get(1).format.bitrate).isEqualTo(1270000); assertThat(variants.get(1).format.bitrate).isEqualTo(1270000);
} }
@Test
public void testPlaylistWithInvalidHeader() throws IOException { public void testPlaylistWithInvalidHeader() throws IOException {
try { try {
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
...@@ -148,6 +166,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -148,6 +166,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
} }
} }
@Test
public void testPlaylistWithClosedCaption() throws IOException { public void testPlaylistWithClosedCaption() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);
assertThat(playlist.muxedCaptionFormats).hasSize(1); assertThat(playlist.muxedCaptionFormats).hasSize(1);
...@@ -157,11 +176,13 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -157,11 +176,13 @@ public class HlsMasterPlaylistParserTest extends TestCase {
assertThat(closedCaptionFormat.language).isEqualTo("es"); assertThat(closedCaptionFormat.language).isEqualTo("es");
} }
@Test
public void testPlaylistWithoutClosedCaptions() throws IOException { public void testPlaylistWithoutClosedCaptions() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC);
assertThat(playlist.muxedCaptionFormats).isEmpty(); assertThat(playlist.muxedCaptionFormats).isEmpty();
} }
@Test
public void testCodecPropagation() throws IOException { public void testCodecPropagation() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG);
...@@ -177,9 +198,8 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -177,9 +198,8 @@ public class HlsMasterPlaylistParserTest extends TestCase {
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException { throws IOException {
Uri playlistUri = Uri.parse(uri); Uri playlistUri = Uri.parse(uri);
ByteArrayInputStream inputStream = new ByteArrayInputStream( ByteArrayInputStream inputStream =
playlistString.getBytes(Charset.forName(C.UTF8_NAME))); new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
} }
} }
...@@ -26,16 +26,19 @@ import java.io.InputStream; ...@@ -26,16 +26,19 @@ import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Test for {@link HlsMediaPlaylistParserTest}. */
* Test for {@link HlsMediaPlaylistParserTest}. @RunWith(RobolectricTestRunner.class)
*/ public class HlsMediaPlaylistParserTest {
public class HlsMediaPlaylistParserTest extends TestCase {
public void testParseMediaPlaylist() throws IOException { @Test
public void testParseMediaPlaylist() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n" String playlistString =
"#EXTM3U\n"
+ "#EXT-X-VERSION:3\n" + "#EXT-X-VERSION:3\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-START:TIME-OFFSET=-25" + "#EXT-X-START:TIME-OFFSET=-25"
...@@ -48,7 +51,8 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -48,7 +51,8 @@ public class HlsMediaPlaylistParserTest extends TestCase {
+ "#EXT-X-BYTERANGE:51370@0\n" + "#EXT-X-BYTERANGE:51370@0\n"
+ "https://priv.example.com/fileSequence2679.ts\n" + "https://priv.example.com/fileSequence2679.ts\n"
+ "\n" + "\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" + "#EXT-X-KEY:METHOD=AES-128,"
+ "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
+ "#EXTINF:7.975,\n" + "#EXTINF:7.975,\n"
+ "#EXT-X-BYTERANGE:51501@2147483648\n" + "#EXT-X-BYTERANGE:51501@2147483648\n"
+ "https://priv.example.com/fileSequence2680.ts\n" + "https://priv.example.com/fileSequence2680.ts\n"
...@@ -67,8 +71,8 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -67,8 +71,8 @@ public class HlsMediaPlaylistParserTest extends TestCase {
+ "#EXTINF:7.975,\n" + "#EXTINF:7.975,\n"
+ "https://priv.example.com/fileSequence2683.ts\n" + "https://priv.example.com/fileSequence2683.ts\n"
+ "#EXT-X-ENDLIST"; + "#EXT-X-ENDLIST";
InputStream inputStream = new ByteArrayInputStream( InputStream inputStream =
playlistString.getBytes(Charset.forName(C.UTF8_NAME))); new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
...@@ -136,6 +140,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -136,6 +140,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts"); assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
} }
@Test
public void testGapTag() throws IOException { public void testGapTag() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test2.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test2.m3u8");
String playlistString = String playlistString =
...@@ -170,5 +175,4 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -170,5 +175,4 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertThat(playlist.segments.get(2).hasGapTag).isTrue(); assertThat(playlist.segments.get(2).hasGapTag).isTrue();
assertThat(playlist.segments.get(3).hasGapTag).isFalse(); assertThat(playlist.segments.get(3).hasGapTag).isFalse();
} }
} }
manifest=src/test/AndroidManifest.xml
...@@ -33,12 +33,9 @@ android { ...@@ -33,12 +33,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestCompile project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
} }
ext { ext {
......
...@@ -18,16 +18,6 @@ ...@@ -18,16 +18,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.source.smoothstreaming.test"> package="com.google.android.exoplayer2.source.smoothstreaming.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer2.source.smoothstreaming.test"
android:name="android.test.InstrumentationTestRunner"/>
</manifest> </manifest>
...@@ -16,27 +16,29 @@ ...@@ -16,27 +16,29 @@
package com.google.android.exoplayer2.source.smoothstreaming.manifest; package com.google.android.exoplayer2.source.smoothstreaming.manifest;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** /** Unit tests for {@link SsManifestParser}. */
* Unit tests for {@link SsManifestParser}. @RunWith(RobolectricTestRunner.class)
*/ public final class SsManifestParserTest {
public final class SsManifestParserTest extends InstrumentationTestCase {
private static final String SAMPLE_ISMC_1 = "sample_ismc_1"; private static final String SAMPLE_ISMC_1 = "sample_ismc_1";
private static final String SAMPLE_ISMC_2 = "sample_ismc_2"; private static final String SAMPLE_ISMC_2 = "sample_ismc_2";
/** /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */
* Simple test to ensure the sample manifests parse without any exceptions being thrown. @Test
*/
public void testParseSmoothStreamingManifest() throws IOException { public void testParseSmoothStreamingManifest() throws IOException {
SsManifestParser parser = new SsManifestParser(); SsManifestParser parser = new SsManifestParser();
parser.parse(Uri.parse("https://example.com/test.ismc"), parser.parse(
TestUtil.getInputStream(getInstrumentation(), SAMPLE_ISMC_1)); Uri.parse("https://example.com/test.ismc"),
parser.parse(Uri.parse("https://example.com/test.ismc"), TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_ISMC_1));
TestUtil.getInputStream(getInstrumentation(), SAMPLE_ISMC_2)); parser.parse(
Uri.parse("https://example.com/test.ismc"),
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_ISMC_2));
} }
} }
...@@ -26,52 +26,49 @@ import java.util.Arrays; ...@@ -26,52 +26,49 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import junit.framework.TestCase; import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** /** Unit tests for {@link SsManifest}. */
* Unit tests for {@link SsManifest}. @RunWith(RobolectricTestRunner.class)
*/ public class SsManifestTest {
public class SsManifestTest extends TestCase {
private static final ProtectionElement DUMMY_PROTECTION_ELEMENT = private static final ProtectionElement DUMMY_PROTECTION_ELEMENT =
new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2}); new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2});
@Test
public void testCopy() throws Exception { public void testCopy() throws Exception {
Format[][] formats = newFormats(2, 3); Format[][] formats = newFormats(2, 3);
SsManifest sourceManifest = newSsManifest( SsManifest sourceManifest =
newStreamElement("1",formats[0]), newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1]));
newStreamElement("2", formats[1]));
List<TrackKey> keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0));
List<TrackKey> keys = Arrays.asList(
new TrackKey(0, 0),
new TrackKey(0, 2),
new TrackKey(1, 0));
// Keys don't need to be in any particular order // Keys don't need to be in any particular order
Collections.shuffle(keys, new Random(0)); Collections.shuffle(keys, new Random(0));
SsManifest copyManifest = sourceManifest.copy(keys); SsManifest copyManifest = sourceManifest.copy(keys);
SsManifest expectedManifest = newSsManifest( SsManifest expectedManifest =
newSsManifest(
newStreamElement("1", formats[0][0], formats[0][2]), newStreamElement("1", formats[0][0], formats[0][2]),
newStreamElement("2", formats[1][0])); newStreamElement("2", formats[1][0]));
assertManifestEquals(expectedManifest, copyManifest); assertManifestEquals(expectedManifest, copyManifest);
} }
@Test
public void testCopyRemoveStreamElement() throws Exception { public void testCopyRemoveStreamElement() throws Exception {
Format[][] formats = newFormats(2, 3); Format[][] formats = newFormats(2, 3);
SsManifest sourceManifest = newSsManifest( SsManifest sourceManifest =
newStreamElement("1", formats[0]), newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1]));
newStreamElement("2", formats[1]));
List<TrackKey> keys = Arrays.asList( List<TrackKey> keys = Arrays.asList(new TrackKey(1, 0));
new TrackKey(1, 0));
// Keys don't need to be in any particular order // Keys don't need to be in any particular order
Collections.shuffle(keys, new Random(0)); Collections.shuffle(keys, new Random(0));
SsManifest copyManifest = sourceManifest.copy(keys); SsManifest copyManifest = sourceManifest.copy(keys);
SsManifest expectedManifest = newSsManifest( SsManifest expectedManifest = newSsManifest(newStreamElement("2", formats[1][0]));
newStreamElement("2", formats[1][0]));
assertManifestEquals(expectedManifest, copyManifest); assertManifestEquals(expectedManifest, copyManifest);
} }
...@@ -117,13 +114,25 @@ public class SsManifestTest extends TestCase { ...@@ -117,13 +114,25 @@ public class SsManifestTest extends TestCase {
} }
private static StreamElement newStreamElement(String name, Format... formats) { private static StreamElement newStreamElement(String name, Format... formats) {
return new StreamElement("baseUri", "chunkTemplate", C.TRACK_TYPE_VIDEO, "subType", return new StreamElement(
1000, name, 1024, 768, 1024, 768, null, formats, Collections.<Long>emptyList(), 0); "baseUri",
"chunkTemplate",
C.TRACK_TYPE_VIDEO,
"subType",
1000,
name,
1024,
768,
1024,
768,
null,
formats,
Collections.<Long>emptyList(),
0);
} }
private static Format newFormat(String id) { private static Format newFormat(String id) {
return Format.createContainerFormat(id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, return Format.createContainerFormat(
Format.NO_VALUE, 0, null); id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, 0, null);
} }
} }
...@@ -33,8 +33,8 @@ android { ...@@ -33,8 +33,8 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
} }
ext { ext {
......
...@@ -25,8 +25,8 @@ android { ...@@ -25,8 +25,8 @@ android {
} }
dependencies { dependencies {
androidTestCompile project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-core')
androidTestCompile project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-dash')
androidTestCompile project(modulePrefix + 'library-hls') androidTestImplementation project(modulePrefix + 'library-hls')
androidTestCompile project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
} }
...@@ -32,7 +32,9 @@ android { ...@@ -32,7 +32,9 @@ android {
} }
dependencies { dependencies {
compile project(modulePrefix + 'library-core') api 'org.mockito:mockito-core:' + mockitoVersion
compile 'org.mockito:mockito-core:' + mockitoVersion api 'com.google.truth:truth:' + truthVersion
compile 'com.google.truth:truth:' + truthVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
...@@ -447,6 +447,36 @@ public abstract class Action { ...@@ -447,6 +447,36 @@ public abstract class Action {
} }
/** Throws a playback exception on the playback thread. */
public static final class ThrowPlaybackException extends Action {
private final ExoPlaybackException exception;
/**
* @param tag A tag to use for logging.
* @param exception The exception to throw.
*/
public ThrowPlaybackException(String tag, ExoPlaybackException exception) {
super(tag, "ThrowPlaybackException:" + exception);
this.exception = exception;
}
@Override
protected void doActionImpl(
SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) {
player
.createMessage(
new Target() {
@Override
public void handleMessage(int messageType, Object payload)
throws ExoPlaybackException {
throw exception;
}
})
.send();
}
}
/** /**
* Schedules a play action to be executed, waits until the player reaches the specified position, * Schedules a play action to be executed, waits until the player reaches the specified position,
* and pauses the player again. * and pauses the player again.
......
...@@ -40,6 +40,7 @@ import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; ...@@ -40,6 +40,7 @@ import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface;
import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.Stop;
import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState;
import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity;
import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed;
...@@ -412,6 +413,16 @@ public final class ActionSchedule { ...@@ -412,6 +413,16 @@ public final class ActionSchedule {
return apply(new ExecuteRunnable(tag, runnable)); return apply(new ExecuteRunnable(tag, runnable));
} }
/**
* Schedules to throw a playback exception on the playback thread.
*
* @param exception The exception to throw.
* @return The builder, for convenience.
*/
public Builder throwPlaybackException(ExoPlaybackException exception) {
return apply(new ThrowPlaybackException(tag, exception));
}
public ActionSchedule build() { public ActionSchedule build() {
CallbackAction callbackAction = new CallbackAction(tag); CallbackAction callbackAction = new CallbackAction(tag);
apply(callbackAction); apply(callbackAction);
......
/*
* Copyright (C) 2017 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.exoplayer2.testutil;
import android.content.Context;
import android.test.InstrumentationTestCase;
import org.mockito.MockitoAnnotations;
/**
* Utility for setting up Mockito for instrumentation tests.
*/
public final class MockitoUtil {
/**
* Sets up Mockito for an instrumentation test.
*
* @param instrumentationTestCase The instrumentation test case class.
*/
public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System.setProperty("dexmaker.dexcache",
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
MockitoAnnotations.initMocks(instrumentationTestCase);
}
/**
* Sets up Mockito for a JUnit4 test.
*
* @param targetContext The target context. Usually obtained from
* {@code InstrumentationRegistry.getTargetContext()}
* @param testClass The JUnit4 test class.
*/
public static void setUpMockito(Context targetContext, Object testClass) {
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System.setProperty("dexmaker.dexcache", targetContext.getCacheDir().getPath());
MockitoAnnotations.initMocks(testClass);
}
private MockitoUtil() {}
}
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.testutil; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.app.Instrumentation;
import android.content.Context; import android.content.Context;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
...@@ -132,20 +131,10 @@ public class TestUtil { ...@@ -132,20 +131,10 @@ public class TestUtil {
return joined; return joined;
} }
public static byte[] getByteArray(Instrumentation instrumentation, String fileName)
throws IOException {
return getByteArray(instrumentation.getContext(), fileName);
}
public static byte[] getByteArray(Context context, String fileName) throws IOException { public static byte[] getByteArray(Context context, String fileName) throws IOException {
return Util.toByteArray(getInputStream(context, fileName)); return Util.toByteArray(getInputStream(context, fileName));
} }
public static InputStream getInputStream(Instrumentation instrumentation, String fileName)
throws IOException {
return getInputStream(instrumentation.getContext(), fileName);
}
public static InputStream getInputStream(Context context, String fileName) throws IOException { public static InputStream getInputStream(Context context, String fileName) throws IOException {
return context.getResources().getAssets().open(fileName); return context.getResources().getAssets().open(fileName);
} }
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.exoplayer2.testutil.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
</manifest>
/*
* Copyright (C) 2017 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.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;
import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.List;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link FakeAdaptiveDataSet}. */
@RunWith(RobolectricTestRunner.class)
public final class FakeAdaptiveDataSetTest {
private static final Format[] TEST_FORMATS = {
Format.createVideoSampleFormat(
null,
MimeTypes.VIDEO_H264,
null,
1000000,
Format.NO_VALUE,
1280,
720,
Format.NO_VALUE,
null,
null),
Format.createVideoSampleFormat(
null,
MimeTypes.VIDEO_H264,
null,
300000,
Format.NO_VALUE,
640,
360,
Format.NO_VALUE,
null,
null)
};
private static final TrackGroup TRACK_GROUP = new TrackGroup(TEST_FORMATS);
@Test
public void testAdaptiveDataSet() {
long chunkDuration = 2 * C.MICROS_PER_SECOND;
FakeAdaptiveDataSet dataSet =
new FakeAdaptiveDataSet(
TRACK_GROUP, 10 * C.MICROS_PER_SECOND, chunkDuration, 0.0, new Random(0));
assertThat(dataSet.getAllData().size()).isEqualTo(TEST_FORMATS.length);
assertThat(dataSet.getUri(0).equals(dataSet.getUri(1))).isFalse();
assertThat(dataSet.getChunkCount()).isEqualTo(5);
assertThat(dataSet.getChunkIndexByPosition(4 * C.MICROS_PER_SECOND)).isEqualTo(2);
assertThat(dataSet.getChunkIndexByPosition(9 * C.MICROS_PER_SECOND)).isEqualTo(4);
for (int i = 0; i < dataSet.getChunkCount(); i++) {
assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration);
}
assertChunkData(dataSet, chunkDuration);
}
@Test
public void testAdaptiveDataSetTrailingSmallChunk() {
long chunkDuration = 3 * C.MICROS_PER_SECOND;
FakeAdaptiveDataSet dataSet =
new FakeAdaptiveDataSet(
TRACK_GROUP, 10 * C.MICROS_PER_SECOND, chunkDuration, 0.0, new Random(0));
assertThat(dataSet.getAllData().size()).isEqualTo(TEST_FORMATS.length);
assertThat(dataSet.getUri(0).equals(dataSet.getUri(1))).isFalse();
assertThat(dataSet.getChunkCount()).isEqualTo(4);
assertThat(dataSet.getChunkIndexByPosition(4 * C.MICROS_PER_SECOND)).isEqualTo(1);
assertThat(dataSet.getChunkIndexByPosition(9 * C.MICROS_PER_SECOND)).isEqualTo(3);
for (int i = 0; i < dataSet.getChunkCount() - 1; i++) {
assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration);
}
assertThat(dataSet.getChunkDuration(3)).isEqualTo(1 * C.MICROS_PER_SECOND);
assertChunkData(dataSet, chunkDuration);
}
@Test
public void testAdaptiveDataSetChunkSizeDistribution() {
double expectedStdDev = 4.0;
FakeAdaptiveDataSet dataSet =
new FakeAdaptiveDataSet(
TRACK_GROUP,
100000 * C.MICROS_PER_SECOND,
1 * C.MICROS_PER_SECOND,
expectedStdDev,
new Random(0));
for (int i = 0; i < TEST_FORMATS.length; i++) {
FakeData data = dataSet.getData(dataSet.getUri(i));
double mean = computeSegmentSizeMean(data.getSegments());
double stddev = computeSegmentSizeStdDev(data.getSegments(), mean);
double relativePercentStdDev = stddev / mean * 100.0;
assertThat(relativePercentStdDev).isWithin(0.02).of(expectedStdDev);
assertThat(mean * 8 / TEST_FORMATS[i].bitrate).isWithin(0.01).of(1.0);
}
}
private void assertChunkData(FakeAdaptiveDataSet dataSet, long chunkDuration) {
for (int i = 0; i < dataSet.getChunkCount(); i++) {
assertThat(dataSet.getStartTime(i)).isEqualTo(chunkDuration * i);
}
for (int s = 0; s < TEST_FORMATS.length; s++) {
FakeData data = dataSet.getData(dataSet.getUri(s));
assertThat(data.getSegments().size()).isEqualTo(dataSet.getChunkCount());
for (int i = 0; i < data.getSegments().size(); i++) {
long expectedLength =
TEST_FORMATS[s].bitrate * dataSet.getChunkDuration(i) / (8 * C.MICROS_PER_SECOND);
assertThat(data.getSegments().get(i).length).isEqualTo(expectedLength);
}
}
}
private static double computeSegmentSizeMean(List<Segment> segments) {
double totalSize = 0.0;
for (Segment segment : segments) {
totalSize += segment.length;
}
return totalSize / segments.size();
}
private static double computeSegmentSizeStdDev(List<Segment> segments, double mean) {
double totalSquaredSize = 0.0;
for (Segment segment : segments) {
totalSquaredSize += (double) segment.length * segment.length;
}
return Math.sqrt(totalSquaredSize / segments.size() - mean * mean);
}
}
/*
* Copyright (C) 2017 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.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import android.os.ConditionVariable;
import android.os.HandlerThread;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Unit test for {@link FakeClock}. */
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public final class FakeClockTest {
private static final long TIMEOUT_MS = 10000;
@Test
public void testAdvanceTime() {
FakeClock fakeClock = new FakeClock(2000);
assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000);
fakeClock.advanceTime(500);
assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500);
fakeClock.advanceTime(0);
assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500);
}
@Test
public void testSleep() throws InterruptedException {
FakeClock fakeClock = new FakeClock(0);
SleeperThread sleeperThread = new SleeperThread(fakeClock, 1000);
sleeperThread.start();
assertThat(sleeperThread.waitUntilAsleep(TIMEOUT_MS)).isTrue();
assertThat(sleeperThread.isSleeping()).isTrue();
fakeClock.advanceTime(1000);
sleeperThread.join(TIMEOUT_MS);
assertThat(sleeperThread.isSleeping()).isFalse();
sleeperThread = new SleeperThread(fakeClock, 0);
sleeperThread.start();
sleeperThread.join();
assertThat(sleeperThread.isSleeping()).isFalse();
SleeperThread[] sleeperThreads = new SleeperThread[5];
sleeperThreads[0] = new SleeperThread(fakeClock, 1000);
sleeperThreads[1] = new SleeperThread(fakeClock, 1000);
sleeperThreads[2] = new SleeperThread(fakeClock, 2000);
sleeperThreads[3] = new SleeperThread(fakeClock, 3000);
sleeperThreads[4] = new SleeperThread(fakeClock, 4000);
for (SleeperThread thread : sleeperThreads) {
thread.start();
assertThat(thread.waitUntilAsleep(TIMEOUT_MS)).isTrue();
}
assertSleepingStates(new boolean[] {true, true, true, true, true}, sleeperThreads);
fakeClock.advanceTime(1500);
assertThat(sleeperThreads[0].waitUntilAwake(TIMEOUT_MS)).isTrue();
assertThat(sleeperThreads[1].waitUntilAwake(TIMEOUT_MS)).isTrue();
assertSleepingStates(new boolean[] {false, false, true, true, true}, sleeperThreads);
fakeClock.advanceTime(2000);
assertThat(sleeperThreads[2].waitUntilAwake(TIMEOUT_MS)).isTrue();
assertThat(sleeperThreads[3].waitUntilAwake(TIMEOUT_MS)).isTrue();
assertSleepingStates(new boolean[] {false, false, false, false, true}, sleeperThreads);
fakeClock.advanceTime(2000);
for (SleeperThread thread : sleeperThreads) {
thread.join(TIMEOUT_MS);
}
assertSleepingStates(new boolean[] {false, false, false, false, false}, sleeperThreads);
}
@Test
public void testPostDelayed() {
HandlerThread handlerThread = new HandlerThread("FakeClockTest thread");
handlerThread.start();
FakeClock fakeClock = new FakeClock(0);
HandlerWrapper handler =
fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null);
TestRunnable[] testRunnables = {
new TestRunnable(),
new TestRunnable(),
new TestRunnable(),
new TestRunnable(),
new TestRunnable()
};
handler.postDelayed(testRunnables[0], 0);
handler.postDelayed(testRunnables[1], 100);
handler.postDelayed(testRunnables[2], 200);
waitForHandler(handler);
assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables);
fakeClock.advanceTime(150);
handler.postDelayed(testRunnables[3], 50);
handler.postDelayed(testRunnables[4], 100);
waitForHandler(handler);
assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables);
fakeClock.advanceTime(50);
waitForHandler(handler);
assertTestRunnableStates(new boolean[] {true, true, true, true, false}, testRunnables);
fakeClock.advanceTime(1000);
waitForHandler(handler);
assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables);
}
private static void assertSleepingStates(boolean[] states, SleeperThread[] sleeperThreads) {
for (int i = 0; i < sleeperThreads.length; i++) {
assertThat(sleeperThreads[i].isSleeping()).isEqualTo(states[i]);
}
}
private static void waitForHandler(HandlerWrapper handler) {
final ConditionVariable handlerFinished = new ConditionVariable();
handler.post(
new Runnable() {
@Override
public void run() {
handlerFinished.open();
}
});
handlerFinished.block();
}
private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) {
for (int i = 0; i < testRunnables.length; i++) {
assertThat(testRunnables[i].hasRun).isEqualTo(states[i]);
}
}
private static final class SleeperThread extends Thread {
private final Clock clock;
private final long sleepDurationMs;
private final CountDownLatch fallAsleepCountDownLatch;
private final CountDownLatch wakeUpCountDownLatch;
private volatile boolean isSleeping;
public SleeperThread(Clock clock, long sleepDurationMs) {
this.clock = clock;
this.sleepDurationMs = sleepDurationMs;
this.fallAsleepCountDownLatch = new CountDownLatch(1);
this.wakeUpCountDownLatch = new CountDownLatch(1);
}
public boolean waitUntilAsleep(long timeoutMs) throws InterruptedException {
return fallAsleepCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
}
public boolean waitUntilAwake(long timeoutMs) throws InterruptedException {
return wakeUpCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
}
public boolean isSleeping() {
return isSleeping;
}
@Override
public void run() {
// This relies on the FakeClock's methods synchronizing on its own monitor to ensure that
// any interactions with it occur only after sleep() has called wait() or returned.
synchronized (clock) {
isSleeping = true;
fallAsleepCountDownLatch.countDown();
clock.sleep(sleepDurationMs);
isSleeping = false;
wakeUpCountDownLatch.countDown();
}
}
}
private static final class TestRunnable implements Runnable {
public boolean hasRun;
@Override
public void run() {
hasRun = true;
}
}
}
/*
* Copyright (C) 2017 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.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;
import java.io.IOException;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link FakeDataSet} */
@RunWith(RobolectricTestRunner.class)
public final class FakeDataSetTest {
@Test
public void testMultipleDataSets() {
byte[][] testData = new byte[4][];
Uri[] uris = new Uri[3];
for (int i = 0; i < 4; i++) {
testData[i] = TestUtil.buildTestData(10, i);
if (i > 0) {
uris[i - 1] = Uri.parse("test_uri_" + i);
}
}
FakeDataSet fakeDataSet =
new FakeDataSet()
.newDefaultData()
.appendReadData(testData[0])
.endData()
.setData(uris[0], testData[1])
.newData(uris[1])
.appendReadData(testData[2])
.endData()
.setData(uris[2], testData[3]);
assertThat(fakeDataSet.getAllData().size()).isEqualTo(4);
assertThat(fakeDataSet.getData("unseen_uri")).isEqualTo(fakeDataSet.getData((Uri) null));
for (int i = 0; i < 3; i++) {
assertThat(fakeDataSet.getData(uris[i]).uri).isEqualTo(uris[i]);
}
assertThat(fakeDataSet.getData((Uri) null).getData()).isEqualTo(testData[0]);
for (int i = 1; i < 4; i++) {
assertThat(fakeDataSet.getData(uris[i - 1]).getData()).isEqualTo(testData[i]);
}
}
@Test
public void testSegmentTypes() {
byte[] testData = TestUtil.buildTestData(3);
Runnable runnable =
new Runnable() {
@Override
public void run() {
// Do nothing.
}
};
IOException exception = new IOException();
FakeDataSet fakeDataSet =
new FakeDataSet()
.newDefaultData()
.appendReadData(testData)
.appendReadData(testData)
.appendReadData(50)
.appendReadAction(runnable)
.appendReadError(exception)
.endData();
List<Segment> segments = fakeDataSet.getData((Uri) null).getSegments();
assertThat(segments.size()).isEqualTo(5);
assertSegment(segments.get(0), testData, 3, 0, null, null);
assertSegment(segments.get(1), testData, 3, 3, null, null);
assertSegment(segments.get(2), null, 50, 6, null, null);
assertSegment(segments.get(3), null, 0, 56, runnable, null);
assertSegment(segments.get(4), null, 0, 56, null, exception);
byte[] allData = new byte[6];
System.arraycopy(testData, 0, allData, 0, 3);
System.arraycopy(testData, 0, allData, 3, 3);
assertThat(fakeDataSet.getData((Uri) null).getData()).isEqualTo(allData);
}
private static void assertSegment(
Segment segment,
byte[] data,
int length,
long byteOffset,
Runnable runnable,
IOException exception) {
if (data != null) {
assertThat(segment.data).isEqualTo(data);
assertThat(data).hasLength(length);
} else {
assertThat(segment.data).isNull();
}
assertThat(segment.length).isEqualTo(length);
assertThat(segment.byteOffset).isEqualTo(byteOffset);
assertThat(segment.action).isEqualTo(runnable);
assertThat(segment.isActionSegment()).isEqualTo(runnable != null);
assertThat(segment.exception).isEqualTo(exception);
assertThat(segment.isErrorSegment()).isEqualTo(exception != null);
}
}
/*
* Copyright (C) 2017 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.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.IOException;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link FakeDataSource}. */
@RunWith(RobolectricTestRunner.class)
public final class FakeDataSourceTest {
private static final String URI_STRING = "test://test.test";
private static final byte[] BUFFER = new byte[500];
private static final byte[] TEST_DATA = TestUtil.buildTestData(15);
private static final byte[] TEST_DATA_PART_1 = Arrays.copyOf(TEST_DATA, 10);
private static final byte[] TEST_DATA_PART_2 = Arrays.copyOfRange(TEST_DATA, 10, 15);
private static Uri uri;
private static FakeDataSet fakeDataSet;
@Before
public void setUp() {
uri = Uri.parse(URI_STRING);
fakeDataSet =
new FakeDataSet()
.newData(uri.toString())
.appendReadData(TEST_DATA_PART_1)
.appendReadData(TEST_DATA_PART_2)
.endData();
}
@Test
public void testReadFull() throws IOException {
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(15);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(10);
assertBuffer(TEST_DATA_PART_1);
assertThat(dataSource.read(BUFFER, 10, BUFFER.length)).isEqualTo(5);
assertBuffer(TEST_DATA);
assertThat(dataSource.read(BUFFER, 15, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
assertBuffer(TEST_DATA);
assertThat(dataSource.read(BUFFER, 20, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
}
@Test
public void testReadPartialOpenEnded() throws IOException {
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
assertThat(dataSource.open(new DataSpec(uri, 7, C.LENGTH_UNSET, null))).isEqualTo(8);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(3);
assertBuffer(TEST_DATA_PART_1, 7, 3);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(5);
assertBuffer(TEST_DATA_PART_2);
assertThat(dataSource.read(BUFFER, 15, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
}
@Test
public void testReadPartialBounded() throws IOException {
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
assertThat(dataSource.open(new DataSpec(uri, 9, 3, null))).isEqualTo(3);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(1);
assertBuffer(TEST_DATA_PART_1, 9, 1);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(2);
assertBuffer(TEST_DATA_PART_2, 0, 2);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
assertThat(dataSource.open(new DataSpec(uri, 11, 4, null))).isEqualTo(4);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(4);
assertBuffer(TEST_DATA_PART_2, 1, 4);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
}
@Test
public void testDummyData() throws IOException {
FakeDataSource dataSource =
new FakeDataSource(
new FakeDataSet()
.newData(uri.toString())
.appendReadData(100)
.appendReadData(TEST_DATA)
.appendReadData(200)
.endData());
assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(315);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(100);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);
assertBuffer(TEST_DATA);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(200);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
}
@Test
public void testException() throws IOException {
String errorMessage = "error, error, error";
IOException exception = new IOException(errorMessage);
FakeDataSource dataSource =
new FakeDataSource(
new FakeDataSet()
.newData(uri.toString())
.appendReadData(TEST_DATA)
.appendReadError(exception)
.appendReadData(TEST_DATA)
.endData());
assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(30);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);
assertBuffer(TEST_DATA);
try {
dataSource.read(BUFFER, 0, BUFFER.length);
fail("IOException expected.");
} catch (IOException e) {
assertThat(e).hasMessageThat().isEqualTo(errorMessage);
}
try {
dataSource.read(BUFFER, 0, BUFFER.length);
fail("IOException expected.");
} catch (IOException e) {
assertThat(e).hasMessageThat().isEqualTo(errorMessage);
}
dataSource.close();
assertThat(dataSource.open(new DataSpec(uri, 15, 15, null))).isEqualTo(15);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);
assertBuffer(TEST_DATA);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
}
@Test
public void testRunnable() throws IOException {
TestRunnable[] runnables = new TestRunnable[3];
for (int i = 0; i < 3; i++) {
runnables[i] = new TestRunnable();
}
FakeDataSource dataSource =
new FakeDataSource(
new FakeDataSet()
.newData(uri.toString())
.appendReadData(TEST_DATA)
.appendReadAction(runnables[0])
.appendReadData(TEST_DATA)
.appendReadAction(runnables[1])
.appendReadAction(runnables[2])
.appendReadData(TEST_DATA)
.endData());
assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(45);
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);
assertBuffer(TEST_DATA);
for (int i = 0; i < 3; i++) {
assertThat(runnables[i].ran).isFalse();
}
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);
assertBuffer(TEST_DATA);
assertThat(runnables[0].ran).isTrue();
assertThat(runnables[1].ran).isFalse();
assertThat(runnables[2].ran).isFalse();
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);
assertBuffer(TEST_DATA);
for (int i = 0; i < 3; i++) {
assertThat(runnables[i].ran).isTrue();
}
assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);
dataSource.close();
}
@Test
public void testOpenSourceFailures() throws IOException {
// Empty data.
FakeDataSource dataSource =
new FakeDataSource(new FakeDataSet().newData(uri.toString()).endData());
try {
dataSource.open(new DataSpec(uri));
fail("IOException expected.");
} catch (IOException e) {
// Expected.
} finally {
dataSource.close();
}
// Non-existent data
dataSource = new FakeDataSource(new FakeDataSet());
try {
dataSource.open(new DataSpec(uri));
fail("IOException expected.");
} catch (IOException e) {
// Expected.
} finally {
dataSource.close();
}
// DataSpec out of bounds.
dataSource =
new FakeDataSource(
new FakeDataSet()
.newDefaultData()
.appendReadData(TestUtil.buildTestData(10))
.endData());
try {
dataSource.open(new DataSpec(uri, 5, 10, null));
fail("IOException expected.");
} catch (IOException e) {
// Expected.
} finally {
dataSource.close();
}
}
private static void assertBuffer(byte[] expected) {
assertBuffer(expected, 0, expected.length);
}
private static void assertBuffer(byte[] expected, int expectedStart, int expectedLength) {
for (int i = 0; i < expectedLength; i++) {
assertThat(BUFFER[i]).isEqualTo(expected[i + expectedStart]);
}
}
private static final class TestRunnable implements Runnable {
public boolean ran;
@Override
public void run() {
ran = true;
}
}
}
manifest=src/test/AndroidManifest.xml
// Copyright (C) 2018 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 from: '../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
lintOptions {
// Truth depends on JUnit, which depends on java.lang.management, which
// is not part of Android. Remove this when JUnit 4.13 or later is used.
// See: https://github.com/junit-team/junit4/pull/1187.
disable 'InvalidPackage'
}
}
dependencies {
api 'org.robolectric:robolectric:' + robolectricVersion
api project(modulePrefix + 'testutils')
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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 package="com.google.android.exoplayer2.testutil"/>
...@@ -29,9 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil; ...@@ -29,9 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
/** /** Assertion methods for {@link Cache}. */
* Assertion methods for {@link Cache}.
*/
public final class CacheAsserts { public final class CacheAsserts {
/** /**
...@@ -135,5 +133,4 @@ public final class CacheAsserts { ...@@ -135,5 +133,4 @@ public final class CacheAsserts {
} }
private CacheAsserts() {} private CacheAsserts() {}
} }
...@@ -19,9 +19,7 @@ import com.google.android.exoplayer2.Format; ...@@ -19,9 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MediaClock;
/** /** Fake abstract {@link Renderer} which is also a {@link MediaClock}. */
* Fake abstract {@link Renderer} which is also a {@link MediaClock}.
*/
public abstract class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { public abstract class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
public FakeMediaClockRenderer(Format... expectedFormats) { public FakeMediaClockRenderer(Format... expectedFormats) {
...@@ -32,5 +30,4 @@ public abstract class FakeMediaClockRenderer extends FakeRenderer implements Med ...@@ -32,5 +30,4 @@ public abstract class FakeMediaClockRenderer extends FakeRenderer implements Med
public MediaClock getMediaClock() { public MediaClock getMediaClock() {
return this; return this;
} }
} }
...@@ -25,8 +25,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; ...@@ -25,8 +25,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.util.List; import java.util.List;
/** /**
* A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number of
* of calls to its methods. * calls to its methods.
*/ */
public final class FakeTrackSelection implements TrackSelection { public final class FakeTrackSelection implements TrackSelection {
...@@ -118,8 +118,8 @@ public final class FakeTrackSelection implements TrackSelection { ...@@ -118,8 +118,8 @@ public final class FakeTrackSelection implements TrackSelection {
} }
@Override @Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, public void updateSelectedTrack(
long availableDurationUs) { long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) {
assertThat(isEnabled).isTrue(); assertThat(isEnabled).isTrue();
} }
...@@ -134,5 +134,4 @@ public final class FakeTrackSelection implements TrackSelection { ...@@ -134,5 +134,4 @@ public final class FakeTrackSelection implements TrackSelection {
assertThat(isEnabled).isTrue(); assertThat(isEnabled).isTrue();
return false; return false;
} }
} }
...@@ -25,9 +25,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; ...@@ -25,9 +25,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /** A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. */
* A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s.
*/
public class FakeTrackSelector extends MappingTrackSelector { public class FakeTrackSelector extends MappingTrackSelector {
private final List<FakeTrackSelection> selectedTrackSelections = new ArrayList<>(); private final List<FakeTrackSelection> selectedTrackSelections = new ArrayList<>();
...@@ -38,17 +36,19 @@ public class FakeTrackSelector extends MappingTrackSelector { ...@@ -38,17 +36,19 @@ public class FakeTrackSelector extends MappingTrackSelector {
} }
/** /**
* @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse {@link
* {@link TrackSelection}s during track selection, when it finds previously-selected track * TrackSelection}s during track selection, when it finds previously-selected track selection
* selection using the same {@link TrackGroup}. * using the same {@link TrackGroup}.
*/ */
public FakeTrackSelector(boolean mayReuseTrackSelection) { public FakeTrackSelector(boolean mayReuseTrackSelection) {
this.mayReuseTrackSelection = mayReuseTrackSelection; this.mayReuseTrackSelection = mayReuseTrackSelection;
} }
@Override @Override
protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, protected TrackSelection[] selectTracks(
TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) RendererCapabilities[] rendererCapabilities,
TrackGroupArray[] rendererTrackGroupArrays,
int[][][] rendererFormatSupports)
throws ExoPlaybackException { throws ExoPlaybackException {
List<FakeTrackSelection> resultList = new ArrayList<>(); List<FakeTrackSelection> resultList = new ArrayList<>();
for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) {
...@@ -76,11 +76,8 @@ public class FakeTrackSelector extends MappingTrackSelector { ...@@ -76,11 +76,8 @@ public class FakeTrackSelector extends MappingTrackSelector {
return trackSelectionForRenderer; return trackSelectionForRenderer;
} }
/** /** Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. */
* Returns list of all {@link FakeTrackSelection}s that this track selector has made so far.
*/
public List<FakeTrackSelection> getSelectedTrackSelections() { public List<FakeTrackSelection> getSelectedTrackSelections() {
return selectedTrackSelections; return selectedTrackSelections;
} }
} }
...@@ -37,9 +37,7 @@ import java.util.concurrent.CountDownLatch; ...@@ -37,9 +37,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /** A runner for {@link MediaSource} tests. */
* A runner for {@link MediaSource} tests.
*/
public class MediaSourceTestRunner { public class MediaSourceTestRunner {
public static final int TIMEOUT_MS = 10000; public static final int TIMEOUT_MS = 10000;
...@@ -78,7 +76,8 @@ public class MediaSourceTestRunner { ...@@ -78,7 +76,8 @@ public class MediaSourceTestRunner {
public void runOnPlaybackThread(final Runnable runnable) { public void runOnPlaybackThread(final Runnable runnable) {
final Throwable[] throwable = new Throwable[1]; final Throwable[] throwable = new Throwable[1];
final ConditionVariable finishedCondition = new ConditionVariable(); final ConditionVariable finishedCondition = new ConditionVariable();
playbackHandler.post(new Runnable() { playbackHandler.post(
new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
...@@ -103,14 +102,15 @@ public class MediaSourceTestRunner { ...@@ -103,14 +102,15 @@ public class MediaSourceTestRunner {
*/ */
public Timeline prepareSource() throws IOException { public Timeline prepareSource() throws IOException {
final IOException[] prepareError = new IOException[1]; final IOException[] prepareError = new IOException[1];
runOnPlaybackThread(new Runnable() { runOnPlaybackThread(
new Runnable() {
@Override @Override
public void run() { public void run() {
mediaSource.prepareSource(player, true, mediaSourceListener); mediaSource.prepareSource(player, true, mediaSourceListener);
try { try {
// TODO: This only catches errors that are set synchronously in prepareSource. To capture // TODO: This only catches errors that are set synchronously in prepareSource. To
// async errors we'll need to poll maybeThrowSourceInfoRefreshError until the first call // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the
// to onSourceInfoRefreshed. // first call to onSourceInfoRefreshed.
mediaSource.maybeThrowSourceInfoRefreshError(); mediaSource.maybeThrowSourceInfoRefreshError();
} catch (IOException e) { } catch (IOException e) {
prepareError[0] = e; prepareError[0] = e;
...@@ -132,7 +132,8 @@ public class MediaSourceTestRunner { ...@@ -132,7 +132,8 @@ public class MediaSourceTestRunner {
*/ */
public MediaPeriod createPeriod(final MediaPeriodId periodId) { public MediaPeriod createPeriod(final MediaPeriodId periodId) {
final MediaPeriod[] holder = new MediaPeriod[1]; final MediaPeriod[] holder = new MediaPeriod[1];
runOnPlaybackThread(new Runnable() { runOnPlaybackThread(
new Runnable() {
@Override @Override
public void run() { public void run() {
holder[0] = mediaSource.createPeriod(periodId, allocator); holder[0] = mediaSource.createPeriod(periodId, allocator);
...@@ -183,7 +184,8 @@ public class MediaSourceTestRunner { ...@@ -183,7 +184,8 @@ public class MediaSourceTestRunner {
* @param mediaPeriod The {@link MediaPeriod} to release. * @param mediaPeriod The {@link MediaPeriod} to release.
*/ */
public void releasePeriod(final MediaPeriod mediaPeriod) { public void releasePeriod(final MediaPeriod mediaPeriod) {
runOnPlaybackThread(new Runnable() { runOnPlaybackThread(
new Runnable() {
@Override @Override
public void run() { public void run() {
mediaSource.releasePeriod(mediaPeriod); mediaSource.releasePeriod(mediaPeriod);
...@@ -191,11 +193,10 @@ public class MediaSourceTestRunner { ...@@ -191,11 +193,10 @@ public class MediaSourceTestRunner {
}); });
} }
/** /** Calls {@link MediaSource#releaseSource()} on the playback thread. */
* Calls {@link MediaSource#releaseSource()} on the playback thread.
*/
public void releaseSource() { public void releaseSource() {
runOnPlaybackThread(new Runnable() { runOnPlaybackThread(
new Runnable() {
@Override @Override
public void run() { public void run() {
mediaSource.releaseSource(); mediaSource.releaseSource();
...@@ -276,9 +277,7 @@ public class MediaSourceTestRunner { ...@@ -276,9 +277,7 @@ public class MediaSourceTestRunner {
releasePeriod(secondMediaPeriod); releasePeriod(secondMediaPeriod);
} }
/** /** Releases the runner. Should be called when the runner is no longer required. */
* Releases the runner. Should be called when the runner is no longer required.
*/
public void release() { public void release() {
playbackThread.quit(); playbackThread.quit();
} }
...@@ -290,7 +289,6 @@ public class MediaSourceTestRunner { ...@@ -290,7 +289,6 @@ public class MediaSourceTestRunner {
Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());
timelines.addLast(timeline); timelines.addLast(timeline);
} }
} }
private static class EventHandlingExoPlayer extends StubExoPlayer private static class EventHandlingExoPlayer extends StubExoPlayer
...@@ -326,5 +324,4 @@ public class MediaSourceTestRunner { ...@@ -326,5 +324,4 @@ public class MediaSourceTestRunner {
return true; return true;
} }
} }
} }
...@@ -15,20 +15,25 @@ ...@@ -15,20 +15,25 @@
*/ */
package com.google.android.exoplayer2.testutil; package com.google.android.exoplayer2.testutil;
/** /** Provides ogg/vorbis test data in bytes for unit tests. */
* Provides ogg/vorbis test data in bytes for unit tests.
*/
public final class OggTestData { public final class OggTestData {
public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) return new FakeExtractorInput.Builder()
.setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build(); .setData(data)
.setSimulateIOErrors(true)
.setSimulateUnknownLength(simulateUnknownLength)
.setSimulatePartialReads(true)
.build();
} }
public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter, public static byte[] buildOggHeader(
int pageSegmentCount) { int headerType, long granule, int pageSequenceCounter, int pageSegmentCount) {
return TestUtil.createByteArray( return TestUtil.createByteArray(
0x4F, 0x67, 0x67, 0x53, // Oggs. 0x4F,
0x67,
0x67,
0x53, // Oggs.
0x00, // Stream revision. 0x00, // Stream revision.
headerType, headerType,
(int) (granule) & 0xFF, (int) (granule) & 0xFF,
...@@ -60,14 +65,11 @@ public final class OggTestData { ...@@ -60,14 +65,11 @@ public final class OggTestData {
*/ */
public static byte[] getVorbisHeaderPages() { public static byte[] getVorbisHeaderPages() {
byte[] data = new byte[VORBIS_HEADER_PAGES.length]; byte[] data = new byte[VORBIS_HEADER_PAGES.length];
System.arraycopy(VORBIS_HEADER_PAGES, 0, data, 0, System.arraycopy(VORBIS_HEADER_PAGES, 0, data, 0, VORBIS_HEADER_PAGES.length);
VORBIS_HEADER_PAGES.length);
return data; return data;
} }
/** /** Returns a valid vorbis identification header in bytes. */
* Returns a valid vorbis identification header in bytes.
*/
public static byte[] getIdentificationHeaderData() { public static byte[] getIdentificationHeaderData() {
int idHeaderStart = 28; int idHeaderStart = 28;
int idHeaderLength = 30; int idHeaderLength = 30;
...@@ -76,19 +78,15 @@ public final class OggTestData { ...@@ -76,19 +78,15 @@ public final class OggTestData {
return idHeaderData; return idHeaderData;
} }
/** /** Returns a valid vorbis comment header with 3 comments including utf8 chars in bytes. */
* Returns a valid vorbis comment header with 3 comments including utf8 chars in bytes.
*/
public static byte[] getCommentHeaderDataUTF8() { public static byte[] getCommentHeaderDataUTF8() {
byte[] commentHeaderData = new byte[COMMENT_HEADER_WITH_UTF8.length]; byte[] commentHeaderData = new byte[COMMENT_HEADER_WITH_UTF8.length];
System.arraycopy(COMMENT_HEADER_WITH_UTF8, 0, commentHeaderData, 0, System.arraycopy(
COMMENT_HEADER_WITH_UTF8.length); COMMENT_HEADER_WITH_UTF8, 0, commentHeaderData, 0, COMMENT_HEADER_WITH_UTF8.length);
return commentHeaderData; return commentHeaderData;
} }
/** /** Returns a valid vorbis setup header in bytes. */
* Returns a valid vorbis setup header in bytes.
*/
public static byte[] getSetupHeaderData() { public static byte[] getSetupHeaderData() {
int setupHeaderStart = 146; int setupHeaderStart = 146;
int setupHeaderLength = VORBIS_HEADER_PAGES.length - setupHeaderStart; int setupHeaderLength = VORBIS_HEADER_PAGES.length - setupHeaderStart;
...@@ -1069,5 +1067,4 @@ public final class OggTestData { ...@@ -1069,5 +1067,4 @@ public final class OggTestData {
(byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x04, (byte) 0x04 (byte) 0x00, (byte) 0x04, (byte) 0x04
}; };
} }
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2.testutil;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.util.ReflectionHelpers.callInstanceMethod; import static org.robolectric.util.ReflectionHelpers.callInstanceMethod;
...@@ -185,9 +185,9 @@ public final class RobolectricUtil { ...@@ -185,9 +185,9 @@ public final class RobolectricUtil {
@Override @Override
public int compareTo(@NonNull PendingMessage other) { public int compareTo(@NonNull PendingMessage other) {
int res = Long.compare(this.when, other.when); int res = Util.compareLong(this.when, other.when);
if (res == 0 && this != other) { if (res == 0 && this != other) {
res = Long.compare(this.sequenceNumber, other.sequenceNumber); res = Util.compareLong(this.sequenceNumber, other.sequenceNumber);
} }
return res; return res;
} }
......
...@@ -271,5 +271,4 @@ public abstract class StubExoPlayer implements ExoPlayer { ...@@ -271,5 +271,4 @@ public abstract class StubExoPlayer implements ExoPlayer {
public long getContentPosition() { public long getContentPosition() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
} }
...@@ -23,9 +23,7 @@ import com.google.android.exoplayer2.Timeline; ...@@ -23,9 +23,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
/** /** Unit test for {@link Timeline}. */
* Unit test for {@link Timeline}.
*/
public final class TimelineAsserts { public final class TimelineAsserts {
private static final int[] REPEAT_MODES = { private static final int[] REPEAT_MODES = {
...@@ -34,9 +32,7 @@ public final class TimelineAsserts { ...@@ -34,9 +32,7 @@ public final class TimelineAsserts {
private TimelineAsserts() {} private TimelineAsserts() {}
/** /** Assert that timeline is empty (i.e. has no windows or periods). */
* Assert that timeline is empty (i.e. has no windows or periods).
*/
public static void assertEmpty(Timeline timeline) { public static void assertEmpty(Timeline timeline) {
assertWindowIds(timeline); assertWindowIds(timeline);
assertPeriodCounts(timeline); assertPeriodCounts(timeline);
...@@ -63,9 +59,7 @@ public final class TimelineAsserts { ...@@ -63,9 +59,7 @@ public final class TimelineAsserts {
} }
} }
/** /** Asserts that window properties {@link Window}.isDynamic are set correctly. */
* Asserts that window properties {@link Window}.isDynamic are set correctly.
*/
public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) {
Window window = new Window(); Window window = new Window();
for (int i = 0; i < timeline.getWindowCount(); i++) { for (int i = 0; i < timeline.getWindowCount(); i++) {
...@@ -78,8 +72,10 @@ public final class TimelineAsserts { ...@@ -78,8 +72,10 @@ public final class TimelineAsserts {
* Asserts that previous window indices for each window depending on the repeat mode and the * Asserts that previous window indices for each window depending on the repeat mode and the
* shuffle mode are equal to the given sequence. * shuffle mode are equal to the given sequence.
*/ */
public static void assertPreviousWindowIndices(Timeline timeline, public static void assertPreviousWindowIndices(
@Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, Timeline timeline,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
int... expectedPreviousWindowIndices) { int... expectedPreviousWindowIndices) {
for (int i = 0; i < timeline.getWindowCount(); i++) { for (int i = 0; i < timeline.getWindowCount(); i++) {
assertThat(timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled)) assertThat(timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled))
...@@ -88,11 +84,14 @@ public final class TimelineAsserts { ...@@ -88,11 +84,14 @@ public final class TimelineAsserts {
} }
/** /**
* Asserts that next window indices for each window depending on the repeat mode and the * Asserts that next window indices for each window depending on the repeat mode and the shuffle
* shuffle mode are equal to the given sequence. * mode are equal to the given sequence.
*/ */
public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode, public static void assertNextWindowIndices(
boolean shuffleModeEnabled, int... expectedNextWindowIndices) { Timeline timeline,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
int... expectedNextWindowIndices) {
for (int i = 0; i < timeline.getWindowCount(); i++) { for (int i = 0; i < timeline.getWindowCount(); i++) {
assertThat(timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled)) assertThat(timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled))
.isEqualTo(expectedNextWindowIndices[i]); .isEqualTo(expectedNextWindowIndices[i]);
...@@ -113,9 +112,9 @@ public final class TimelineAsserts { ...@@ -113,9 +112,9 @@ public final class TimelineAsserts {
} }
/** /**
* Asserts that period counts for each window are set correctly. Also asserts that * Asserts that period counts for each window are set correctly. Also asserts that {@link
* {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it * Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it asserts
* asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}. * the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}.
*/ */
public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) {
int windowCount = timeline.getWindowCount(); int windowCount = timeline.getWindowCount();
...@@ -147,8 +146,8 @@ public final class TimelineAsserts { ...@@ -147,8 +146,8 @@ public final class TimelineAsserts {
.isEqualTo(i + 1); .isEqualTo(i + 1);
} else { } else {
int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false); int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false);
int nextPeriod = nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET int nextPeriod =
: accumulatedPeriodCounts[nextWindow]; nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindow];
assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false)) assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false))
.isEqualTo(nextPeriod); .isEqualTo(nextPeriod);
} }
...@@ -156,9 +155,7 @@ public final class TimelineAsserts { ...@@ -156,9 +155,7 @@ public final class TimelineAsserts {
} }
} }
/** /** Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. */
* Asserts that periods' {@link Period#getAdGroupCount()} are set correctly.
*/
public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) { public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) {
Period period = new Period(); Period period = new Period();
for (int i = 0; i < timeline.getPeriodCount(); i++) { for (int i = 0; i < timeline.getPeriodCount(); i++) {
...@@ -166,5 +163,4 @@ public final class TimelineAsserts { ...@@ -166,5 +163,4 @@ public final class TimelineAsserts {
assertThat(period.getAdGroupCount()).isEqualTo(expectedAdGroupCounts[i]); assertThat(period.getAdGroupCount()).isEqualTo(expectedAdGroupCounts[i]);
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment