Commit b6155d2e by ojw28 Committed by GitHub

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

r2.7.1
parents 052de3c6 ff43df1e
Showing with 1351 additions and 917 deletions
......@@ -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 ##
......
# 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 {
......
<?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();
}
}
......@@ -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,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;
......@@ -880,6 +888,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) {
queueEditor.onAddQueueItem(player, description);
......@@ -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;
......@@ -165,11 +164,6 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
}
@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 {
......
......@@ -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);
}
});
}
private void deliverMessage(PlayerMessage 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) 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;
private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) {
public final long duration;
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);
......@@ -655,6 +656,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
* Returns whether to force rendering an output buffer.
*
* @param earlyUs The time until the current buffer should be presented in microseconds. A
* negative value indicates that the buffer is late.
* @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in
* microseconds.
* @return Returns whether to force rendering an output buffer.
*/
protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {
return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000;
}
/**
* Skips the output buffer with the specified index.
*
* @param codec The codec that owns the output buffer.
......@@ -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);
}
......@@ -1289,6 +1285,104 @@ public final class ExoPlayerTest {
}
@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);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
......@@ -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 {
......
......@@ -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>
/*
* 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}));
}
}
......@@ -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));
}
}
......@@ -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.
}
}
}
......@@ -16,85 +16,88 @@
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.
*/
/** 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();
+ "<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();
+ "<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));
}
......@@ -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 {
......
......@@ -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>
......@@ -15,9 +15,10 @@
*/
package com.google.android.exoplayer2.source.hls.offline;
/**
* Data for HLS downloading tests.
*/
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";
......@@ -33,45 +34,50 @@ package com.google.android.exoplayer2.source.hls.offline;
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();
+ "#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();
+ "#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();
+ "#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_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_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_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_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_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_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";
@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();
}
}
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]));
List<TrackKey> keys = Arrays.asList(
new TrackKey(0, 0),
new TrackKey(0, 2),
new TrackKey(1, 0));
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));
// 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);
}
}
......@@ -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 {
......
......@@ -25,8 +25,8 @@ android {
}
dependencies {
androidTestCompile project(modulePrefix + 'library-core')
androidTestCompile project(modulePrefix + 'library-dash')
androidTestCompile project(modulePrefix + 'library-hls')
androidTestCompile project(modulePrefix + 'testutils')
androidTestImplementation project(modulePrefix + 'library-core')
androidTestImplementation project(modulePrefix + 'library-dash')
androidTestImplementation project(modulePrefix + 'library-hls')
androidTestImplementation project(modulePrefix + 'testutils')
}
......@@ -32,7 +32,9 @@ android {
}
dependencies {
compile project(modulePrefix + 'library-core')
compile 'org.mockito:mockito-core:' + mockitoVersion
compile 'com.google.truth:truth:' + truthVersion
api 'org.mockito:mockito-core:' + mockitoVersion
api 'com.google.truth:truth:' + truthVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils-robolectric')
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment