mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +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:
|
||||
|
||||
```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
|
||||
|
|
@ -51,9 +51,9 @@ dependencies on the Core, DASH and UI library modules, as might be required for
|
|||
an app that plays DASH content:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:exoplayer-core:2.X.X'
|
||||
compile '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-core:2.X.X'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash: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
|
||||
|
|
@ -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:
|
||||
|
||||
```gradle
|
||||
compile project(':exoplayer-library-core')
|
||||
compile project(':exoplayer-library-dash')
|
||||
compile project(':exoplayer-library-ui')
|
||||
implementation project(':exoplayer-library-core')
|
||||
implementation project(':exoplayer-library-dash')
|
||||
implementation project(':exoplayer-library-ui')
|
||||
```
|
||||
|
||||
## Developing ExoPlayer ##
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
# 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 ###
|
||||
|
||||
* Player interface:
|
||||
|
|
@ -21,7 +41,7 @@
|
|||
* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are
|
||||
performed. The `SeekParameters` class contains defaults for exact seeking and
|
||||
seeking to the closest sync points before, either side or after specified seek
|
||||
positions. `SeekParameters` are not currently supported when playing HLS
|
||||
positions. `SeekParameters` are not currently supported when playing HLS
|
||||
streams.
|
||||
* DefaultTrackSelector:
|
||||
* Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
|
||||
|
|
|
|||
|
|
@ -12,13 +12,16 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.7.1'
|
||||
releaseVersionCode = 2701
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
// by the library requires API level 16 or greater.
|
||||
minSdkVersion = 14
|
||||
compileSdkVersion = 27
|
||||
targetSdkVersion = 27
|
||||
compileSdkVersion = 27
|
||||
buildToolsVersion = '26.0.2'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '27.0.0'
|
||||
|
|
@ -28,7 +31,6 @@ project.ext {
|
|||
junitVersion = '4.12'
|
||||
truthVersion = '0.39'
|
||||
robolectricVersion = '3.7.1'
|
||||
releaseVersion = '2.7.0'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ include modulePrefix + 'library-hls'
|
|||
include modulePrefix + 'library-smoothstreaming'
|
||||
include modulePrefix + 'library-ui'
|
||||
include modulePrefix + 'testutils'
|
||||
include modulePrefix + 'testutils-robolectric'
|
||||
include modulePrefix + 'extension-ffmpeg'
|
||||
include modulePrefix + 'extension-flac'
|
||||
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-ui').projectDir = new File(rootDir, 'library/ui')
|
||||
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-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
|
@ -42,11 +44,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'extension-cast')
|
||||
compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cast')
|
||||
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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.castdemo"
|
||||
android:versionCode="2700"
|
||||
android:versionName="2.7.0">
|
||||
package="com.google.android.exoplayer2.castdemo">
|
||||
|
||||
<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"
|
||||
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.DefaultEventListener;
|
||||
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.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
|
|
@ -281,8 +282,12 @@ import java.util.ArrayList;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, Object manifest, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
if (timeline.isEmpty()) {
|
||||
castMediaQueueCreationPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
|
@ -41,10 +43,11 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'extension-ima')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,10 @@
|
|||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo"
|
||||
android:versionCode="2700"
|
||||
android:versionName="2.7.0">
|
||||
package="com.google.android.exoplayer2.imademo">
|
||||
|
||||
<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"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
|
@ -55,15 +57,16 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-ima')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-opus')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-vp9')
|
||||
withExtensionsCompile project(path: modulePrefix + 'extension-rtmp')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
|
||||
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"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2700"
|
||||
android:versionName="2.7.0">
|
||||
package="com.google.android.exoplayer2.demo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -26,22 +26,24 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// This dependency is necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
||||
// is included via:
|
||||
// These dependencies are necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
||||
// 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-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
compile 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
testCompile project(modulePrefix + 'testutils')
|
||||
testCompile 'junit:junit:' + junitVersion
|
||||
testCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'org.robolectric:robolectric:' + robolectricVersion
|
||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'junit:junit:' + junitVersion
|
||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
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.testutil.TimelineAsserts;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
|
|
@ -25,11 +26,9 @@ import org.junit.Test;
|
|||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** Tests for {@link CastTimelineTracker}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
|
||||
public class CastTimelineTrackerTest {
|
||||
|
||||
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});
|
||||
|
||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||
mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS);
|
||||
mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
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);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
|
|
@ -62,7 +61,7 @@ public class CastTimelineTrackerTest {
|
|||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS);
|
||||
mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
|
|
@ -80,7 +79,7 @@ public class CastTimelineTrackerTest {
|
|||
DURATION_5_MS,
|
||||
MediaInfo.UNKNOWN_DURATION
|
||||
});
|
||||
mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS);
|
||||
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
|
|
@ -89,7 +88,7 @@ public class CastTimelineTrackerTest {
|
|||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
|
||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
|
|
@ -98,7 +97,7 @@ public class CastTimelineTrackerTest {
|
|||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS);
|
||||
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
|
|
@ -112,7 +111,7 @@ public class CastTimelineTrackerTest {
|
|||
int[] itemIds, String[] contentIds, long[] durationsMs) {
|
||||
ArrayList<MediaQueueItem> items = new ArrayList<>();
|
||||
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);
|
||||
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
|
||||
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
|
||||
|
|
@ -123,10 +122,11 @@ public class CastTimelineTrackerTest {
|
|||
return status;
|
||||
}
|
||||
|
||||
private static MediaInfo mockMediaInfo(String contentId, long durationMs) {
|
||||
MediaInfo mediaInfo = Mockito.mock(MediaInfo.class);
|
||||
Mockito.when(mediaInfo.getContentId()).thenReturn(contentId);
|
||||
Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs);
|
||||
return mediaInfo;
|
||||
private static MediaInfo getMediaInfo(String contentId, long durationMs) {
|
||||
return new MediaInfo.Builder(contentId)
|
||||
.setStreamDuration(durationMs)
|
||||
.setContentType(MimeTypes.APPLICATION_MP4)
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
manifest=src/test/AndroidManifest.xml
|
||||
|
|
@ -35,16 +35,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile files('libs/cronet_api.jar')
|
||||
compile files('libs/cronet_impl_common_java.jar')
|
||||
compile files('libs/cronet_impl_native_java.jar')
|
||||
androidTestCompile project(modulePrefix + 'library')
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
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
|
||||
api files('libs/cronet_api.jar')
|
||||
implementation files('libs/cronet_impl_common_java.jar')
|
||||
implementation files('libs/cronet_impl_native_java.jar')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -18,16 +18,6 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<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"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -19,9 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
|
|||
import static org.mockito.Mockito.times;
|
||||
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.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -30,11 +27,11 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
/**
|
||||
* Tests for {@link ByteArrayUploadDataProvider}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
/** Tests for {@link ByteArrayUploadDataProvider}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class ByteArrayUploadDataProviderTest {
|
||||
|
||||
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
|
||||
public void setUp() {
|
||||
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
|
||||
MockitoAnnotations.initMocks(this);
|
||||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
|
||||
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);
|
||||
}
|
||||
|
|
@ -90,5 +87,4 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
verify(mockUploadDataSink).onRewindSucceeded();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -31,10 +31,8 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
import android.net.Uri;
|
||||
import android.os.ConditionVariable;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.os.SystemClock;
|
||||
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.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
||||
|
|
@ -50,6 +48,7 @@ import java.util.Arrays;
|
|||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
|
@ -61,13 +60,14 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.shadows.ShadowSystemClock;
|
||||
|
||||
/**
|
||||
* Tests for {@link CronetDataSource}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
/** Tests for {@link CronetDataSource}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class CronetDataSourceTest {
|
||||
|
||||
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
||||
|
|
@ -85,18 +85,11 @@ public final class CronetDataSourceTest {
|
|||
private UrlResponseInfo testUrlResponseInfo;
|
||||
|
||||
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
||||
@Mock
|
||||
private UrlRequest mockUrlRequest;
|
||||
@Mock
|
||||
private Predicate<String> mockContentTypePredicate;
|
||||
@Mock
|
||||
private TransferListener<CronetDataSource> mockTransferListener;
|
||||
@Mock
|
||||
private Clock mockClock;
|
||||
@Mock
|
||||
private Executor mockExecutor;
|
||||
@Mock
|
||||
private NetworkException mockNetworkException;
|
||||
@Mock private UrlRequest mockUrlRequest;
|
||||
@Mock private Predicate<String> mockContentTypePredicate;
|
||||
@Mock private TransferListener<CronetDataSource> mockTransferListener;
|
||||
@Mock private Executor mockExecutor;
|
||||
@Mock private NetworkException mockNetworkException;
|
||||
@Mock private CronetEngine mockCronetEngine;
|
||||
|
||||
private CronetDataSource dataSourceUnderTest;
|
||||
|
|
@ -104,30 +97,31 @@ public final class CronetDataSourceTest {
|
|||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
false));
|
||||
MockitoAnnotations.initMocks(this);
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
false));
|
||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||
when(mockCronetEngine.newUrlRequestBuilder(
|
||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||
.thenReturn(mockUrlRequestBuilder);
|
||||
when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
|
||||
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
||||
mockStatusResponse();
|
||||
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
|
||||
testPostDataSpec = new DataSpec(
|
||||
Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
|
||||
testPostDataSpec =
|
||||
new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
|
||||
testResponseHeader = new HashMap<>();
|
||||
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
||||
// 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.
|
||||
final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
|
||||
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
// Invoke the callback for the previous request.
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
mockNetworkException);
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest2,
|
||||
testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest2).start();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
// Invoke the callback for the previous request.
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest, testUrlResponseInfo, mockNetworkException);
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest2)
|
||||
.start();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -253,8 +246,8 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRequestOpenFailDueToDnsFailure() {
|
||||
mockResponseStartFailure();
|
||||
when(mockNetworkException.getErrorCode()).thenReturn(
|
||||
NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
when(mockNetworkException.getErrorCode())
|
||||
.thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
|
@ -524,8 +517,8 @@ public final class CronetDataSourceTest {
|
|||
assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||
assertThat(returnedBuffer).isEqualTo(new byte[16]);
|
||||
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
||||
verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest,
|
||||
C.RESULT_END_OF_INPUT);
|
||||
verify(mockTransferListener, never())
|
||||
.onBytesTransferred(dataSourceUnderTest, C.RESULT_END_OF_INPUT);
|
||||
// There should still be only one call to read on cronet.
|
||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
// Check for connection not automatically closed.
|
||||
|
|
@ -534,10 +527,10 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testConnectTimeout() {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
public void testConnectTimeout() throws InterruptedException {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final ConditionVariable timedOutCondition = new ConditionVariable();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
|
|
@ -551,29 +544,29 @@ public final class CronetDataSourceTest {
|
|||
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
|
||||
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
||||
.isEqualTo(TEST_CONNECTION_STATUS);
|
||||
timedOutCondition.open();
|
||||
timedOutLatch.countDown();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
startCondition.block();
|
||||
|
||||
// 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.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS);
|
||||
timedOutCondition.block();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectInterrupted() {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
public void testConnectInterrupted() throws InterruptedException {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final ConditionVariable timedOutCondition = new ConditionVariable();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
|
||||
Thread thread =
|
||||
new Thread() {
|
||||
|
|
@ -588,7 +581,7 @@ public final class CronetDataSourceTest {
|
|||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
||||
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
||||
.isEqualTo(TEST_INVALID_CONNECTION_STATUS);
|
||||
timedOutCondition.open();
|
||||
timedOutLatch.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -596,29 +589,29 @@ public final class CronetDataSourceTest {
|
|||
startCondition.block();
|
||||
|
||||
// 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.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutCondition.block();
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectResponseBeforeTimeout() {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
public void testConnectResponseBeforeTimeout() throws InterruptedException {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final ConditionVariable openCondition = new ConditionVariable();
|
||||
final CountDownLatch openLatch = new CountDownLatch(1);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
openCondition.open();
|
||||
openLatch.countDown();
|
||||
} catch (HttpDataSourceException e) {
|
||||
fail();
|
||||
}
|
||||
|
|
@ -627,20 +620,20 @@ public final class CronetDataSourceTest {
|
|||
startCondition.block();
|
||||
|
||||
// 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.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertThat(openCondition.block(50)).isFalse();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(openLatch);
|
||||
// The response arrives just in time.
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
openCondition.block();
|
||||
openLatch.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectIncreasesConnectionTimeout() throws InterruptedException {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final ConditionVariable timedOutCondition = new ConditionVariable();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
final AtomicInteger openExceptions = new AtomicInteger(0);
|
||||
|
||||
new Thread() {
|
||||
|
|
@ -654,40 +647,36 @@ public final class CronetDataSourceTest {
|
|||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
||||
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
|
||||
openExceptions.getAndIncrement();
|
||||
timedOutCondition.open();
|
||||
timedOutLatch.countDown();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
startCondition.block();
|
||||
|
||||
// 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.
|
||||
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// A redirect arrives just in time.
|
||||
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
|
||||
"RandomRedirectedUrl1");
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
||||
|
||||
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
|
||||
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
|
||||
// Give the thread some time to run.
|
||||
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);
|
||||
// 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.
|
||||
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
|
||||
"RandomRedirectedUrl2");
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
||||
|
||||
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
||||
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
|
||||
// Give the thread some time to run.
|
||||
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);
|
||||
// We should still be trying to open as we approach the new timeout.
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we timeout.
|
||||
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs);
|
||||
timedOutCondition.block();
|
||||
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
assertThat(openExceptions.get()).isEqualTo(1);
|
||||
|
|
@ -707,20 +696,22 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
true));
|
||||
public void
|
||||
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true));
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
|
|
@ -736,21 +727,23 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
||||
throws HttpDataSourceException {
|
||||
public void
|
||||
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
||||
throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
true));
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true));
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
|
|
@ -778,18 +771,19 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock,
|
||||
null,
|
||||
true));
|
||||
dataSourceUnderTest =
|
||||
spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
mockExecutor,
|
||||
mockContentTypePredicate,
|
||||
mockTransferListener,
|
||||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
Clock.DEFAULT,
|
||||
null,
|
||||
true));
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
|
||||
|
|
@ -804,8 +798,9 @@ public final class CronetDataSourceTest {
|
|||
|
||||
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
|
||||
// the subsequent open() call succeeds.
|
||||
doThrow(new NullPointerException()).when(mockTransferListener).onTransferEnd(
|
||||
dataSourceUnderTest);
|
||||
doThrow(new NullPointerException())
|
||||
.when(mockTransferListener)
|
||||
.onTransferEnd(dataSourceUnderTest);
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
try {
|
||||
dataSourceUnderTest.close();
|
||||
|
|
@ -833,13 +828,12 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testReadInterrupted() throws HttpDataSourceException {
|
||||
when(mockClock.elapsedRealtime()).thenReturn(0L);
|
||||
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
final ConditionVariable startCondition = buildReadStartedCondition();
|
||||
final ConditionVariable timedOutCondition = new ConditionVariable();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
Thread thread =
|
||||
new Thread() {
|
||||
|
|
@ -851,17 +845,17 @@ public final class CronetDataSourceTest {
|
|||
} catch (HttpDataSourceException e) {
|
||||
// Expected.
|
||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
||||
timedOutCondition.open();
|
||||
timedOutLatch.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
startCondition.block();
|
||||
|
||||
assertThat(timedOutCondition.block(50)).isFalse();
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
timedOutCondition.block();
|
||||
timedOutLatch.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -876,122 +870,135 @@ public final class CronetDataSourceTest {
|
|||
// Helper methods.
|
||||
|
||||
private void mockStatusResponse() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
UrlRequest.StatusListener statusListener =
|
||||
(UrlRequest.StatusListener) invocation.getArguments()[0];
|
||||
statusListener.onStatus(TEST_CONNECTION_STATUS);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).getStatus(any(UrlRequest.StatusListener.class));
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
UrlRequest.StatusListener statusListener =
|
||||
(UrlRequest.StatusListener) invocation.getArguments()[0];
|
||||
statusListener.onStatus(TEST_CONNECTION_STATUS);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.getStatus(any(UrlRequest.StatusListener.class));
|
||||
}
|
||||
|
||||
private void mockResponseStartSuccess() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void mockResponseStartRedirect() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(307), // statusCode
|
||||
"http://redirect.location.com");
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(307), // statusCode
|
||||
"http://redirect.location.com");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void mockSingleRedirectSuccess() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (!redirectCalled) {
|
||||
redirectCalled = true;
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
||||
"http://example.com/video/redirect");
|
||||
} else {
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (!redirectCalled) {
|
||||
redirectCalled = true;
|
||||
dataSourceUnderTest.onRedirectReceived(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfoWithUrl("http://example.com/video", 300),
|
||||
"http://example.com/video/redirect");
|
||||
} else {
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void mockFollowRedirectSuccess() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).followRedirect();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.followRedirect();
|
||||
}
|
||||
|
||||
private void mockResponseStartFailure() {
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void mockReadSuccess(int position, int length) {
|
||||
final int[] positionAndRemaining = new int[] {position, length};
|
||||
doAnswer(new Answer<Void>() {
|
||||
@Override
|
||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (positionAndRemaining[1] == 0) {
|
||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
||||
} else {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||
positionAndRemaining[0] += readLength;
|
||||
positionAndRemaining[1] -= readLength;
|
||||
dataSourceUnderTest.onReadCompleted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
inputBuffer);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
doAnswer(
|
||||
new Answer<Void>() {
|
||||
@Override
|
||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
if (positionAndRemaining[1] == 0) {
|
||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
||||
} else {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||
positionAndRemaining[0] += readLength;
|
||||
positionAndRemaining[1] -= readLength;
|
||||
dataSourceUnderTest.onReadCompleted(
|
||||
mockUrlRequest, testUrlResponseInfo, inputBuffer);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.read(any(ByteBuffer.class));
|
||||
}
|
||||
|
||||
private void mockReadFailure() {
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.read(any(ByteBuffer.class));
|
||||
}
|
||||
|
|
@ -999,13 +1006,13 @@ public final class CronetDataSourceTest {
|
|||
private ConditionVariable buildReadStartedCondition() {
|
||||
final ConditionVariable startedCondition = new ConditionVariable();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
startedCondition.open();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
startedCondition.open();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.read(any(ByteBuffer.class));
|
||||
return startedCondition;
|
||||
|
|
@ -1013,16 +1020,26 @@ public final class CronetDataSourceTest {
|
|||
|
||||
private ConditionVariable buildUrlRequestStartedCondition() {
|
||||
final ConditionVariable startedCondition = new ConditionVariable();
|
||||
doAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
startedCondition.open();
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
doAnswer(
|
||||
new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
startedCondition.open();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.when(mockUrlRequest)
|
||||
.start();
|
||||
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) {
|
||||
return buildTestDataBuffer(position, length).array();
|
||||
}
|
||||
|
|
@ -1045,5 +1062,4 @@ public final class CronetDataSourceTest {
|
|||
testBuffer.flip();
|
||||
return testBuffer;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
manifest=src/test/AndroidManifest.xml
|
||||
|
|
@ -31,7 +31,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.google.vr:sdk-audio:1.80.0'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ alongside content.
|
|||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
// This dependency is necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
|
||||
// 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-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
compile 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
|
||||
compile 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
||||
androidTestCompile project(modulePrefix + 'library')
|
||||
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
|
||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ import android.support.annotation.Nullable;
|
|||
import android.view.ViewGroup;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
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.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 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
|
||||
public final class ImaAdsMediaSource extends CompositeMediaSource<Void> {
|
||||
public final class ImaAdsMediaSource implements MediaSource {
|
||||
|
||||
private final AdsMediaSource adsMediaSource;
|
||||
private Listener listener;
|
||||
|
||||
/**
|
||||
* 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
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
super.prepareSource(player, isTopLevelSource, listener);
|
||||
this.listener = listener;
|
||||
prepareChildSource(/* id= */ null, adsMediaSource);
|
||||
public void prepareSource(
|
||||
final ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
|
||||
adsMediaSource.prepareSource(
|
||||
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
|
||||
|
|
@ -92,8 +104,7 @@ public final class ImaAdsMediaSource extends CompositeMediaSource<Void> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onChildSourceInfoRefreshed(
|
||||
Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
|
||||
listener.onSourceInfoRefreshed(this, timeline, manifest);
|
||||
public void releaseSource() {
|
||||
adsMediaSource.releaseSource();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ ExoPlayer.
|
|||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile('com.android.support:leanback-v17:' + supportLibraryVersion)
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation('com.android.support:leanback-v17:' + supportLibraryVersion)
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-media-compat:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -105,23 +105,24 @@ public final class MediaSessionConnector {
|
|||
*/
|
||||
public interface PlaybackPreparer extends CommandReceiver {
|
||||
|
||||
long ACTIONS = PlaybackStateCompat.ACTION_PREPARE
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_URI
|
||||
| PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
||||
| PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||
| PlaybackStateCompat.ACTION_PLAY_FROM_URI;
|
||||
long ACTIONS =
|
||||
PlaybackStateCompat.ACTION_PREPARE
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
|
||||
| PlaybackStateCompat.ACTION_PREPARE_FROM_URI
|
||||
| PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
||||
| 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
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE},
|
||||
* {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID},
|
||||
* {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH},
|
||||
* {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI},
|
||||
* {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID},
|
||||
* {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and
|
||||
* {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}.
|
||||
* bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, {@link
|
||||
* PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, {@link
|
||||
* PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, {@link
|
||||
* PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, {@link
|
||||
* PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, {@link
|
||||
* PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and {@link
|
||||
* PlaybackStateCompat#ACTION_PLAY_FROM_URI}.
|
||||
*
|
||||
* @return The bitmask of the supported media actions.
|
||||
*/
|
||||
|
|
@ -264,15 +265,6 @@ public final class MediaSessionConnector {
|
|||
*/
|
||||
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)}.
|
||||
*/
|
||||
|
|
@ -291,9 +283,14 @@ public final class MediaSessionConnector {
|
|||
* See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(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);
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +338,7 @@ public final class MediaSessionConnector {
|
|||
private PlaybackPreparer playbackPreparer;
|
||||
private QueueNavigator queueNavigator;
|
||||
private QueueEditor queueEditor;
|
||||
private RatingCallback ratingCallback;
|
||||
private ExoPlaybackException playbackException;
|
||||
|
||||
/**
|
||||
|
|
@ -471,6 +469,17 @@ public final class MediaSessionConnector {
|
|||
: 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) {
|
||||
if (commandReceiver != null && commandReceiver.getCommands() != null) {
|
||||
for (String command : commandReceiver.getCommands()) {
|
||||
|
|
@ -539,8 +548,8 @@ public final class MediaSessionConnector {
|
|||
actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(
|
||||
player));
|
||||
}
|
||||
if (queueEditor != null) {
|
||||
actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player));
|
||||
if (ratingCallback != null) {
|
||||
actions |= RatingCallback.ACTIONS;
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
|
@ -634,6 +643,10 @@ public final class MediaSessionConnector {
|
|||
& PlaybackPreparer.ACTIONS & action) != 0;
|
||||
}
|
||||
|
||||
private boolean canDispatchToRatingCallback(long action) {
|
||||
return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
|
||||
}
|
||||
|
||||
private boolean canDispatchToPlaybackController(long action) {
|
||||
return (playbackController.getSupportedPlaybackActions(player)
|
||||
& PlaybackController.ACTIONS & action) != 0;
|
||||
|
|
@ -644,11 +657,6 @@ public final class MediaSessionConnector {
|
|||
& 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 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
|
||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||
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.Nullable;
|
||||
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.MediaSessionCompat;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
|
@ -164,11 +163,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
|
|||
this.equalityChecker = equalityChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSupportedQueueEditorActions(@Nullable Player player) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(Player player, MediaDescriptionCompat description) {
|
||||
onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount());
|
||||
|
|
@ -200,11 +194,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
|
|||
queueMediaSource.removeMediaSource(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRating(Player player, RatingCompat rating) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// CommandReceiver implementation.
|
||||
|
||||
@NonNull
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ licensed separately.
|
|||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile('com.squareup.okhttp3:okhttp:3.9.0') {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation('com.squareup.okhttp3:okhttp:3.9.0') {
|
||||
exclude group: 'org.json'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'net.butterflytv.utils:rtmp-client:3.0.1'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
androidTestCompile 'com.google.truth:truth:' + truthVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
private VpxDecoder decoder;
|
||||
private VpxInputBuffer inputBuffer;
|
||||
private VpxOutputBuffer outputBuffer;
|
||||
private VpxOutputBuffer nextOutputBuffer;
|
||||
private DrmSession<ExoMediaCrypto> drmSession;
|
||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||
|
||||
|
|
@ -128,7 +127,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
|
||||
private Bitmap bitmap;
|
||||
private boolean renderedFirstFrame;
|
||||
private boolean forceRenderFrame;
|
||||
private long joiningDeadlineMs;
|
||||
private Surface surface;
|
||||
private VpxOutputBufferRenderer outputBufferRenderer;
|
||||
|
|
@ -144,6 +142,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
private int droppedFrames;
|
||||
private int consecutiveDroppedFrameCount;
|
||||
private int buffersInCodecCount;
|
||||
private long lastRenderTimeUs;
|
||||
|
||||
protected DecoderCounters decoderCounters;
|
||||
|
||||
|
|
@ -254,7 +253,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
try {
|
||||
// Rendering loop.
|
||||
TraceUtil.beginSection("drainAndFeed");
|
||||
while (drainOutputBuffer(positionUs)) {}
|
||||
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
|
||||
while (feedInputBuffer()) {}
|
||||
TraceUtil.endSection();
|
||||
} catch (VpxDecoderException e) {
|
||||
|
|
@ -319,6 +318,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
protected void onStarted() {
|
||||
droppedFrames = 0;
|
||||
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -379,7 +379,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
@CallSuper
|
||||
protected void flushDecoder() throws ExoPlaybackException {
|
||||
waitingForKeys = false;
|
||||
forceRenderFrame = false;
|
||||
buffersInCodecCount = 0;
|
||||
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
|
||||
releaseDecoder();
|
||||
|
|
@ -390,10 +389,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
}
|
||||
if (nextOutputBuffer != null) {
|
||||
nextOutputBuffer.release();
|
||||
nextOutputBuffer = null;
|
||||
}
|
||||
decoder.flush();
|
||||
decoderReceivedBuffers = false;
|
||||
}
|
||||
|
|
@ -408,13 +403,11 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
|
||||
inputBuffer = null;
|
||||
outputBuffer = null;
|
||||
nextOutputBuffer = null;
|
||||
decoder.release();
|
||||
decoder = null;
|
||||
decoderCounters.decoderReleaseCount++;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
decoderReceivedBuffers = false;
|
||||
forceRenderFrame = false;
|
||||
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 nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET}
|
||||
* if the next output buffer is unavailable.
|
||||
* @param positionUs The current playback position.
|
||||
* @param joiningDeadlineMs The joining deadline.
|
||||
* @return Returns whether to drop the current output buffer.
|
||||
* @param earlyUs The time until the buffer should be presented in microseconds. A 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 shouldDropOutputBuffer(
|
||||
long outputBufferTimeUs,
|
||||
long nextOutputBufferTimeUs,
|
||||
long positionUs,
|
||||
long joiningDeadlineMs) {
|
||||
return isBufferLate(outputBufferTimeUs - positionUs)
|
||||
&& (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
|
||||
protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
|
||||
return isBufferLate(earlyUs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -506,11 +492,26 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
*
|
||||
* @param earlyUs The time until the current buffer should be presented in microseconds. A
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
@ -543,6 +544,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
int bufferMode = outputBuffer.mode;
|
||||
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
|
||||
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
if (!renderRgb && !renderYuv) {
|
||||
dropOutputBuffer(outputBuffer);
|
||||
} 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
|
||||
* #processOutputBuffer(long)}.
|
||||
* #processOutputBuffer(long, long)}.
|
||||
*
|
||||
* @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.
|
||||
* @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 {
|
||||
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
|
||||
if (outputBuffer == null) {
|
||||
if (nextOutputBuffer != null) {
|
||||
outputBuffer = nextOutputBuffer;
|
||||
nextOutputBuffer = null;
|
||||
} else {
|
||||
outputBuffer = decoder.dequeueOutputBuffer();
|
||||
}
|
||||
outputBuffer = decoder.dequeueOutputBuffer();
|
||||
if (outputBuffer == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -778,10 +776,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||
buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
|
||||
}
|
||||
|
||||
if (nextOutputBuffer == null) {
|
||||
nextOutputBuffer = decoder.dequeueOutputBuffer();
|
||||
}
|
||||
|
||||
if (outputBuffer.isEndOfStream()) {
|
||||
if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
|
||||
// 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 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.
|
||||
*
|
||||
* @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.
|
||||
* @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) {
|
||||
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
|
||||
if (isBufferLate(outputBuffer.timeUs - positionUs)) {
|
||||
forceRenderFrame = false;
|
||||
if (isBufferLate(earlyUs)) {
|
||||
skipOutputBuffer(outputBuffer);
|
||||
onProcessedOutputBuffer(outputBuffer.timeUs);
|
||||
outputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (forceRenderFrame) {
|
||||
forceRenderFrame = false;
|
||||
long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
|
||||
boolean isStarted = getState() == STATE_STARTED;
|
||||
if (!renderedFirstFrame
|
||||
|| (isStarted
|
||||
&& shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
|
||||
renderOutputBuffer(outputBuffer);
|
||||
onProcessedOutputBuffer(outputBuffer.timeUs);
|
||||
outputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
long nextOutputBufferTimeUs =
|
||||
nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream()
|
||||
? nextOutputBuffer.timeUs
|
||||
: C.TIME_UNSET;
|
||||
|
||||
long earlyUs = outputBuffer.timeUs - positionUs;
|
||||
if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) {
|
||||
forceRenderFrame = true;
|
||||
if (!isStarted) {
|
||||
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);
|
||||
onProcessedOutputBuffer(outputBuffer.timeUs);
|
||||
outputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have yet to render a frame to the current output (either initially or immediately
|
||||
// following a seek), render one irrespective of the state or current position.
|
||||
if (!renderedFirstFrame || (getState() == STATE_STARTED && earlyUs <= 30000)) {
|
||||
if (earlyUs < 30000) {
|
||||
renderOutputBuffer(outputBuffer);
|
||||
onProcessedOutputBuffer(outputBuffer.timeUs);
|
||||
outputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'library-smoothstreaming')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
api project(modulePrefix + 'library-core')
|
||||
api project(modulePrefix + 'library-dash')
|
||||
api project(modulePrefix + 'library-hls')
|
||||
api project(modulePrefix + 'library-smoothstreaming')
|
||||
api project(modulePrefix + 'library-ui')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ android {
|
|||
}
|
||||
test {
|
||||
java.srcDirs += "../../testutils/src/main/java/"
|
||||
java.srcDirs += "../../testutils_robolectric/src/main/java/"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,15 +45,15 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.truth:truth:' + truthVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'com.google.truth:truth:' + truthVersion
|
||||
testCompile 'junit:junit:' + junitVersion
|
||||
testCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'org.robolectric:robolectric:' + robolectricVersion
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||
testImplementation 'junit:junit:' + junitVersion
|
||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
|
|||
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
|
||||
try {
|
||||
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,
|
||||
length == C.LENGTH_UNSET ? completeData.length : offset + length);
|
||||
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
// because it uses a callback.
|
||||
hasPendingPrepare = true;
|
||||
pendingOperationAcks++;
|
||||
internalPlayer.prepare(mediaSource, resetPosition);
|
||||
internalPlayer.prepare(mediaSource, resetPosition, resetState);
|
||||
updatePlaybackInfo(
|
||||
playbackInfo,
|
||||
/* positionDiscontinuity= */ false,
|
||||
|
|
@ -567,10 +567,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
@DiscontinuityReason int positionDiscontinuityReason) {
|
||||
pendingOperationAcks -= operationAcks;
|
||||
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) {
|
||||
// Replace internal unset start position with externally visible start position of zero.
|
||||
playbackInfo =
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ import java.util.Collections;
|
|||
seekParameters = SeekParameters.DEFAULT;
|
||||
playbackInfo =
|
||||
new PlaybackInfo(
|
||||
/* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
|
||||
Timeline.EMPTY, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
|
||||
playbackInfoUpdate = new PlaybackInfoUpdate();
|
||||
rendererCapabilities = new RendererCapabilities[renderers.length];
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
|
|
@ -176,8 +176,9 @@ import java.util.Collections;
|
|||
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
|
||||
}
|
||||
|
||||
public void prepare(MediaSource mediaSource, boolean resetPosition) {
|
||||
handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
|
||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
||||
handler
|
||||
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
|
||||
.sendToTarget();
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +287,10 @@ import java.util.Collections;
|
|||
try {
|
||||
switch (msg.what) {
|
||||
case MSG_PREPARE:
|
||||
prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
|
||||
prepareInternal(
|
||||
(MediaSource) msg.obj,
|
||||
/* resetPosition= */ msg.arg1 != 0,
|
||||
/* resetState= */ msg.arg2 != 0);
|
||||
break;
|
||||
case MSG_SET_PLAY_WHEN_READY:
|
||||
setPlayWhenReadyInternal(msg.arg1 != 0);
|
||||
|
|
@ -339,7 +343,7 @@ import java.util.Collections;
|
|||
}
|
||||
maybeNotifyPlaybackInfoChanged();
|
||||
} catch (ExoPlaybackException e) {
|
||||
Log.e(TAG, "Renderer error.", e);
|
||||
Log.e(TAG, "Playback error.", e);
|
||||
stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
|
||||
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
|
||||
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++;
|
||||
resetInternal(/* releaseMediaSource= */ true, resetPosition, /* resetState= */ true);
|
||||
resetInternal(/* releaseMediaSource= */ true, resetPosition, resetState);
|
||||
loadControl.onPrepared();
|
||||
this.mediaSource = mediaSource;
|
||||
setState(Player.STATE_BUFFERING);
|
||||
|
|
@ -576,7 +580,6 @@ import java.util.Collections;
|
|||
}
|
||||
|
||||
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
|
||||
Timeline timeline = playbackInfo.timeline;
|
||||
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||
|
||||
MediaPeriodId periodId;
|
||||
|
|
@ -607,7 +610,7 @@ import java.util.Collections;
|
|||
}
|
||||
|
||||
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.
|
||||
pendingInitialSeekPosition = seekPosition;
|
||||
} else if (periodPositionUs == C.TIME_UNSET) {
|
||||
|
|
@ -752,7 +755,7 @@ import java.util.Collections;
|
|||
|
||||
private int getFirstPeriodIndex() {
|
||||
Timeline timeline = playbackInfo.timeline;
|
||||
return timeline == null || timeline.isEmpty()
|
||||
return timeline.isEmpty()
|
||||
? 0
|
||||
: timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
|
||||
.firstPeriodIndex;
|
||||
|
|
@ -779,7 +782,7 @@ import java.util.Collections;
|
|||
pendingInitialSeekPosition = null;
|
||||
}
|
||||
if (resetState) {
|
||||
queue.setTimeline(null);
|
||||
queue.setTimeline(Timeline.EMPTY);
|
||||
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
|
||||
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
|
||||
}
|
||||
|
|
@ -788,11 +791,11 @@ import java.util.Collections;
|
|||
}
|
||||
playbackInfo =
|
||||
new PlaybackInfo(
|
||||
resetState ? null : playbackInfo.timeline,
|
||||
resetState ? Timeline.EMPTY : playbackInfo.timeline,
|
||||
resetState ? null : playbackInfo.manifest,
|
||||
resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId,
|
||||
// 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,
|
||||
playbackInfo.playbackState,
|
||||
/* 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 no delivery time is specified, trigger immediate message delivery.
|
||||
sendMessageToTarget(message);
|
||||
} else if (playbackInfo.timeline == null) {
|
||||
} else if (mediaSource == null || pendingPrepareCount > 0) {
|
||||
// Still waiting for initial timeline to resolve position.
|
||||
pendingMessages.add(new PendingMessageInfo(message));
|
||||
} 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()) {
|
||||
deliverMessage(message);
|
||||
if (playbackInfo.playbackState == Player.STATE_READY
|
||||
|
|
@ -838,22 +841,24 @@ import java.util.Collections;
|
|||
}
|
||||
|
||||
private void sendMessageToTargetThread(final PlayerMessage message) {
|
||||
message
|
||||
.getHandler()
|
||||
.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
deliverMessage(message);
|
||||
}
|
||||
});
|
||||
Handler handler = message.getHandler();
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
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 {
|
||||
message.getTarget().handleMessage(message.getType(), message.getPayload());
|
||||
} catch (ExoPlaybackException e) {
|
||||
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
|
||||
} finally {
|
||||
message.markAsProcessed(/* isDelivered= */ true);
|
||||
}
|
||||
|
|
@ -899,7 +904,8 @@ import java.util.Collections;
|
|||
return true;
|
||||
}
|
||||
|
||||
private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) {
|
||||
private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)
|
||||
throws ExoPlaybackException {
|
||||
if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1130,7 +1136,7 @@ import java.util.Collections;
|
|||
playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
|
||||
resolvePendingMessagePositions();
|
||||
|
||||
if (oldTimeline == null) {
|
||||
if (pendingPrepareCount > 0) {
|
||||
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
|
||||
pendingPrepareCount = 0;
|
||||
if (pendingInitialSeekPosition != null) {
|
||||
|
|
@ -1292,8 +1298,8 @@ import java.util.Collections;
|
|||
SeekPosition seekPosition, boolean trySubsequentPeriods) {
|
||||
Timeline timeline = playbackInfo.timeline;
|
||||
Timeline seekTimeline = seekPosition.timeline;
|
||||
if (timeline == null) {
|
||||
// We don't have a timeline yet, so we can't resolve the position.
|
||||
if (timeline.isEmpty()) {
|
||||
// We don't have a valid timeline yet, so we can't resolve the position.
|
||||
return null;
|
||||
}
|
||||
if (seekTimeline.isEmpty()) {
|
||||
|
|
@ -1349,7 +1355,7 @@ import java.util.Collections;
|
|||
// The player has no media source yet.
|
||||
return;
|
||||
}
|
||||
if (playbackInfo.timeline == null) {
|
||||
if (pendingPrepareCount > 0) {
|
||||
// We're waiting to get information about periods.
|
||||
mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -27,27 +27,23 @@ public final class ExoPlayerLibraryInfo {
|
|||
*/
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
* <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
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// 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}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
|||
*/
|
||||
/* package */ final class PlaybackInfo {
|
||||
|
||||
public final @Nullable Timeline timeline;
|
||||
public final Timeline timeline;
|
||||
public final @Nullable Object manifest;
|
||||
public final MediaPeriodId periodId;
|
||||
public final long startPositionUs;
|
||||
|
|
@ -37,7 +37,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
|||
public volatile long bufferedPositionUs;
|
||||
|
||||
public PlaybackInfo(
|
||||
@Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
|
||||
Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
|
||||
this(
|
||||
timeline,
|
||||
/* manifest= */ null,
|
||||
|
|
@ -50,7 +50,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
|||
}
|
||||
|
||||
public PlaybackInfo(
|
||||
@Nullable Timeline timeline,
|
||||
Timeline timeline,
|
||||
@Nullable Object manifest,
|
||||
MediaPeriodId periodId,
|
||||
long startPositionUs,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ public final class PlayerMessage {
|
|||
*
|
||||
* @param messageType The message type.
|
||||
* @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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1443,7 +1443,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
|
||||
}
|
||||
|
||||
if (Util.SDK_INT <= 26) {
|
||||
if (Util.SDK_INT <= 28) {
|
||||
if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0
|
||||
&& state == PLAYSTATE_PLAYING) {
|
||||
// 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_subt = Util.getIntegerCodeForString("subt");
|
||||
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");
|
||||
|
||||
/**
|
||||
|
|
@ -128,7 +127,8 @@ import java.util.List;
|
|||
|
||||
int sampleCount = sampleSizeBox.getSampleCount();
|
||||
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.
|
||||
|
|
@ -193,6 +193,7 @@ import java.util.List;
|
|||
long[] timestamps;
|
||||
int[] flags;
|
||||
long timestampTimeUnits = 0;
|
||||
long duration;
|
||||
|
||||
if (!isRechunkable) {
|
||||
offsets = new long[sampleCount];
|
||||
|
|
@ -260,6 +261,7 @@ import java.util.List;
|
|||
offset += sizes[i];
|
||||
remainingSamplesInChunk--;
|
||||
}
|
||||
duration = timestampTimeUnits + timestampOffset;
|
||||
|
||||
Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
|
||||
// Remove trailing ctts entries with 0-valued sample counts.
|
||||
|
|
@ -294,13 +296,15 @@ import java.util.List;
|
|||
maximumSize = rechunkedResults.maximumSize;
|
||||
timestamps = rechunkedResults.timestamps;
|
||||
flags = rechunkedResults.flags;
|
||||
duration = rechunkedResults.duration;
|
||||
}
|
||||
long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);
|
||||
|
||||
if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
|
||||
// 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.
|
||||
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
|
||||
|
|
@ -317,10 +321,11 @@ import java.util.List;
|
|||
long editStartTime = track.editListMediaTimes[0];
|
||||
long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
|
||||
track.timescale, track.movieTimescale);
|
||||
long lastSampleEndTime = timestampTimeUnits;
|
||||
if (timestamps[0] <= editStartTime && editStartTime < timestamps[1]
|
||||
&& timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) {
|
||||
long paddingTimeUnits = lastSampleEndTime - editEndTime;
|
||||
if (timestamps[0] <= editStartTime
|
||||
&& editStartTime < timestamps[1]
|
||||
&& timestamps[timestamps.length - 1] < editEndTime
|
||||
&& editEndTime <= duration) {
|
||||
long paddingTimeUnits = duration - editEndTime;
|
||||
long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],
|
||||
track.format.sampleRate, track.timescale);
|
||||
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
|
||||
|
|
@ -330,7 +335,7 @@ import java.util.List;
|
|||
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
|
||||
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
|
||||
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
|
||||
// unfragmented files open to interpretation. We handle this as a special case and include all
|
||||
// samples in the edit.
|
||||
long editStartTime = track.editListMediaTimes[0];
|
||||
for (int i = 0; i < timestamps.length; i++) {
|
||||
timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0],
|
||||
C.MICROS_PER_SECOND, track.timescale);
|
||||
timestamps[i] =
|
||||
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.
|
||||
|
|
@ -354,13 +363,15 @@ import java.util.List;
|
|||
int nextSampleIndex = 0;
|
||||
boolean copyMetadata = false;
|
||||
for (int i = 0; i < track.editListDurations.length; i++) {
|
||||
long mediaTime = track.editListMediaTimes[i];
|
||||
if (mediaTime != -1) {
|
||||
long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
|
||||
track.movieTimescale);
|
||||
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
|
||||
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample,
|
||||
false);
|
||||
long editMediaTime = track.editListMediaTimes[i];
|
||||
if (editMediaTime != -1) {
|
||||
long editDuration =
|
||||
Util.scaleLargeTimestamp(
|
||||
track.editListDurations[i], track.timescale, track.movieTimescale);
|
||||
int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true);
|
||||
int endIndex =
|
||||
Util.binarySearchCeil(
|
||||
timestamps, editMediaTime + editDuration, omitClippedSample, false);
|
||||
editedSampleCount += endIndex - startIndex;
|
||||
copyMetadata |= nextSampleIndex != startIndex;
|
||||
nextSampleIndex = endIndex;
|
||||
|
|
@ -377,12 +388,13 @@ import java.util.List;
|
|||
long pts = 0;
|
||||
int sampleIndex = 0;
|
||||
for (int i = 0; i < track.editListDurations.length; i++) {
|
||||
long mediaTime = track.editListMediaTimes[i];
|
||||
long duration = track.editListDurations[i];
|
||||
if (mediaTime != -1) {
|
||||
long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
|
||||
track.movieTimescale);
|
||||
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
|
||||
long editMediaTime = track.editListMediaTimes[i];
|
||||
long editDuration = track.editListDurations[i];
|
||||
if (editMediaTime != -1) {
|
||||
long endMediaTime =
|
||||
editMediaTime
|
||||
+ Util.scaleLargeTimestamp(editDuration, track.timescale, track.movieTimescale);
|
||||
int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true);
|
||||
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
|
||||
if (copyMetadata) {
|
||||
int count = endIndex - startIndex;
|
||||
|
|
@ -392,8 +404,9 @@ import java.util.List;
|
|||
}
|
||||
for (int j = startIndex; j < endIndex; j++) {
|
||||
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
|
||||
long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime,
|
||||
C.MICROS_PER_SECOND, track.timescale);
|
||||
long timeInSegmentUs =
|
||||
Util.scaleLargeTimestamp(
|
||||
timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale);
|
||||
editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
|
||||
if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
|
||||
editedMaximumSize = sizes[j];
|
||||
|
|
@ -401,8 +414,9 @@ import java.util.List;
|
|||
sampleIndex++;
|
||||
}
|
||||
}
|
||||
pts += duration;
|
||||
pts += editDuration;
|
||||
}
|
||||
long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale);
|
||||
|
||||
boolean hasSyncSample = false;
|
||||
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.
|
||||
Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
|
||||
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,
|
||||
editedFlags);
|
||||
return new TrackSampleTable(
|
||||
editedOffsets,
|
||||
editedSizes,
|
||||
editedMaximumSize,
|
||||
editedTimestamps,
|
||||
editedFlags,
|
||||
editedDurationUs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -33,13 +33,21 @@ import com.google.android.exoplayer2.util.Util;
|
|||
public final int maximumSize;
|
||||
public final long[] timestamps;
|
||||
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.sizes = sizes;
|
||||
this.maximumSize = maximumSize;
|
||||
this.timestamps = timestamps;
|
||||
this.flags = flags;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -95,8 +103,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||
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);
|
||||
|
||||
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) {
|
||||
firstVideoTrackIndex = tracks.size();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,19 @@ import com.google.android.exoplayer2.util.Util;
|
|||
* Sample 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,
|
||||
int[] flags) {
|
||||
public TrackSampleTable(
|
||||
long[] offsets,
|
||||
int[] sizes,
|
||||
int maximumSize,
|
||||
long[] timestampsUs,
|
||||
int[] flags,
|
||||
long durationUs) {
|
||||
Assertions.checkArgument(sizes.length == timestampsUs.length);
|
||||
Assertions.checkArgument(offsets.length == timestampsUs.length);
|
||||
Assertions.checkArgument(flags.length == timestampsUs.length);
|
||||
|
|
@ -60,6 +70,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
this.maximumSize = maximumSize;
|
||||
this.timestampsUs = timestampsUs;
|
||||
this.flags = flags;
|
||||
this.durationUs = durationUs;
|
||||
sampleCount = offsets.length;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,12 +100,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
@C.VideoScalingMode
|
||||
private int scalingMode;
|
||||
private boolean renderedFirstFrame;
|
||||
private boolean forceRenderFrame;
|
||||
private long joiningDeadlineMs;
|
||||
private long droppedFrameAccumulationStartTimeMs;
|
||||
private int droppedFrames;
|
||||
private int consecutiveDroppedFrameCount;
|
||||
private int buffersInCodecCount;
|
||||
private long lastRenderTimeUs;
|
||||
|
||||
private int pendingRotationDegrees;
|
||||
private float pendingPixelWidthHeightRatio;
|
||||
|
|
@ -314,6 +314,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
super.onStarted();
|
||||
droppedFrames = 0;
|
||||
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -438,7 +439,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
super.releaseCodec();
|
||||
} finally {
|
||||
buffersInCodecCount = 0;
|
||||
forceRenderFrame = false;
|
||||
if (dummySurface != null) {
|
||||
if (surface == dummySurface) {
|
||||
surface = null;
|
||||
|
|
@ -454,7 +454,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
protected void flushCodec() throws ExoPlaybackException {
|
||||
super.flushCodec();
|
||||
buffersInCodecCount = 0;
|
||||
forceRenderFrame = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -546,15 +545,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
if (surface == dummySurface) {
|
||||
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
|
||||
if (isBufferLate(earlyUs)) {
|
||||
forceRenderFrame = false;
|
||||
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!renderedFirstFrame || forceRenderFrame) {
|
||||
forceRenderFrame = false;
|
||||
long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
|
||||
boolean isStarted = getState() == STATE_STARTED;
|
||||
if (!renderedFirstFrame
|
||||
|| (isStarted
|
||||
&& shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
|
||||
if (Util.SDK_INT >= 21) {
|
||||
renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
|
||||
} else {
|
||||
|
|
@ -563,13 +564,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (getState() != STATE_STARTED) {
|
||||
if (!isStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current
|
||||
// iteration of the rendering loop.
|
||||
long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
|
||||
long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs;
|
||||
earlyUs -= elapsedSinceStartOfLoopUs;
|
||||
|
||||
// Compute the buffer's desired release time in nanoseconds.
|
||||
|
|
@ -583,7 +584,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
|
||||
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
|
||||
&& maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) {
|
||||
forceRenderFrame = true;
|
||||
return false;
|
||||
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
|
||||
dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
|
||||
|
|
@ -607,6 +607,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
Thread.sleep((earlyUs - 10000) / 1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
|
||||
|
|
@ -654,6 +655,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
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.
|
||||
*
|
||||
|
|
@ -738,6 +752,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
TraceUtil.beginSection("releaseOutputBuffer");
|
||||
codec.releaseOutputBuffer(index, true);
|
||||
TraceUtil.endSection();
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
consecutiveDroppedFrameCount = 0;
|
||||
maybeNotifyRenderedFirstFrame();
|
||||
|
|
@ -753,12 +768,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
* @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs,
|
||||
long releaseTimeNs) {
|
||||
protected void renderOutputBufferV21(
|
||||
MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {
|
||||
maybeNotifyVideoSizeChanged();
|
||||
TraceUtil.beginSection("releaseOutputBuffer");
|
||||
codec.releaseOutputBuffer(index, releaseTimeNs);
|
||||
TraceUtil.endSection();
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
consecutiveDroppedFrameCount = 0;
|
||||
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.FakeTrackSelection;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -1169,10 +1171,8 @@ public final class ExoPlayerTest {
|
|||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder("testReprepareAfterPlaybackError")
|
||||
.waitForPlaybackState(Player.STATE_BUFFERING)
|
||||
// Cause an internal exception by seeking to an invalid position while the media source
|
||||
// is still being prepared and the player doesn't immediately know it will fail.
|
||||
.seek(/* windowIndex= */ 100, /* positionMs= */ 0)
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
|
||||
.waitForPlaybackState(Player.STATE_IDLE)
|
||||
.prepareSource(
|
||||
new FakeMediaSource(timeline, /* manifest= */ null),
|
||||
|
|
@ -1203,11 +1203,8 @@ public final class ExoPlayerTest {
|
|||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder("testReprepareAfterPlaybackError")
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_BUFFERING)
|
||||
// Cause an internal exception by seeking to an invalid position while the media source
|
||||
// is still being prepared and the player doesn't immediately know it will fail.
|
||||
.seek(/* windowIndex= */ 100, /* positionMs= */ 0)
|
||||
.waitForSeekProcessed()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
|
||||
.waitForPlaybackState(Player.STATE_IDLE)
|
||||
.seek(/* positionMs= */ 50)
|
||||
.waitForSeekProcessed()
|
||||
|
|
@ -1246,8 +1243,7 @@ public final class ExoPlayerTest {
|
|||
testRunner.assertTimelinesEqual(timeline, timeline);
|
||||
testRunner.assertTimelineChangeReasonsEqual(
|
||||
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
|
||||
testRunner.assertPositionDiscontinuityReasonsEqual(
|
||||
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK);
|
||||
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
|
||||
assertThat(positionHolder[0]).isEqualTo(50);
|
||||
assertThat(positionHolder[1]).isEqualTo(50);
|
||||
}
|
||||
|
|
@ -1288,6 +1284,104 @@ public final class ExoPlayerTest {
|
|||
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
|
||||
public void testSendMessagesDuringPreparation() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
|
|
@ -1421,7 +1515,8 @@ public final class ExoPlayerTest {
|
|||
new FakeMediaSource(timeline, null),
|
||||
/* resetPosition= */ false,
|
||||
/* resetState= */ true)
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.waitForPlaybackState(Player.STATE_BUFFERING)
|
||||
.waitForPlaybackState(Player.STATE_ENDED)
|
||||
.build();
|
||||
new Builder()
|
||||
.setTimeline(timeline)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
import android.util.Pair;
|
||||
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.testutil.RobolectricUtil;
|
||||
import java.util.HashMap;
|
||||
import org.junit.After;
|
||||
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.Player;
|
||||
import com.google.android.exoplayer2.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
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.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import java.io.IOException;
|
||||
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.Player;
|
||||
import com.google.android.exoplayer2.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
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.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import android.os.Handler;
|
|||
import android.os.HandlerThread;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
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.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import java.io.IOException;
|
||||
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.Player;
|
||||
import com.google.android.exoplayer2.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ import static com.google.common.truth.Truth.assertThat;
|
|||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
|
|
|||
|
|
@ -33,17 +33,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
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
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
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.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
|
@ -1113,7 +1114,9 @@ public final class DashMediaSource implements MediaSource {
|
|||
|
||||
@Override
|
||||
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 {
|
||||
Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine);
|
||||
if (!matcher.matches()) {
|
||||
|
|
|
|||
|
|
@ -18,16 +18,6 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.source.dash.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<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"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="25"/>
|
||||
|
||||
</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.util.MimeTypes;
|
||||
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}.
|
||||
*/
|
||||
public final class DashUtilTest extends TestCase {
|
||||
/** Unit tests for {@link DashUtil}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class DashUtilTest {
|
||||
|
||||
@Test
|
||||
public void testLoadDrmInitDataFromManifest() throws Exception {
|
||||
Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData())));
|
||||
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
|
||||
assertThat(drmInitData).isEqualTo(newDrmInitData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDrmInitDataMissing() throws Exception {
|
||||
Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */)));
|
||||
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
|
||||
assertThat(drmInitData).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDrmInitDataNoRepresentations() throws Exception {
|
||||
Period period = newPeriod(newAdaptationSets(/* no representation */));
|
||||
Period period = newPeriod(newAdaptationSets(/* no representation */ ));
|
||||
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
|
||||
assertThat(drmInitData).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadDrmInitDataNoAdaptationSets() throws Exception {
|
||||
Period period = newPeriod(/* no adaptation set */);
|
||||
Period period = newPeriod(/* no adaptation set */ );
|
||||
DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);
|
||||
assertThat(drmInitData).isNull();
|
||||
}
|
||||
|
|
@ -68,8 +73,18 @@ public final class DashUtilTest extends TestCase {
|
|||
}
|
||||
|
||||
private static Representation newRepresentations(DrmInitData drmInitData) {
|
||||
Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4,
|
||||
MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0);
|
||||
Format format =
|
||||
Format.createVideoContainerFormat(
|
||||
"id",
|
||||
MimeTypes.VIDEO_MP4,
|
||||
MimeTypes.VIDEO_H264,
|
||||
"",
|
||||
Format.NO_VALUE,
|
||||
1024,
|
||||
768,
|
||||
Format.NO_VALUE,
|
||||
null,
|
||||
0);
|
||||
if (drmInitData != null) {
|
||||
format = format.copyWithDrmInitData(drmInitData);
|
||||
}
|
||||
|
|
@ -77,8 +92,7 @@ public final class DashUtilTest extends TestCase {
|
|||
}
|
||||
|
||||
private static DrmInitData newDrmInitData() {
|
||||
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
|
||||
new byte[] {1, 4, 7, 0, 3, 6}));
|
||||
return new DrmInitData(
|
||||
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 android.net.Uri;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
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}.
|
||||
*/
|
||||
public class DashManifestParserTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link DashManifestParser}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class DashManifestParserTest {
|
||||
|
||||
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_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template";
|
||||
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 {
|
||||
DashManifestParser parser = new DashManifestParser();
|
||||
parser.parse(Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_1));
|
||||
parser.parse(Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE));
|
||||
parser.parse(
|
||||
Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_1));
|
||||
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 {
|
||||
DashManifestParser parser = new DashManifestParser();
|
||||
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_3_SEGMENT_TEMPLATE));
|
||||
DashManifest mpd =
|
||||
parser.parse(
|
||||
Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_3_SEGMENT_TEMPLATE));
|
||||
|
||||
assertThat(mpd.getPeriodCount()).isEqualTo(1);
|
||||
|
||||
|
|
@ -75,11 +83,13 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
public void testParseMediaPresentationDescriptionCanParseEventStream()
|
||||
throws IOException {
|
||||
@Test
|
||||
public void testParseMediaPresentationDescriptionCanParseEventStream() throws IOException {
|
||||
DashManifestParser parser = new DashManifestParser();
|
||||
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_4_EVENT_STREAM));
|
||||
DashManifest mpd =
|
||||
parser.parse(
|
||||
Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_4_EVENT_STREAM));
|
||||
|
||||
Period period = mpd.getPeriod(0);
|
||||
assertThat(period.eventStreams).hasSize(3);
|
||||
|
|
@ -87,8 +97,14 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
|||
// assert text-only event stream
|
||||
EventStream eventStream1 = period.eventStreams.get(0);
|
||||
assertThat(eventStream1.events.length).isEqualTo(1);
|
||||
EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0,
|
||||
"+ 1 800 10101010".getBytes(), 0);
|
||||
EventMessage expectedEvent1 =
|
||||
new EventMessage(
|
||||
"urn:uuid:XYZY",
|
||||
"call",
|
||||
10000,
|
||||
0,
|
||||
"+ 1 800 10101010".getBytes(Charset.forName(C.UTF8_NAME)),
|
||||
0);
|
||||
assertThat(eventStream1.events[0]).isEqualTo(expectedEvent1);
|
||||
|
||||
// assert CData-structured event stream
|
||||
|
|
@ -135,6 +151,7 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
|||
1000000000));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCea608AccessibilityChannel() {
|
||||
assertThat(
|
||||
DashManifestParser.parseCea608AccessibilityChannel(
|
||||
|
|
@ -175,6 +192,7 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
|||
.isEqualTo(Format.NO_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCea708AccessibilityChannel() {
|
||||
assertThat(
|
||||
DashManifestParser.parseCea708AccessibilityChannel(
|
||||
|
|
@ -226,5 +244,4 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
|||
private static List<Descriptor> buildCea708AccessibilityDescriptors(String value) {
|
||||
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.List;
|
||||
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}.
|
||||
*/
|
||||
public class DashManifestTest extends TestCase {
|
||||
/** Unit tests for {@link DashManifest}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class DashManifestTest {
|
||||
|
||||
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
|
||||
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
|
||||
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
|
||||
|
||||
@Test
|
||||
public void testCopy() throws Exception {
|
||||
Representation[][][] representations = newRepresentations(3, 2, 3);
|
||||
DashManifest sourceManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0]),
|
||||
newAdaptationSet(3, representations[0][1])),
|
||||
newPeriod("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 sourceManifest =
|
||||
newDashManifest(
|
||||
10,
|
||||
newPeriod(
|
||||
"1",
|
||||
1,
|
||||
newAdaptationSet(2, representations[0][0]),
|
||||
newAdaptationSet(3, representations[0][1])),
|
||||
newPeriod(
|
||||
"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(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(0, 0, 1),
|
||||
new RepresentationKey(0, 1, 2),
|
||||
|
||||
new RepresentationKey(1, 0, 1),
|
||||
new RepresentationKey(1, 1, 0),
|
||||
new RepresentationKey(1, 1, 2),
|
||||
|
||||
new RepresentationKey(2, 0, 1),
|
||||
new RepresentationKey(2, 0, 2),
|
||||
new RepresentationKey(2, 1, 0));
|
||||
List<RepresentationKey> keys =
|
||||
Arrays.asList(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(0, 0, 1),
|
||||
new RepresentationKey(0, 1, 2),
|
||||
new RepresentationKey(1, 0, 1),
|
||||
new RepresentationKey(1, 1, 0),
|
||||
new RepresentationKey(1, 1, 2),
|
||||
new RepresentationKey(2, 0, 1),
|
||||
new RepresentationKey(2, 0, 2),
|
||||
new RepresentationKey(2, 1, 0));
|
||||
// Keys don't need to be in any particular order
|
||||
Collections.shuffle(keys, new Random(0));
|
||||
|
||||
DashManifest copyManifest = sourceManifest.copy(keys);
|
||||
|
||||
DashManifest expectedManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||
newAdaptationSet(3, representations[0][1][2])),
|
||||
newPeriod("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])));
|
||||
DashManifest expectedManifest =
|
||||
newDashManifest(
|
||||
10,
|
||||
newPeriod(
|
||||
"1",
|
||||
1,
|
||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||
newAdaptationSet(3, representations[0][1][2])),
|
||||
newPeriod(
|
||||
"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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception {
|
||||
Representation[][][] representations = newRepresentations(2, 1, 1);
|
||||
DashManifest sourceManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0])));
|
||||
DashManifest sourceManifest =
|
||||
newDashManifest(
|
||||
10,
|
||||
newPeriod("1", 1, newAdaptationSet(2, representations[0][0])),
|
||||
newPeriod("4", 4, newAdaptationSet(5, representations[1][0])));
|
||||
|
||||
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(1, 0, 0)));
|
||||
DashManifest copyManifest =
|
||||
sourceManifest.copy(
|
||||
Arrays.asList(new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)));
|
||||
|
||||
DashManifest expectedManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0])));
|
||||
DashManifest expectedManifest =
|
||||
newDashManifest(
|
||||
10,
|
||||
newPeriod("1", 1, newAdaptationSet(2, representations[0][0])),
|
||||
newPeriod("4", 4, newAdaptationSet(5, representations[1][0])));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopySkipPeriod() throws Exception {
|
||||
Representation[][][] representations = newRepresentations(3, 2, 3);
|
||||
DashManifest sourceManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0]),
|
||||
newAdaptationSet(3, representations[0][1])),
|
||||
newPeriod("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 sourceManifest =
|
||||
newDashManifest(
|
||||
10,
|
||||
newPeriod(
|
||||
"1",
|
||||
1,
|
||||
newAdaptationSet(2, representations[0][0]),
|
||||
newAdaptationSet(3, representations[0][1])),
|
||||
newPeriod(
|
||||
"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(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(0, 0, 1),
|
||||
new RepresentationKey(0, 1, 2),
|
||||
DashManifest copyManifest =
|
||||
sourceManifest.copy(
|
||||
Arrays.asList(
|
||||
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),
|
||||
new RepresentationKey(2, 0, 2),
|
||||
new RepresentationKey(2, 1, 0)));
|
||||
|
||||
DashManifest expectedManifest = newDashManifest(7,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||
newAdaptationSet(3, representations[0][1][2])),
|
||||
newPeriod("7", 4,
|
||||
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
||||
newAdaptationSet(9, representations[2][1][0])));
|
||||
DashManifest expectedManifest =
|
||||
newDashManifest(
|
||||
7,
|
||||
newPeriod(
|
||||
"1",
|
||||
1,
|
||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||
newAdaptationSet(3, representations[0][1][2])),
|
||||
newPeriod(
|
||||
"7",
|
||||
4,
|
||||
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
||||
newAdaptationSet(9, representations[2][1][0])));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
|
|
@ -164,8 +198,8 @@ public class DashManifestTest extends TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts,
|
||||
int representationCounts) {
|
||||
private static Representation[][][] newRepresentations(
|
||||
int periodCount, int adaptationSetCounts, int representationCounts) {
|
||||
Representation[][][] representations = new Representation[periodCount][][];
|
||||
for (int i = 0; i < periodCount; i++) {
|
||||
representations[i] = new Representation[adaptationSetCounts][];
|
||||
|
|
@ -184,8 +218,8 @@ public class DashManifestTest extends TestCase {
|
|||
}
|
||||
|
||||
private static DashManifest newDashManifest(int duration, Period... periods) {
|
||||
return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY,
|
||||
Arrays.asList(periods));
|
||||
return new DashManifest(
|
||||
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) {
|
||||
|
|
@ -195,5 +229,4 @@ public class DashManifestTest extends TestCase {
|
|||
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
|
||||
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 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}.
|
||||
*/
|
||||
public class RangedUriTest extends TestCase {
|
||||
/** Unit test for {@link RangedUri}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class RangedUriTest {
|
||||
|
||||
private static final String BASE_URI = "http://www.test.com/";
|
||||
private static final String PARTIAL_URI = "path/file.ext";
|
||||
private static final String FULL_URI = BASE_URI + PARTIAL_URI;
|
||||
|
||||
@Test
|
||||
public void testMerge() {
|
||||
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
|
||||
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
|
||||
|
|
@ -36,6 +38,7 @@ public class RangedUriTest extends TestCase {
|
|||
assertMerge(rangeA, rangeB, expected, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeUnbounded() {
|
||||
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
|
||||
RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);
|
||||
|
|
@ -43,6 +46,7 @@ public class RangedUriTest extends TestCase {
|
|||
assertMerge(rangeA, rangeB, expected, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMerge() {
|
||||
// A and B do not overlap, so should not merge
|
||||
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
|
||||
|
|
@ -65,6 +69,7 @@ public class RangedUriTest extends TestCase {
|
|||
assertNonMerge(rangeA, rangeB, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeWithBaseUri() {
|
||||
RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10);
|
||||
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
|
||||
|
|
@ -85,5 +90,4 @@ public class RangedUriTest extends TestCase {
|
|||
merged = rangeB.attemptMerge(rangeA, baseUrl);
|
||||
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.source.dash.manifest.SegmentBase.SingleSegmentBase;
|
||||
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}.
|
||||
*/
|
||||
public class RepresentationTest extends TestCase {
|
||||
/** Unit test for {@link Representation}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class RepresentationTest {
|
||||
|
||||
@Test
|
||||
public void testGetCacheKey() {
|
||||
String uri = "http://www.google.com";
|
||||
SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
|
||||
Format format = Format.createVideoContainerFormat("0", 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);
|
||||
Format format =
|
||||
Format.createVideoContainerFormat(
|
||||
"0",
|
||||
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");
|
||||
|
||||
format = Format.createVideoContainerFormat("150", 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);
|
||||
format =
|
||||
Format.createVideoContainerFormat(
|
||||
"150",
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,14 +16,17 @@
|
|||
package com.google.android.exoplayer2.source.dash.manifest;
|
||||
|
||||
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}.
|
||||
*/
|
||||
public class UrlTemplateTest extends TestCase {
|
||||
/** Unit test for {@link UrlTemplate}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class UrlTemplateTest {
|
||||
|
||||
@Test
|
||||
public void testRealExamples() {
|
||||
String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)";
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFull() {
|
||||
String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$";
|
||||
UrlTemplate urlTemplate = UrlTemplate.compile(template);
|
||||
|
|
@ -48,6 +52,7 @@ public class UrlTemplateTest extends TestCase {
|
|||
assertThat(url).isEqualTo("650000_a_abc1_b_5000_c_10");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullWithDollarEscaping() {
|
||||
String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$";
|
||||
UrlTemplate urlTemplate = UrlTemplate.compile(template);
|
||||
|
|
@ -55,6 +60,7 @@ public class UrlTemplateTest extends TestCase {
|
|||
assertThat(url).isEqualTo("$650000$_a$_abc1_b_5000_c_10$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSubstitution() {
|
||||
String template = "$IllegalId$";
|
||||
try {
|
||||
|
|
@ -64,5 +70,4 @@ public class UrlTemplateTest extends TestCase {
|
|||
// 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.assertDataCached;
|
||||
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.when;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.offline.DownloadException;
|
||||
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.FakeDataSource;
|
||||
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.upstream.DataSpec;
|
||||
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.IOException;
|
||||
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.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DashDownloader}.
|
||||
*/
|
||||
public class DashDownloaderTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link DashDownloader}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class DashDownloaderTest {
|
||||
|
||||
private SimpleCache cache;
|
||||
private File tempFolder;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
MockitoUtil.setUpMockito(this);
|
||||
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
MockitoAnnotations.initMocks(this);
|
||||
tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
|
||||
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
|
||||
}
|
||||
|
||||
@Override
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
Util.recursiveDelete(tempFolder);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetManifest() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD);
|
||||
FakeDataSet fakeDataSet = new FakeDataSet().setData(TEST_MPD_URI, TEST_MPD);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
DashManifest manifest = dashDownloader.getManifest();
|
||||
|
|
@ -80,15 +83,17 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadManifestFailure() throws Exception {
|
||||
byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10);
|
||||
byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length);
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.newData(TEST_MPD_URI)
|
||||
.appendReadData(testMpdFirstPart)
|
||||
.appendReadError(new IOException())
|
||||
.appendReadData(testMpdSecondPart)
|
||||
.endData();
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.newData(TEST_MPD_URI)
|
||||
.appendReadData(testMpdFirstPart)
|
||||
.appendReadError(new IOException())
|
||||
.appendReadData(testMpdSecondPart)
|
||||
.endData();
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
// fails on the first try
|
||||
|
|
@ -108,13 +113,15 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadRepresentation() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||
|
|
@ -123,17 +130,19 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadRepresentationInSmallParts() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.newData("audio_segment_1")
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.newData("audio_segment_1")
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||
|
|
@ -142,16 +151,18 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadRepresentations() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(
|
||||
|
|
@ -161,19 +172,21 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadAllRepresentations() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3)
|
||||
.setRandomData("period_2_segment_1", 1)
|
||||
.setRandomData("period_2_segment_2", 2)
|
||||
.setRandomData("period_2_segment_3", 3);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3)
|
||||
.setRandomData("period_2_segment_1", 1)
|
||||
.setRandomData("period_2_segment_2", 2)
|
||||
.setRandomData("period_2_segment_3", 3);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
// dashDownloader.selectRepresentations() isn't called
|
||||
|
|
@ -195,21 +208,23 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
dashDownloader.remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressiveDownload() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
|
||||
Factory factory = mock(Factory.class);
|
||||
when(factory.createDataSource()).thenReturn(fakeDataSource);
|
||||
DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI,
|
||||
new DownloaderConstructorHelper(cache, factory));
|
||||
DashDownloader dashDownloader =
|
||||
new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
|
||||
|
||||
dashDownloader.selectRepresentations(
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressiveDownloadSeparatePeriods() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("period_2_segment_1", 1)
|
||||
.setRandomData("period_2_segment_2", 2)
|
||||
.setRandomData("period_2_segment_3", 3);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("period_2_segment_1", 1)
|
||||
.setRandomData("period_2_segment_2", 2)
|
||||
.setRandomData("period_2_segment_3", 3);
|
||||
FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
|
||||
Factory factory = mock(Factory.class);
|
||||
when(factory.createDataSource()).thenReturn(fakeDataSource);
|
||||
DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI,
|
||||
new DownloaderConstructorHelper(cache, factory));
|
||||
DashDownloader dashDownloader =
|
||||
new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
|
||||
|
||||
dashDownloader.selectRepresentations(
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadRepresentationFailure() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.newData("audio_segment_2")
|
||||
.appendReadData(TestUtil.buildTestData(2))
|
||||
.appendReadError(new IOException())
|
||||
.appendReadData(TestUtil.buildTestData(3))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.newData("audio_segment_2")
|
||||
.appendReadData(TestUtil.buildTestData(2))
|
||||
.appendReadError(new IOException())
|
||||
.appendReadData(TestUtil.buildTestData(3))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||
|
|
@ -285,17 +304,19 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCounters() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.newData("audio_segment_2")
|
||||
.appendReadData(TestUtil.buildTestData(2))
|
||||
.appendReadError(new IOException())
|
||||
.appendReadData(TestUtil.buildTestData(3))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.newData("audio_segment_2")
|
||||
.appendReadData(TestUtil.buildTestData(2))
|
||||
.appendReadError(new IOException())
|
||||
.appendReadData(TestUtil.buildTestData(3))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListener() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||
|
|
@ -340,16 +363,18 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveAll() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
dashDownloader.selectRepresentations(
|
||||
new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)});
|
||||
|
|
@ -360,10 +385,12 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepresentationWithoutIndex() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)
|
||||
.setRandomData("test_segment_1", 4);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)
|
||||
.setRandomData("test_segment_1", 4);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
|
||||
|
|
@ -379,13 +406,15 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectRepresentationsClearsPreviousSelection() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6);
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
|
||||
dashDownloader.selectRepresentations(
|
||||
|
|
@ -401,11 +430,13 @@ public class DashDownloaderTest extends InstrumentationTestCase {
|
|||
return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory));
|
||||
}
|
||||
|
||||
private static void assertCounters(DashDownloader dashDownloader, int totalSegments,
|
||||
int downloadedSegments, int downloadedBytes) {
|
||||
private static void assertCounters(
|
||||
DashDownloader dashDownloader,
|
||||
int totalSegments,
|
||||
int downloadedSegments,
|
||||
int downloadedBytes) {
|
||||
assertThat(dashDownloader.getTotalSegments()).isEqualTo(totalSegments);
|
||||
assertThat(dashDownloader.getDownloadedSegments()).isEqualTo(downloadedSegments);
|
||||
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 {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
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.
|
||||
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
||||
} else {
|
||||
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
|
||||
targetPositionUs - mediaPlaylist.startTimeUs, true,
|
||||
!playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
|
||||
chunkMediaSequence =
|
||||
Util.binarySearchFloor(
|
||||
mediaPlaylist.segments,
|
||||
targetPositionUs,
|
||||
/* inclusive= */ true,
|
||||
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
|
||||
+ mediaPlaylist.mediaSequence;
|
||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
|
||||
// We try getting the next chunk without adapting in case that's the reason for falling
|
||||
// behind the live window.
|
||||
|
|
@ -320,7 +324,9 @@ import java.util.List;
|
|||
}
|
||||
|
||||
// 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
|
||||
+ segment.relativeDiscontinuitySequence;
|
||||
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
||||
|
|
|
|||
|
|
@ -366,28 +366,50 @@ public final class HlsMediaSource implements MediaSource,
|
|||
@Override
|
||||
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
|
||||
SinglePeriodTimeline timeline;
|
||||
long presentationStartTimeMs = playlist.hasProgramDateTime ? 0 : C.TIME_UNSET;
|
||||
long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs)
|
||||
: 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;
|
||||
if (playlistTracker.isLive()) {
|
||||
long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs)
|
||||
: C.TIME_UNSET;
|
||||
long offsetFromInitialStartTimeUs =
|
||||
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
long periodDurationUs =
|
||||
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
|
||||
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
|
||||
if (windowDefaultStartPositionUs == C.TIME_UNSET) {
|
||||
windowDefaultStartPositionUs = segments.isEmpty() ? 0
|
||||
: segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;
|
||||
}
|
||||
timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs,
|
||||
periodDurationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs,
|
||||
true, !playlist.hasEndTag);
|
||||
timeline =
|
||||
new SinglePeriodTimeline(
|
||||
presentationStartTimeMs,
|
||||
windowStartTimeMs,
|
||||
periodDurationUs,
|
||||
/* windowDurationUs= */ playlist.durationUs,
|
||||
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
|
||||
windowDefaultStartPositionUs,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ !playlist.hasEndTag);
|
||||
} else /* not live */ {
|
||||
if (windowDefaultStartPositionUs == C.TIME_UNSET) {
|
||||
windowDefaultStartPositionUs = 0;
|
||||
}
|
||||
timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs,
|
||||
playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs,
|
||||
windowDefaultStartPositionUs, true, false);
|
||||
timeline =
|
||||
new SinglePeriodTimeline(
|
||||
presentationStartTimeMs,
|
||||
windowStartTimeMs,
|
||||
/* periodDurationUs= */ playlist.durationUs,
|
||||
/* windowDurationUs= */ playlist.durationUs,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
windowDefaultStartPositionUs,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false);
|
||||
}
|
||||
sourceListener.onSourceInfoRefreshed(this, timeline,
|
||||
new HlsManifest(playlistTracker.getMasterPlaylist(), playlist));
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
* @param mediaPlaylist The primary playlist new snapshot.
|
||||
*/
|
||||
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -128,6 +127,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
private HlsUrl primaryHlsUrl;
|
||||
private HlsMediaPlaylist primaryUrlSnapshot;
|
||||
private boolean isLive;
|
||||
private long initialStartTimeUs;
|
||||
|
||||
/**
|
||||
* @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");
|
||||
playlistBundles = new IdentityHashMap<>();
|
||||
playlistRefreshHandler = new Handler();
|
||||
initialStartTimeUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,6 +209,11 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
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
|
||||
* 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) {
|
||||
// This is the first primary url snapshot.
|
||||
isLive = !newSnapshot.hasEndTag;
|
||||
initialStartTimeUs = newSnapshot.startTimeUs;
|
||||
}
|
||||
primaryUrlSnapshot = newSnapshot;
|
||||
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
|
||||
|
|
|
|||
|
|
@ -18,16 +18,6 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.source.hls.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<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"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
</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 android.net.Uri;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
|
||||
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.util.Util;
|
||||
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}. */
|
||||
public class HlsDownloaderTest extends InstrumentationTestCase {
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class HlsDownloaderTest {
|
||||
|
||||
private SimpleCache cache;
|
||||
private File tempFolder;
|
||||
private FakeDataSet fakeDataSet;
|
||||
private HlsDownloader hlsDownloader;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
|
||||
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
|
||||
|
||||
fakeDataSet = new FakeDataSet()
|
||||
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
|
||||
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
|
||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
|
||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11)
|
||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12)
|
||||
.setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA)
|
||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13)
|
||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14)
|
||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15);
|
||||
fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
|
||||
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
|
||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
|
||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11)
|
||||
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12)
|
||||
.setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA)
|
||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13)
|
||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14)
|
||||
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15);
|
||||
hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI);
|
||||
}
|
||||
|
||||
@Override
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
Util.recursiveDelete(tempFolder);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadManifest() throws Exception {
|
||||
HlsMasterPlaylist manifest = hlsDownloader.getManifest();
|
||||
|
||||
|
|
@ -83,17 +89,23 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectRepresentationsClearsPreviousSelection() throws Exception {
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI});
|
||||
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 + "fileSequence1.ts",
|
||||
MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCounterMethods() throws Exception {
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
|
||||
hlsDownloader.download(null);
|
||||
|
|
@ -104,12 +116,12 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitStatus() throws Exception {
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
|
||||
hlsDownloader.download(null);
|
||||
|
||||
HlsDownloader newHlsDownloader =
|
||||
getHlsDownloader(MASTER_PLAYLIST_URI);
|
||||
HlsDownloader newHlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI);
|
||||
newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
|
||||
newHlsDownloader.init();
|
||||
|
||||
|
|
@ -119,16 +131,22 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadRepresentation() throws Exception {
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
|
||||
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 + "fileSequence1.ts",
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadMultipleRepresentations() throws Exception {
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI});
|
||||
hlsDownloader.download(null);
|
||||
|
|
@ -136,9 +154,11 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadAllRepresentations() throws Exception {
|
||||
// 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 + "fileSequence1.ts", 11)
|
||||
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12)
|
||||
|
|
@ -167,6 +187,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
hlsDownloader.remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveAll() throws Exception {
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI});
|
||||
hlsDownloader.download(null);
|
||||
|
|
@ -175,27 +196,32 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadMediaPlaylist() throws Exception {
|
||||
hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI);
|
||||
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
|
||||
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 + "fileSequence1.ts",
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadEncMediaPlaylist() throws Exception {
|
||||
fakeDataSet = new FakeDataSet()
|
||||
.setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)
|
||||
.setRandomData("enc.key", 8)
|
||||
.setRandomData("enc2.key", 9)
|
||||
.setRandomData("fileSequence0.ts", 10)
|
||||
.setRandomData("fileSequence1.ts", 11)
|
||||
.setRandomData("fileSequence2.ts", 12);
|
||||
hlsDownloader =
|
||||
getHlsDownloader(ENC_MEDIA_PLAYLIST_URI);
|
||||
fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)
|
||||
.setRandomData("enc.key", 8)
|
||||
.setRandomData("enc2.key", 9)
|
||||
.setRandomData("fileSequence0.ts", 10)
|
||||
.setRandomData("fileSequence1.ts", 11)
|
||||
.setRandomData("fileSequence2.ts", 12);
|
||||
hlsDownloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI);
|
||||
hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI});
|
||||
hlsDownloader.download(null);
|
||||
|
||||
|
|
@ -204,8 +230,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase {
|
|||
|
||||
private HlsDownloader getHlsDownloader(String mediaPlaylistUri) {
|
||||
Factory factory = new Factory(null).setFakeDataSet(fakeDataSet);
|
||||
return new HlsDownloader(Uri.parse(mediaPlaylistUri),
|
||||
new DownloaderConstructorHelper(cache, factory));
|
||||
return new HlsDownloader(
|
||||
Uri.parse(mediaPlaylistUri), new DownloaderConstructorHelper(cache, factory));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.source.hls.playlist;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
|
@ -26,70 +27,85 @@ import java.io.ByteArrayInputStream;
|
|||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
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}.
|
||||
*/
|
||||
public class HlsMasterPlaylistParserTest extends TestCase {
|
||||
/** Test for {@link HlsMasterPlaylistParserTest}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class HlsMasterPlaylistParserTest {
|
||||
|
||||
private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
|
||||
|
||||
private static final String PLAYLIST_SIMPLE = " #EXTM3U \n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n"
|
||||
+ "http://example.com/mid.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n"
|
||||
+ "http://example.com/hi.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
||||
+ "http://example.com/audio-only.m3u8";
|
||||
private static final String PLAYLIST_SIMPLE =
|
||||
" #EXTM3U \n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n"
|
||||
+ "http://example.com/mid.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n"
|
||||
+ "http://example.com/hi.m3u8\n"
|
||||
+ "\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"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\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_AVG_BANDWIDTH =
|
||||
" #EXTM3U \n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\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"
|
||||
+ "#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_INVALID_HEADER =
|
||||
"#EXTMU3\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"
|
||||
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
private static final String PLAYLIST_WITH_CC =
|
||||
" #EXTM3U \n"
|
||||
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,"
|
||||
+ "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
|
||||
private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n"
|
||||
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
|
||||
+ "CLOSED-CAPTIONS=NONE\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
private static final String PLAYLIST_WITHOUT_CC =
|
||||
" #EXTM3U \n"
|
||||
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,"
|
||||
+ "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
|
||||
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
|
||||
+ "CLOSED-CAPTIONS=NONE\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
|
||||
private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG = "#EXTM3U\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
||||
+ "uri1.m3u8\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
||||
+ "uri2.m3u8\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n"
|
||||
+ "uri1.m3u8\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n"
|
||||
+ "uri2.m3u8\n"
|
||||
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\","
|
||||
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/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";
|
||||
private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
||||
+ "uri1.m3u8\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n"
|
||||
+ "uri2.m3u8\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n"
|
||||
+ "uri1.m3u8\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n"
|
||||
+ "uri2.m3u8\n"
|
||||
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\","
|
||||
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/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 {
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMasterPlaylistWithBandwdithAverage() throws IOException {
|
||||
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI,
|
||||
PLAYLIST_WITH_AVG_BANDWIDTH);
|
||||
HlsMasterPlaylist masterPlaylist =
|
||||
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH);
|
||||
|
||||
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
|
||||
|
|
@ -139,6 +156,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
|
|||
assertThat(variants.get(1).format.bitrate).isEqualTo(1270000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaylistWithInvalidHeader() throws IOException {
|
||||
try {
|
||||
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
|
||||
|
|
@ -148,6 +166,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaylistWithClosedCaption() throws IOException {
|
||||
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);
|
||||
assertThat(playlist.muxedCaptionFormats).hasSize(1);
|
||||
|
|
@ -157,11 +176,13 @@ public class HlsMasterPlaylistParserTest extends TestCase {
|
|||
assertThat(closedCaptionFormat.language).isEqualTo("es");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaylistWithoutClosedCaptions() throws IOException {
|
||||
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC);
|
||||
assertThat(playlist.muxedCaptionFormats).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCodecPropagation() throws IOException {
|
||||
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)
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse(uri);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
ByteArrayInputStream inputStream =
|
||||
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -26,49 +26,53 @@ import java.io.InputStream;
|
|||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
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}.
|
||||
*/
|
||||
public class HlsMediaPlaylistParserTest extends TestCase {
|
||||
/** Test for {@link HlsMediaPlaylistParserTest}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class HlsMediaPlaylistParserTest {
|
||||
|
||||
public void testParseMediaPlaylist() throws IOException {
|
||||
@Test
|
||||
public void testParseMediaPlaylist() throws Exception {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString = "#EXTM3U\n"
|
||||
+ "#EXT-X-VERSION:3\n"
|
||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||
+ "#EXT-X-START:TIME-OFFSET=-25"
|
||||
+ "#EXT-X-TARGETDURATION:8\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
||||
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
||||
+ "#EXT-X-ALLOW-CACHE:YES\n"
|
||||
+ "\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "#EXT-X-BYTERANGE:51370@0\n"
|
||||
+ "https://priv.example.com/fileSequence2679.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "#EXT-X-BYTERANGE:51501@2147483648\n"
|
||||
+ "https://priv.example.com/fileSequence2680.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-KEY:METHOD=NONE\n"
|
||||
+ "#EXTINF:7.941,\n"
|
||||
+ "#EXT-X-BYTERANGE:51501\n" // @2147535149
|
||||
+ "https://priv.example.com/fileSequence2681.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DISCONTINUITY\n"
|
||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "#EXT-X-BYTERANGE:51740\n" // @2147586650
|
||||
+ "https://priv.example.com/fileSequence2682.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "https://priv.example.com/fileSequence2683.ts\n"
|
||||
+ "#EXT-X-ENDLIST";
|
||||
InputStream inputStream = new ByteArrayInputStream(
|
||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-VERSION:3\n"
|
||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||
+ "#EXT-X-START:TIME-OFFSET=-25"
|
||||
+ "#EXT-X-TARGETDURATION:8\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
||||
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
||||
+ "#EXT-X-ALLOW-CACHE:YES\n"
|
||||
+ "\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "#EXT-X-BYTERANGE:51370@0\n"
|
||||
+ "https://priv.example.com/fileSequence2679.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-KEY:METHOD=AES-128,"
|
||||
+ "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "#EXT-X-BYTERANGE:51501@2147483648\n"
|
||||
+ "https://priv.example.com/fileSequence2680.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-KEY:METHOD=NONE\n"
|
||||
+ "#EXTINF:7.941,\n"
|
||||
+ "#EXT-X-BYTERANGE:51501\n" // @2147535149
|
||||
+ "https://priv.example.com/fileSequence2681.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DISCONTINUITY\n"
|
||||
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "#EXT-X-BYTERANGE:51740\n" // @2147586650
|
||||
+ "https://priv.example.com/fileSequence2682.ts\n"
|
||||
+ "\n"
|
||||
+ "#EXTINF:7.975,\n"
|
||||
+ "https://priv.example.com/fileSequence2683.ts\n"
|
||||
+ "#EXT-X-ENDLIST";
|
||||
InputStream inputStream =
|
||||
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
|
||||
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
|
||||
|
|
@ -136,6 +140,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGapTag() throws IOException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test2.m3u8");
|
||||
String playlistString =
|
||||
|
|
@ -170,5 +175,4 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertThat(playlist.segments.get(2).hasGapTag).isTrue();
|
||||
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 {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -18,16 +18,6 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.source.smoothstreaming.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<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"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -16,27 +16,29 @@
|
|||
package com.google.android.exoplayer2.source.smoothstreaming.manifest;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
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}.
|
||||
*/
|
||||
public final class SsManifestParserTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link SsManifestParser}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class SsManifestParserTest {
|
||||
|
||||
private static final String SAMPLE_ISMC_1 = "sample_ismc_1";
|
||||
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 {
|
||||
SsManifestParser parser = new SsManifestParser();
|
||||
parser.parse(Uri.parse("https://example.com/test.ismc"),
|
||||
TestUtil.getInputStream(getInstrumentation(), SAMPLE_ISMC_1));
|
||||
parser.parse(Uri.parse("https://example.com/test.ismc"),
|
||||
TestUtil.getInputStream(getInstrumentation(), SAMPLE_ISMC_2));
|
||||
parser.parse(
|
||||
Uri.parse("https://example.com/test.ismc"),
|
||||
TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_ISMC_1));
|
||||
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.List;
|
||||
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}.
|
||||
*/
|
||||
public class SsManifestTest extends TestCase {
|
||||
/** Unit tests for {@link SsManifest}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class SsManifestTest {
|
||||
|
||||
private static final ProtectionElement DUMMY_PROTECTION_ELEMENT =
|
||||
new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2});
|
||||
|
||||
@Test
|
||||
public void testCopy() throws Exception {
|
||||
Format[][] formats = newFormats(2, 3);
|
||||
SsManifest sourceManifest = newSsManifest(
|
||||
newStreamElement("1",formats[0]),
|
||||
newStreamElement("2", formats[1]));
|
||||
SsManifest sourceManifest =
|
||||
newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1]));
|
||||
|
||||
List<TrackKey> keys = Arrays.asList(
|
||||
new TrackKey(0, 0),
|
||||
new TrackKey(0, 2),
|
||||
new TrackKey(1, 0));
|
||||
List<TrackKey> keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0));
|
||||
// Keys don't need to be in any particular order
|
||||
Collections.shuffle(keys, new Random(0));
|
||||
|
||||
SsManifest copyManifest = sourceManifest.copy(keys);
|
||||
|
||||
SsManifest expectedManifest = newSsManifest(
|
||||
newStreamElement("1", formats[0][0], formats[0][2]),
|
||||
newStreamElement("2", formats[1][0]));
|
||||
SsManifest expectedManifest =
|
||||
newSsManifest(
|
||||
newStreamElement("1", formats[0][0], formats[0][2]),
|
||||
newStreamElement("2", formats[1][0]));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyRemoveStreamElement() throws Exception {
|
||||
Format[][] formats = newFormats(2, 3);
|
||||
SsManifest sourceManifest = newSsManifest(
|
||||
newStreamElement("1", formats[0]),
|
||||
newStreamElement("2", formats[1]));
|
||||
SsManifest sourceManifest =
|
||||
newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1]));
|
||||
|
||||
List<TrackKey> keys = Arrays.asList(
|
||||
new TrackKey(1, 0));
|
||||
List<TrackKey> keys = Arrays.asList(new TrackKey(1, 0));
|
||||
// Keys don't need to be in any particular order
|
||||
Collections.shuffle(keys, new Random(0));
|
||||
|
||||
SsManifest copyManifest = sourceManifest.copy(keys);
|
||||
|
||||
SsManifest expectedManifest = newSsManifest(
|
||||
newStreamElement("2", formats[1][0]));
|
||||
SsManifest expectedManifest = newSsManifest(newStreamElement("2", formats[1][0]));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
|
|
@ -117,13 +114,25 @@ public class SsManifestTest extends TestCase {
|
|||
}
|
||||
|
||||
private static StreamElement newStreamElement(String name, Format... formats) {
|
||||
return new StreamElement("baseUri", "chunkTemplate", C.TRACK_TYPE_VIDEO, "subType",
|
||||
1000, name, 1024, 768, 1024, 768, null, formats, Collections.<Long>emptyList(), 0);
|
||||
return new StreamElement(
|
||||
"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) {
|
||||
return Format.createContainerFormat(id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
|
||||
Format.NO_VALUE, 0, null);
|
||||
return Format.createContainerFormat(
|
||||
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 {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue