mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
b6155d2e08
125 changed files with 4189 additions and 2599 deletions
14
README.md
14
README.md
|
|
@ -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
|
||||||
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
|
||||||
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 ##
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,25 @@
|
||||||
# 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 @@
|
||||||
// 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 {
|
||||||
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'
|
||||||
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
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<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.
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
||||||
23
extensions/cast/src/test/AndroidManifest.xml
Normal file
23
extensions/cast/src/test/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -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 @@
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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,30 +97,31 @@ 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 =
|
||||||
new CronetDataSource(
|
spy(
|
||||||
mockCronetEngine,
|
new CronetDataSource(
|
||||||
mockExecutor,
|
mockCronetEngine,
|
||||||
mockContentTypePredicate,
|
mockExecutor,
|
||||||
mockTransferListener,
|
mockContentTypePredicate,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
mockTransferListener,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
TEST_READ_TIMEOUT_MS,
|
||||||
mockClock,
|
true, // resetTimeoutOnRedirects
|
||||||
null,
|
Clock.DEFAULT,
|
||||||
false));
|
null,
|
||||||
|
false));
|
||||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||||
when(mockCronetEngine.newUrlRequestBuilder(
|
when(mockCronetEngine.newUrlRequestBuilder(
|
||||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||||
.thenReturn(mockUrlRequestBuilder);
|
.thenReturn(mockUrlRequestBuilder);
|
||||||
when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
|
when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
|
||||||
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
||||||
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 {
|
||||||
// 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(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
// Invoke the callback for the previous request.
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onFailed(
|
// Invoke the callback for the previous request.
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onFailed(
|
||||||
testUrlResponseInfo,
|
mockUrlRequest, testUrlResponseInfo, mockNetworkException);
|
||||||
mockNetworkException);
|
dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo);
|
||||||
dataSourceUnderTest.onResponseStarted(
|
return null;
|
||||||
mockUrlRequest2,
|
}
|
||||||
testUrlResponseInfo);
|
})
|
||||||
return null;
|
.when(mockUrlRequest2)
|
||||||
}
|
.start();
|
||||||
}).when(mockUrlRequest2).start();
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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,20 +696,22 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
public void
|
||||||
throws HttpDataSourceException {
|
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
||||||
dataSourceUnderTest = spy(
|
throws HttpDataSourceException {
|
||||||
new CronetDataSource(
|
dataSourceUnderTest =
|
||||||
mockCronetEngine,
|
spy(
|
||||||
mockExecutor,
|
new CronetDataSource(
|
||||||
mockContentTypePredicate,
|
mockCronetEngine,
|
||||||
mockTransferListener,
|
mockExecutor,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
mockContentTypePredicate,
|
||||||
TEST_READ_TIMEOUT_MS,
|
mockTransferListener,
|
||||||
true, // resetTimeoutOnRedirects
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
mockClock,
|
TEST_READ_TIMEOUT_MS,
|
||||||
null,
|
true, // resetTimeoutOnRedirects
|
||||||
true));
|
Clock.DEFAULT,
|
||||||
|
null,
|
||||||
|
true));
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
|
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
|
|
@ -736,21 +727,23 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
public void
|
||||||
throws HttpDataSourceException {
|
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
||||||
|
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 =
|
||||||
new CronetDataSource(
|
spy(
|
||||||
mockCronetEngine,
|
new CronetDataSource(
|
||||||
mockExecutor,
|
mockCronetEngine,
|
||||||
mockContentTypePredicate,
|
mockExecutor,
|
||||||
mockTransferListener,
|
mockContentTypePredicate,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
mockTransferListener,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
TEST_READ_TIMEOUT_MS,
|
||||||
mockClock,
|
true, // resetTimeoutOnRedirects
|
||||||
null,
|
Clock.DEFAULT,
|
||||||
true));
|
null,
|
||||||
|
true));
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
|
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
|
|
@ -778,18 +771,19 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
dataSourceUnderTest = spy(
|
dataSourceUnderTest =
|
||||||
new CronetDataSource(
|
spy(
|
||||||
mockCronetEngine,
|
new CronetDataSource(
|
||||||
mockExecutor,
|
mockCronetEngine,
|
||||||
mockContentTypePredicate,
|
mockExecutor,
|
||||||
mockTransferListener,
|
mockContentTypePredicate,
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
mockTransferListener,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
TEST_READ_TIMEOUT_MS,
|
||||||
mockClock,
|
true, // resetTimeoutOnRedirects
|
||||||
null,
|
Clock.DEFAULT,
|
||||||
true));
|
null,
|
||||||
|
true));
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
mockFollowRedirectSuccess();
|
mockFollowRedirectSuccess();
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
} 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,122 +870,135 @@ public final class CronetDataSourceTest {
|
||||||
// Helper methods.
|
// Helper methods.
|
||||||
|
|
||||||
private void mockStatusResponse() {
|
private void mockStatusResponse() {
|
||||||
doAnswer(new Answer<Object>() {
|
doAnswer(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
UrlRequest.StatusListener statusListener =
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
(UrlRequest.StatusListener) invocation.getArguments()[0];
|
UrlRequest.StatusListener statusListener =
|
||||||
statusListener.onStatus(TEST_CONNECTION_STATUS);
|
(UrlRequest.StatusListener) invocation.getArguments()[0];
|
||||||
return null;
|
statusListener.onStatus(TEST_CONNECTION_STATUS);
|
||||||
}
|
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(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
dataSourceUnderTest.onResponseStarted(
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||||
testUrlResponseInfo);
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
})
|
||||||
}).when(mockUrlRequest).start();
|
.when(mockUrlRequest)
|
||||||
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockResponseStartRedirect() {
|
private void mockResponseStartRedirect() {
|
||||||
doAnswer(new Answer<Object>() {
|
doAnswer(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
dataSourceUnderTest.onRedirectReceived(
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onRedirectReceived(
|
||||||
createUrlResponseInfo(307), // statusCode
|
mockUrlRequest,
|
||||||
"http://redirect.location.com");
|
createUrlResponseInfo(307), // statusCode
|
||||||
return null;
|
"http://redirect.location.com");
|
||||||
}
|
return null;
|
||||||
}).when(mockUrlRequest).start();
|
}
|
||||||
|
})
|
||||||
|
.when(mockUrlRequest)
|
||||||
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockSingleRedirectSuccess() {
|
private void mockSingleRedirectSuccess() {
|
||||||
doAnswer(new Answer<Object>() {
|
doAnswer(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
if (!redirectCalled) {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
redirectCalled = true;
|
if (!redirectCalled) {
|
||||||
dataSourceUnderTest.onRedirectReceived(
|
redirectCalled = true;
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onRedirectReceived(
|
||||||
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
mockUrlRequest,
|
||||||
"http://example.com/video/redirect");
|
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
||||||
} else {
|
"http://example.com/video/redirect");
|
||||||
dataSourceUnderTest.onResponseStarted(
|
} else {
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||||
testUrlResponseInfo);
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
})
|
||||||
}).when(mockUrlRequest).start();
|
.when(mockUrlRequest)
|
||||||
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockFollowRedirectSuccess() {
|
private void mockFollowRedirectSuccess() {
|
||||||
doAnswer(new Answer<Object>() {
|
doAnswer(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
dataSourceUnderTest.onResponseStarted(
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||||
testUrlResponseInfo);
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
})
|
||||||
}).when(mockUrlRequest).followRedirect();
|
.when(mockUrlRequest)
|
||||||
|
.followRedirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockResponseStartFailure() {
|
private void mockResponseStartFailure() {
|
||||||
doAnswer(new Answer<Object>() {
|
doAnswer(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
dataSourceUnderTest.onFailed(
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onFailed(
|
||||||
createUrlResponseInfo(500), // statusCode
|
mockUrlRequest,
|
||||||
mockNetworkException);
|
createUrlResponseInfo(500), // statusCode
|
||||||
return null;
|
mockNetworkException);
|
||||||
}
|
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(
|
||||||
@Override
|
new Answer<Void>() {
|
||||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
if (positionAndRemaining[1] == 0) {
|
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
if (positionAndRemaining[1] == 0) {
|
||||||
} else {
|
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
||||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
} else {
|
||||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||||
positionAndRemaining[0] += readLength;
|
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||||
positionAndRemaining[1] -= readLength;
|
positionAndRemaining[0] += readLength;
|
||||||
dataSourceUnderTest.onReadCompleted(
|
positionAndRemaining[1] -= readLength;
|
||||||
mockUrlRequest,
|
dataSourceUnderTest.onReadCompleted(
|
||||||
testUrlResponseInfo,
|
mockUrlRequest, testUrlResponseInfo, inputBuffer);
|
||||||
inputBuffer);
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
})
|
||||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
.when(mockUrlRequest)
|
||||||
|
.read(any(ByteBuffer.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockReadFailure() {
|
private void mockReadFailure() {
|
||||||
doAnswer(
|
doAnswer(
|
||||||
new Answer<Object>() {
|
new Answer<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
dataSourceUnderTest.onFailed(
|
dataSourceUnderTest.onFailed(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
createUrlResponseInfo(500), // statusCode
|
createUrlResponseInfo(500), // statusCode
|
||||||
mockNetworkException);
|
mockNetworkException);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when(mockUrlRequest)
|
.when(mockUrlRequest)
|
||||||
.read(any(ByteBuffer.class));
|
.read(any(ByteBuffer.class));
|
||||||
}
|
}
|
||||||
|
|
@ -999,13 +1006,13 @@ public final class CronetDataSourceTest {
|
||||||
private ConditionVariable buildReadStartedCondition() {
|
private ConditionVariable buildReadStartedCondition() {
|
||||||
final ConditionVariable startedCondition = new ConditionVariable();
|
final ConditionVariable startedCondition = new ConditionVariable();
|
||||||
doAnswer(
|
doAnswer(
|
||||||
new Answer<Object>() {
|
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)
|
.when(mockUrlRequest)
|
||||||
.read(any(ByteBuffer.class));
|
.read(any(ByteBuffer.class));
|
||||||
return startedCondition;
|
return startedCondition;
|
||||||
|
|
@ -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(
|
||||||
@Override
|
new Answer<Object>() {
|
||||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
@Override
|
||||||
startedCondition.open();
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
return null;
|
startedCondition.open();
|
||||||
}
|
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 {
|
||||||
testBuffer.flip();
|
testBuffer.flip();
|
||||||
return testBuffer;
|
return testBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -31,7 +31,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
// |-- 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;
|
||||||
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;
|
||||||
* @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> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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.
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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,23 +105,24 @@ public final class MediaSessionConnector {
|
||||||
*/
|
*/
|
||||||
public interface PlaybackPreparer extends CommandReceiver {
|
public interface PlaybackPreparer extends CommandReceiver {
|
||||||
|
|
||||||
long ACTIONS = PlaybackStateCompat.ACTION_PREPARE
|
long ACTIONS =
|
||||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
|
PlaybackStateCompat.ACTION_PREPARE
|
||||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
|
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
|
||||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_URI
|
| PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
|
||||||
| PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
| PlaybackStateCompat.ACTION_PREPARE_FROM_URI
|
||||||
| PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
| PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
||||||
| PlaybackStateCompat.ACTION_PLAY_FROM_URI;
|
| PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||||
|
| PlaybackStateCompat.ACTION_PLAY_FROM_URI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
* 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 {
|
||||||
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 {
|
||||||
: 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 {
|
||||||
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 {
|
||||||
& 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 {
|
||||||
& 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;
|
||||||
|
|
@ -879,6 +887,13 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetRating(RatingCompat rating) {
|
||||||
|
if (canDispatchToRatingCallback(PlaybackStateCompat.ACTION_SET_RATING)) {
|
||||||
|
ratingCallback.onSetRating(player, rating);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||||
if (queueEditor != null) {
|
if (queueEditor != null) {
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
|
|
@ -164,11 +163,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
|
||||||
this.equalityChecker = equalityChecker;
|
this.equalityChecker = equalityChecker;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getSupportedQueueEditorActions(@Nullable Player player) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@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
|
||||||
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.
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,11 +492,26 @@ 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.
|
||||||
*
|
*
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 = decoder.dequeueOutputBuffer();
|
||||||
outputBuffer = nextOutputBuffer;
|
|
||||||
nextOutputBuffer = null;
|
|
||||||
} else {
|
|
||||||
outputBuffer = decoder.dequeueOutputBuffer();
|
|
||||||
}
|
|
||||||
if (outputBuffer == null) {
|
if (outputBuffer == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
* 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()
|
|
||||||
? nextOutputBuffer.timeUs
|
|
||||||
: C.TIME_UNSET;
|
|
||||||
|
|
||||||
long earlyUs = outputBuffer.timeUs - positionUs;
|
|
||||||
if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) {
|
|
||||||
forceRenderFrame = true;
|
|
||||||
return false;
|
return false;
|
||||||
} else if (shouldDropOutputBuffer(
|
}
|
||||||
outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) {
|
|
||||||
|
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
|
||||||
|
&& maybeDropBuffersToKeyframe(positionUs)) {
|
||||||
|
return false;
|
||||||
|
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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;
|
||||||
// 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;
|
||||||
@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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
// 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 {
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
*/
|
*/
|
||||||
/* 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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
*
|
*
|
||||||
* @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 {
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
// 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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
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;
|
||||||
// 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;
|
||||||
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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
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;
|
||||||
* 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;
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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);
|
||||||
|
|
@ -654,6 +655,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||||
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 output buffer with the specified index.
|
* Skips the output buffer with the specified index.
|
||||||
*
|
*
|
||||||
|
|
@ -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 {
|
||||||
* @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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
@ -1288,6 +1284,104 @@ public final class ExoPlayerTest {
|
||||||
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
|
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Test
|
||||||
public void testSendMessagesDuringPreparation() throws Exception {
|
public void testSendMessagesDuringPreparation() throws Exception {
|
||||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.offline;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data for DASH downloading tests.
|
|
||||||
*/
|
|
||||||
/* package */ interface DashDownloadTestData {
|
|
||||||
|
|
||||||
Uri TEST_MPD_URI = Uri.parse("test.mpd");
|
|
||||||
|
|
||||||
byte[] TEST_MPD =
|
|
||||||
("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
||||||
+ "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"static\" "
|
|
||||||
+ " mediaPresentationDuration=\"PT31S\">\n"
|
|
||||||
+ " <Period duration=\"PT16S\" >\n"
|
|
||||||
+ " <AdaptationSet>\n"
|
|
||||||
+ " <SegmentList>\n"
|
|
||||||
+ " <SegmentTimeline>\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " </SegmentTimeline>\n"
|
|
||||||
+ " </SegmentList>\n"
|
|
||||||
+ " <Representation>\n"
|
|
||||||
+ " <SegmentList>\n"
|
|
||||||
// Bounded range data
|
|
||||||
+ " <Initialization range=\"0-9\" sourceURL=\"audio_init_data\" />\n"
|
|
||||||
// Unbounded range data
|
|
||||||
+ " <SegmentURL media=\"audio_segment_1\" />\n"
|
|
||||||
+ " <SegmentURL media=\"audio_segment_2\" />\n"
|
|
||||||
+ " <SegmentURL media=\"audio_segment_3\" />\n"
|
|
||||||
+ " </SegmentList>\n"
|
|
||||||
+ " </Representation>\n"
|
|
||||||
+ " </AdaptationSet>\n"
|
|
||||||
+ " <AdaptationSet>\n"
|
|
||||||
// This segment list has a 1 second offset to make sure the progressive download order
|
|
||||||
+ " <SegmentList>\n"
|
|
||||||
+ " <SegmentTimeline>\n"
|
|
||||||
+ " <S t=\"1\" d=\"5\" />\n" // 1s offset
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " </SegmentTimeline>\n"
|
|
||||||
+ " </SegmentList>\n"
|
|
||||||
+ " <Representation>\n"
|
|
||||||
+ " <SegmentList>\n"
|
|
||||||
+ " <SegmentURL media=\"text_segment_1\" />\n"
|
|
||||||
+ " <SegmentURL media=\"text_segment_2\" />\n"
|
|
||||||
+ " <SegmentURL media=\"text_segment_3\" />\n"
|
|
||||||
+ " </SegmentList>\n"
|
|
||||||
+ " </Representation>\n"
|
|
||||||
+ " </AdaptationSet>\n"
|
|
||||||
+ " </Period>\n"
|
|
||||||
+ " <Period>\n"
|
|
||||||
+ " <SegmentList>\n"
|
|
||||||
+ " <SegmentTimeline>\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " <S d=\"5\" />\n"
|
|
||||||
+ " </SegmentTimeline>\n"
|
|
||||||
+ " </SegmentList>\n"
|
|
||||||
+ " <AdaptationSet>\n"
|
|
||||||
+ " <Representation>\n"
|
|
||||||
+ " <SegmentList>\n"
|
|
||||||
+ " <SegmentURL media=\"period_2_segment_1\" />\n"
|
|
||||||
+ " <SegmentURL media=\"period_2_segment_2\" />\n"
|
|
||||||
+ " <SegmentURL media=\"period_2_segment_3\" />\n"
|
|
||||||
+ " </SegmentList>\n"
|
|
||||||
+ " </Representation>\n"
|
|
||||||
+ " </AdaptationSet>\n"
|
|
||||||
+ " </Period>\n"
|
|
||||||
+ "</MPD>").getBytes();
|
|
||||||
|
|
||||||
byte[] TEST_MPD_NO_INDEX =
|
|
||||||
("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
||||||
+ "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"dynamic\">\n"
|
|
||||||
+ " <Period start=\"PT6462826.784S\" >\n"
|
|
||||||
+ " <AdaptationSet>\n"
|
|
||||||
+ " <Representation>\n"
|
|
||||||
+ " <SegmentBase indexRange='0-10'/>\n"
|
|
||||||
+ " </Representation>\n"
|
|
||||||
+ " </AdaptationSet>\n"
|
|
||||||
+ " </Period>\n"
|
|
||||||
+ "</MPD>").getBytes();
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
@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 @@
|
||||||
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>
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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}));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,356 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
// 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 {
|
||||||
1000000000));
|
1000000000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testParseCea608AccessibilityChannel() {
|
public void testParseCea608AccessibilityChannel() {
|
||||||
assertThat(
|
assertThat(
|
||||||
DashManifestParser.parseCea608AccessibilityChannel(
|
DashManifestParser.parseCea608AccessibilityChannel(
|
||||||
|
|
@ -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 {
|
||||||
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,109 +24,143 @@ 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(
|
||||||
newAdaptationSet(2, representations[0][0]),
|
10,
|
||||||
newAdaptationSet(3, representations[0][1])),
|
newPeriod(
|
||||||
newPeriod("4", 4,
|
"1",
|
||||||
newAdaptationSet(5, representations[1][0]),
|
1,
|
||||||
newAdaptationSet(6, representations[1][1])),
|
newAdaptationSet(2, representations[0][0]),
|
||||||
newPeriod("7", 7,
|
newAdaptationSet(3, representations[0][1])),
|
||||||
newAdaptationSet(8, representations[2][0]),
|
newPeriod(
|
||||||
newAdaptationSet(9, representations[2][1])));
|
"4",
|
||||||
|
4,
|
||||||
|
newAdaptationSet(5, representations[1][0]),
|
||||||
|
newAdaptationSet(6, representations[1][1])),
|
||||||
|
newPeriod(
|
||||||
|
"7",
|
||||||
|
7,
|
||||||
|
newAdaptationSet(8, representations[2][0]),
|
||||||
|
newAdaptationSet(9, representations[2][1])));
|
||||||
|
|
||||||
List<RepresentationKey> keys = Arrays.asList(
|
List<RepresentationKey> keys =
|
||||||
new RepresentationKey(0, 0, 0),
|
Arrays.asList(
|
||||||
new RepresentationKey(0, 0, 1),
|
new RepresentationKey(0, 0, 0),
|
||||||
new RepresentationKey(0, 1, 2),
|
new RepresentationKey(0, 0, 1),
|
||||||
|
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));
|
|
||||||
// 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));
|
||||||
|
|
||||||
DashManifest copyManifest = sourceManifest.copy(keys);
|
DashManifest copyManifest = sourceManifest.copy(keys);
|
||||||
|
|
||||||
DashManifest expectedManifest = newDashManifest(10,
|
DashManifest expectedManifest =
|
||||||
newPeriod("1", 1,
|
newDashManifest(
|
||||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
10,
|
||||||
newAdaptationSet(3, representations[0][1][2])),
|
newPeriod(
|
||||||
newPeriod("4", 4,
|
"1",
|
||||||
newAdaptationSet(5, representations[1][0][1]),
|
1,
|
||||||
newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),
|
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||||
newPeriod("7", 7,
|
newAdaptationSet(3, representations[0][1][2])),
|
||||||
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
newPeriod(
|
||||||
newAdaptationSet(9, representations[2][1][0])));
|
"4",
|
||||||
|
4,
|
||||||
|
newAdaptationSet(5, representations[1][0][1]),
|
||||||
|
newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),
|
||||||
|
newPeriod(
|
||||||
|
"7",
|
||||||
|
7,
|
||||||
|
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
||||||
|
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(
|
||||||
newAdaptationSet(2, representations[0][0]),
|
10,
|
||||||
newAdaptationSet(3, representations[0][1])),
|
newPeriod(
|
||||||
newPeriod("4", 4,
|
"1",
|
||||||
newAdaptationSet(5, representations[1][0]),
|
1,
|
||||||
newAdaptationSet(6, representations[1][1])),
|
newAdaptationSet(2, representations[0][0]),
|
||||||
newPeriod("7", 7,
|
newAdaptationSet(3, representations[0][1])),
|
||||||
newAdaptationSet(8, representations[2][0]),
|
newPeriod(
|
||||||
newAdaptationSet(9, representations[2][1])));
|
"4",
|
||||||
|
4,
|
||||||
|
newAdaptationSet(5, representations[1][0]),
|
||||||
|
newAdaptationSet(6, representations[1][1])),
|
||||||
|
newPeriod(
|
||||||
|
"7",
|
||||||
|
7,
|
||||||
|
newAdaptationSet(8, representations[2][0]),
|
||||||
|
newAdaptationSet(9, representations[2][1])));
|
||||||
|
|
||||||
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
|
DashManifest copyManifest =
|
||||||
new RepresentationKey(0, 0, 0),
|
sourceManifest.copy(
|
||||||
new RepresentationKey(0, 0, 1),
|
Arrays.asList(
|
||||||
new RepresentationKey(0, 1, 2),
|
new RepresentationKey(0, 0, 0),
|
||||||
|
new RepresentationKey(0, 0, 1),
|
||||||
|
new RepresentationKey(0, 1, 2),
|
||||||
|
new RepresentationKey(2, 0, 1),
|
||||||
|
new RepresentationKey(2, 0, 2),
|
||||||
|
new RepresentationKey(2, 1, 0)));
|
||||||
|
|
||||||
new RepresentationKey(2, 0, 1),
|
DashManifest expectedManifest =
|
||||||
new RepresentationKey(2, 0, 2),
|
newDashManifest(
|
||||||
new RepresentationKey(2, 1, 0)));
|
7,
|
||||||
|
newPeriod(
|
||||||
DashManifest expectedManifest = newDashManifest(7,
|
"1",
|
||||||
newPeriod("1", 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(
|
||||||
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
"7",
|
||||||
newAdaptationSet(9, representations[2][1][0])));
|
4,
|
||||||
|
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
||||||
|
newAdaptationSet(9, representations[2][1][0])));
|
||||||
assertManifestEquals(expectedManifest, copyManifest);
|
assertManifestEquals(expectedManifest, copyManifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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;
|
||||||
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 @@
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
// Expected.
|
// Expected.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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.offline;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
/** Data for DASH downloading tests. */
|
||||||
|
/* package */ interface DashDownloadTestData {
|
||||||
|
|
||||||
|
Uri TEST_MPD_URI = Uri.parse("test.mpd");
|
||||||
|
|
||||||
|
byte[] TEST_MPD =
|
||||||
|
("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
+ "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"static\" "
|
||||||
|
+ " mediaPresentationDuration=\"PT31S\">\n"
|
||||||
|
+ " <Period duration=\"PT16S\" >\n"
|
||||||
|
+ " <AdaptationSet>\n"
|
||||||
|
+ " <SegmentList>\n"
|
||||||
|
+ " <SegmentTimeline>\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " </SegmentTimeline>\n"
|
||||||
|
+ " </SegmentList>\n"
|
||||||
|
+ " <Representation>\n"
|
||||||
|
+ " <SegmentList>\n"
|
||||||
|
// Bounded range data
|
||||||
|
+ " <Initialization\n"
|
||||||
|
+ " range=\"0-9\" sourceURL=\"audio_init_data\" />\n"
|
||||||
|
// Unbounded range data
|
||||||
|
+ " <SegmentURL media=\"audio_segment_1\" />\n"
|
||||||
|
+ " <SegmentURL media=\"audio_segment_2\" />\n"
|
||||||
|
+ " <SegmentURL media=\"audio_segment_3\" />\n"
|
||||||
|
+ " </SegmentList>\n"
|
||||||
|
+ " </Representation>\n"
|
||||||
|
+ " </AdaptationSet>\n"
|
||||||
|
+ " <AdaptationSet>\n"
|
||||||
|
// This segment list has a 1 second offset to make sure the progressive download order
|
||||||
|
+ " <SegmentList>\n"
|
||||||
|
+ " <SegmentTimeline>\n"
|
||||||
|
+ " <S t=\"1\" d=\"5\" />\n" // 1s offset
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " </SegmentTimeline>\n"
|
||||||
|
+ " </SegmentList>\n"
|
||||||
|
+ " <Representation>\n"
|
||||||
|
+ " <SegmentList>\n"
|
||||||
|
+ " <SegmentURL media=\"text_segment_1\" />\n"
|
||||||
|
+ " <SegmentURL media=\"text_segment_2\" />\n"
|
||||||
|
+ " <SegmentURL media=\"text_segment_3\" />\n"
|
||||||
|
+ " </SegmentList>\n"
|
||||||
|
+ " </Representation>\n"
|
||||||
|
+ " </AdaptationSet>\n"
|
||||||
|
+ " </Period>\n"
|
||||||
|
+ " <Period>\n"
|
||||||
|
+ " <SegmentList>\n"
|
||||||
|
+ " <SegmentTimeline>\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " <S d=\"5\" />\n"
|
||||||
|
+ " </SegmentTimeline>\n"
|
||||||
|
+ " </SegmentList>\n"
|
||||||
|
+ " <AdaptationSet>\n"
|
||||||
|
+ " <Representation>\n"
|
||||||
|
+ " <SegmentList>\n"
|
||||||
|
+ " <SegmentURL media=\"period_2_segment_1\" />\n"
|
||||||
|
+ " <SegmentURL media=\"period_2_segment_2\" />\n"
|
||||||
|
+ " <SegmentURL media=\"period_2_segment_3\" />\n"
|
||||||
|
+ " </SegmentList>\n"
|
||||||
|
+ " </Representation>\n"
|
||||||
|
+ " </AdaptationSet>\n"
|
||||||
|
+ " </Period>\n"
|
||||||
|
+ "</MPD>")
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
|
||||||
|
byte[] TEST_MPD_NO_INDEX =
|
||||||
|
("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
+ "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"dynamic\">\n"
|
||||||
|
+ " <Period start=\"PT6462826.784S\" >\n"
|
||||||
|
+ " <AdaptationSet>\n"
|
||||||
|
+ " <Representation>\n"
|
||||||
|
+ " <SegmentBase indexRange='0-10'/>\n"
|
||||||
|
+ " </Representation>\n"
|
||||||
|
+ " </AdaptationSet>\n"
|
||||||
|
+ " </Period>\n"
|
||||||
|
+ "</MPD>")
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
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,15 +83,17 @@ 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 =
|
||||||
.newData(TEST_MPD_URI)
|
new FakeDataSet()
|
||||||
.appendReadData(testMpdFirstPart)
|
.newData(TEST_MPD_URI)
|
||||||
.appendReadError(new IOException())
|
.appendReadData(testMpdFirstPart)
|
||||||
.appendReadData(testMpdSecondPart)
|
.appendReadError(new IOException())
|
||||||
.endData();
|
.appendReadData(testMpdSecondPart)
|
||||||
|
.endData();
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
// fails on the first try
|
// fails on the first try
|
||||||
|
|
@ -108,13 +113,15 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6);
|
.setRandomData("audio_segment_2", 5)
|
||||||
|
.setRandomData("audio_segment_3", 6);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||||
|
|
@ -123,17 +130,19 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.newData("audio_segment_1")
|
.setRandomData("audio_init_data", 10)
|
||||||
.appendReadData(TestUtil.buildTestData(10))
|
.newData("audio_segment_1")
|
||||||
.appendReadData(TestUtil.buildTestData(10))
|
.appendReadData(TestUtil.buildTestData(10))
|
||||||
.appendReadData(TestUtil.buildTestData(10))
|
.appendReadData(TestUtil.buildTestData(10))
|
||||||
.endData()
|
.appendReadData(TestUtil.buildTestData(10))
|
||||||
.setRandomData("audio_segment_2", 5)
|
.endData()
|
||||||
.setRandomData("audio_segment_3", 6);
|
.setRandomData("audio_segment_2", 5)
|
||||||
|
.setRandomData("audio_segment_3", 6);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||||
|
|
@ -142,16 +151,18 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6)
|
.setRandomData("audio_segment_2", 5)
|
||||||
.setRandomData("text_segment_1", 1)
|
.setRandomData("audio_segment_3", 6)
|
||||||
.setRandomData("text_segment_2", 2)
|
.setRandomData("text_segment_1", 1)
|
||||||
.setRandomData("text_segment_3", 3);
|
.setRandomData("text_segment_2", 2)
|
||||||
|
.setRandomData("text_segment_3", 3);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(
|
dashDownloader.selectRepresentations(
|
||||||
|
|
@ -161,19 +172,21 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6)
|
.setRandomData("audio_segment_2", 5)
|
||||||
.setRandomData("text_segment_1", 1)
|
.setRandomData("audio_segment_3", 6)
|
||||||
.setRandomData("text_segment_2", 2)
|
.setRandomData("text_segment_1", 1)
|
||||||
.setRandomData("text_segment_3", 3)
|
.setRandomData("text_segment_2", 2)
|
||||||
.setRandomData("period_2_segment_1", 1)
|
.setRandomData("text_segment_3", 3)
|
||||||
.setRandomData("period_2_segment_2", 2)
|
.setRandomData("period_2_segment_1", 1)
|
||||||
.setRandomData("period_2_segment_3", 3);
|
.setRandomData("period_2_segment_2", 2)
|
||||||
|
.setRandomData("period_2_segment_3", 3);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
// dashDownloader.selectRepresentations() isn't called
|
// dashDownloader.selectRepresentations() isn't called
|
||||||
|
|
@ -195,21 +208,23 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6)
|
.setRandomData("audio_segment_2", 5)
|
||||||
.setRandomData("text_segment_1", 1)
|
.setRandomData("audio_segment_3", 6)
|
||||||
.setRandomData("text_segment_2", 2)
|
.setRandomData("text_segment_1", 1)
|
||||||
.setRandomData("text_segment_3", 3);
|
.setRandomData("text_segment_2", 2)
|
||||||
|
.setRandomData("text_segment_3", 3);
|
||||||
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,21 +242,23 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6)
|
.setRandomData("audio_segment_2", 5)
|
||||||
.setRandomData("period_2_segment_1", 1)
|
.setRandomData("audio_segment_3", 6)
|
||||||
.setRandomData("period_2_segment_2", 2)
|
.setRandomData("period_2_segment_1", 1)
|
||||||
.setRandomData("period_2_segment_3", 3);
|
.setRandomData("period_2_segment_2", 2)
|
||||||
|
.setRandomData("period_2_segment_3", 3);
|
||||||
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,17 +276,19 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.newData("audio_segment_2")
|
.setRandomData("audio_segment_1", 4)
|
||||||
.appendReadData(TestUtil.buildTestData(2))
|
.newData("audio_segment_2")
|
||||||
.appendReadError(new IOException())
|
.appendReadData(TestUtil.buildTestData(2))
|
||||||
.appendReadData(TestUtil.buildTestData(3))
|
.appendReadError(new IOException())
|
||||||
.endData()
|
.appendReadData(TestUtil.buildTestData(3))
|
||||||
.setRandomData("audio_segment_3", 6);
|
.endData()
|
||||||
|
.setRandomData("audio_segment_3", 6);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||||
|
|
@ -285,17 +304,19 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.newData("audio_segment_2")
|
.setRandomData("audio_segment_1", 4)
|
||||||
.appendReadData(TestUtil.buildTestData(2))
|
.newData("audio_segment_2")
|
||||||
.appendReadError(new IOException())
|
.appendReadData(TestUtil.buildTestData(2))
|
||||||
.appendReadData(TestUtil.buildTestData(3))
|
.appendReadError(new IOException())
|
||||||
.endData()
|
.appendReadData(TestUtil.buildTestData(3))
|
||||||
.setRandomData("audio_segment_3", 6);
|
.endData()
|
||||||
|
.setRandomData("audio_segment_3", 6);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET);
|
assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET);
|
||||||
|
|
@ -319,13 +340,15 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6);
|
.setRandomData("audio_segment_2", 5)
|
||||||
|
.setRandomData("audio_segment_3", 6);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||||
|
|
@ -340,16 +363,18 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6)
|
.setRandomData("audio_segment_2", 5)
|
||||||
.setRandomData("text_segment_1", 1)
|
.setRandomData("audio_segment_3", 6)
|
||||||
.setRandomData("text_segment_2", 2)
|
.setRandomData("text_segment_1", 1)
|
||||||
.setRandomData("text_segment_3", 3);
|
.setRandomData("text_segment_2", 2)
|
||||||
|
.setRandomData("text_segment_3", 3);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
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)});
|
||||||
|
|
@ -360,10 +385,12 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)
|
new FakeDataSet()
|
||||||
.setRandomData("test_segment_1", 4);
|
.setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)
|
||||||
|
.setRandomData("test_segment_1", 4);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||||
|
|
@ -379,13 +406,15 @@ 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 =
|
||||||
.setData(TEST_MPD_URI, TEST_MPD)
|
new FakeDataSet()
|
||||||
.setRandomData("audio_init_data", 10)
|
.setData(TEST_MPD_URI, TEST_MPD)
|
||||||
.setRandomData("audio_segment_1", 4)
|
.setRandomData("audio_init_data", 10)
|
||||||
.setRandomData("audio_segment_2", 5)
|
.setRandomData("audio_segment_1", 4)
|
||||||
.setRandomData("audio_segment_3", 6);
|
.setRandomData("audio_segment_2", 5)
|
||||||
|
.setRandomData("audio_segment_3", 6);
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
|
|
||||||
dashDownloader.selectRepresentations(
|
dashDownloader.selectRepresentations(
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
1
library/dash/src/test/resources/robolectric.properties
Normal file
1
library/dash/src/test/resources/robolectric.properties
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.hls.offline;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data for HLS downloading tests.
|
|
||||||
*/
|
|
||||||
/* package */ interface HlsDownloadTestData {
|
|
||||||
|
|
||||||
String MASTER_PLAYLIST_URI = "test.m3u8";
|
|
||||||
|
|
||||||
String MEDIA_PLAYLIST_0_DIR = "gear0/";
|
|
||||||
String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8";
|
|
||||||
String MEDIA_PLAYLIST_1_DIR = "gear1/";
|
|
||||||
String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8";
|
|
||||||
String MEDIA_PLAYLIST_2_DIR = "gear2/";
|
|
||||||
String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8";
|
|
||||||
String MEDIA_PLAYLIST_3_DIR = "gear3/";
|
|
||||||
String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8";
|
|
||||||
|
|
||||||
byte[] MASTER_PLAYLIST_DATA =
|
|
||||||
("#EXTM3U\n"
|
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n"
|
|
||||||
+ MEDIA_PLAYLIST_1_URI + "\n"
|
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
|
|
||||||
+ MEDIA_PLAYLIST_2_URI + "\n"
|
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
|
|
||||||
+ MEDIA_PLAYLIST_3_URI + "\n"
|
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n"
|
|
||||||
+ MEDIA_PLAYLIST_0_URI).getBytes();
|
|
||||||
|
|
||||||
byte[] MEDIA_PLAYLIST_DATA =
|
|
||||||
("#EXTM3U\n"
|
|
||||||
+ "#EXT-X-TARGETDURATION:10\n"
|
|
||||||
+ "#EXT-X-VERSION:3\n"
|
|
||||||
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
|
||||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
|
||||||
+ "#EXTINF:9.97667,\n"
|
|
||||||
+ "fileSequence0.ts\n"
|
|
||||||
+ "#EXTINF:9.97667,\n"
|
|
||||||
+ "fileSequence1.ts\n"
|
|
||||||
+ "#EXTINF:9.97667,\n"
|
|
||||||
+ "fileSequence2.ts\n"
|
|
||||||
+ "#EXT-X-ENDLIST").getBytes();
|
|
||||||
|
|
||||||
String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8";
|
|
||||||
|
|
||||||
byte[] ENC_MEDIA_PLAYLIST_DATA =
|
|
||||||
("#EXTM3U\n"
|
|
||||||
+ "#EXT-X-TARGETDURATION:10\n"
|
|
||||||
+ "#EXT-X-VERSION:3\n"
|
|
||||||
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
|
||||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
|
||||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n"
|
|
||||||
+ "#EXTINF:9.97667,\n"
|
|
||||||
+ "fileSequence0.ts\n"
|
|
||||||
+ "#EXTINF:9.97667,\n"
|
|
||||||
+ "fileSequence1.ts\n"
|
|
||||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n"
|
|
||||||
+ "#EXTINF:9.97667,\n"
|
|
||||||
+ "fileSequence2.ts\n"
|
|
||||||
+ "#EXT-X-ENDLIST").getBytes();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
@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
|
||||||
* @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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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 @@
|
||||||
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>
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* 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.hls.offline;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
/** Data for HLS downloading tests. */
|
||||||
|
/* package */ interface HlsDownloadTestData {
|
||||||
|
|
||||||
|
String MASTER_PLAYLIST_URI = "test.m3u8";
|
||||||
|
|
||||||
|
String MEDIA_PLAYLIST_0_DIR = "gear0/";
|
||||||
|
String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8";
|
||||||
|
String MEDIA_PLAYLIST_1_DIR = "gear1/";
|
||||||
|
String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8";
|
||||||
|
String MEDIA_PLAYLIST_2_DIR = "gear2/";
|
||||||
|
String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8";
|
||||||
|
String MEDIA_PLAYLIST_3_DIR = "gear3/";
|
||||||
|
String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8";
|
||||||
|
|
||||||
|
byte[] MASTER_PLAYLIST_DATA =
|
||||||
|
("#EXTM3U\n"
|
||||||
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n"
|
||||||
|
+ MEDIA_PLAYLIST_1_URI
|
||||||
|
+ "\n"
|
||||||
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
|
||||||
|
+ MEDIA_PLAYLIST_2_URI
|
||||||
|
+ "\n"
|
||||||
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
|
||||||
|
+ MEDIA_PLAYLIST_3_URI
|
||||||
|
+ "\n"
|
||||||
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n"
|
||||||
|
+ MEDIA_PLAYLIST_0_URI)
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
|
||||||
|
byte[] MEDIA_PLAYLIST_DATA =
|
||||||
|
("#EXTM3U\n"
|
||||||
|
+ "#EXT-X-TARGETDURATION:10\n"
|
||||||
|
+ "#EXT-X-VERSION:3\n"
|
||||||
|
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
||||||
|
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||||
|
+ "#EXTINF:9.97667,\n"
|
||||||
|
+ "fileSequence0.ts\n"
|
||||||
|
+ "#EXTINF:9.97667,\n"
|
||||||
|
+ "fileSequence1.ts\n"
|
||||||
|
+ "#EXTINF:9.97667,\n"
|
||||||
|
+ "fileSequence2.ts\n"
|
||||||
|
+ "#EXT-X-ENDLIST")
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
|
||||||
|
String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8";
|
||||||
|
|
||||||
|
byte[] ENC_MEDIA_PLAYLIST_DATA =
|
||||||
|
("#EXTM3U\n"
|
||||||
|
+ "#EXT-X-TARGETDURATION:10\n"
|
||||||
|
+ "#EXT-X-VERSION:3\n"
|
||||||
|
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
||||||
|
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||||
|
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n"
|
||||||
|
+ "#EXTINF:9.97667,\n"
|
||||||
|
+ "fileSequence0.ts\n"
|
||||||
|
+ "#EXTINF:9.97667,\n"
|
||||||
|
+ "fileSequence1.ts\n"
|
||||||
|
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n"
|
||||||
|
+ "#EXTINF:9.97667,\n"
|
||||||
|
+ "fileSequence2.ts\n"
|
||||||
|
+ "#EXT-X-ENDLIST")
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
}
|
||||||
|
|
@ -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,40 +41,47 @@ 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 =
|
||||||
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
|
new FakeDataSet()
|
||||||
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
|
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
|
||||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
|
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
|
||||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11)
|
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
|
||||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12)
|
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11)
|
||||||
.setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA)
|
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12)
|
||||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13)
|
.setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA)
|
||||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14)
|
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13)
|
||||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15);
|
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14)
|
||||||
|
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15);
|
||||||
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 {
|
||||||
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 {
|
||||||
.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 {
|
||||||
.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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 =
|
||||||
.setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)
|
new FakeDataSet()
|
||||||
.setRandomData("enc.key", 8)
|
.setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)
|
||||||
.setRandomData("enc2.key", 9)
|
.setRandomData("enc.key", 8)
|
||||||
.setRandomData("fileSequence0.ts", 10)
|
.setRandomData("enc2.key", 9)
|
||||||
.setRandomData("fileSequence1.ts", 11)
|
.setRandomData("fileSequence0.ts", 10)
|
||||||
.setRandomData("fileSequence2.ts", 12);
|
.setRandomData("fileSequence1.ts", 11)
|
||||||
hlsDownloader =
|
.setRandomData("fileSequence2.ts", 12);
|
||||||
getHlsDownloader(ENC_MEDIA_PLAYLIST_URI);
|
hlsDownloader = 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 {
|
||||||
|
|
||||||
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 @@
|
||||||
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,70 +27,85 @@ 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 =
|
||||||
+ "\n"
|
" #EXTM3U \n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
+ "\n"
|
||||||
+ "http://example.com/low.m3u8\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||||
+ "\n"
|
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
+ "http://example.com/low.m3u8\n"
|
||||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
+ "\n"
|
||||||
+ "\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n"
|
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||||
+ "http://example.com/mid.m3u8\n"
|
+ "\n"
|
||||||
+ "\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n"
|
+ "http://example.com/mid.m3u8\n"
|
||||||
+ "http://example.com/hi.m3u8\n"
|
+ "\n"
|
||||||
+ "\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
+ "http://example.com/hi.m3u8\n"
|
||||||
+ "http://example.com/audio-only.m3u8";
|
+ "\n"
|
||||||
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
||||||
|
+ "http://example.com/audio-only.m3u8";
|
||||||
|
|
||||||
private static final String PLAYLIST_WITH_AVG_BANDWIDTH = " #EXTM3U \n"
|
private static final String PLAYLIST_WITH_AVG_BANDWIDTH =
|
||||||
+ "\n"
|
" #EXTM3U \n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
+ "\n"
|
||||||
+ "http://example.com/low.m3u8\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||||
+ "\n"
|
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000,"
|
+ "http://example.com/low.m3u8\n"
|
||||||
+ "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
+ "\n"
|
||||||
+ "http://example.com/spaces_in_codecs.m3u8\n";
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000,"
|
||||||
|
+ "CODECS=\"mp4a.40.2 , avc1.66.30 \"\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"
|
||||||
+ "http://example.com/low.m3u8\n";
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||||
|
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\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,"
|
||||||
+ "http://example.com/low.m3u8\n";
|
+ "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";
|
||||||
|
|
||||||
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,"
|
||||||
+ "CLOSED-CAPTIONS=NONE\n"
|
+ "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
|
||||||
+ "http://example.com/low.m3u8\n";
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||||
|
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
|
||||||
|
+ "CLOSED-CAPTIONS=NONE\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 =
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
"#EXTM3U\n"
|
||||||
+ "uri1.m3u8\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
+ "uri1.m3u8\n"
|
||||||
+ "uri2.m3u8\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n"
|
+ "uri2.m3u8\n"
|
||||||
+ "uri1.m3u8\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n"
|
||||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n"
|
+ "uri1.m3u8\n"
|
||||||
+ "uri2.m3u8\n"
|
+ "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n"
|
||||||
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\","
|
+ "uri2.m3u8\n"
|
||||||
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n"
|
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\","
|
||||||
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\","
|
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n"
|
||||||
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n";
|
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\","
|
||||||
|
+ "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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
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 {
|
||||||
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,49 +26,53 @@ 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 =
|
||||||
+ "#EXT-X-VERSION:3\n"
|
"#EXTM3U\n"
|
||||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
+ "#EXT-X-VERSION:3\n"
|
||||||
+ "#EXT-X-START:TIME-OFFSET=-25"
|
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||||
+ "#EXT-X-TARGETDURATION:8\n"
|
+ "#EXT-X-START:TIME-OFFSET=-25"
|
||||||
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
+ "#EXT-X-TARGETDURATION:8\n"
|
||||||
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
||||||
+ "#EXT-X-ALLOW-CACHE:YES\n"
|
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
||||||
+ "\n"
|
+ "#EXT-X-ALLOW-CACHE:YES\n"
|
||||||
+ "#EXTINF:7.975,\n"
|
+ "\n"
|
||||||
+ "#EXT-X-BYTERANGE:51370@0\n"
|
+ "#EXTINF:7.975,\n"
|
||||||
+ "https://priv.example.com/fileSequence2679.ts\n"
|
+ "#EXT-X-BYTERANGE:51370@0\n"
|
||||||
+ "\n"
|
+ "https://priv.example.com/fileSequence2679.ts\n"
|
||||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
|
+ "\n"
|
||||||
+ "#EXTINF:7.975,\n"
|
+ "#EXT-X-KEY:METHOD=AES-128,"
|
||||||
+ "#EXT-X-BYTERANGE:51501@2147483648\n"
|
+ "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
|
||||||
+ "https://priv.example.com/fileSequence2680.ts\n"
|
+ "#EXTINF:7.975,\n"
|
||||||
+ "\n"
|
+ "#EXT-X-BYTERANGE:51501@2147483648\n"
|
||||||
+ "#EXT-X-KEY:METHOD=NONE\n"
|
+ "https://priv.example.com/fileSequence2680.ts\n"
|
||||||
+ "#EXTINF:7.941,\n"
|
+ "\n"
|
||||||
+ "#EXT-X-BYTERANGE:51501\n" // @2147535149
|
+ "#EXT-X-KEY:METHOD=NONE\n"
|
||||||
+ "https://priv.example.com/fileSequence2681.ts\n"
|
+ "#EXTINF:7.941,\n"
|
||||||
+ "\n"
|
+ "#EXT-X-BYTERANGE:51501\n" // @2147535149
|
||||||
+ "#EXT-X-DISCONTINUITY\n"
|
+ "https://priv.example.com/fileSequence2681.ts\n"
|
||||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n"
|
+ "\n"
|
||||||
+ "#EXTINF:7.975,\n"
|
+ "#EXT-X-DISCONTINUITY\n"
|
||||||
+ "#EXT-X-BYTERANGE:51740\n" // @2147586650
|
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n"
|
||||||
+ "https://priv.example.com/fileSequence2682.ts\n"
|
+ "#EXTINF:7.975,\n"
|
||||||
+ "\n"
|
+ "#EXT-X-BYTERANGE:51740\n" // @2147586650
|
||||||
+ "#EXTINF:7.975,\n"
|
+ "https://priv.example.com/fileSequence2682.ts\n"
|
||||||
+ "https://priv.example.com/fileSequence2683.ts\n"
|
+ "\n"
|
||||||
+ "#EXT-X-ENDLIST";
|
+ "#EXTINF:7.975,\n"
|
||||||
InputStream inputStream = new ByteArrayInputStream(
|
+ "https://priv.example.com/fileSequence2683.ts\n"
|
||||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
+ "#EXT-X-ENDLIST";
|
||||||
|
InputStream inputStream =
|
||||||
|
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 {
|
||||||
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 {
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
1
library/hls/src/test/resources/robolectric.properties
Normal file
1
library/hls/src/test/resources/robolectric.properties
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -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 @@
|
||||||
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 @@
|
||||||
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;
|
||||||
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(
|
List<TrackKey> keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0));
|
||||||
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 =
|
||||||
newStreamElement("1", formats[0][0], formats[0][2]),
|
newSsManifest(
|
||||||
newStreamElement("2", formats[1][0]));
|
newStreamElement("1", formats[0][0], formats[0][2]),
|
||||||
|
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
manifest=src/test/AndroidManifest.xml
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue