contentTypePredicate;
private final TransferListener super DataSource> transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
+ private final HttpDataSource.Factory fallbackFactory;
- public CronetDataSourceFactory(CronetEngine cronetEngine,
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
+ * fallback {@link HttpDataSource.Factory} will be used instead.
+ *
+ * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
+ * no suitable CronetEngine can be build.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate contentTypePredicate,
- TransferListener super DataSource> transferListener) {
- this(cronetEngine, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
+ TransferListener super DataSource> transferListener,
+ HttpDataSource.Factory fallbackFactory) {
+ this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
}
- public CronetDataSourceFactory(CronetEngine cronetEngine,
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
+ * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
+ Executor executor, Predicate contentTypePredicate,
+ TransferListener super DataSource> transferListener, String userAgent) {
+ this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
+ new DefaultHttpDataSourceFactory(userAgent, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
+ }
+
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
+ * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate contentTypePredicate,
TransferListener super DataSource> transferListener, int connectTimeoutMs,
- int readTimeoutMs, boolean resetTimeoutOnRedirects) {
- this.cronetEngine = cronetEngine;
+ int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
+ this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
+ readTimeoutMs, resetTimeoutOnRedirects));
+ }
+
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
+ * fallback {@link HttpDataSource.Factory} will be used instead.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
+ * no suitable CronetEngine can be build.
+ */
+ public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
+ Executor executor, Predicate contentTypePredicate,
+ TransferListener super DataSource> transferListener, int connectTimeoutMs,
+ int readTimeoutMs, boolean resetTimeoutOnRedirects,
+ HttpDataSource.Factory fallbackFactory) {
+ this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor;
this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
+ this.fallbackFactory = fallbackFactory;
}
@Override
- protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
+ protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
+ CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
+ if (cronetEngine == null) {
+ return fallbackFactory.createDataSource();
+ }
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
new file mode 100644
index 0000000000..efe30d6525
--- /dev/null
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -0,0 +1,238 @@
+/*
+ * 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.ext.cronet;
+
+import android.content.Context;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import org.chromium.net.CronetEngine;
+import org.chromium.net.CronetProvider;
+
+/**
+ * A wrapper class for a {@link CronetEngine}.
+ */
+public final class CronetEngineWrapper {
+
+ private static final String TAG = "CronetEngineWrapper";
+
+ private final CronetEngine cronetEngine;
+ private final @CronetEngineSource int cronetEngineSource;
+
+ /**
+ * Source of {@link CronetEngine}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
+ public @interface CronetEngineSource {}
+ /**
+ * Natively bundled Cronet implementation.
+ */
+ public static final int SOURCE_NATIVE = 0;
+ /**
+ * Cronet implementation from GMSCore.
+ */
+ public static final int SOURCE_GMS = 1;
+ /**
+ * Other (unknown) Cronet implementation.
+ */
+ public static final int SOURCE_UNKNOWN = 2;
+ /**
+ * User-provided Cronet engine.
+ */
+ public static final int SOURCE_USER_PROVIDED = 3;
+ /**
+ * No Cronet implementation available. Fallback Http provider is used if possible.
+ */
+ public static final int SOURCE_UNAVAILABLE = 4;
+
+ /**
+ * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
+ * {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet
+ * if both are available.
+ *
+ * @param context A context.
+ */
+ public CronetEngineWrapper(Context context) {
+ this(context, false);
+ }
+
+ /**
+ * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
+ * {@link CronetProvider} based on user preference.
+ *
+ * @param context A context.
+ * @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
+ * bundled Cronet if both are available.
+ */
+ public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
+ CronetEngine cronetEngine = null;
+ @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
+ List cronetProviders = CronetProvider.getAllProviders(context);
+ // Remove disabled and fallback Cronet providers from list
+ for (int i = cronetProviders.size() - 1; i >= 0; i--) {
+ if (!cronetProviders.get(i).isEnabled()
+ || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {
+ cronetProviders.remove(i);
+ }
+ }
+ // Sort remaining providers by type and version.
+ CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet);
+ Collections.sort(cronetProviders, providerComparator);
+ for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
+ String providerName = cronetProviders.get(i).getName();
+ try {
+ cronetEngine = cronetProviders.get(i).createBuilder().build();
+ if (providerComparator.isNativeProvider(providerName)) {
+ cronetEngineSource = SOURCE_NATIVE;
+ } else if (providerComparator.isGMSCoreProvider(providerName)) {
+ cronetEngineSource = SOURCE_GMS;
+ } else {
+ cronetEngineSource = SOURCE_UNKNOWN;
+ }
+ Log.d(TAG, "CronetEngine built using " + providerName);
+ } catch (SecurityException e) {
+ Log.w(TAG, "Failed to build CronetEngine. Please check if current process has "
+ + "android.permission.ACCESS_NETWORK_STATE.");
+ } catch (UnsatisfiedLinkError e) {
+ Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are "
+ + "bundled into your app.");
+ }
+ }
+ if (cronetEngine == null) {
+ Log.w(TAG, "Cronet not available. Using fallback provider.");
+ }
+ this.cronetEngine = cronetEngine;
+ this.cronetEngineSource = cronetEngineSource;
+ }
+
+ /**
+ * Creates a wrapper for an existing CronetEngine.
+ *
+ * @param cronetEngine An existing CronetEngine.
+ */
+ public CronetEngineWrapper(CronetEngine cronetEngine) {
+ this.cronetEngine = cronetEngine;
+ this.cronetEngineSource = SOURCE_USER_PROVIDED;
+ }
+
+ /**
+ * Returns the source of the wrapped {@link CronetEngine}.
+ *
+ * @return A {@link CronetEngineSource} value.
+ */
+ public @CronetEngineSource int getCronetEngineSource() {
+ return cronetEngineSource;
+ }
+
+ /**
+ * Returns the wrapped {@link CronetEngine}.
+ *
+ * @return The CronetEngine, or null if no CronetEngine is available.
+ */
+ /* package */ CronetEngine getCronetEngine() {
+ return cronetEngine;
+ }
+
+ private static class CronetProviderComparator implements Comparator {
+
+ private final String gmsCoreCronetName;
+ private final boolean preferGMSCoreCronet;
+
+ public CronetProviderComparator(boolean preferGMSCoreCronet) {
+ // GMSCore CronetProvider classes are only available in some configurations.
+ // Thus, we use reflection to copy static name.
+ String gmsCoreVersionString = null;
+ try {
+ Class> cronetProviderInstallerClass =
+ Class.forName("com.google.android.gms.net.CronetProviderInstaller");
+ Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME");
+ gmsCoreVersionString = (String) providerNameField.get(null);
+ } catch (ClassNotFoundException e) {
+ // GMSCore CronetProvider not available.
+ } catch (NoSuchFieldException e) {
+ // GMSCore CronetProvider not available.
+ } catch (IllegalAccessException e) {
+ // GMSCore CronetProvider not available.
+ }
+ gmsCoreCronetName = gmsCoreVersionString;
+ this.preferGMSCoreCronet = preferGMSCoreCronet;
+ }
+
+ @Override
+ public int compare(CronetProvider providerLeft, CronetProvider providerRight) {
+ int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());
+ int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());
+ if (typePreferenceLeft != typePreferenceRight) {
+ return typePreferenceLeft - typePreferenceRight;
+ }
+ return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());
+ }
+
+ public boolean isNativeProvider(String providerName) {
+ return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName);
+ }
+
+ public boolean isGMSCoreProvider(String providerName) {
+ return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName);
+ }
+
+ /**
+ * Convert Cronet provider name into a sortable preference value.
+ * Smaller values are preferred.
+ */
+ private int evaluateCronetProviderType(String providerName) {
+ if (isNativeProvider(providerName)) {
+ return 1;
+ }
+ if (isGMSCoreProvider(providerName)) {
+ return preferGMSCoreCronet ? 0 : 2;
+ }
+ // Unknown provider type.
+ return -1;
+ }
+
+ /**
+ * Compares version strings of format "12.123.35.23".
+ */
+ private static int compareVersionStrings(String versionLeft, String versionRight) {
+ if (versionLeft == null || versionRight == null) {
+ return 0;
+ }
+ String[] versionStringsLeft = versionLeft.split("\\.");
+ String[] versionStringsRight = versionRight.split("\\.");
+ int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
+ for (int i = 0; i < minLength; i++) {
+ if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
+ try {
+ int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);
+ int versionIntRight = Integer.parseInt(versionStringsRight[i]);
+ return versionIntLeft - versionIntRight;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+ }
+ return 0;
+ }
+ }
+
+}
diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md
index 4ce9173ec9..ab3e5ffb94 100644
--- a/extensions/ffmpeg/README.md
+++ b/extensions/ffmpeg/README.md
@@ -9,11 +9,10 @@ audio.
## Build instructions ##
-* Checkout ExoPlayer along with Extensions
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
```
-* In your project, you can add a dependency on the extension by using a rule
- like this:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-ffmpeg'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-ffmpeg')
-}
-```
-
-* Now, when you build your app, the extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index 0eddd017a4..9820818f3e 100644
--- a/extensions/ffmpeg/build.gradle
+++ b/extensions/ffmpeg/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,7 +31,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
}
ext {
diff --git a/extensions/flac/README.md b/extensions/flac/README.md
index 2f3b067d6f..a35dac7858 100644
--- a/extensions/flac/README.md
+++ b/extensions/flac/README.md
@@ -10,11 +10,10 @@ ExoPlayer to play Flac audio on Android devices.
## Build Instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4
```
-* In your project, you can add a dependency to the Flac Extension by using a
- rule like this:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-flac'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-flac')
-}
-```
-
-* Now, when you build your app, the Flac extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index 4a6b8e0e5a..4d840d34ac 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,8 +31,8 @@ android {
}
dependencies {
- compile project(':library-core')
- androidTestCompile project(':testutils')
+ compile project(modulePrefix + 'library-core')
+ androidTestCompile project(modulePrefix + 'testutils')
}
ext {
diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml
index 0a62db3bb5..73032ab50c 100644
--- a/extensions/flac/src/androidTest/AndroidManifest.xml
+++ b/extensions/flac/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index 4196f1ea63..5954985100 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FlacExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public class FlacExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index 21f01f0cca..a49ae073ef 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -126,6 +126,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
}
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc
index e4925cb462..6c6e57f5f7 100644
--- a/extensions/flac/src/main/jni/flac_parser.cc
+++ b/extensions/flac/src/main/jni/flac_parser.cc
@@ -22,6 +22,7 @@
#include
#include
+#include
#define LOG_TAG "FLACParser"
#define ALOGE(...) \
diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md
index bae5de4812..ad28569121 100644
--- a/extensions/gvr/README.md
+++ b/extensions/gvr/README.md
@@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields.
-## Using the extension ##
+[Google VR SDK for Android]: https://developers.google.com/vr/android/
+[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
+
+## Getting the extension ##
The easiest way to use the extension is to add it as a gradle dependency. You
need to make sure you have the jcenter repository included in the `build.gradle`
@@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
-## Using GvrAudioProcessor ##
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+## Using the extension ##
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
return a GvrAudioProcessor.
* If constructing renderers directly, pass a GvrAudioProcessor to
MediaCodecAudioRenderer's constructor.
-[Google VR SDK for Android]: https://developers.google.com/vr/android/
-[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index f622a73758..66665576bb 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -24,8 +25,8 @@ android {
}
dependencies {
- compile project(':library-core')
- compile 'com.google.vr:sdk-audio:1.30.0'
+ compile project(modulePrefix + 'library-core')
+ compile 'com.google.vr:sdk-audio:1.60.1'
}
ext {
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
index 980424904d..a56bc7f0a9 100644
--- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
@@ -82,6 +82,9 @@ public final class GvrAudioProcessor implements AudioProcessor {
maybeReleaseGvrAudioSurround();
int surroundFormat;
switch (channelCount) {
+ case 1:
+ surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
+ break;
case 2:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break;
diff --git a/extensions/ima/README.md b/extensions/ima/README.md
new file mode 100644
index 0000000000..dd4603ef4e
--- /dev/null
+++ b/extensions/ima/README.md
@@ -0,0 +1,43 @@
+# ExoPlayer IMA extension #
+
+## Description ##
+
+The IMA extension is a [MediaSource][] implementation wrapping the
+[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads
+alongside content.
+
+[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
+[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+
+## Getting the extension ##
+
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+
+## Using the extension ##
+
+Pass a single-window content `MediaSource` to `ImaAdsMediaSource`'s constructor,
+along with a `ViewGroup` that is on top of the player and the ad tag URI to
+show. The IMA documentation includes some [sample ad tags][] for testing. Then
+pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`.
+
+You can try the IMA extension in the ExoPlayer demo app. To do this you must
+select and build one of the `withExtensions` build variants of the demo app in
+Android Studio. You can find IMA test content in the "IMA sample ad tags"
+section of the app.
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags
+
+## Known issues ##
+
+This is a preview version with some known issues:
+
+* Tapping the 'More info' button on an ad in the demo app will pause the
+ activity, which destroys the ImaAdsMediaSource. Played ad breaks will be
+ shown to the user again if the demo app returns to the foreground.
+* Ad loading timeouts are currently propagated as player errors, rather than
+ being silently handled by resuming content.
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
new file mode 100644
index 0000000000..b2dd2ab97b
--- /dev/null
+++ b/extensions/ima/build.gradle
@@ -0,0 +1,25 @@
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+ buildToolsVersion project.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion project.ext.targetSdkVersion
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+}
+
+dependencies {
+ compile project(modulePrefix + 'library-core')
+ compile 'com.android.support:support-annotations:' + supportLibraryVersion
+ compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4'
+ compile 'com.google.android.gms:play-services-ads:11.0.1'
+ 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
+}
diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..22fb518c58
--- /dev/null
+++ b/extensions/ima/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
new file mode 100644
index 0000000000..0b14f16256
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -0,0 +1,614 @@
+/*
+ * 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.ext.ima;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.ViewGroup;
+import com.google.ads.interactivemedia.v3.api.Ad;
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
+import com.google.ads.interactivemedia.v3.api.AdEvent;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
+import com.google.ads.interactivemedia.v3.api.AdPodInfo;
+import com.google.ads.interactivemedia.v3.api.AdsLoader;
+import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
+import com.google.ads.interactivemedia.v3.api.AdsManager;
+import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+import com.google.ads.interactivemedia.v3.api.AdsRequest;
+import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
+import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
+import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loads ads using the IMA SDK. All methods are called on the main thread.
+ */
+/* package */ final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer,
+ ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ImaAdsLoader";
+
+ /**
+ * Listener for ad loader events. All methods are called on the main thread.
+ */
+ public interface EventListener {
+
+ /**
+ * Called when the times of ad groups are known.
+ *
+ * @param adGroupTimesUs The times of ad groups, in microseconds.
+ */
+ void onAdGroupTimesUsLoaded(long[] adGroupTimesUs);
+
+ /**
+ * Called when an ad group has been played to the end.
+ *
+ * @param adGroupIndex The index of the ad group.
+ */
+ void onAdGroupPlayedToEnd(int adGroupIndex);
+
+ /**
+ * Called when the URI for the media of an ad has been loaded.
+ *
+ * @param adGroupIndex The index of the ad group containing the ad with the media URI.
+ * @param adIndexInAdGroup The index of the ad in its ad group.
+ * @param uri The URI for the ad's media.
+ */
+ void onAdUriLoaded(int adGroupIndex, int adIndexInAdGroup, Uri uri);
+
+ /**
+ * Called when an ad group has loaded.
+ *
+ * @param adGroupIndex The index of the ad group containing the ad.
+ * @param adCountInAdGroup The number of ads in the ad group.
+ */
+ void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup);
+
+ /**
+ * Called when there was an error loading ads.
+ *
+ * @param error The error.
+ */
+ void onLoadError(IOException error);
+
+ }
+
+ /**
+ * Whether to enable preloading of ads in {@link AdsRenderingSettings}.
+ */
+ private static final boolean ENABLE_PRELOADING = true;
+
+ private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
+ private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
+
+ /**
+ * Threshold before the end of content at which IMA is notified that content is complete if the
+ * player buffers, in milliseconds.
+ */
+ private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
+
+ private final EventListener eventListener;
+ private final ExoPlayer player;
+ private final Timeline.Period period;
+ private final List adCallbacks;
+ private final AdsLoader adsLoader;
+
+ private AdsManager adsManager;
+ private long[] adGroupTimesUs;
+ private int[] adsLoadedInAdGroup;
+ private Timeline timeline;
+ private long contentDurationMs;
+
+ private boolean released;
+
+ // Fields tracking IMA's state.
+
+ /**
+ * The index of the current ad group that IMA is loading.
+ */
+ private int adGroupIndex;
+ /**
+ * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #playAd()} and not
+ * {@link #stopAd()}.
+ */
+ private boolean playingAd;
+ /**
+ * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #pauseAd()} since
+ * a preceding call to {@link #playAd()} for the current ad.
+ */
+ private boolean pausedInAd;
+ /**
+ * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
+ */
+ private boolean sentContentComplete;
+
+ // Fields tracking the player/loader state.
+
+ /**
+ * If the player is playing an ad, stores the ad group index. {@link C#INDEX_UNSET} otherwise.
+ */
+ private int playingAdGroupIndex;
+ /**
+ * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
+ * otherwise.
+ */
+ private int playingAdIndexInAdGroup;
+ /**
+ * If a content period has finished but IMA has not yet sent an ad event with
+ * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
+ * {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
+ * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
+ */
+ private long fakeContentProgressElapsedRealtimeMs;
+ /**
+ * Stores the pending content position when a seek operation was intercepted to play an ad.
+ */
+ private long pendingContentPositionMs;
+ /**
+ * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
+ */
+ private boolean sentPendingContentPositionMs;
+
+ /**
+ * Creates a new IMA ads loader.
+ *
+ * @param context The context.
+ * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
+ * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
+ * more information.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
+ * use the default settings. If set, the player type and version fields may be overwritten.
+ * @param player The player instance that will play the loaded ad schedule.
+ * @param eventListener Listener for ad loader events.
+ */
+ public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup,
+ ImaSdkSettings imaSdkSettings, ExoPlayer player, EventListener eventListener) {
+ this.eventListener = eventListener;
+ this.player = player;
+ period = new Timeline.Period();
+ adCallbacks = new ArrayList<>(1);
+
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ pendingContentPositionMs = C.TIME_UNSET;
+ adGroupIndex = C.INDEX_UNSET;
+ contentDurationMs = C.TIME_UNSET;
+
+ player.addListener(this);
+
+ ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
+ AdDisplayContainer adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
+ adDisplayContainer.setPlayer(this);
+ adDisplayContainer.setAdContainer(adUiViewGroup);
+
+ if (imaSdkSettings == null) {
+ imaSdkSettings = imaSdkFactory.createImaSdkSettings();
+ }
+ imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
+ imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
+
+ AdsRequest request = imaSdkFactory.createAdsRequest();
+ request.setAdTagUrl(adTagUri.toString());
+ request.setAdDisplayContainer(adDisplayContainer);
+ request.setContentProgressProvider(this);
+
+ adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
+ adsLoader.addAdErrorListener(this);
+ adsLoader.addAdsLoadedListener(this);
+ adsLoader.requestAds(request);
+ }
+
+ /**
+ * Releases the loader. Must be called when the instance is no longer needed.
+ */
+ public void release() {
+ if (adsManager != null) {
+ adsManager.destroy();
+ adsManager = null;
+ }
+ player.removeListener(this);
+ released = true;
+ }
+
+ // AdsLoader.AdsLoadedListener implementation.
+
+ @Override
+ public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
+ adsManager = adsManagerLoadedEvent.getAdsManager();
+ adsManager.addAdErrorListener(this);
+ adsManager.addAdEventListener(this);
+ if (ENABLE_PRELOADING) {
+ ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
+ AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
+ adsRenderingSettings.setEnablePreloading(true);
+ adsManager.init(adsRenderingSettings);
+ if (DEBUG) {
+ Log.d(TAG, "Initialized with preloading");
+ }
+ } else {
+ adsManager.init();
+ if (DEBUG) {
+ Log.d(TAG, "Initialized without preloading");
+ }
+ }
+ adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
+ adsLoadedInAdGroup = new int[adGroupTimesUs.length];
+ eventListener.onAdGroupTimesUsLoaded(adGroupTimesUs);
+ }
+
+ // AdEvent.AdEventListener implementation.
+
+ @Override
+ public void onAdEvent(AdEvent adEvent) {
+ Ad ad = adEvent.getAd();
+ if (DEBUG) {
+ Log.d(TAG, "onAdEvent " + adEvent.getType());
+ }
+ if (released) {
+ // The ads manager may pass CONTENT_RESUME_REQUESTED after it is destroyed.
+ return;
+ }
+ switch (adEvent.getType()) {
+ case LOADED:
+ // The ad position is not always accurate when using preloading. See [Internal: b/62613240].
+ AdPodInfo adPodInfo = ad.getAdPodInfo();
+ int podIndex = adPodInfo.getPodIndex();
+ adGroupIndex = podIndex == -1 ? adGroupTimesUs.length - 1 : podIndex;
+ int adPosition = adPodInfo.getAdPosition();
+ int adCountInAdGroup = adPodInfo.getTotalAds();
+ adsManager.start();
+ if (DEBUG) {
+ Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group "
+ + adGroupIndex);
+ }
+ eventListener.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
+ break;
+ case CONTENT_PAUSE_REQUESTED:
+ // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
+ // before sending CONTENT_RESUME_REQUESTED.
+ pauseContentInternal();
+ break;
+ case SKIPPED: // Fall through.
+ case CONTENT_RESUME_REQUESTED:
+ resumeContentInternal();
+ break;
+ case ALL_ADS_COMPLETED:
+ // Do nothing. The ads manager will be released when the source is released.
+ default:
+ break;
+ }
+ }
+
+ // AdErrorEvent.AdErrorListener implementation.
+
+ @Override
+ public void onAdError(AdErrorEvent adErrorEvent) {
+ if (DEBUG) {
+ Log.d(TAG, "onAdError " + adErrorEvent);
+ }
+ IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError());
+ eventListener.onLoadError(exception);
+ // TODO: Provide a timeline to the player if it doesn't have one yet, so the content can play.
+ }
+
+ // ContentProgressProvider implementation.
+
+ @Override
+ public VideoProgressUpdate getContentProgress() {
+ if (pendingContentPositionMs != C.TIME_UNSET) {
+ sentPendingContentPositionMs = true;
+ return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
+ }
+ if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
+ long adGroupTimeMs = C.usToMs(adGroupTimesUs[adGroupIndex]);
+ if (adGroupTimeMs == C.TIME_END_OF_SOURCE) {
+ adGroupTimeMs = contentDurationMs;
+ }
+ long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
+ return new VideoProgressUpdate(adGroupTimeMs + elapsedSinceEndMs, contentDurationMs);
+ }
+ if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ }
+ return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
+ }
+
+ // VideoAdPlayer implementation.
+
+ @Override
+ public VideoProgressUpdate getAdProgress() {
+ if (!player.isPlayingAd()) {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ }
+ return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration());
+ }
+
+ @Override
+ public void loadAd(String adUriString) {
+ int adIndexInAdGroup = adsLoadedInAdGroup[adGroupIndex]++;
+ if (DEBUG) {
+ Log.d(TAG, "loadAd at index " + adIndexInAdGroup + " in ad group " + adGroupIndex);
+ }
+ eventListener.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
+ }
+
+ @Override
+ public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
+ adCallbacks.add(videoAdPlayerCallback);
+ }
+
+ @Override
+ public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
+ adCallbacks.remove(videoAdPlayerCallback);
+ }
+
+ @Override
+ public void playAd() {
+ if (DEBUG) {
+ Log.d(TAG, "playAd");
+ }
+ if (playingAd && !pausedInAd) {
+ // Work around an issue where IMA does not always call stopAd before resuming content.
+ // See [Internal: b/38354028].
+ if (DEBUG) {
+ Log.d(TAG, "Unexpected playAd without stopAd");
+ }
+ stopAdInternal();
+ }
+ player.setPlayWhenReady(true);
+ if (!playingAd) {
+ playingAd = true;
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onPlay();
+ }
+ } else if (pausedInAd) {
+ pausedInAd = false;
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onResume();
+ }
+ }
+ }
+
+ @Override
+ public void stopAd() {
+ if (!playingAd) {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring unexpected stopAd");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "stopAd");
+ }
+ stopAdInternal();
+ }
+
+ @Override
+ public void pauseAd() {
+ if (DEBUG) {
+ Log.d(TAG, "pauseAd");
+ }
+ if (released || !playingAd) {
+ // This method is called after content is resumed, and may also be called after release.
+ return;
+ }
+ pausedInAd = true;
+ player.setPlayWhenReady(false);
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onPause();
+ }
+ }
+
+ @Override
+ public void resumeAd() {
+ // This method is never called. See [Internal: b/18931719].
+ throw new IllegalStateException();
+ }
+
+ // ExoPlayer.EventListener implementation.
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, Object manifest) {
+ if (timeline.isEmpty()) {
+ // The player is being re-prepared and this source will be released.
+ return;
+ }
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ this.timeline = timeline;
+ contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs);
+ if (player.isPlayingAd()) {
+ playingAdGroupIndex = player.getCurrentAdGroupIndex();
+ playingAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup();
+ }
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLoadingChanged(boolean isLoading) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ if (!playingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) {
+ checkForContentComplete();
+ } else if (playingAd && playbackState == ExoPlayer.STATE_ENDED) {
+ // IMA is waiting for the ad playback to finish so invoke the callback now.
+ // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onEnded();
+ }
+ }
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException error) {
+ if (player.isPlayingAd()) {
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onError();
+ }
+ }
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ if (!player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
+ long positionUs = C.msToUs(player.getCurrentPosition());
+ int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs);
+ if (adGroupIndex != C.INDEX_UNSET) {
+ sentPendingContentPositionMs = false;
+ pendingContentPositionMs = player.getCurrentPosition();
+ }
+ return;
+ }
+
+ boolean adFinished = (!player.isPlayingAd() && playingAdGroupIndex != C.INDEX_UNSET)
+ || (player.isPlayingAd() && playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup());
+ if (adFinished) {
+ // IMA is waiting for the ad playback to finish so invoke the callback now.
+ // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
+ for (VideoAdPlayerCallback callback : adCallbacks) {
+ callback.onEnded();
+ }
+ }
+
+ if (player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
+ player.setPlayWhenReady(false);
+ // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
+ Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
+ fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
+ if (adGroupIndex == adGroupTimesUs.length - 1) {
+ adsLoader.contentComplete();
+ if (DEBUG) {
+ Log.d(TAG, "adsLoader.contentComplete");
+ }
+ }
+ }
+ boolean isPlayingAd = player.isPlayingAd();
+ playingAdGroupIndex = isPlayingAd ? player.getCurrentAdGroupIndex() : C.INDEX_UNSET;
+ playingAdIndexInAdGroup = isPlayingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
+ // Internal methods.
+
+ /**
+ * Resumes the player, ensuring the current period is a content period by seeking if necessary.
+ */
+ private void resumeContentInternal() {
+ if (contentDurationMs != C.TIME_UNSET) {
+ if (playingAd) {
+ // Work around an issue where IMA does not always call stopAd before resuming content.
+ // See [Internal: b/38354028].
+ if (DEBUG) {
+ Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
+ }
+ stopAdInternal();
+ }
+ }
+ player.setPlayWhenReady(true);
+ clearFlags();
+ }
+
+ private void pauseContentInternal() {
+ if (sentPendingContentPositionMs) {
+ pendingContentPositionMs = C.TIME_UNSET;
+ sentPendingContentPositionMs = false;
+ }
+ // IMA is requesting to pause content, so stop faking the content position.
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ player.setPlayWhenReady(false);
+ clearFlags();
+ }
+
+ private void stopAdInternal() {
+ Assertions.checkState(playingAd);
+ player.setPlayWhenReady(false);
+ if (!player.isPlayingAd()) {
+ eventListener.onAdGroupPlayedToEnd(adGroupIndex);
+ adGroupIndex = C.INDEX_UNSET;
+ }
+ clearFlags();
+ }
+
+ private void clearFlags() {
+ // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until
+ // the content is resumed.
+ playingAd = false;
+ pausedInAd = false;
+ }
+
+ private void checkForContentComplete() {
+ if (contentDurationMs != C.TIME_UNSET
+ && player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
+ && !sentContentComplete) {
+ adsLoader.contentComplete();
+ if (DEBUG) {
+ Log.d(TAG, "adsLoader.contentComplete");
+ }
+ sentContentComplete = true;
+ }
+ }
+
+ private static long[] getAdGroupTimesUs(List cuePoints) {
+ if (cuePoints.isEmpty()) {
+ // If no cue points are specified, there is a preroll ad.
+ return new long[] {0};
+ }
+
+ int count = cuePoints.size();
+ long[] adGroupTimesUs = new long[count];
+ for (int i = 0; i < count; i++) {
+ double cuePoint = cuePoints.get(i);
+ adGroupTimesUs[i] =
+ cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
+ }
+ return adGroupTimesUs;
+ }
+
+}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
new file mode 100644
index 0000000000..5e96bd26dc
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
@@ -0,0 +1,360 @@
+/*
+ * 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.ext.ima;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.ViewGroup;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link MediaSource} that inserts ads linearly with a provided content media source using the
+ * Interactive Media Ads SDK for ad loading and tracking.
+ */
+public final class ImaAdsMediaSource implements MediaSource {
+
+ private final MediaSource contentMediaSource;
+ private final DataSource.Factory dataSourceFactory;
+ private final Context context;
+ private final Uri adTagUri;
+ private final ViewGroup adUiViewGroup;
+ private final ImaSdkSettings imaSdkSettings;
+ private final Handler mainHandler;
+ private final AdListener adLoaderListener;
+ private final Map adMediaSourceByMediaPeriod;
+ private final Timeline.Period period;
+
+ private Handler playerHandler;
+ private ExoPlayer player;
+ private volatile boolean released;
+
+ // Accessed on the player thread.
+ private Timeline contentTimeline;
+ private Object contentManifest;
+ private long[] adGroupTimesUs;
+ private boolean[] hasPlayedAdGroup;
+ private int[] adCounts;
+ private MediaSource[][] adGroupMediaSources;
+ private boolean[][] isAdAvailable;
+ private long[][] adDurationsUs;
+ private MediaSource.Listener listener;
+ private IOException adLoadError;
+
+ // Accessed on the main thread.
+ private ImaAdsLoader imaAdsLoader;
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by
+ * {@code contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param dataSourceFactory Factory for data sources used to load ad media.
+ * @param context The context.
+ * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
+ * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
+ * more information.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad user
+ * interface.
+ */
+ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
+ Context context, Uri adTagUri, ViewGroup adUiViewGroup) {
+ this(contentMediaSource, dataSourceFactory, context, adTagUri, adUiViewGroup, null);
+ }
+
+ /**
+ * Constructs a new source that inserts ads linearly with the content specified by
+ * {@code contentMediaSource}.
+ *
+ * @param contentMediaSource The {@link MediaSource} providing the content to play.
+ * @param dataSourceFactory Factory for data sources used to load ad media.
+ * @param context The context.
+ * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
+ * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
+ * more information.
+ * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
+ * use the default settings. If set, the player type and version fields may be overwritten.
+ */
+ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
+ Context context, Uri adTagUri, ViewGroup adUiViewGroup, ImaSdkSettings imaSdkSettings) {
+ this.contentMediaSource = contentMediaSource;
+ this.dataSourceFactory = dataSourceFactory;
+ this.context = context;
+ this.adTagUri = adTagUri;
+ this.adUiViewGroup = adUiViewGroup;
+ this.imaSdkSettings = imaSdkSettings;
+ mainHandler = new Handler(Looper.getMainLooper());
+ adLoaderListener = new AdListener();
+ adMediaSourceByMediaPeriod = new HashMap<>();
+ period = new Timeline.Period();
+ adGroupMediaSources = new MediaSource[0][];
+ isAdAvailable = new boolean[0][];
+ adDurationsUs = new long[0][];
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ Assertions.checkArgument(isTopLevelSource);
+ this.listener = listener;
+ this.player = player;
+ playerHandler = new Handler();
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ imaAdsLoader = new ImaAdsLoader(context, adTagUri, adUiViewGroup, imaSdkSettings,
+ ImaAdsMediaSource.this.player, adLoaderListener);
+ }
+ });
+ contentMediaSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest);
+ }
+ });
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ if (adLoadError != null) {
+ throw adLoadError;
+ }
+ contentMediaSource.maybeThrowSourceInfoRefreshError();
+ for (MediaSource[] mediaSources : adGroupMediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ if (id.isAd()) {
+ MediaSource mediaSource = adGroupMediaSources[id.adGroupIndex][id.adIndexInAdGroup];
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
+ adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
+ return mediaPeriod;
+ } else {
+ return contentMediaSource.createPeriod(id, allocator);
+ }
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
+ adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
+ } else {
+ contentMediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void releaseSource() {
+ released = true;
+ adLoadError = null;
+ contentMediaSource.releaseSource();
+ for (MediaSource[] mediaSources : adGroupMediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ mediaSource.releaseSource();
+ }
+ }
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // TODO: The source will be released when the application is paused/stopped, which can occur
+ // if the user taps on the ad. In this case, we should keep the ads manager alive but pause
+ // it, instead of destroying it.
+ imaAdsLoader.release();
+ imaAdsLoader = null;
+ }
+ });
+ }
+
+ // Internal methods.
+
+ private void onAdGroupTimesUsLoaded(long[] adGroupTimesUs) {
+ Assertions.checkState(this.adGroupTimesUs == null);
+ int adGroupCount = adGroupTimesUs.length;
+ this.adGroupTimesUs = adGroupTimesUs;
+ hasPlayedAdGroup = new boolean[adGroupCount];
+ adCounts = new int[adGroupCount];
+ Arrays.fill(adCounts, C.LENGTH_UNSET);
+ adGroupMediaSources = new MediaSource[adGroupCount][];
+ Arrays.fill(adGroupMediaSources, new MediaSource[0]);
+ isAdAvailable = new boolean[adGroupCount][];
+ Arrays.fill(isAdAvailable, new boolean[0]);
+ adDurationsUs = new long[adGroupCount][];
+ Arrays.fill(adDurationsUs, new long[0]);
+ maybeUpdateSourceInfo();
+ }
+
+ private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ contentTimeline = timeline;
+ contentManifest = manifest;
+ maybeUpdateSourceInfo();
+ }
+
+ private void onAdGroupPlayedToEnd(int adGroupIndex) {
+ hasPlayedAdGroup[adGroupIndex] = true;
+ maybeUpdateSourceInfo();
+ }
+
+ private void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, Uri uri) {
+ MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory,
+ new DefaultExtractorsFactory(), mainHandler, adLoaderListener);
+ int oldAdCount = adGroupMediaSources[adGroupIndex].length;
+ if (adIndexInAdGroup >= oldAdCount) {
+ int adCount = adIndexInAdGroup + 1;
+ adGroupMediaSources[adGroupIndex] = Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
+ isAdAvailable[adGroupIndex] = Arrays.copyOf(isAdAvailable[adGroupIndex], adCount);
+ adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
+ Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
+ }
+ adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
+ isAdAvailable[adGroupIndex][adIndexInAdGroup] = true;
+ adMediaSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
+ }
+ });
+ }
+
+ private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
+ maybeUpdateSourceInfo();
+ }
+
+ private void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup) {
+ if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
+ adCounts[adGroupIndex] = adCountInAdGroup;
+ maybeUpdateSourceInfo();
+ }
+ }
+
+ private void maybeUpdateSourceInfo() {
+ if (adGroupTimesUs != null && contentTimeline != null) {
+ SinglePeriodAdTimeline timeline = new SinglePeriodAdTimeline(contentTimeline, adGroupTimesUs,
+ hasPlayedAdGroup, adCounts, isAdAvailable, adDurationsUs);
+ listener.onSourceInfoRefreshed(timeline, contentManifest);
+ }
+ }
+
+ /**
+ * Listener for ad loading events. All methods are called on the main thread.
+ */
+ private final class AdListener implements ImaAdsLoader.EventListener,
+ ExtractorMediaSource.EventListener {
+
+ @Override
+ public void onAdGroupTimesUsLoaded(final long[] adGroupTimesUs) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ ImaAdsMediaSource.this.onAdGroupTimesUsLoaded(adGroupTimesUs);
+ }
+ });
+ }
+
+ @Override
+ public void onAdGroupPlayedToEnd(final int adGroupIndex) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ ImaAdsMediaSource.this.onAdGroupPlayedToEnd(adGroupIndex);
+ }
+ });
+ }
+
+ @Override
+ public void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, final Uri uri) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ ImaAdsMediaSource.this.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, uri);
+ }
+ });
+ }
+
+ @Override
+ public void onAdGroupLoaded(final int adGroupIndex, final int adCountInAdGroup) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ ImaAdsMediaSource.this.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
+ }
+ });
+ }
+
+ @Override
+ public void onLoadError(final IOException error) {
+ if (released) {
+ return;
+ }
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (released) {
+ return;
+ }
+ adLoadError = error;
+ }
+ });
+ }
+
+ }
+
+}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java
new file mode 100644
index 0000000000..78d3bb9e73
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ext.ima;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link Timeline} for sources that have ads.
+ */
+public final class SinglePeriodAdTimeline extends Timeline {
+
+ private final Timeline contentTimeline;
+ private final long[] adGroupTimesUs;
+ private final boolean[] hasPlayedAdGroup;
+ private final int[] adCounts;
+ private final boolean[][] isAdAvailable;
+ private final long[][] adDurationsUs;
+
+ /**
+ * Creates a new timeline with a single period containing the specified ads.
+ *
+ * @param contentTimeline The timeline of the content alongside which ads will be played. It must
+ * have one window and one period.
+ * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
+ * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
+ * the period has a postroll ad.
+ * @param hasPlayedAdGroup Whether each ad group has been played.
+ * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
+ * if the number of ads is not yet known.
+ * @param isAdAvailable Whether each ad in each ad group is available.
+ * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
+ * may be {@link C#TIME_UNSET} if the duration is not yet known.
+ */
+ public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs,
+ boolean[] hasPlayedAdGroup, int[] adCounts, boolean[][] isAdAvailable,
+ long[][] adDurationsUs) {
+ Assertions.checkState(contentTimeline.getPeriodCount() == 1);
+ Assertions.checkState(contentTimeline.getWindowCount() == 1);
+ this.contentTimeline = contentTimeline;
+ this.adGroupTimesUs = adGroupTimesUs;
+ this.hasPlayedAdGroup = hasPlayedAdGroup;
+ this.adCounts = adCounts;
+ this.isAdAvailable = isAdAvailable;
+ this.adDurationsUs = adDurationsUs;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ contentTimeline.getPeriod(periodIndex, period, setIds);
+ period.set(period.id, period.uid, period.windowIndex, period.durationUs,
+ period.getPositionInWindowUs(), adGroupTimesUs, hasPlayedAdGroup, adCounts,
+ isAdAvailable, adDurationsUs);
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return contentTimeline.getIndexOfPeriod(uid);
+ }
+
+}
diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md
index d84dcb44ec..52d5fabf38 100644
--- a/extensions/okhttp/README.md
+++ b/extensions/okhttp/README.md
@@ -5,19 +5,12 @@
The OkHttp Extension is an [HttpDataSource][] implementation using Square's
[OkHttp][].
-## Using the extension ##
+[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
+[OkHttp]: https://square.github.io/okhttp/
-The easiest way to use the extension is to add it as a gradle dependency. You
-need to make sure you have the jcenter repository included in the `build.gradle`
-file in the root of your project:
+## Getting the extension ##
-```gradle
-repositories {
- jcenter()
-}
-```
-
-Next, include the following in your module's `build.gradle` file:
+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'
@@ -26,5 +19,8 @@ compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
-[OkHttp]: https://square.github.io/okhttp/
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index f47f1a8556..0aba07d118 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -29,7 +30,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
compile('com.squareup.okhttp3:okhttp:3.6.0') {
exclude group: 'org.json'
}
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index 47850c0637..167fc68e86 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -16,6 +16,8 @@
package com.google.android.exoplayer2.ext.okhttp;
import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -45,13 +47,14 @@ public class OkHttpDataSource implements HttpDataSource {
private static final AtomicReference skipBufferReference = new AtomicReference<>();
- private final Call.Factory callFactory;
- private final String userAgent;
- private final Predicate contentTypePredicate;
- private final TransferListener super OkHttpDataSource> listener;
- private final CacheControl cacheControl;
- private final RequestProperties defaultRequestProperties;
- private final RequestProperties requestProperties;
+ @NonNull private final Call.Factory callFactory;
+ @NonNull private final RequestProperties requestProperties;
+
+ @Nullable private final String userAgent;
+ @Nullable private final Predicate contentTypePredicate;
+ @Nullable private final TransferListener super OkHttpDataSource> listener;
+ @Nullable private final CacheControl cacheControl;
+ @Nullable private final RequestProperties defaultRequestProperties;
private DataSpec dataSpec;
private Response response;
@@ -67,33 +70,34 @@ public class OkHttpDataSource implements HttpDataSource {
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
- * @param userAgent The User-Agent string that should be used.
+ * @param userAgent An optional User-Agent string.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*/
- public OkHttpDataSource(Call.Factory callFactory, String userAgent,
- Predicate contentTypePredicate) {
+ public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ @Nullable Predicate contentTypePredicate) {
this(callFactory, userAgent, contentTypePredicate, null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
- * @param userAgent The User-Agent string that should be used.
+ * @param userAgent An optional User-Agent string.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
* @param listener An optional listener.
*/
- public OkHttpDataSource(Call.Factory callFactory, String userAgent,
- Predicate contentTypePredicate, TransferListener super OkHttpDataSource> listener) {
+ public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ @Nullable Predicate contentTypePredicate,
+ @Nullable TransferListener super OkHttpDataSource> listener) {
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
- * @param userAgent The User-Agent string that should be used.
+ * @param userAgent An optional User-Agent string.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
@@ -102,11 +106,12 @@ public class OkHttpDataSource implements HttpDataSource {
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
* the server as HTTP headers on every request.
*/
- public OkHttpDataSource(Call.Factory callFactory, String userAgent,
- Predicate contentTypePredicate, TransferListener super OkHttpDataSource> listener,
- CacheControl cacheControl, RequestProperties defaultRequestProperties) {
+ public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ @Nullable Predicate contentTypePredicate,
+ @Nullable TransferListener super OkHttpDataSource> listener,
+ @Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
this.callFactory = Assertions.checkNotNull(callFactory);
- this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.userAgent = userAgent;
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.cacheControl = cacheControl;
@@ -280,7 +285,10 @@ public class OkHttpDataSource implements HttpDataSource {
}
builder.addHeader("Range", rangeRequest);
}
- builder.addHeader("User-Agent", userAgent);
+ if (userAgent != null) {
+ builder.addHeader("User-Agent", userAgent);
+ }
+
if (!allowGzip) {
builder.addHeader("Accept-Encoding", "identity");
}
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
index 5228065db1..32fc5a58cb 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
@@ -28,31 +30,32 @@ import okhttp3.Call;
*/
public final class OkHttpDataSourceFactory extends BaseFactory {
- private final Call.Factory callFactory;
- private final String userAgent;
- private final TransferListener super DataSource> listener;
- private final CacheControl cacheControl;
+ @NonNull private final Call.Factory callFactory;
+ @Nullable private final String userAgent;
+ @Nullable private final TransferListener super DataSource> listener;
+ @Nullable private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
- * @param userAgent The User-Agent string that should be used.
+ * @param userAgent An optional User-Agent string.
* @param listener An optional listener.
*/
- public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
- TransferListener super DataSource> listener) {
+ public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ @Nullable TransferListener super DataSource> listener) {
this(callFactory, userAgent, listener, null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
- * @param userAgent The User-Agent string that should be used.
+ * @param userAgent An optional User-Agent string.
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
- public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
- TransferListener super DataSource> listener, CacheControl cacheControl) {
+ public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ @Nullable TransferListener super DataSource> listener,
+ @Nullable CacheControl cacheControl) {
this.callFactory = callFactory;
this.userAgent = userAgent;
this.listener = listener;
diff --git a/extensions/opus/README.md b/extensions/opus/README.md
index 36ca2b7261..ae42a9c310 100644
--- a/extensions/opus/README.md
+++ b/extensions/opus/README.md
@@ -10,11 +10,10 @@ ExoPlayer to play Opus audio on Android devices.
## Build Instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4
```
-* In your project, you can add a dependency to the Opus Extension by using a
-rule like this:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-opus'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-opus')
-}
-```
-
-* Now, when you build your app, the Opus extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
## Notes ##
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index 31d5450fdd..41b428070f 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,7 +31,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
}
ext {
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index c819529692..e77590dc65 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index 263934d982..76e19b0ebe 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -126,6 +126,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
}
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md
index 53ef4b0bfd..8bdfe652e6 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -10,11 +10,10 @@ VP9 video on Android devices.
## Build Instructions ##
-* Checkout ExoPlayer along with Extensions:
-
-```
-git clone https://github.com/google/ExoPlayer.git
-```
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][]. In addition, it's necessary to build the extension's
+native components as follows:
* Set the following environment variables:
@@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
* Download the [Android NDK][] and set its location in an environment variable:
-[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-
```
NDK_PATH=""
```
@@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4
```
-* In your project, you can add a dependency to the VP9 Extension by using a the
- following rule:
-
-```
-// in settings.gradle
-include ':..:ExoPlayer:library'
-include ':..:ExoPlayer:extension-vp9'
-
-// in build.gradle
-dependencies {
- compile project(':..:ExoPlayer:library')
- compile project(':..:ExoPlayer:extension-vp9')
-}
-```
-
-* Now, when you build your app, the VP9 extension will be built and the native
- libraries will be packaged along with the APK.
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
## Notes ##
@@ -94,4 +76,3 @@ dependencies {
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
please note that `generate_libvpx_android_configs.sh` and the makefiles need
to be modified to work with arbitrary versions of libvpx and libyuv.
-
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 5068586a4a..de6dc65f74 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -30,7 +31,7 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
}
ext {
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index d9fa8af2c3..b8b28fc346 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index 2647776b74..669d77cdeb 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -158,6 +158,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
}
}
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ // Do nothing.
+ }
+
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
index c44c703bb1..9b0355a9e7 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
@@ -20,6 +20,7 @@ import android.graphics.Canvas;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
+import android.support.annotation.IntDef;
import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
@@ -30,6 +31,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
@@ -38,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders video using the native VP9 decoder.
*/
public final class LibvpxVideoRenderer extends BaseRenderer {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
+ REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
+ private @interface ReinitializationState {}
+ /**
+ * The decoder does not need to be re-initialized.
+ */
+ private static final int REINITIALIZATION_STATE_NONE = 0;
+ /**
+ * The input format has changed in a way that requires the decoder to be re-initialized, but we
+ * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
+ * ensure that it outputs any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
+ /**
+ * The input format has changed in a way that requires the decoder to be re-initialized, and we've
+ * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
+ * end of stream signal to indicate that it has output any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
+
/**
* The type of a message that can be passed to an instance of this class via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
@@ -71,12 +96,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private DecoderCounters decoderCounters;
private Format format;
private VpxDecoder decoder;
- private DecoderInputBuffer inputBuffer;
+ private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
private VpxOutputBuffer nextOutputBuffer;
private DrmSession drmSession;
private DrmSession pendingDrmSession;
+ @ReinitializationState
+ private int decoderReinitializationState;
+ private boolean decoderReceivedBuffers;
+
private Bitmap bitmap;
private boolean renderedFirstFrame;
private long joiningDeadlineMs;
@@ -153,6 +182,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
}
@Override
@@ -185,49 +215,25 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- // We have a format.
- drmSession = pendingDrmSession;
- ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- } else if (drmSessionState == DrmSession.STATE_OPENED
- || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
- mediaCrypto = drmSession.getMediaCrypto();
- } else {
- // The drm session isn't open yet.
- return;
- }
- }
- try {
- if (decoder == null) {
- // If we don't have a decoder yet, we need to instantiate one.
- long codecInitializingTimestamp = SystemClock.elapsedRealtime();
- TraceUtil.beginSection("createVpxDecoder");
- decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
- decoder.setOutputMode(outputMode);
+ // If we don't have a decoder yet, we need to instantiate one.
+ maybeInitDecoder();
+
+ if (decoder != null) {
+ try {
+ // Rendering loop.
+ TraceUtil.beginSection("drainAndFeed");
+ while (drainOutputBuffer(positionUs)) {}
+ while (feedInputBuffer()) {}
TraceUtil.endSection();
- long codecInitializedTimestamp = SystemClock.elapsedRealtime();
- eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
- codecInitializedTimestamp - codecInitializingTimestamp);
- decoderCounters.decoderInitCount++;
+ } catch (VpxDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
}
- TraceUtil.beginSection("drainAndFeed");
- while (drainOutputBuffer(positionUs)) {}
- while (feedInputBuffer()) {}
- TraceUtil.endSection();
- } catch (VpxDecoderException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
+ decoderCounters.ensureUpdated();
}
- decoderCounters.ensureUpdated();
}
- private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException {
- if (outputStreamEnded) {
- return false;
- }
-
+ private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException,
+ VpxDecoderException {
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
if (outputBuffer == null) {
if (nextOutputBuffer != null) {
@@ -247,15 +253,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
if (outputBuffer.isEndOfStream()) {
- outputStreamEnded = true;
- outputBuffer.release();
- outputBuffer = null;
+ if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+ // We're waiting to re-initialize the decoder, and have now processed all final buffers.
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ outputBuffer.release();
+ outputBuffer = null;
+ outputStreamEnded = true;
+ }
return false;
}
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
- if (outputBuffer.timeUs <= positionUs) {
+ if (isBufferLate(outputBuffer.timeUs - positionUs)) {
skipBuffer();
return true;
}
@@ -280,23 +292,20 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
-
/**
* Returns whether the current frame should be dropped.
*
* @param outputBufferTimeUs The timestamp of the current output buffer.
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or
- * {@link TIME_UNSET} if the next output buffer is unavailable.
+ * {@link C#TIME_UNSET} if the next output buffer is unavailable.
* @param positionUs The current playback position.
* @param joiningDeadlineMs The joining deadline.
* @return Returns whether to drop the current output buffer.
*/
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
long positionUs, long joiningDeadlineMs) {
- // Drop the frame if we're joining and are more than 30ms late, or if we have the next frame
- // and that's also late. Else we'll render what we have.
- return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000)
- || (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs);
+ return isBufferLate(outputBufferTimeUs - positionUs)
+ && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
}
private void renderBuffer() {
@@ -356,7 +365,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
- if (inputStreamEnded) {
+ if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ || inputStreamEnded) {
+ // We need to reinitialize the decoder or the input stream has ended.
return false;
}
@@ -367,6 +378,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+ inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
int result;
if (waitingForKeys) {
// We've already read an encrypted sample into buffer, and are waiting for keys.
@@ -394,36 +413,43 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
inputBuffer.flip();
+ inputBuffer.colorInfo = formatHolder.format.colorInfo;
decoder.queueInputBuffer(inputBuffer);
+ decoderReceivedBuffers = true;
decoderCounters.inputBufferCount++;
inputBuffer = null;
return true;
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null) {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
- && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
- private void flushDecoder() {
- inputBuffer = null;
+ private void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false;
- if (outputBuffer != null) {
- outputBuffer.release();
- outputBuffer = null;
+ if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ inputBuffer = null;
+ if (outputBuffer != null) {
+ outputBuffer.release();
+ outputBuffer = null;
+ }
+ if (nextOutputBuffer != null) {
+ nextOutputBuffer.release();
+ nextOutputBuffer = null;
+ }
+ decoder.flush();
+ decoderReceivedBuffers = false;
}
- if (nextOutputBuffer != null) {
- nextOutputBuffer.release();
- nextOutputBuffer = null;
- }
- decoder.flush();
}
@Override
@@ -461,7 +487,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
@Override
- protected void onPositionReset(long positionUs, boolean joining) {
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false;
outputStreamEnded = false;
clearRenderedFirstFrame();
@@ -480,18 +506,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
protected void onStarted() {
droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
- joiningDeadlineMs = C.TIME_UNSET;
}
@Override
protected void onStopped() {
+ joiningDeadlineMs = C.TIME_UNSET;
maybeNotifyDroppedFrames();
}
@Override
protected void onDisabled() {
- inputBuffer = null;
- outputBuffer = null;
format = null;
waitingForKeys = false;
clearReportedVideoSize();
@@ -518,20 +542,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- private void releaseDecoder() {
+ private void maybeInitDecoder() throws ExoPlaybackException {
if (decoder != null) {
- decoder.release();
- decoder = null;
- decoderCounters.decoderReleaseCount++;
- waitingForKeys = false;
- if (drmSession != null && pendingDrmSession != drmSession) {
- try {
- drmSessionManager.releaseSession(drmSession);
- } finally {
- drmSession = null;
+ return;
+ }
+
+ drmSession = pendingDrmSession;
+ ExoMediaCrypto mediaCrypto = null;
+ if (drmSession != null) {
+ mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ throw ExoPlaybackException.createForRenderer(drmError, getIndex());
}
+ // The drm session isn't open yet.
+ return;
}
}
+
+ try {
+ long codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createVpxDecoder");
+ decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
+ decoder.setOutputMode(outputMode);
+ TraceUtil.endSection();
+ long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
+ codecInitializedTimestamp - codecInitializingTimestamp);
+ decoderCounters.decoderInitCount++;
+ } catch (VpxDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ private void releaseDecoder() {
+ if (decoder == null) {
+ return;
+ }
+
+ inputBuffer = null;
+ outputBuffer = null;
+ nextOutputBuffer = null;
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ decoderReceivedBuffers = false;
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@@ -555,6 +612,17 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ if (pendingDrmSession != drmSession) {
+ if (decoderReceivedBuffers) {
+ // Signal end of stream and wait for any final output buffers before re-initialization.
+ decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ // There aren't any final output buffers, so release the decoder immediately.
+ releaseDecoder();
+ maybeInitDecoder();
+ }
+ }
+
eventDispatcher.inputFormatChanged(format);
}
@@ -654,4 +722,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ private static boolean isBufferLate(long earlyUs) {
+ // Class a buffer as late if it should have been presented more than 30ms ago.
+ return earlyUs < -30000;
+ }
+
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index 73ec7c2f96..4bec5bdf4c 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
@@ -27,7 +26,7 @@ import java.nio.ByteBuffer;
* Vpx decoder.
*/
/* package */ final class VpxDecoder extends
- SimpleDecoder {
+ SimpleDecoder {
public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0;
@@ -54,7 +53,7 @@ import java.nio.ByteBuffer;
*/
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
- super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
+ super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
}
@@ -85,8 +84,8 @@ import java.nio.ByteBuffer;
}
@Override
- protected DecoderInputBuffer createInputBuffer() {
- return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
+ protected VpxInputBuffer createInputBuffer() {
+ return new VpxInputBuffer();
}
@Override
@@ -100,7 +99,7 @@ import java.nio.ByteBuffer;
}
@Override
- protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
+ protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
boolean reset) {
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
@@ -128,6 +127,7 @@ import java.nio.ByteBuffer;
} else if (getFrameResult == -1) {
return new VpxDecoderException("Buffer initialization failed.");
}
+ outputBuffer.colorInfo = inputBuffer.colorInfo;
return null;
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java
new file mode 100644
index 0000000000..fcae9dc6bc
--- /dev/null
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java
@@ -0,0 +1,32 @@
+/*
+ * 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.ext.vp9;
+
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.video.ColorInfo;
+
+/**
+ * Input buffer to a {@link VpxDecoder}.
+ */
+/* package */ final class VpxInputBuffer extends DecoderInputBuffer {
+
+ public ColorInfo colorInfo;
+
+ public VpxInputBuffer() {
+ super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
+ }
+
+}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
index db3cf49b0c..2618bf7c62 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.decoder.OutputBuffer;
+import com.google.android.exoplayer2.video.ColorInfo;
import java.nio.ByteBuffer;
/**
@@ -37,6 +38,8 @@ import java.nio.ByteBuffer;
public ByteBuffer data;
public int width;
public int height;
+ public ColorInfo colorInfo;
+
/**
* YUV planes for YUV mode.
*/
diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
index 15dbabdb1f..5f058d0551 100755
--- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
+++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
@@ -51,8 +51,7 @@ config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
config[3]+=" --disable-avx2 --enable-pic"
arch[4]="arm64-v8a"
-config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon"
-config[4]+=" --disable-neon-asm"
+config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
arch[5]="x86_64"
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
diff --git a/library/all/build.gradle b/library/all/build.gradle
index 63943ada77..79ed9c747b 100644
--- a/library/all/build.gradle
+++ b/library/all/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -24,11 +25,11 @@ android {
}
dependencies {
- compile project(':library-core')
- compile project(':library-dash')
- compile project(':library-hls')
- compile project(':library-smoothstreaming')
- compile project(':library-ui')
+ 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')
}
ext {
diff --git a/library/core/build.gradle b/library/core/build.gradle
index bb0adaa4c7..65a7353607 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
+apply from: '../../constants.gradle'
android {
compileSdkVersion project.ext.compileSdkVersion
@@ -22,6 +23,7 @@ android {
targetSdkVersion project.ext.targetSdkVersion
}
+ // Workaround to prevent circular dependency on project :testutils.
sourceSets {
androidTest {
java.srcDirs += "../../testutils/src/main/java/"
diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml
index 9eab386b51..aeddc611cf 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -24,11 +24,13 @@
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
+
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3 b/library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3
new file mode 100644
index 0000000000..c8b49c8cd5
Binary files /dev/null and b/library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3 differ
diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump
index 1932ab78f7..f533e14c3f 100644
--- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump
+++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump
@@ -30,5 +30,6 @@ track 1:
time = 0
flags = 1073741824
data = length 39, hash B7FE77F4
+ crypto mode = 1
encryption key = length 16, hash 4CE944CF
tracksEnded = true
diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump
index 8751c99b20..d84c549dea 100644
--- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump
+++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump
@@ -30,5 +30,6 @@ track 1:
time = 0
flags = 1073741824
data = length 24, hash E58668B1
+ crypto mode = 1
encryption key = length 16, hash 4CE944CF
tracksEnded = true
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump
index 16816917b7..5ba8cc29ae 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump
index fec523f971..f698fd28cf 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump
index a4a60989ed..8d803d0bac 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump
index a77575bb0c..09f6267270 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump
index 16816917b7..5ba8cc29ae 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump
index 7be7d02493..73e537f8c8 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump
index 34f19c6bce..3b7dc3fd1e 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump
index 68484d2cf4..b6a6741fcc 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump
index 8b2e7858b0..738002f7ef 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump
index 8d398efdb8..a237fd0dfc 100644
--- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump
@@ -8,7 +8,7 @@ track 0:
bitrate = -1
id = null
containerMimeType = null
- sampleMimeType = audio/x-flac
+ sampleMimeType = audio/flac
maxInputSize = 768000
width = -1
height = -1
diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump
index 536f76adad..8e2c5125a3 100644
--- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump
@@ -9,7 +9,7 @@ track 0:
id = null
containerMimeType = null
sampleMimeType = audio/vorbis
- maxInputSize = 65025
+ maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump
index 7490773bd5..aa25303ac3 100644
--- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump
@@ -9,7 +9,7 @@ track 0:
id = null
containerMimeType = null
sampleMimeType = audio/vorbis
- maxInputSize = 65025
+ maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump
index 82ad16e701..58969058fa 100644
--- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump
@@ -9,7 +9,7 @@ track 0:
id = null
containerMimeType = null
sampleMimeType = audio/vorbis
- maxInputSize = 65025
+ maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump
index 810b66901c..4c789a8431 100644
--- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump
@@ -9,7 +9,7 @@ track 0:
id = null
containerMimeType = null
sampleMimeType = audio/vorbis
- maxInputSize = 65025
+ maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump
index 8e86ca340d..2f163572bf 100644
--- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump
+++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump
@@ -9,7 +9,7 @@ track 0:
id = null
containerMimeType = null
sampleMimeType = audio/vorbis
- maxInputSize = 65025
+ maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
index 2c10bfe6a0..3bc8805a76 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -15,28 +15,20 @@
*/
package com.google.android.exoplayer2;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.util.Pair;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
+import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.testutil.FakeRenderer;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.util.MimeTypes;
-import java.io.IOException;
-import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
/**
@@ -62,7 +54,7 @@ public final class ExoPlayerTest extends TestCase {
* error.
*/
public void testPlayEmptyTimeline() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = Timeline.EMPTY;
MediaSource mediaSource = new FakeMediaSource(timeline, null);
FakeRenderer renderer = new FakeRenderer(null);
@@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes a single period.
*/
public void testPlaySinglePeriodTimeline() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Object manifest = new Object();
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
@@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes three periods.
*/
public void testPlayMultiPeriodTimeline() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0),
@@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase {
* source.
*/
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
- final PlayerWrapper playerWrapper = new PlayerWrapper();
+ final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10),
@@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase {
}
public void testRepreparationGivesFreshSourceInfo() throws Exception {
- PlayerWrapper playerWrapper = new PlayerWrapper();
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
@@ -218,501 +210,54 @@ public final class ExoPlayerTest extends TestCase {
Pair.create(timeline, thirdSourceManifest));
}
- /**
- * Wraps a player with its own handler thread.
- */
- private static final class PlayerWrapper implements ExoPlayer.EventListener {
-
- private final CountDownLatch sourceInfoCountDownLatch;
- private final CountDownLatch endedCountDownLatch;
- private final HandlerThread playerThread;
- private final Handler handler;
- private final LinkedList> sourceInfos;
-
- private ExoPlayer player;
- private TrackGroupArray trackGroups;
- private Exception exception;
-
- // Written only on the main thread.
- private volatile int positionDiscontinuityCount;
-
- public PlayerWrapper() {
- sourceInfoCountDownLatch = new CountDownLatch(1);
- endedCountDownLatch = new CountDownLatch(1);
- playerThread = new HandlerThread("ExoPlayerTest thread");
- playerThread.start();
- handler = new Handler(playerThread.getLooper());
- sourceInfos = new LinkedList<>();
- }
-
- // Called on the test thread.
-
- public void blockUntilEnded(long timeoutMs) throws Exception {
- if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
- exception = new TimeoutException("Test playback timed out waiting for playback to end.");
- }
- release();
- // Throw any pending exception (from playback, timing out or releasing).
- if (exception != null) {
- throw exception;
- }
- }
-
- public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception {
- if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
- throw new TimeoutException("Test playback timed out waiting for source info.");
- }
- }
-
- public void setup(final MediaSource mediaSource, final Renderer... renderers) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- try {
- player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
- player.addListener(PlayerWrapper.this);
- player.setPlayWhenReady(true);
- player.prepare(mediaSource);
- } catch (Exception e) {
- handleError(e);
- }
+ public void testRepeatModeChanges() throws Exception {
+ Timeline timeline = new FakeTimeline(
+ new TimelineWindowDefinition(true, false, 100000),
+ new TimelineWindowDefinition(true, false, 100000),
+ new TimelineWindowDefinition(true, false, 100000));
+ final int[] actionSchedule = { // 0 -> 1
+ ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1
+ ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2
+ ExoPlayer.REPEAT_MODE_ONE, // 2 -> 2
+ ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0
+ ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0
+ -1, // 0 -> 0
+ ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1
+ -1, // 1 -> 2
+ -1 // 2 -> ended
+ };
+ int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
+ final LinkedList windowIndices = new LinkedList<>();
+ final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
+ ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
+ @Override
+ @SuppressWarnings("ResourceType")
+ public void onPositionDiscontinuity() {
+ super.onPositionDiscontinuity();
+ int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
+ if (actionSchedule[actionIndex] != -1) {
+ player.setRepeatMode(actionSchedule[actionIndex]);
}
- });
- }
-
- public void prepare(final MediaSource mediaSource) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- try {
- player.prepare(mediaSource);
- } catch (Exception e) {
- handleError(e);
- }
- }
- });
- }
-
- public void release() throws InterruptedException {
- handler.post(new Runnable() {
- @Override
- public void run() {
- try {
- if (player != null) {
- player.release();
- }
- } catch (Exception e) {
- handleError(e);
- } finally {
- playerThread.quit();
- }
- }
- });
- playerThread.join();
- }
-
- private void handleError(Exception exception) {
- if (this.exception == null) {
- this.exception = exception;
+ windowIndices.add(player.getCurrentWindowIndex());
+ actionCounter.countDown();
}
- endedCountDownLatch.countDown();
+ };
+ MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
+ FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
+ playerWrapper.setup(mediaSource, renderer);
+ boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ playerWrapper.release();
+ assertTrue("Test playback timed out waiting for action schedule to end.", finished);
+ if (playerWrapper.exception != null) {
+ throw playerWrapper.exception;
}
-
- @SafeVarargs
- public final void assertSourceInfosEquals(Pair... sourceInfos) {
- assertEquals(sourceInfos.length, this.sourceInfos.size());
- for (Pair sourceInfo : sourceInfos) {
- assertEquals(sourceInfo, this.sourceInfos.remove());
- }
+ assertEquals(expectedWindowIndices.length, windowIndices.size());
+ for (int i = 0; i < expectedWindowIndices.length; i++) {
+ assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
}
-
- // ExoPlayer.EventListener implementation.
-
- @Override
- public void onLoadingChanged(boolean isLoading) {
- // Do nothing.
- }
-
- @Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED) {
- endedCountDownLatch.countDown();
- }
- }
-
- @Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
- sourceInfos.add(Pair.create(timeline, manifest));
- sourceInfoCountDownLatch.countDown();
- }
-
- @Override
- public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
- this.trackGroups = trackGroups;
- }
-
- @Override
- public void onPlayerError(ExoPlaybackException exception) {
- handleError(exception);
- }
-
- @SuppressWarnings("NonAtomicVolatileUpdate")
- @Override
- public void onPositionDiscontinuity() {
- positionDiscontinuityCount++;
- }
-
- @Override
- public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- // Do nothing.
- }
-
- }
-
- private static final class TimelineWindowDefinition {
-
- public final boolean isSeekable;
- public final boolean isDynamic;
- public final long durationUs;
-
- public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
- this.isSeekable = isSeekable;
- this.isDynamic = isDynamic;
- this.durationUs = durationUs;
- }
-
- }
-
- private static final class FakeTimeline extends Timeline {
-
- private final TimelineWindowDefinition[] windowDefinitions;
-
- public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
- this.windowDefinitions = windowDefinitions;
- }
-
- @Override
- public int getWindowCount() {
- return windowDefinitions.length;
- }
-
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
- Object id = setIds ? windowIndex : null;
- return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
- windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
- }
-
- @Override
- public int getPeriodCount() {
- return windowDefinitions.length;
- }
-
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
- Object id = setIds ? periodIndex : null;
- return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false);
- }
-
- @Override
- public int getIndexOfPeriod(Object uid) {
- if (!(uid instanceof Integer)) {
- return C.INDEX_UNSET;
- }
- int index = (Integer) uid;
- return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
- }
-
- }
-
- /**
- * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
- * the period will return a {@link FakeMediaPeriod}.
- */
- private static class FakeMediaSource implements MediaSource {
-
- private final Timeline timeline;
- private final Object manifest;
- private final TrackGroupArray trackGroupArray;
- private final ArrayList activeMediaPeriods;
-
- private boolean preparedSource;
- private boolean releasedSource;
-
- public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
- this.timeline = timeline;
- this.manifest = manifest;
- TrackGroup[] trackGroups = new TrackGroup[formats.length];
- for (int i = 0; i < formats.length; i++) {
- trackGroups[i] = new TrackGroup(formats[i]);
- }
- trackGroupArray = new TrackGroupArray(trackGroups);
- activeMediaPeriods = new ArrayList<>();
- }
-
- @Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- assertFalse(preparedSource);
- preparedSource = true;
- listener.onSourceInfoRefreshed(timeline, manifest);
- }
-
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- assertTrue(preparedSource);
- }
-
- @Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- Assertions.checkIndex(index, 0, timeline.getPeriodCount());
- assertTrue(preparedSource);
- assertFalse(releasedSource);
- assertEquals(0, positionUs);
- FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
- activeMediaPeriods.add(mediaPeriod);
- return mediaPeriod;
- }
-
- @Override
- public void releasePeriod(MediaPeriod mediaPeriod) {
- assertTrue(preparedSource);
- assertFalse(releasedSource);
- FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
- assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
- fakeMediaPeriod.release();
- }
-
- @Override
- public void releaseSource() {
- assertTrue(preparedSource);
- assertFalse(releasedSource);
- assertTrue(activeMediaPeriods.isEmpty());
- releasedSource = true;
- }
-
- }
-
- /**
- * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that
- * track will give the player a {@link FakeSampleStream}.
- */
- private static final class FakeMediaPeriod implements MediaPeriod {
-
- private final TrackGroupArray trackGroupArray;
-
- private boolean preparedPeriod;
-
- public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
- this.trackGroupArray = trackGroupArray;
- }
-
- public void release() {
- preparedPeriod = false;
- }
-
- @Override
- public void prepare(Callback callback) {
- assertFalse(preparedPeriod);
- preparedPeriod = true;
- callback.onPrepared(this);
- }
-
- @Override
- public void maybeThrowPrepareError() throws IOException {
- assertTrue(preparedPeriod);
- }
-
- @Override
- public TrackGroupArray getTrackGroups() {
- assertTrue(preparedPeriod);
- return trackGroupArray;
- }
-
- @Override
- public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
- SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
- assertTrue(preparedPeriod);
- int rendererCount = selections.length;
- for (int i = 0; i < rendererCount; i++) {
- if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
- streams[i] = null;
- }
- }
- for (int i = 0; i < rendererCount; i++) {
- if (streams[i] == null && selections[i] != null) {
- TrackSelection selection = selections[i];
- assertEquals(1, selection.length());
- assertEquals(0, selection.getIndexInTrackGroup(0));
- TrackGroup trackGroup = selection.getTrackGroup();
- assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET);
- streams[i] = new FakeSampleStream(trackGroup.getFormat(0));
- streamResetFlags[i] = true;
- }
- }
- return 0;
- }
-
- @Override
- public void discardBuffer(long positionUs) {
- // Do nothing.
- }
-
- @Override
- public long readDiscontinuity() {
- assertTrue(preparedPeriod);
- return C.TIME_UNSET;
- }
-
- @Override
- public long getBufferedPositionUs() {
- assertTrue(preparedPeriod);
- return C.TIME_END_OF_SOURCE;
- }
-
- @Override
- public long seekToUs(long positionUs) {
- assertTrue(preparedPeriod);
- assertEquals(0, positionUs);
- return positionUs;
- }
-
- @Override
- public long getNextLoadPositionUs() {
- assertTrue(preparedPeriod);
- return C.TIME_END_OF_SOURCE;
- }
-
- @Override
- public boolean continueLoading(long positionUs) {
- assertTrue(preparedPeriod);
- return false;
- }
-
- }
-
- /**
- * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag
- * on its input buffer.
- */
- private static final class FakeSampleStream implements SampleStream {
-
- private final Format format;
-
- private boolean readFormat;
-
- public FakeSampleStream(Format format) {
- this.format = format;
- }
-
- @Override
- public boolean isReady() {
- return true;
- }
-
- @Override
- public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
- boolean formatRequired) {
- if (formatRequired || !readFormat) {
- formatHolder.format = format;
- readFormat = true;
- return C.RESULT_FORMAT_READ;
- } else {
- buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
- return C.RESULT_BUFFER_READ;
- }
- }
-
- @Override
- public void maybeThrowError() throws IOException {
- // Do nothing.
- }
-
- @Override
- public void skipData(long positionUs) {
- // Do nothing.
- }
-
- }
-
- /**
- * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
- * verifies that it reads a given {@link Format}.
- */
- private static class FakeRenderer extends BaseRenderer {
-
- private final Format expectedFormat;
-
- public int positionResetCount;
- public int formatReadCount;
- public int bufferReadCount;
- public boolean isEnded;
-
- public FakeRenderer(Format expectedFormat) {
- super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
- : MimeTypes.getTrackType(expectedFormat.sampleMimeType));
- this.expectedFormat = expectedFormat;
- }
-
- @Override
- protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
- positionResetCount++;
- isEnded = false;
- }
-
- @Override
- public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
- if (isEnded) {
- return;
- }
-
- // Verify the format matches the expected format.
- FormatHolder formatHolder = new FormatHolder();
- DecoderInputBuffer buffer =
- new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
- int result = readSource(formatHolder, buffer, false);
- if (result == C.RESULT_FORMAT_READ) {
- formatReadCount++;
- assertEquals(expectedFormat, formatHolder.format);
- } else if (result == C.RESULT_BUFFER_READ) {
- bufferReadCount++;
- if (buffer.isEndOfStream()) {
- isEnded = true;
- }
- }
- }
-
- @Override
- public boolean isReady() {
- return isSourceReady();
- }
-
- @Override
- public boolean isEnded() {
- return isEnded;
- }
-
- @Override
- public int supportsFormat(Format format) throws ExoPlaybackException {
- return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED
- : FORMAT_UNSUPPORTED_TYPE;
- }
-
- }
-
- private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
-
- public FakeMediaClockRenderer(Format expectedFormat) {
- super(expectedFormat);
- }
-
- @Override
- public MediaClock getMediaClock() {
- return this;
- }
-
+ assertEquals(9, playerWrapper.positionDiscontinuityCount);
+ assertTrue(renderer.isEnded);
+ playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java
index a47a3fb12d..316fb11e9a 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java
@@ -53,9 +53,9 @@ public final class FormatTest extends TestCase {
}
public void testParcelable() {
- DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4,
+ DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4,
TestUtil.buildTestData(128, 1 /* data seed */));
- DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM,
+ DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM,
TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3};
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java
new file mode 100644
index 0000000000..d69f40283f
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import junit.framework.TestCase;
+
+/**
+ * Unit test for {@link Timeline}.
+ */
+public class TimelineTest extends TestCase {
+
+ public void testEmptyTimeline() {
+ TimelineAsserts.assertEmpty(Timeline.EMPTY);
+ }
+
+ public void testSinglePeriodTimeline() {
+ Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ }
+
+ public void testMultiPeriodTimeline() {
+ Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 5);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ }
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
index df2e8756a5..aa8cbfdb62 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
@@ -31,16 +31,16 @@ import junit.framework.TestCase;
*/
public class DrmInitDataTest extends TestCase {
- private static final SchemeData DATA_1 =
- new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
- private static final SchemeData DATA_2 =
- new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
- private static final SchemeData DATA_1B =
- new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
- private static final SchemeData DATA_2B =
- new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
- private static final SchemeData DATA_UNIVERSAL =
- new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */));
+ private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
+ TestUtil.buildTestData(128, 1 /* data seed */));
+ private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
+ TestUtil.buildTestData(128, 2 /* data seed */));
+ private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
+ TestUtil.buildTestData(128, 1 /* data seed */));
+ private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
+ TestUtil.buildTestData(128, 2 /* data seed */));
+ private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4,
+ TestUtil.buildTestData(128, 3 /* data seed */));
public void testParcelable() {
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
index afd690762b..9f5b067b5e 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
@@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
}
private static DrmInitData newDrmInitData() {
- return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
+ return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType",
new byte[] {1, 4, 7, 0, 3, 6}));
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
index 321181621e..4587c98317 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FlvExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class FlvExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new FlvExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
index 48eee69b50..57beec3ac6 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Tests for {@link MatroskaExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class MatroskaExtractorTest extends InstrumentationTestCase {
public void testMkvSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new MatroskaExtractor();
@@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
}
public void testWebmSubsampleEncryption() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new MatroskaExtractor();
@@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
}
public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new MatroskaExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
index c70710f1ee..3ad6a74bc9 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link Mp3Extractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Mp3ExtractorTest extends InstrumentationTestCase {
public void testMp3Sample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new Mp3Extractor();
@@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
}
public void testTrimmedMp3Sample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new Mp3Extractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
index 95ad8b446e..d8da8760e4 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FragmentedMp4Extractor}.
@@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
+ ExtractorAsserts
+ .assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
}
public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing.
- TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
+ ExtractorAsserts.assertOutput(
+ getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
}
public void testAtomWithZeroSize() throws Exception {
- TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
+ ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
getInstrumentation(), ParserException.class);
}
- private static TestUtil.ExtractorFactory getExtractorFactory() {
+ private static ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0);
}
- private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
- return new TestUtil.ExtractorFactory() {
+ private static ExtractorFactory getExtractorFactory(final int flags) {
+ return new ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor(flags, null);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
index 6ad777da70..a534d6dd24 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
import android.annotation.TargetApi;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Tests for {@link Mp4Extractor}.
@@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Mp4ExtractorTest extends InstrumentationTestCase {
public void testMp4Sample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new Mp4Extractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
index 04a6131652..26b7991869 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
@@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
-import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory;
import java.io.IOException;
/**
@@ -35,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase {
};
public void testOpus() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
+ ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
}
public void testFlac() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation());
+ ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation());
}
public void testFlacNoSeektable() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
+ ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
getInstrumentation());
}
public void testVorbis() throws Exception {
- TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation());
+ ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg",
+ getInstrumentation());
}
public void testSniffVorbis() throws Exception {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
index 4e99e2745e..5a9d60512c 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
@@ -19,7 +19,8 @@ import android.annotation.TargetApi;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
public final class RawCcExtractorTest extends InstrumentationTestCase {
public void testRawCcSample() throws Exception {
- TestUtil.assertOutput(
- new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(
+ new ExtractorFactory() {
@Override
public Extractor create() {
return new RawCcExtractor(
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
index ab44e3aed3..1c18e44373 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link Ac3Extractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class Ac3ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new Ac3Extractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
index e30a863d07..bc05be6fa8 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link AdtsExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class AdtsExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new AdtsExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
index ef97bef0ff..e6937ccbc8 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link PsExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class PsExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new PsExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
index 7bf722cd8f..09c9facab0 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
@@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
@@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new TsExtractor();
@@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
fileData = out.toByteArray();
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new TsExtractor();
@@ -75,7 +77,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
- TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
+ TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
factory);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
@@ -100,7 +102,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
- TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
+ TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
factory);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
index a416d644b7..7c969fd386 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
-import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link WavExtractor}.
@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class WavExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
- TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
+ ExtractorAsserts.assertOutput(new ExtractorFactory() {
@Override
public Extractor create() {
return new WavExtractor();
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
index 0933fb858b..1a15b750ac 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
@@ -15,20 +15,17 @@
*/
package com.google.android.exoplayer2.source;
-import static org.mockito.Mockito.doAnswer;
-
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
-import com.google.android.exoplayer2.source.MediaSource.Listener;
+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.TestUtil;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
/**
* Unit tests for {@link ClippingMediaSource}.
@@ -38,15 +35,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
private static final long TEST_PERIOD_DURATION_US = 1000000;
private static final long TEST_CLIP_AMOUNT_US = 300000;
- @Mock
- private MediaSource mockMediaSource;
- private Timeline clippedTimeline;
private Window window;
private Period period;
@Override
protected void setUp() throws Exception {
- TestUtil.setUpMockito(this);
window = new Timeline.Window();
period = new Timeline.Period();
}
@@ -109,35 +102,30 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
clippedTimeline.getPeriod(0, period).getDurationUs());
}
+ public void testWindowAndPeriodIndices() {
+ Timeline timeline = new FakeTimeline(
+ new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
+ Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
+ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
+ TimelineAsserts.assertWindowIds(clippedTimeline, 111);
+ TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
+ TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ }
+
/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
- private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
- mockMediaSourceSourceWithTimeline(timeline);
- new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
- new Listener() {
- @Override
- public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
- clippedTimeline = timeline;
- }
- });
- return clippedTimeline;
- }
-
- /**
- * Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
- */
- private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
- doAnswer(new Answer() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
- listener.onSourceInfoRefreshed(timeline, null);
- return null;
- }
- }).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
- Mockito.any(MediaSource.Listener.class));
- return mockMediaSource;
+ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
+ MediaSource mediaSource = new FakeMediaSource(timeline, null);
+ return TestUtil.extractTimelineFromMediaSource(
+ new ClippingMediaSource(mediaSource, startMs, endMs));
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
new file mode 100644
index 0000000000..49f34f7b2b
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2016 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;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+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.TestUtil;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link ConcatenatingMediaSource}.
+ */
+public final class ConcatenatingMediaSourceTest extends TestCase {
+
+ public void testSingleMediaSource() {
+ Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 3);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+
+ timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111));
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ TimelineAsserts.assertPeriodCounts(timeline, 3);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0);
+ }
+
+ public void testMultipleMediaSources() {
+ Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222),
+ createFakeTimeline(3, 333) };
+ Timeline timeline = getConcatenatedTimeline(false, timelines);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET,
+ 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
+
+ timeline = getConcatenatedTimeline(true, timelines);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 2, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 1, 2, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
+ }
+
+ public void testNestedMediaSources() {
+ Timeline timeline = getConcatenatedTimeline(false,
+ getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)),
+ getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444)));
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 3, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ 1, 2, 3, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 0);
+ }
+
+ /**
+ * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns
+ * the concatenated timeline.
+ */
+ private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic,
+ Timeline... timelines) {
+ MediaSource[] mediaSources = new MediaSource[timelines.length];
+ for (int i = 0; i < timelines.length; i++) {
+ mediaSources[i] = new FakeMediaSource(timelines[i], null);
+ }
+ return TestUtil.extractTimelineFromMediaSource(
+ new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources));
+ }
+
+ private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
+ return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
new file mode 100644
index 0000000000..520d99892a
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
@@ -0,0 +1,533 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.MediaSource.Listener;
+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.TimelineAsserts;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link DynamicConcatenatingMediaSource}
+ */
+public final class DynamicConcatenatingMediaSourceTest extends TestCase {
+
+ private static final int TIMEOUT_MS = 10000;
+
+ private Timeline timeline;
+ private boolean timelineUpdated;
+
+ public void testPlaylistChangesAfterPreparation() throws InterruptedException {
+ timeline = null;
+ FakeMediaSource[] childSources = createMediaSources(7);
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ prepareAndListenToTimelineUpdates(mediaSource);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertEmpty(timeline);
+
+ // Add first source.
+ mediaSource.addMediaSource(childSources[0]);
+ waitForTimelineUpdate();
+ assertNotNull(timeline);
+ TimelineAsserts.assertPeriodCounts(timeline, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111);
+
+ // Add at front of queue.
+ mediaSource.addMediaSource(0, childSources[1]);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 1);
+ TimelineAsserts.assertWindowIds(timeline, 222, 111);
+
+ // Add at back of queue.
+ mediaSource.addMediaSource(childSources[2]);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
+
+ // Add in the middle.
+ mediaSource.addMediaSource(1, childSources[3]);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333);
+
+ // Add bulk.
+ mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4],
+ (MediaSource) childSources[5], (MediaSource) childSources[6]));
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
+
+ // Remove in the middle.
+ mediaSource.removeMediaSource(3);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(3);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(3);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(1);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
+ for (int i = 3; i <= 6; i++) {
+ childSources[i].assertReleased();
+ }
+
+ // Assert correct next and previous indices behavior after some insertions and removals.
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
+
+ // Remove at front of queue.
+ mediaSource.removeMediaSource(0);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 3);
+ TimelineAsserts.assertWindowIds(timeline, 111, 333);
+ childSources[1].assertReleased();
+
+ // Remove at back of queue.
+ mediaSource.removeMediaSource(1);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111);
+ childSources[2].assertReleased();
+
+ // Remove last source.
+ mediaSource.removeMediaSource(0);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertEmpty(timeline);
+ childSources[3].assertReleased();
+ }
+
+ public void testPlaylistChangesBeforePreparation() throws InterruptedException {
+ timeline = null;
+ FakeMediaSource[] childSources = createMediaSources(4);
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ mediaSource.addMediaSource(childSources[0]);
+ mediaSource.addMediaSource(childSources[1]);
+ mediaSource.addMediaSource(0, childSources[2]);
+ mediaSource.removeMediaSource(1);
+ mediaSource.addMediaSource(1, childSources[3]);
+ assertNull(timeline);
+
+ prepareAndListenToTimelineUpdates(mediaSource);
+ waitForTimelineUpdate();
+ assertNotNull(timeline);
+ TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2);
+ TimelineAsserts.assertWindowIds(timeline, 333, 444, 222);
+
+ mediaSource.releaseSource();
+ for (int i = 1; i < 4; i++) {
+ childSources[i].assertReleased();
+ }
+ }
+
+ public void testPlaylistWithLazyMediaSource() throws InterruptedException {
+ timeline = null;
+ FakeMediaSource[] childSources = createMediaSources(2);
+ LazyMediaSource[] lazySources = new LazyMediaSource[4];
+ for (int i = 0; i < 4; i++) {
+ lazySources[i] = new LazyMediaSource();
+ }
+
+ //Add lazy sources before preparation
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ mediaSource.addMediaSource(lazySources[0]);
+ mediaSource.addMediaSource(0, childSources[0]);
+ mediaSource.removeMediaSource(1);
+ mediaSource.addMediaSource(1, lazySources[1]);
+ assertNull(timeline);
+ prepareAndListenToTimelineUpdates(mediaSource);
+ waitForTimelineUpdate();
+ assertNotNull(timeline);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111, null);
+ TimelineAsserts.assertWindowIsDynamic(timeline, false, true);
+
+ lazySources[1].triggerTimelineUpdate(createFakeTimeline(8));
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 9);
+ TimelineAsserts.assertWindowIds(timeline, 111, 999);
+ TimelineAsserts.assertWindowIsDynamic(timeline, false, false);
+
+ //Add lazy sources after preparation
+ mediaSource.addMediaSource(1, lazySources[2]);
+ waitForTimelineUpdate();
+ mediaSource.addMediaSource(2, childSources[1]);
+ waitForTimelineUpdate();
+ mediaSource.addMediaSource(0, lazySources[3]);
+ waitForTimelineUpdate();
+ mediaSource.removeMediaSource(2);
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9);
+ TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999);
+ TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false);
+
+ lazySources[3].triggerTimelineUpdate(createFakeTimeline(7));
+ waitForTimelineUpdate();
+ TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9);
+ TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999);
+ TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false);
+
+ mediaSource.releaseSource();
+ childSources[0].assertReleased();
+ childSources[1].assertReleased();
+ }
+
+ public void testIllegalArguments() {
+ DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
+ MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null);
+
+ // Null sources.
+ try {
+ mediaSource.addMediaSource(null);
+ fail("Null mediaSource not allowed.");
+ } catch (NullPointerException e) {
+ // Expected.
+ }
+
+ MediaSource[] mediaSources = { validSource, null };
+ try {
+ mediaSource.addMediaSources(Arrays.asList(mediaSources));
+ fail("Null mediaSource not allowed.");
+ } catch (NullPointerException e) {
+ // Expected.
+ }
+
+ // Duplicate sources.
+ mediaSource.addMediaSource(validSource);
+ try {
+ mediaSource.addMediaSource(validSource);
+ fail("Duplicate mediaSource not allowed.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ mediaSources = new MediaSource[] {
+ new FakeMediaSource(createFakeTimeline(2), null), validSource };
+ try {
+ mediaSource.addMediaSources(Arrays.asList(mediaSources));
+ fail("Duplicate mediaSource not allowed.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) {
+ mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
+ timeline = newTimeline;
+ synchronized (DynamicConcatenatingMediaSourceTest.this) {
+ timelineUpdated = true;
+ DynamicConcatenatingMediaSourceTest.this.notify();
+ }
+ }
+ });
+ }
+
+ private synchronized void waitForTimelineUpdate() throws InterruptedException {
+ long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS;
+ while (!timelineUpdated) {
+ wait(TIMEOUT_MS);
+ if (System.currentTimeMillis() >= timeoutMs) {
+ fail("No timeline update occurred within timeout.");
+ }
+ }
+ timelineUpdated = false;
+ }
+
+ private static FakeMediaSource[] createMediaSources(int count) {
+ FakeMediaSource[] sources = new FakeMediaSource[count];
+ for (int i = 0; i < count; i++) {
+ sources[i] = new FakeMediaSource(createFakeTimeline(i), null);
+ }
+ return sources;
+ }
+
+ private static FakeTimeline createFakeTimeline(int index) {
+ return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111));
+ }
+
+ private static class LazyMediaSource implements MediaSource {
+
+ private Listener listener;
+
+ public void triggerTimelineUpdate(Timeline timeline) {
+ listener.onSourceInfoRefreshed(timeline, null);
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ return null;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ }
+
+ @Override
+ public void releaseSource() {
+ }
+
+ }
+
+ /**
+ * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread.
+ */
+ private static class StubExoPlayer implements ExoPlayer, Handler.Callback {
+
+ private final Handler handler;
+
+ public StubExoPlayer() {
+ HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper(), this);
+ }
+
+ @Override
+ public Looper getPlaybackLooper() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addListener(EventListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeListener(EventListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getPlaybackState() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRepeatMode() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isLoading() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekToDefaultPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekToDefaultPosition(int windowIndex) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void stop() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void release() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void sendMessages(ExoPlayerMessage... messages) {
+ handler.obtainMessage(0, messages).sendToTarget();
+ }
+
+ @Override
+ public void blockingSendMessages(ExoPlayerMessage... messages) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRendererCount() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object getCurrentManifest() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getDuration() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getBufferedPercentage() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCurrentWindowDynamic() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCurrentWindowSeekable() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isPlayingAd() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj;
+ for (ExoPlayerMessage message : messages) {
+ try {
+ message.target.handleMessage(message.messageType, message.message);
+ } catch (ExoPlaybackException e) {
+ fail("Unexpected ExoPlaybackException.");
+ }
+ }
+ return true;
+ }
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
new file mode 100644
index 0000000000..87e6bb9983
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+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.TestUtil;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link LoopingMediaSource}.
+ */
+public class LoopingMediaSourceTest extends TestCase {
+
+ private final Timeline multiWindowTimeline;
+
+ public LoopingMediaSourceTest() {
+ multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource(
+ new FakeTimeline(new TimelineWindowDefinition(1, 111),
+ new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null));
+ }
+
+ public void testSingleLoop() {
+ Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ 1, 2, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
+ }
+
+ public void testMultiLoop() {
+ Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL,
+ 8, 0, 1, 2, 3, 4, 5, 6, 7);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF,
+ 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL,
+ 1, 2, 3, 4, 5, 6, 7, 8, 0);
+ }
+
+ public void testInfiniteLoop() {
+ Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE);
+ TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 2, 0, 1);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 1, 2, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2);
+ TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0);
+ }
+
+ /**
+ * Wraps the specified timeline in a {@link LoopingMediaSource} and returns
+ * the looping timeline.
+ */
+ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
+ MediaSource mediaSource = new FakeMediaSource(timeline, null);
+ return TestUtil.extractTimelineFromMediaSource(
+ new LoopingMediaSource(mediaSource, loopCount));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
new file mode 100644
index 0000000000..76ea0e34cf
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -0,0 +1,688 @@
+/*
+ * 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;
+
+import android.test.MoreAsserts;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+/**
+ * Test for {@link SampleQueue}.
+ */
+public class SampleQueueTest extends TestCase {
+
+ private static final int ALLOCATION_SIZE = 16;
+
+ private static final Format TEST_FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0);
+ private static final Format TEST_FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0);
+ private static final Format TEST_FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0);
+ private static final byte[] TEST_DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
+
+ /*
+ * TEST_SAMPLE_SIZES and TEST_SAMPLE_OFFSETS are intended to test various boundary cases (with
+ * respect to the allocation size). TEST_SAMPLE_OFFSETS values are defined as the backward offsets
+ * (as expected by SampleQueue.sampleMetadata) assuming that TEST_DATA has been written to the
+ * sampleQueue in full. The allocations are filled as follows, where | indicates a boundary
+ * between allocations and x indicates a byte that doesn't belong to a sample:
+ *
+ * x|xx|x|x|||xx|
+ */
+ private static final int[] TEST_SAMPLE_SIZES = new int[] {
+ ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 2, ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 1,
+ ALLOCATION_SIZE, ALLOCATION_SIZE * 2, ALLOCATION_SIZE * 2 - 2, ALLOCATION_SIZE
+ };
+ private static final int[] TEST_SAMPLE_OFFSETS = new int[] {
+ ALLOCATION_SIZE * 9, ALLOCATION_SIZE * 8 + 1, ALLOCATION_SIZE * 7, ALLOCATION_SIZE * 6 + 1,
+ ALLOCATION_SIZE * 5, ALLOCATION_SIZE * 3, ALLOCATION_SIZE + 1, 0
+ };
+ private static final long[] TEST_SAMPLE_TIMESTAMPS = new long[] {
+ 0, 1000, 2000, 3000, 4000, 5000, 6000, 7000
+ };
+ private static final long LAST_SAMPLE_TIMESTAMP =
+ TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1];
+ private static final int[] TEST_SAMPLE_FLAGS = new int[] {
+ C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0
+ };
+ private static final Format[] TEST_SAMPLE_FORMATS = new Format[] {
+ TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_2, TEST_FORMAT_2,
+ TEST_FORMAT_2, TEST_FORMAT_2
+ };
+ private static final int TEST_DATA_SECOND_KEYFRAME_INDEX = 4;
+
+ private Allocator allocator;
+ private SampleQueue sampleQueue;
+ private FormatHolder formatHolder;
+ private DecoderInputBuffer inputBuffer;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
+ sampleQueue = new SampleQueue(allocator);
+ formatHolder = new FormatHolder();
+ inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ allocator = null;
+ sampleQueue = null;
+ formatHolder = null;
+ inputBuffer = null;
+ }
+
+ public void testResetReleasesAllocations() {
+ writeTestData();
+ assertAllocationCount(10);
+ sampleQueue.reset();
+ assertAllocationCount(0);
+ }
+
+ public void testReadWithoutWrite() {
+ assertNoSamplesToRead(null);
+ }
+
+ public void testReadFormatDeduplicated() {
+ sampleQueue.format(TEST_FORMAT_1);
+ assertReadFormat(false, TEST_FORMAT_1);
+ // If the same format is input then it should be de-duplicated (i.e. not output again).
+ sampleQueue.format(TEST_FORMAT_1);
+ assertNoSamplesToRead(TEST_FORMAT_1);
+ // The same applies for a format that's equal (but a different object).
+ sampleQueue.format(TEST_FORMAT_1_COPY);
+ assertNoSamplesToRead(TEST_FORMAT_1);
+ }
+
+ public void testReadSingleSamples() {
+ sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
+
+ assertAllocationCount(1);
+ // Nothing to read.
+ assertNoSamplesToRead(null);
+
+ sampleQueue.format(TEST_FORMAT_1);
+
+ // Read the format.
+ assertReadFormat(false, TEST_FORMAT_1);
+ // Nothing to read.
+ assertNoSamplesToRead(TEST_FORMAT_1);
+
+ sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
+
+ // If formatRequired, should read the format rather than the sample.
+ assertReadFormat(true, TEST_FORMAT_1);
+ // Otherwise should read the sample.
+ assertSampleRead(1000, true, TEST_DATA, 0, ALLOCATION_SIZE);
+ // Allocation should still be held.
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // The allocation should have been released.
+ assertAllocationCount(0);
+
+ // Nothing to read.
+ assertNoSamplesToRead(TEST_FORMAT_1);
+
+ // Write a second sample followed by one byte that does not belong to it.
+ sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
+ sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null);
+
+ // If formatRequired, should read the format rather than the sample.
+ assertReadFormat(true, TEST_FORMAT_1);
+ // Read the sample.
+ assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1);
+ // Allocation should still be held.
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // The last byte written to the sample queue may belong to a sample whose metadata has yet to be
+ // written, so an allocation should still be held.
+ assertAllocationCount(1);
+
+ // Write metadata for a third sample containing the remaining byte.
+ sampleQueue.sampleMetadata(3000, 0, 1, 0, null);
+
+ // If formatRequired, should read the format rather than the sample.
+ assertReadFormat(true, TEST_FORMAT_1);
+ // Read the sample.
+ assertSampleRead(3000, false, TEST_DATA, ALLOCATION_SIZE - 1, 1);
+ // Allocation should still be held.
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // The allocation should have been released.
+ assertAllocationCount(0);
+ }
+
+ public void testReadMultiSamples() {
+ writeTestData();
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ assertAllocationCount(10);
+ assertReadTestData();
+ assertAllocationCount(10);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ }
+
+ public void testReadMultiSamplesTwice() {
+ writeTestData();
+ writeTestData();
+ assertAllocationCount(20);
+ assertReadTestData(TEST_FORMAT_2);
+ assertReadTestData(TEST_FORMAT_2);
+ assertAllocationCount(20);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ }
+
+ public void testReadMultiWithRewind() {
+ writeTestData();
+ assertReadTestData();
+ assertEquals(8, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Rewind.
+ sampleQueue.rewind();
+ assertAllocationCount(10);
+ // Read again.
+ assertEquals(0, sampleQueue.getReadIndex());
+ assertReadTestData();
+ }
+
+ public void testRewindAfterDiscard() {
+ writeTestData();
+ assertReadTestData();
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ // Rewind.
+ sampleQueue.rewind();
+ assertAllocationCount(0);
+ // Can't read again.
+ assertEquals(8, sampleQueue.getReadIndex());
+ assertReadEndOfStream(false);
+ }
+
+ public void testAdvanceToEnd() {
+ writeTestData();
+ sampleQueue.advanceToEnd();
+ assertAllocationCount(10);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ // Despite skipping all samples, we should still read the last format, since this is the
+ // expected format for a subsequent sample.
+ assertReadFormat(false, TEST_FORMAT_2);
+ // Once the format has been read, there's nothing else to read.
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToEndRetainsUnassignedData() {
+ sampleQueue.format(TEST_FORMAT_1);
+ sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
+ sampleQueue.advanceToEnd();
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be
+ // written.
+ assertAllocationCount(1);
+ // We should be able to read the format.
+ assertReadFormat(false, TEST_FORMAT_1);
+ // Once the format has been read, there's nothing else to read.
+ assertNoSamplesToRead(TEST_FORMAT_1);
+
+ sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
+ // Once the metadata has been written, check the sample can be read as expected.
+ assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE);
+ assertNoSamplesToRead(TEST_FORMAT_1);
+ assertAllocationCount(1);
+ sampleQueue.discardToRead();
+ assertAllocationCount(0);
+ }
+
+ public void testAdvanceToBeforeBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
+ // Should fail and have no effect.
+ assertFalse(result);
+ assertReadTestData();
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToStartOfBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
+ // Should succeed but have no effect (we're already at the first frame).
+ assertTrue(result);
+ assertReadTestData();
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToEndOfBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
+ // Should succeed and skip to 2nd keyframe.
+ assertTrue(result);
+ assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToAfterBuffer() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
+ // Should fail and have no effect.
+ assertFalse(result);
+ assertReadTestData();
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testAdvanceToAfterBufferAllowed() {
+ writeTestData();
+ boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
+ // Should succeed and skip to 2nd keyframe.
+ assertTrue(result);
+ assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardToEnd() {
+ writeTestData();
+ // Should discard everything.
+ sampleQueue.discardToEnd();
+ assertEquals(8, sampleQueue.getReadIndex());
+ assertAllocationCount(0);
+ // We should still be able to read the upstream format.
+ assertReadFormat(false, TEST_FORMAT_2);
+ // We should be able to write and read subsequent samples.
+ writeTestData();
+ assertReadTestData(TEST_FORMAT_2);
+ }
+
+ public void testDiscardToStopAtReadPosition() {
+ writeTestData();
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
+ assertEquals(0, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Read the first sample.
+ assertReadTestData(null, 0, 1);
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true);
+ assertEquals(1, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Should discard the read sample.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true);
+ assertAllocationCount(9);
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
+ assertAllocationCount(9);
+ // Should be able to read the remaining samples.
+ assertReadTestData(TEST_FORMAT_1, 1, 7);
+ assertEquals(8, sampleQueue.getReadIndex());
+ // Should discard up to the second last sample
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true);
+ assertAllocationCount(3);
+ // Should discard up the last sample
+ sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);
+ assertAllocationCount(1);
+ }
+
+ public void testDiscardToDontStopAtReadPosition() {
+ writeTestData();
+ // Shouldn't discard anything.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false);
+ assertEquals(0, sampleQueue.getReadIndex());
+ assertAllocationCount(10);
+ // Should discard the first sample.
+ sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false);
+ assertEquals(1, sampleQueue.getReadIndex());
+ assertAllocationCount(9);
+ // Should be able to read the remaining samples.
+ assertReadTestData(TEST_FORMAT_1, 1, 7);
+ }
+
+ public void testDiscardUpstream() {
+ writeTestData();
+ sampleQueue.discardUpstreamSamples(8);
+ assertAllocationCount(10);
+ sampleQueue.discardUpstreamSamples(7);
+ assertAllocationCount(9);
+ sampleQueue.discardUpstreamSamples(6);
+ assertAllocationCount(7);
+ sampleQueue.discardUpstreamSamples(5);
+ assertAllocationCount(5);
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(4);
+ sampleQueue.discardUpstreamSamples(3);
+ assertAllocationCount(3);
+ sampleQueue.discardUpstreamSamples(2);
+ assertAllocationCount(2);
+ sampleQueue.discardUpstreamSamples(1);
+ assertAllocationCount(1);
+ sampleQueue.discardUpstreamSamples(0);
+ assertAllocationCount(0);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardUpstreamMulti() {
+ writeTestData();
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(4);
+ sampleQueue.discardUpstreamSamples(0);
+ assertAllocationCount(0);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardUpstreamBeforeRead() {
+ writeTestData();
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(4);
+ assertReadTestData(null, 0, 4);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testDiscardUpstreamAfterRead() {
+ writeTestData();
+ assertReadTestData(null, 0, 3);
+ sampleQueue.discardUpstreamSamples(8);
+ assertAllocationCount(10);
+ sampleQueue.discardToRead();
+ assertAllocationCount(7);
+ sampleQueue.discardUpstreamSamples(7);
+ assertAllocationCount(6);
+ sampleQueue.discardUpstreamSamples(6);
+ assertAllocationCount(4);
+ sampleQueue.discardUpstreamSamples(5);
+ assertAllocationCount(2);
+ sampleQueue.discardUpstreamSamples(4);
+ assertAllocationCount(1);
+ sampleQueue.discardUpstreamSamples(3);
+ assertAllocationCount(0);
+ assertReadFormat(false, TEST_FORMAT_2);
+ assertNoSamplesToRead(TEST_FORMAT_2);
+ }
+
+ public void testLargestQueuedTimestampWithDiscardUpstream() {
+ writeTestData();
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 1);
+ // Discarding from upstream should reduce the largest timestamp.
+ assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2],
+ sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(0);
+ // Discarding everything from upstream without reading should unset the largest timestamp.
+ assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs());
+ }
+
+ public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() {
+ long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};
+ writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, decodeOrderTimestamps,
+ TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
+ assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 2);
+ // Discarding the last two samples should not change the largest timestamp, due to the decode
+ // ordering of the timestamps.
+ assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3);
+ // Once a third sample is discarded, the largest timestamp should have changed.
+ assertEquals(4000, sampleQueue.getLargestQueuedTimestampUs());
+ sampleQueue.discardUpstreamSamples(0);
+ // Discarding everything from upstream without reading should unset the largest timestamp.
+ assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs());
+ }
+
+ public void testLargestQueuedTimestampWithRead() {
+ writeTestData();
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ assertReadTestData();
+ // Reading everything should not reduce the largest timestamp.
+ assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs());
+ }
+
+ // Internal methods.
+
+ /**
+ * Writes standard test data to {@code sampleQueue}.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void writeTestData() {
+ writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, TEST_SAMPLE_TIMESTAMPS,
+ TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
+ }
+
+ /**
+ * Writes the specified test data to {@code sampleQueue}.
+ *
+ *
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets,
+ long[] sampleTimestamps, Format[] sampleFormats, int[] sampleFlags) {
+ sampleQueue.sampleData(new ParsableByteArray(data), data.length);
+ Format format = null;
+ for (int i = 0; i < sampleTimestamps.length; i++) {
+ if (sampleFormats[i] != format) {
+ sampleQueue.format(sampleFormats[i]);
+ format = sampleFormats[i];
+ }
+ sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i],
+ sampleOffsets[i], null);
+ }
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ */
+ private void assertReadTestData() {
+ assertReadTestData(null, 0);
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ *
+ * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
+ */
+ private void assertReadTestData(Format startFormat) {
+ assertReadTestData(startFormat, 0);
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ *
+ * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
+ * @param firstSampleIndex The index of the first sample that's expected to be read.
+ */
+ private void assertReadTestData(Format startFormat, int firstSampleIndex) {
+ assertReadTestData(startFormat, firstSampleIndex,
+ TEST_SAMPLE_TIMESTAMPS.length - firstSampleIndex);
+ }
+
+ /**
+ * Asserts correct reading of standard test data from {@code sampleQueue}.
+ *
+ * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
+ * @param firstSampleIndex The index of the first sample that's expected to be read.
+ * @param sampleCount The number of samples to read.
+ */
+ private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) {
+ Format format = startFormat;
+ for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) {
+ // Use equals() on the read side despite using referential equality on the write side, since
+ // sampleQueue de-duplicates written formats using equals().
+ if (!TEST_SAMPLE_FORMATS[i].equals(format)) {
+ // If the format has changed, we should read it.
+ assertReadFormat(false, TEST_SAMPLE_FORMATS[i]);
+ format = TEST_SAMPLE_FORMATS[i];
+ }
+ // If we require the format, we should always read it.
+ assertReadFormat(true, TEST_SAMPLE_FORMATS[i]);
+ // Assert the sample is as expected.
+ assertSampleRead(TEST_SAMPLE_TIMESTAMPS[i],
+ (TEST_SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0,
+ TEST_DATA,
+ TEST_DATA.length - TEST_SAMPLE_OFFSETS[i] - TEST_SAMPLE_SIZES[i],
+ TEST_SAMPLE_SIZES[i]);
+ }
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} is behaving correctly, given there are no samples to read and
+ * the last format to be written to the sample queue is {@code endFormat}.
+ *
+ * @param endFormat The last format to be written to the sample queue, or null of no format has
+ * been written.
+ */
+ private void assertNoSamplesToRead(Format endFormat) {
+ // If not formatRequired or loadingFinished, should read nothing.
+ assertReadNothing(false);
+ // If formatRequired, should read the end format if set, else read nothing.
+ if (endFormat == null) {
+ assertReadNothing(true);
+ } else {
+ assertReadFormat(true, endFormat);
+ }
+ // If loadingFinished, should read end of stream.
+ assertReadEndOfStream(false);
+ assertReadEndOfStream(true);
+ // Having read end of stream should not affect other cases.
+ assertReadNothing(false);
+ if (endFormat == null) {
+ assertReadNothing(true);
+ } else {
+ assertReadFormat(true, endFormat);
+ }
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}.
+ *
+ * @param formatRequired The value of {@code formatRequired} passed to readData.
+ */
+ private void assertReadNothing(boolean formatRequired) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);
+ assertEquals(C.RESULT_NOTHING_READ, result);
+ // formatHolder should not be populated.
+ assertNull(formatHolder.format);
+ // inputBuffer should not be populated.
+ assertInputBufferContainsNoSampleData();
+ assertInputBufferHasNoDefaultFlagsSet();
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the
+ * {@link DecoderInputBuffer#isEndOfStream()} is set.
+ *
+ * @param formatRequired The value of {@code formatRequired} passed to readData.
+ */
+ private void assertReadEndOfStream(boolean formatRequired) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0);
+ assertEquals(C.RESULT_BUFFER_READ, result);
+ // formatHolder should not be populated.
+ assertNull(formatHolder.format);
+ // inputBuffer should not contain sample data, but end of stream flag should be set.
+ assertInputBufferContainsNoSampleData();
+ assertTrue(inputBuffer.isEndOfStream());
+ assertFalse(inputBuffer.isDecodeOnly());
+ assertFalse(inputBuffer.isEncrypted());
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_FORMAT_READ} and that the format
+ * holder is filled with a {@link Format} that equals {@code format}.
+ *
+ * @param formatRequired The value of {@code formatRequired} passed to readData.
+ * @param format The expected format.
+ */
+ private void assertReadFormat(boolean formatRequired, Format format) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);
+ assertEquals(C.RESULT_FORMAT_READ, result);
+ // formatHolder should be populated.
+ assertEquals(format, formatHolder.format);
+ // inputBuffer should not be populated.
+ assertInputBufferContainsNoSampleData();
+ assertInputBufferHasNoDefaultFlagsSet();
+ }
+
+ /**
+ * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is
+ * filled with the specified sample data.
+ *
+ * @param timeUs The expected buffer timestamp.
+ * @param isKeyframe The expected keyframe flag.
+ * @param sampleData An array containing the expected sample data.
+ * @param offset The offset in {@code sampleData} of the expected sample data.
+ * @param length The length of the expected sample data.
+ */
+ private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset,
+ int length) {
+ clearFormatHolderAndInputBuffer();
+ int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0);
+ assertEquals(C.RESULT_BUFFER_READ, result);
+ // formatHolder should not be populated.
+ assertNull(formatHolder.format);
+ // inputBuffer should be populated.
+ assertEquals(timeUs, inputBuffer.timeUs);
+ assertEquals(isKeyframe, inputBuffer.isKeyFrame());
+ assertFalse(inputBuffer.isDecodeOnly());
+ assertFalse(inputBuffer.isEncrypted());
+ inputBuffer.flip();
+ assertEquals(length, inputBuffer.data.limit());
+ byte[] readData = new byte[length];
+ inputBuffer.data.get(readData);
+ MoreAsserts.assertEquals(Arrays.copyOfRange(sampleData, offset, offset + length), readData);
+ }
+
+ /**
+ * Asserts the number of allocations currently in use by {@code sampleQueue}.
+ *
+ * @param count The expected number of allocations.
+ */
+ private void assertAllocationCount(int count) {
+ assertEquals(ALLOCATION_SIZE * count, allocator.getTotalBytesAllocated());
+ }
+
+ /**
+ * Asserts {@code inputBuffer} does not contain any sample data.
+ */
+ private void assertInputBufferContainsNoSampleData() {
+ if (inputBuffer.data == null) {
+ return;
+ }
+ inputBuffer.flip();
+ assertEquals(0, inputBuffer.data.limit());
+ }
+
+ private void assertInputBufferHasNoDefaultFlagsSet() {
+ assertFalse(inputBuffer.isEndOfStream());
+ assertFalse(inputBuffer.isDecodeOnly());
+ assertFalse(inputBuffer.isEncrypted());
+ }
+
+ private void clearFormatHolderAndInputBuffer() {
+ formatHolder.format = null;
+ inputBuffer.clear();
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java
index 381aaa34ae..492cf036b4 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java
@@ -157,39 +157,43 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(2, output.size());
Cue ttmlCue = output.get(0);
assertEquals("lorem", ttmlCue.text.toString());
- assertEquals(10.f / 100.f, ttmlCue.position);
- assertEquals(10.f / 100.f, ttmlCue.line);
- assertEquals(20.f / 100.f, ttmlCue.size);
+ assertEquals(10f / 100f, ttmlCue.position);
+ assertEquals(10f / 100f, ttmlCue.line);
+ assertEquals(20f / 100f, ttmlCue.size);
ttmlCue = output.get(1);
assertEquals("amet", ttmlCue.text.toString());
- assertEquals(60.f / 100.f, ttmlCue.position);
- assertEquals(10.f / 100.f, ttmlCue.line);
- assertEquals(20.f / 100.f, ttmlCue.size);
+ assertEquals(60f / 100f, ttmlCue.position);
+ assertEquals(10f / 100f, ttmlCue.line);
+ assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(5000000);
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("ipsum", ttmlCue.text.toString());
- assertEquals(40.f / 100.f, ttmlCue.position);
- assertEquals(40.f / 100.f, ttmlCue.line);
- assertEquals(20.f / 100.f, ttmlCue.size);
+ assertEquals(40f / 100f, ttmlCue.position);
+ assertEquals(40f / 100f, ttmlCue.line);
+ assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(9000000);
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("dolor", ttmlCue.text.toString());
- assertEquals(10.f / 100.f, ttmlCue.position);
- assertEquals(80.f / 100.f, ttmlCue.line);
+ assertEquals(Cue.DIMEN_UNSET, ttmlCue.position);
+ assertEquals(Cue.DIMEN_UNSET, ttmlCue.line);
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size);
+ // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
+ // assertEquals(10f / 100f, ttmlCue.position);
+ // assertEquals(80f / 100f, ttmlCue.line);
+ // assertEquals(1f, ttmlCue.size);
output = subtitle.getCues(21000000);
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("She first said this", ttmlCue.text.toString());
- assertEquals(45.f / 100.f, ttmlCue.position);
- assertEquals(45.f / 100.f, ttmlCue.line);
- assertEquals(35.f / 100.f, ttmlCue.size);
+ assertEquals(45f / 100f, ttmlCue.position);
+ assertEquals(45f / 100f, ttmlCue.line);
+ assertEquals(35f / 100f, ttmlCue.size);
output = subtitle.getCues(25000000);
ttmlCue = output.get(0);
assertEquals("She first said this\nThen this", ttmlCue.text.toString());
@@ -197,8 +201,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(1, output.size());
ttmlCue = output.get(0);
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
- assertEquals(45.f / 100.f, ttmlCue.position);
- assertEquals(45.f / 100.f, ttmlCue.line);
+ assertEquals(45f / 100f, ttmlCue.position);
+ assertEquals(45f / 100f, ttmlCue.line);
}
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java
index f471370e4c..d6be100877 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java
@@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase {
String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
StringBuilder builder = new StringBuilder();
- assertEquals(CssParser.parseNextToken(input, builder), "lorem");
- assertEquals(CssParser.parseNextToken(input, builder), ":");
- assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
- assertEquals(CssParser.parseNextToken(input, builder), "{");
- assertEquals(CssParser.parseNextToken(input, builder), "dolor");
- assertEquals(CssParser.parseNextToken(input, builder), "}");
- assertEquals(CssParser.parseNextToken(input, builder), "#sit");
- assertEquals(CssParser.parseNextToken(input, builder), ",");
- assertEquals(CssParser.parseNextToken(input, builder), "amet");
- assertEquals(CssParser.parseNextToken(input, builder), ";");
- assertEquals(CssParser.parseNextToken(input, builder), "lorem");
- assertEquals(CssParser.parseNextToken(input, builder), ":");
- assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
- assertEquals(CssParser.parseNextToken(input, builder), "dolor");
- assertEquals(CssParser.parseNextToken(input, builder), "(");
- assertEquals(CssParser.parseNextToken(input, builder), "(");
- assertEquals(CssParser.parseNextToken(input, builder), ")");
- assertEquals(CssParser.parseNextToken(input, builder), ")");
- assertEquals(CssParser.parseNextToken(input, builder), null);
+ assertEquals("lorem", CssParser.parseNextToken(input, builder));
+ assertEquals(":", CssParser.parseNextToken(input, builder));
+ assertEquals("ipsum", CssParser.parseNextToken(input, builder));
+ assertEquals("{", CssParser.parseNextToken(input, builder));
+ assertEquals("dolor", CssParser.parseNextToken(input, builder));
+ assertEquals("}", CssParser.parseNextToken(input, builder));
+ assertEquals("#sit", CssParser.parseNextToken(input, builder));
+ assertEquals(",", CssParser.parseNextToken(input, builder));
+ assertEquals("amet", CssParser.parseNextToken(input, builder));
+ assertEquals(";", CssParser.parseNextToken(input, builder));
+ assertEquals("lorem", CssParser.parseNextToken(input, builder));
+ assertEquals(":", CssParser.parseNextToken(input, builder));
+ assertEquals("ipsum", CssParser.parseNextToken(input, builder));
+ assertEquals("dolor", CssParser.parseNextToken(input, builder));
+ assertEquals("(", CssParser.parseNextToken(input, builder));
+ assertEquals("(", CssParser.parseNextToken(input, builder));
+ assertEquals(")", CssParser.parseNextToken(input, builder));
+ assertEquals(")", CssParser.parseNextToken(input, builder));
+ assertEquals(null, CssParser.parseNextToken(input, builder));
}
public void testStyleScoreSystem() {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java
new file mode 100644
index 0000000000..c31c651384
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2016 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.trackselection;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link MappingTrackSelector}.
+ */
+public final class MappingTrackSelectorTest extends TestCase {
+
+ private static final RendererCapabilities VIDEO_CAPABILITIES =
+ new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO);
+ private static final RendererCapabilities AUDIO_CAPABILITIES =
+ new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO);
+ private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] {
+ VIDEO_CAPABILITIES, AUDIO_CAPABILITIES
+ };
+
+ private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup(
+ Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE,
+ Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, null));
+ private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup(
+ Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,
+ Format.NO_VALUE, 2, 44100, null, null, 0, null));
+ private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray(
+ VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP);
+
+ private static final TrackSelection[] TRACK_SELECTIONS = new TrackSelection[] {
+ new FixedTrackSelection(VIDEO_TRACK_GROUP, 0),
+ new FixedTrackSelection(AUDIO_TRACK_GROUP, 0)
+ };
+
+ /**
+ * Tests that the video and audio track groups are mapped onto the correct renderers.
+ */
+ public void testMapping() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();
+ trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
+ trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP);
+ trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP);
+ }
+
+ /**
+ * Tests that the video and audio track groups are mapped onto the correct renderers when the
+ * renderer ordering is reversed.
+ */
+ public void testMappingReverseOrder() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();
+ RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] {
+ AUDIO_CAPABILITIES, VIDEO_CAPABILITIES};
+ trackSelector.selectTracks(reverseOrderRendererCapabilities, TRACK_GROUPS);
+ trackSelector.assertMappedTrackGroups(0, AUDIO_TRACK_GROUP);
+ trackSelector.assertMappedTrackGroups(1, VIDEO_TRACK_GROUP);
+ }
+
+ /**
+ * Tests video and audio track groups are mapped onto the correct renderers when there are
+ * multiple track groups of the same type.
+ */
+ public void testMappingMulti() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();
+ TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP,
+ VIDEO_TRACK_GROUP);
+ trackSelector.selectTracks(RENDERER_CAPABILITIES, multiTrackGroups);
+ trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP, VIDEO_TRACK_GROUP);
+ trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP);
+ }
+
+ /**
+ * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[],
+ * TrackGroupArray[], int[][][])} is propagated correctly to the result of
+ * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}.
+ */
+ public void testSelectTracks() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
+ TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
+ assertEquals(TRACK_SELECTIONS[0], result.selections.get(0));
+ assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
+ }
+
+ /**
+ * Tests that a null override clears a track selection.
+ */
+ public void testSelectTracksWithNullOverride() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
+ trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null);
+ TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
+ assertNull(result.selections.get(0));
+ assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
+ }
+
+ /**
+ * Tests that a null override can be cleared.
+ */
+ public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
+ trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null);
+ trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP));
+ TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS);
+ assertEquals(TRACK_SELECTIONS[0], result.selections.get(0));
+ assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
+ }
+
+ /**
+ * Tests that an override is not applied for a different set of available track groups.
+ */
+ public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException {
+ FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS);
+ trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null);
+ TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES,
+ new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP));
+ assertEquals(TRACK_SELECTIONS[0], result.selections.get(0));
+ assertEquals(TRACK_SELECTIONS[1], result.selections.get(1));
+ }
+
+ /**
+ * A {@link MappingTrackSelector} that returns a fixed result from
+ * {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])}.
+ */
+ private static final class FakeMappingTrackSelector extends MappingTrackSelector {
+
+ private final TrackSelection[] result;
+ private TrackGroupArray[] lastRendererTrackGroupArrays;
+
+ public FakeMappingTrackSelector(TrackSelection... result) {
+ this.result = result.length == 0 ? null : result;
+ }
+
+ @Override
+ protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports)
+ throws ExoPlaybackException {
+ lastRendererTrackGroupArrays = rendererTrackGroupArrays;
+ return result == null ? new TrackSelection[rendererCapabilities.length] : result;
+ }
+
+ public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) {
+ assertEquals(expected.length, lastRendererTrackGroupArrays[rendererIndex].length);
+ for (int i = 0; i < expected.length; i++) {
+ assertEquals(expected[i], lastRendererTrackGroupArrays[rendererIndex].get(i));
+ }
+ }
+
+ }
+
+ /**
+ * A {@link RendererCapabilities} that advertises adaptive support for all tracks of a given type.
+ */
+ private static final class FakeRendererCapabilities implements RendererCapabilities {
+
+ private final int trackType;
+
+ public FakeRendererCapabilities(int trackType) {
+ this.trackType = trackType;
+ }
+
+ @Override
+ public int getTrackType() {
+ return trackType;
+ }
+
+ @Override
+ public int supportsFormat(Format format) throws ExoPlaybackException {
+ return MimeTypes.getTrackType(format.sampleMimeType) == trackType
+ ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE;
+ }
+
+ @Override
+ public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+ return ADAPTIVE_SEAMLESS;
+ }
+
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java
new file mode 100644
index 0000000000..102c89ec2b
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.upstream;
+
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.testutil.TestUtil;
+
+/**
+ * Unit tests for {@link AssetDataSource}.
+ */
+public final class AssetDataSourceTest extends InstrumentationTestCase {
+
+ private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
+
+ public void testReadFileUri() throws Exception {
+ AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext());
+ DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH));
+ TestUtil.assertDataSourceContent(dataSource, dataSpec,
+ TestUtil.getByteArray(getInstrumentation(), DATA_PATH));
+ }
+
+ public void testReadAssetUri() throws Exception {
+ AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext());
+ DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH));
+ TestUtil.assertDataSourceContent(dataSource, dataSpec,
+ TestUtil.getByteArray(getInstrumentation(), DATA_PATH));
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
new file mode 100644
index 0000000000..834e7e1374
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.upstream;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * Unit tests for {@link ContentDataSource}.
+ */
+public final class ContentDataSourceTest extends InstrumentationTestCase {
+
+ private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
+ private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
+
+ public void testReadValidUri() throws Exception {
+ ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
+ Uri contentUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .path(DATA_PATH).build();
+ DataSpec dataSpec = new DataSpec(contentUri);
+ TestUtil.assertDataSourceContent(dataSource, dataSpec,
+ TestUtil.getByteArray(getInstrumentation(), DATA_PATH));
+ }
+
+ public void testReadInvalidUri() throws Exception {
+ ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
+ Uri contentUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .build();
+ DataSpec dataSpec = new DataSpec(contentUri);
+ try {
+ dataSource.open(dataSpec);
+ fail();
+ } catch (ContentDataSource.ContentDataSourceException e) {
+ // Expected.
+ assertTrue(e.getCause() instanceof FileNotFoundException);
+ } finally {
+ dataSource.close();
+ }
+ }
+
+ /**
+ * A {@link ContentProvider} for the test.
+ */
+ public static final class TestContentProvider extends ContentProvider {
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(@NonNull Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
+ throws FileNotFoundException {
+ if (uri.getPath() == null) {
+ return null;
+ }
+ try {
+ return getContext().getAssets().openFd(uri.getPath().replaceFirst("/", ""));
+ } catch (IOException e) {
+ FileNotFoundException exception = new FileNotFoundException(e.getMessage());
+ exception.initCause(e);
+ throw exception;
+ }
+ }
+
+ @Override
+ public String getType(@NonNull Uri uri) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri insert(@NonNull Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, String selection,
+ String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, ContentValues values,
+ String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
index c76e4989d8..ca7d5d6214 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
+
import android.net.Uri;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
@@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private static final String KEY_1 = "key 1";
private static final String KEY_2 = "key 2";
- private File cacheDir;
- private SimpleCache simpleCache;
+ private File tempFolder;
+ private SimpleCache cache;
@Override
- protected void setUp() throws Exception {
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
- simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
+ public void setUp() throws Exception {
+ super.setUp();
+ tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
}
@Override
- protected void tearDown() throws Exception {
- Util.recursiveDelete(cacheDir);
+ public void tearDown() throws Exception {
+ Util.recursiveDelete(tempFolder);
+ super.tearDown();
}
public void testMaxCacheFileSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, false, false);
- File[] files = cacheDir.listFiles();
- for (File file : files) {
- if (!file.getName().equals(CachedContentIndex.FILE_NAME)) {
- assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
+ for (String key : cache.getKeys()) {
+ for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {
+ assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE);
+ assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE);
}
}
}
@@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
// Read partial at EOS but don't cross it so length is unknown
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
- assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
+ assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1));
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
@@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
- MoreAsserts.assertEmpty(simpleCache.getKeys());
+ MoreAsserts.assertEmpty(cache.getKeys());
}
public void testReadOnlyCache() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
assertReadDataContentLength(cacheDataSource, false, false);
- assertEquals(0, cacheDir.list().length);
+ assertCacheEmpty(cache);
}
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
@@ -155,30 +159,30 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
assertReadData(cacheDataSource, unknownLength, 0, length);
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
- simpleCache.getContentLength(KEY_1));
+ cache.getContentLength(KEY_1));
}
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
int length) throws IOException {
- int actualLength = TEST_DATA.length - position;
+ int testDataLength = TEST_DATA.length - position;
if (length != C.LENGTH_UNSET) {
- actualLength = Math.min(actualLength, length);
+ testDataLength = Math.min(testDataLength, length);
}
- assertEquals(unknownLength ? length : actualLength,
+ assertEquals(unknownLength ? length : testDataLength,
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
byte[] buffer = new byte[100];
- int index = 0;
+ int totalBytesRead = 0;
while (true) {
- int read = cacheDataSource.read(buffer, index, buffer.length - index);
+ int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead);
if (read == C.RESULT_END_OF_INPUT) {
break;
}
- index += read;
+ totalBytesRead += read;
}
- assertEquals(actualLength, index);
- MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength),
- Arrays.copyOf(buffer, index));
+ assertEquals(testDataLength, totalBytesRead);
+ MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength),
+ Arrays.copyOf(buffer, totalBytesRead));
cacheDataSource.close();
}
@@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
- new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
+ new CacheDataSink(cache, MAX_CACHE_FILE_SIZE));
}
private CacheDataSource createCacheDataSource(boolean setReadException,
@@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
if (setReadException) {
fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
}
- return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
+ return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink,
flags, null);
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
new file mode 100644
index 0000000000..110819d2dc
--- /dev/null
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.upstream.cache;
+
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
+import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
+
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.FakeDataSource;
+import com.google.android.exoplayer2.testutil.FakeDataSource.FakeDataSet;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.File;
+import org.mockito.Answers;
+import org.mockito.Mock;
+
+/**
+ * Tests {@link CacheUtil}.
+ */
+public class CacheUtilTest extends InstrumentationTestCase {
+
+ /**
+ * Abstract fake Cache implementation used by the test. This class must be public so Mockito can
+ * create a proxy for it.
+ */
+ public abstract static class AbstractFakeCache implements Cache {
+ // This array is set to alternating length of cached and not cached regions in tests:
+ // spansAndGaps = {, ,
+ // , , ... }
+ // Ideally it should end with a cached region but it shouldn't matter for any code.
+ private int[] spansAndGaps;
+ private long contentLength;
+
+ private void init() {
+ spansAndGaps = new int[]{};
+ contentLength = C.LENGTH_UNSET;
+ }
+
+ @Override
+ public long getCachedBytes(String key, long position, long length) {
+ for (int i = 0; i < spansAndGaps.length; i++) {
+ int spanOrGap = spansAndGaps[i];
+ if (position < spanOrGap) {
+ long left = Math.min(spanOrGap - position, length);
+ return (i & 1) == 1 ? -left : left;
+ }
+ position -= spanOrGap;
+ }
+ return -length;
+ }
+
+ @Override
+ public long getContentLength(String key) {
+ return contentLength;
+ }
+ }
+
+ @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache;
+ private File tempFolder;
+ private SimpleCache cache;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ TestUtil.setUpMockito(this);
+ mockCache.init();
+ tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ Util.recursiveDelete(tempFolder);
+ super.tearDown();
+ }
+
+ public void testGenerateKey() throws Exception {
+ assertNotNull(CacheUtil.generateKey(Uri.EMPTY));
+
+ Uri testUri = Uri.parse("test");
+ String key = CacheUtil.generateKey(testUri);
+ assertNotNull(key);
+
+ // Should generate the same key for the same input
+ assertEquals(key, CacheUtil.generateKey(testUri));
+
+ // Should generate different key for different input
+ assertFalse(key.equals(CacheUtil.generateKey(Uri.parse("test2"))));
+ }
+
+ public void testGetKey() throws Exception {
+ Uri testUri = Uri.parse("test");
+ String key = "key";
+ // If DataSpec.key is present, returns it
+ assertEquals(key, CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, key)));
+ // If not generates a new one using DataSpec.uri
+ assertEquals(CacheUtil.generateKey(testUri),
+ CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, null)));
+ }
+
+ public void testGetCachedCachingCounters() throws Exception {
+ DataSpec dataSpec = new DataSpec(Uri.parse("test"));
+ CachingCounters counters = CacheUtil.getCached(dataSpec, mockCache, null);
+ // getCached should create a CachingCounters and return it
+ assertNotNull(counters);
+
+ CachingCounters newCounters = CacheUtil.getCached(dataSpec, mockCache, counters);
+ // getCached should set and return given CachingCounters
+ assertEquals(counters, newCounters);
+ }
+
+ public void testGetCachedNoData() throws Exception {
+ CachingCounters counters =
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null);
+
+ assertCounters(counters, 0, 0, C.LENGTH_UNSET);
+ }
+
+ public void testGetCachedDataUnknownLength() throws Exception {
+ // Mock there is 100 bytes cached at the beginning
+ mockCache.spansAndGaps = new int[]{100};
+ CachingCounters counters =
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null);
+
+ assertCounters(counters, 100, 0, C.LENGTH_UNSET);
+ }
+
+ public void testGetCachedNoDataKnownLength() throws Exception {
+ mockCache.contentLength = 1000;
+ CachingCounters counters =
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null);
+
+ assertCounters(counters, 0, 0, 1000);
+ }
+
+ public void testGetCached() throws Exception {
+ mockCache.contentLength = 1000;
+ mockCache.spansAndGaps = new int[]{100, 100, 200};
+ CachingCounters counters =
+ CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null);
+
+ assertCounters(counters, 300, 0, 1000);
+ }
+
+ public void testCache() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ CachingCounters counters =
+ CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, null);
+
+ assertCounters(counters, 0, 100, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheSetOffsetAndLength() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);
+ CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null);
+
+ assertCounters(counters, 0, 20, 20);
+
+ CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters);
+
+ assertCounters(counters, 20, 80, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheUnknownLength() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
+ .setSimulateUnknownLength(true)
+ .appendReadData(TestUtil.buildTestData(100)).endData();
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ DataSpec dataSpec = new DataSpec(Uri.parse("test_data"));
+ CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null);
+
+ assertCounters(counters, 0, 100, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheUnknownLengthPartialCaching() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
+ .setSimulateUnknownLength(true)
+ .appendReadData(TestUtil.buildTestData(100)).endData();
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);
+ CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null);
+
+ assertCounters(counters, 0, 20, 20);
+
+ CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters);
+
+ assertCounters(counters, 20, 80, 100);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheLengthExceedsActualDataLength() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);
+ CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null);
+
+ assertCounters(counters, 0, 100, 1000);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testCacheThrowEOFException() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri testUri = Uri.parse("test_data");
+ DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);
+
+ try {
+ CacheUtil.cache(dataSpec, cache, new CacheDataSource(cache, dataSource),
+ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null,
+ /*enableEOFException*/ true);
+ fail();
+ } catch (EOFException e) {
+ // Do nothing.
+ }
+ }
+
+ public void testCachePolling() throws Exception {
+ final CachingCounters counters = new CachingCounters();
+ FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data")
+ .appendReadData(TestUtil.buildTestData(100))
+ .appendReadAction(new Runnable() {
+ @Override
+ public void run() {
+ assertCounters(counters, 0, 100, 300);
+ }
+ })
+ .appendReadData(TestUtil.buildTestData(100))
+ .appendReadAction(new Runnable() {
+ @Override
+ public void run() {
+ assertCounters(counters, 0, 200, 300);
+ }
+ })
+ .appendReadData(TestUtil.buildTestData(100)).endData();
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters);
+
+ assertCounters(counters, 0, 300, 300);
+ assertCachedData(cache, fakeDataSet);
+ }
+
+ public void testRemove() throws Exception {
+ FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
+ FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
+
+ Uri uri = Uri.parse("test_data");
+ CacheUtil.cache(new DataSpec(uri), cache,
+ // set maxCacheFileSize to 10 to make sure there are multiple spans
+ new CacheDataSource(cache, dataSource, 0, 10),
+ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, true);
+ CacheUtil.remove(cache, CacheUtil.generateKey(uri));
+
+ assertCacheEmpty(cache);
+ }
+
+ private static void assertCounters(CachingCounters counters, int alreadyCachedBytes,
+ int downloadedBytes, int totalBytes) {
+ assertEquals(alreadyCachedBytes, counters.alreadyCachedBytes);
+ assertEquals(downloadedBytes, counters.downloadedBytes);
+ assertEquals(totalBytes, counters.totalBytes);
+ }
+
+}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java
index 923d1d8aaa..1d9aff0723 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java
@@ -146,6 +146,7 @@ public class UtilTest extends TestCase {
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800"));
+ assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-800"));
}
public void testUnescapeInvalidFileName() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index 44fb6d68ae..a88a1dd615 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -96,7 +96,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
this.stream = stream;
readEndOfStream = false;
streamOffsetUs = offsetUs;
- onStreamChanged(formats);
+ onStreamChanged(formats, offsetUs);
}
@Override
@@ -142,9 +142,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
public final void disable() {
Assertions.checkState(state == STATE_ENABLED);
state = STATE_DISABLED;
- onDisabled();
stream = null;
streamIsFinal = false;
+ onDisabled();
}
// RendererCapabilities implementation.
@@ -183,16 +183,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* The default implementation is a no-op.
*
* @param formats The enabled formats.
+ * @param offsetUs The offset that will be added to the timestamps of buffers read via
+ * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input
+ * buffers have monotonically increasing timestamps.
* @throws ExoPlaybackException If an error occurs.
*/
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
// Do nothing.
}
/**
* Called when the position is reset. This occurs when the renderer is enabled after
- * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity
- * is encountered.
+ * {@link #onStreamChanged(Format[], long)} has been called, and also when a position
+ * discontinuity is encountered.
*
* After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples
* starting from a key frame.
@@ -300,8 +303,6 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
/**
* Returns whether the upstream source is ready.
- *
- * @return Whether the source is ready.
*/
protected final boolean isSourceReady() {
return readEndOfStream ? streamIsFinal : stream.isReady();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index 35a69df39e..e8c47d9811 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -31,6 +31,7 @@ import java.util.UUID;
/**
* Defines constants used by the library.
*/
+@SuppressWarnings("InlinedApi")
public final class C {
private C() {}
@@ -83,12 +84,12 @@ public final class C {
public static final String UTF16_NAME = "UTF-16";
/**
- * * The name of the serif font family.
+ * The name of the serif font family.
*/
public static final String SERIF_NAME = "serif";
/**
- * * The name of the sans-serif font family.
+ * The name of the sans-serif font family.
*/
public static final String SANS_SERIF_NAME = "sans-serif";
@@ -101,24 +102,20 @@ public final class C {
/**
* @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
*/
- @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
- @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CBC
*/
- @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
/**
* Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
* {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
*/
- @SuppressWarnings("InlinedApi")
public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
/**
@@ -160,28 +157,24 @@ public final class C {
/**
* @see AudioFormat#ENCODING_AC3
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/**
* @see AudioFormat#ENCODING_E_AC3
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/**
* @see AudioFormat#ENCODING_DTS
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/**
* @see AudioFormat#ENCODING_DTS_HD
*/
- @SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
/**
* @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
*/
- @SuppressWarnings({"InlinedApi", "deprecation"})
+ @SuppressWarnings("deprecation")
public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
@@ -189,13 +182,17 @@ public final class C {
* Stream types for an {@link android.media.AudioTrack}.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
- STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
+ @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_DTMF, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION,
+ STREAM_TYPE_RING, STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL, STREAM_TYPE_USE_DEFAULT})
public @interface StreamType {}
/**
* @see AudioManager#STREAM_ALARM
*/
public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
+ /**
+ * @see AudioManager#STREAM_DTMF
+ */
+ public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF;
/**
* @see AudioManager#STREAM_MUSIC
*/
@@ -216,11 +213,143 @@ public final class C {
* @see AudioManager#STREAM_VOICE_CALL
*/
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
+ /**
+ * @see AudioManager#USE_DEFAULT_STREAM_TYPE
+ */
+ public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE;
/**
* The default stream type used by audio renderers.
*/
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
+ /**
+ * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONTENT_TYPE_MOVIE, CONTENT_TYPE_MUSIC, CONTENT_TYPE_SONIFICATION, CONTENT_TYPE_SPEECH,
+ CONTENT_TYPE_UNKNOWN})
+ public @interface AudioContentType {}
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE
+ */
+ public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC
+ */
+ public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION
+ */
+ public static final int CONTENT_TYPE_SONIFICATION =
+ android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH
+ */
+ public static final int CONTENT_TYPE_SPEECH =
+ android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
+ /**
+ * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN
+ */
+ public static final int CONTENT_TYPE_UNKNOWN =
+ android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN;
+
+ /**
+ * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+ *
+ * Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting the
+ * flag when tunneling is enabled via a track selector.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_AUDIBILITY_ENFORCED})
+ public @interface AudioFlags {}
+ /**
+ * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED
+ */
+ public static final int FLAG_AUDIBILITY_ENFORCED =
+ android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
+
+ /**
+ * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
+ USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION,
+ USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
+ USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT,
+ USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION,
+ USAGE_VOICE_COMMUNICATION_SIGNALLING})
+ public @interface AudioUsage {}
+ /**
+ * @see android.media.AudioAttributes#USAGE_ALARM
+ */
+ public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
+ /**
+ * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY
+ */
+ public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
+ android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
+ /**
+ * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
+ */
+ public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =
+ android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+ /**
+ * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION
+ */
+ public static final int USAGE_ASSISTANCE_SONIFICATION =
+ android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
+ /**
+ * @see android.media.AudioAttributes#USAGE_GAME
+ */
+ public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME;
+ /**
+ * @see android.media.AudioAttributes#USAGE_MEDIA
+ */
+ public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION
+ */
+ public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED
+ */
+ public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT
+ */
+ public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST
+ */
+ public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT
+ */
+ public static final int USAGE_NOTIFICATION_EVENT =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
+ /**
+ * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE
+ */
+ public static final int USAGE_NOTIFICATION_RINGTONE =
+ android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
+ /**
+ * @see android.media.AudioAttributes#USAGE_UNKNOWN
+ */
+ public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN;
+ /**
+ * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION
+ */
+ public static final int USAGE_VOICE_COMMUNICATION =
+ android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
+ /**
+ * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING
+ */
+ public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
+ android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+
/**
* Flags which can apply to a buffer containing a media sample.
*/
@@ -231,12 +360,10 @@ public final class C {
/**
* Indicates that a buffer holds a synchronization sample.
*/
- @SuppressWarnings("InlinedApi")
public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
/**
* Flag for empty buffers that signal that the end of the stream was reached.
*/
- @SuppressWarnings("InlinedApi")
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
/**
* Indicates that a buffer is (at least partially) encrypted.
@@ -256,13 +383,11 @@ public final class C {
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
- @SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
- @SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
/**
@@ -498,16 +623,25 @@ public final class C {
/**
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
- * type of the underlying {@link android.media.AudioTrack}. See also
- * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
- * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
+ * should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} instance that will
+ * configure the underlying audio track. If not set, the default audio attributes will be used.
+ * They are suitable for general media playback.
*
- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
- * introduce a brief gap in audio output. Note also that tracks in the same audio session must
- * share the same routing, so a new audio session id will be generated.
+ * Setting the audio attributes during playback may introduce a short gap in audio output as the
+ * audio track is recreated. A new audio session id will also be generated.
+ *
+ * If tunneling is enabled by the track selector, the specified audio attributes will be ignored,
+ * but they will take effect if audio is later played without tunneling.
+ *
+ * If the device is running a build before platform API version 21, audio attributes cannot be set
+ * directly on the underlying audio track. In this case, the usage will be mapped onto an
+ * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
+ *
+ * To get audio attributes that are equivalent to a legacy stream type, pass the stream type to
+ * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build
+ * an audio attributes instance.
*/
- public static final int MSG_SET_STREAM_TYPE = 3;
+ public static final int MSG_SET_AUDIO_ATTRIBUTES = 3;
/**
* The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
@@ -564,17 +698,14 @@ public final class C {
/**
* @see MediaFormat#COLOR_STANDARD_BT709
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
/**
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
/**
* @see MediaFormat#COLOR_STANDARD_BT2020
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
/**
@@ -586,17 +717,14 @@ public final class C {
/**
* @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;
/**
* @see MediaFormat#COLOR_TRANSFER_ST2084
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;
/**
* @see MediaFormat#COLOR_TRANSFER_HLG
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
/**
@@ -608,12 +736,10 @@ public final class C {
/**
* @see MediaFormat#COLOR_RANGE_LIMITED
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
/**
* @see MediaFormat#COLOR_RANGE_FULL
*/
- @SuppressWarnings("InlinedApi")
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
/**
@@ -632,24 +758,24 @@ public final class C {
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
- * {@link #TIME_UNSET} values.
+ * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
*
* @param timeUs The time in microseconds.
* @return The corresponding time in milliseconds.
*/
public static long usToMs(long timeUs) {
- return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000);
+ return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000);
}
/**
* Converts a time in milliseconds to the corresponding time in microseconds, preserving
- * {@link #TIME_UNSET} values.
+ * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values.
*
* @param timeMs The time in milliseconds.
* @return The corresponding time in microseconds.
*/
public static long msToUs(long timeMs) {
- return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
+ return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index ab521e3733..067cb9fa3a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
+import android.os.Looper;
+import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
@@ -30,6 +32,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* An extensible media player exposing traditional high-level media player functionality, such as
@@ -88,7 +92,9 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* thread. The application's main thread is ideal. Accessing an instance from multiple threads is
* discouraged, however if an application does wish to do this then it may do so provided that it
* ensures accesses are synchronized.
- *
Registered listeners are called on the thread that created the ExoPlayer instance.
+ * Registered listeners are called on the thread that created the ExoPlayer instance, unless
+ * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case,
+ * registered listeners will be called on the application's main thread.
* An internal playback thread is responsible for playback. Injected player components such as
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
* thread.
@@ -113,8 +119,8 @@ public interface ExoPlayer {
* Called when the timeline and/or manifest has been refreshed.
*
* Note that if the timeline has changed then a position discontinuity may also have occurred.
- * For example the current period index may have changed as a result of periods being added or
- * removed from the timeline. The will not be reported via a separate call to
+ * For example, the current period index may have changed as a result of periods being added or
+ * removed from the timeline. This will not be reported via a separate call to
* {@link #onPositionDiscontinuity()}.
*
* @param timeline The latest timeline. Never null, but may be empty.
@@ -148,6 +154,13 @@ public interface ExoPlayer {
*/
void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+ /**
+ * Called when the value of {@link #getRepeatMode()} changes.
+ *
+ * @param repeatMode The {@link RepeatMode} used for playback.
+ */
+ void onRepeatModeChanged(@RepeatMode int repeatMode);
+
/**
* Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
* immediately after this method is called. The player instance can still be used, and
@@ -251,9 +264,36 @@ public interface ExoPlayer {
*/
int STATE_ENDED = 4;
+ /**
+ * Repeat modes for playback.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
+ public @interface RepeatMode {}
+ /**
+ * Normal playback without repetition.
+ */
+ int REPEAT_MODE_OFF = 0;
+ /**
+ * "Repeat One" mode to repeat the currently playing window infinitely.
+ */
+ int REPEAT_MODE_ONE = 1;
+ /**
+ * "Repeat All" mode to repeat the entire timeline infinitely.
+ */
+ int REPEAT_MODE_ALL = 2;
+
+ /**
+ * Gets the {@link Looper} associated with the playback thread.
+ *
+ * @return The {@link Looper} associated with the playback thread.
+ */
+ Looper getPlaybackLooper();
+
/**
* Register a listener to receive events from the player. The listener's methods will be called on
- * the thread that was used to construct the player.
+ * the thread that was used to construct the player. However, if the thread used to construct the
+ * player does not have a {@link Looper}, then the listener will be called on the main thread.
*
* @param listener The listener to register.
*/
@@ -310,6 +350,20 @@ public interface ExoPlayer {
*/
boolean getPlayWhenReady();
+ /**
+ * Sets the {@link RepeatMode} to be used for playback.
+ *
+ * @param repeatMode A repeat mode.
+ */
+ void setRepeatMode(@RepeatMode int repeatMode);
+
+ /**
+ * Returns the current {@link RepeatMode} used for playback.
+ *
+ * @return The current repeat mode.
+ */
+ @RepeatMode int getRepeatMode();
+
/**
* Whether the player is currently loading the source.
*
@@ -492,4 +546,21 @@ public interface ExoPlayer {
*/
boolean isCurrentWindowSeekable();
+ /**
+ * Returns whether the player is currently playing an ad.
+ */
+ boolean isPlayingAd();
+
+ /**
+ * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period
+ * currently being played. Returns {@link C#INDEX_UNSET} otherwise.
+ */
+ int getCurrentAdGroupIndex();
+
+ /**
+ * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ int getCurrentAdIndexInAdGroup();
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index 7aecd20d4e..97a310c3da 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -16,7 +16,6 @@
package com.google.android.exoplayer2;
import android.content.Context;
-import android.os.Looper;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@@ -29,8 +28,7 @@ public final class ExoPlayerFactory {
private ExoPlayerFactory() {}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -45,8 +43,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}. Available extension renderers are not used.
+ * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
*
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -63,8 +60,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -86,8 +82,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -112,8 +107,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates a {@link SimpleExoPlayer} instance.
*
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -123,8 +117,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates a {@link SimpleExoPlayer} instance.
*
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -135,8 +128,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates a {@link SimpleExoPlayer} instance.
*
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -148,8 +140,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates an {@link ExoPlayer} instance.
*
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@@ -159,8 +150,7 @@ public final class ExoPlayerFactory {
}
/**
- * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
- * {@link Looper}.
+ * Creates an {@link ExoPlayer} instance.
*
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index 4131b97954..96dd0bd113 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -24,6 +24,7 @@ import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -51,6 +52,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private boolean tracksSelected;
private boolean playWhenReady;
+ private @RepeatMode int repeatMode;
private int playbackState;
private int pendingSeekAcks;
private int pendingPrepareAcks;
@@ -83,6 +85,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.renderers = Assertions.checkNotNull(renderers);
this.trackSelector = Assertions.checkNotNull(trackSelector);
this.playWhenReady = false;
+ this.repeatMode = REPEAT_MODE_OFF;
this.playbackState = STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>();
emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
@@ -92,7 +95,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections;
playbackParameters = PlaybackParameters.DEFAULT;
- eventHandler = new Handler() {
+ Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
+ eventHandler = new Handler(eventLooper) {
@Override
public void handleMessage(Message msg) {
ExoPlayerImpl.this.handleEvent(msg);
@@ -100,7 +104,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
};
playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
- eventHandler, playbackInfo, this);
+ repeatMode, eventHandler, playbackInfo, this);
+ }
+
+ @Override
+ public Looper getPlaybackLooper() {
+ return internalPlayer.getPlaybackLooper();
}
@Override
@@ -163,6 +172,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playWhenReady;
}
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ if (this.repeatMode != repeatMode) {
+ this.repeatMode = repeatMode;
+ internalPlayer.setRepeatMode(repeatMode);
+ for (EventListener listener : listeners) {
+ listener.onRepeatModeChanged(repeatMode);
+ }
+ }
+ }
+
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return repeatMode;
+ }
+
@Override
public boolean isLoading() {
return isLoading;
@@ -194,10 +219,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskingPeriodIndex = 0;
} else {
timeline.getWindow(windowIndex, window);
- long resolvedPositionMs =
- positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs;
+ long resolvedPositionUs =
+ positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : C.msToUs(positionMs);
int periodIndex = window.firstPeriodIndex;
- long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs);
+ long periodPositionUs = window.getPositionInFirstPeriodUs() + resolvedPositionUs;
long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
&& periodIndex < window.lastPeriodIndex) {
@@ -257,7 +282,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingPeriodIndex;
} else {
- return playbackInfo.periodIndex;
+ return playbackInfo.periodId.periodIndex;
}
}
@@ -266,7 +291,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowIndex;
} else {
- return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex;
+ return timeline.getPeriod(playbackInfo.periodId.periodIndex, period).windowIndex;
}
}
@@ -275,7 +300,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty()) {
return C.TIME_UNSET;
}
- return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ if (isPlayingAd()) {
+ MediaPeriodId periodId = playbackInfo.periodId;
+ timeline.getPeriod(periodId.periodIndex, period);
+ long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup);
+ return C.usToMs(adDurationUs);
+ } else {
+ return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
}
@Override
@@ -283,7 +315,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
- timeline.getPeriod(playbackInfo.periodIndex, period);
+ timeline.getPeriod(playbackInfo.periodId.periodIndex, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs);
}
}
@@ -294,7 +326,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
- timeline.getPeriod(playbackInfo.periodIndex, period);
+ timeline.getPeriod(playbackInfo.periodId.periodIndex, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs);
}
}
@@ -304,10 +336,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty()) {
return 0;
}
- long bufferedPosition = getBufferedPosition();
+ long position = getBufferedPosition();
long duration = getDuration();
- return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0
- : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
+ return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
+ : (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100));
}
@Override
@@ -320,6 +352,21 @@ import java.util.concurrent.CopyOnWriteArraySet;
return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
}
+ @Override
+ public boolean isPlayingAd() {
+ return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
+ }
+
@Override
public int getRendererCount() {
return renderers.length;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index bf5b3f6482..f6a0bdd08e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -17,14 +17,18 @@ package com.google.android.exoplayer2;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo;
+import com.google.android.exoplayer2.source.ClippingMediaPeriod;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -48,21 +52,25 @@ import java.io.IOException;
*/
public static final class PlaybackInfo {
- public final int periodIndex;
+ public final MediaPeriodId periodId;
public final long startPositionUs;
public volatile long positionUs;
public volatile long bufferedPositionUs;
public PlaybackInfo(int periodIndex, long startPositionUs) {
- this.periodIndex = periodIndex;
+ this(new MediaPeriodId(periodIndex), startPositionUs);
+ }
+
+ public PlaybackInfo(MediaPeriodId periodId, long startPositionUs) {
+ this.periodId = periodId;
this.startPositionUs = startPositionUs;
positionUs = startPositionUs;
bufferedPositionUs = startPositionUs;
}
- public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
- PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs);
+ public PlaybackInfo copyWithPeriodId(MediaPeriodId periodId) {
+ PlaybackInfo playbackInfo = new PlaybackInfo(periodId.periodIndex, startPositionUs);
playbackInfo.positionUs = positionUs;
playbackInfo.bufferedPositionUs = bufferedPositionUs;
return playbackInfo;
@@ -112,6 +120,7 @@ import java.io.IOException;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
private static final int MSG_CUSTOM = 11;
+ private static final int MSG_SET_REPEAT_MODE = 12;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -143,6 +152,7 @@ import java.io.IOException;
private final ExoPlayer player;
private final Timeline.Window window;
private final Timeline.Period period;
+ private final MediaPeriodInfoSequence mediaPeriodInfoSequence;
private PlaybackInfo playbackInfo;
private PlaybackParameters playbackParameters;
@@ -155,6 +165,7 @@ import java.io.IOException;
private boolean rebuffering;
private boolean isLoading;
private int state;
+ private @ExoPlayer.RepeatMode int repeatMode;
private int customMessagesSent;
private int customMessagesProcessed;
private long elapsedRealtimeUs;
@@ -170,12 +181,13 @@ import java.io.IOException;
private Timeline timeline;
public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
- LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
- PlaybackInfo playbackInfo, ExoPlayer player) {
+ LoadControl loadControl, boolean playWhenReady, @ExoPlayer.RepeatMode int repeatMode,
+ Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) {
this.renderers = renderers;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
this.playWhenReady = playWhenReady;
+ this.repeatMode = repeatMode;
this.eventHandler = eventHandler;
this.state = ExoPlayer.STATE_IDLE;
this.playbackInfo = playbackInfo;
@@ -190,6 +202,7 @@ import java.io.IOException;
enabledRenderers = new Renderer[0];
window = new Timeline.Window();
period = new Timeline.Period();
+ mediaPeriodInfoSequence = new MediaPeriodInfoSequence();
trackSelector.init(this);
playbackParameters = PlaybackParameters.DEFAULT;
@@ -210,6 +223,10 @@ import java.io.IOException;
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
}
+ public void setRepeatMode(@ExoPlayer.RepeatMode int repeatMode) {
+ handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget();
+ }
+
public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
.sendToTarget();
@@ -263,6 +280,10 @@ import java.io.IOException;
internalPlaybackThread.quit();
}
+ public Looper getPlaybackLooper() {
+ return internalPlaybackThread.getLooper();
+ }
+
// MediaSource.Listener implementation.
@Override
@@ -304,6 +325,10 @@ import java.io.IOException;
setPlayWhenReadyInternal(msg.arg1 != 0);
return true;
}
+ case MSG_SET_REPEAT_MODE: {
+ setRepeatModeInternal(msg.arg1);
+ return true;
+ }
case MSG_DO_SOME_WORK: {
doSomeWork();
return true;
@@ -411,6 +436,60 @@ import java.io.IOException;
}
}
+ private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode)
+ throws ExoPlaybackException {
+ this.repeatMode = repeatMode;
+ mediaPeriodInfoSequence.setRepeatMode(repeatMode);
+
+ // Find the last existing period holder that matches the new period order.
+ MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null
+ ? playingPeriodHolder : loadingPeriodHolder;
+ if (lastValidPeriodHolder == null) {
+ return;
+ }
+ while (true) {
+ int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex,
+ period, window, repeatMode);
+ while (lastValidPeriodHolder.next != null
+ && !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
+ lastValidPeriodHolder = lastValidPeriodHolder.next;
+ }
+ if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null
+ || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) {
+ break;
+ }
+ lastValidPeriodHolder = lastValidPeriodHolder.next;
+ }
+
+ // Release any period holders that don't match the new period order.
+ int loadingPeriodHolderIndex = loadingPeriodHolder.index;
+ int readingPeriodHolderIndex =
+ readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET;
+ if (lastValidPeriodHolder.next != null) {
+ releasePeriodHoldersFrom(lastValidPeriodHolder.next);
+ lastValidPeriodHolder.next = null;
+ }
+
+ // Update the period info for the last holder, as it may now be the last period in the timeline.
+ lastValidPeriodHolder.info =
+ mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info);
+
+ // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed.
+ boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index;
+ if (!seenLoadingPeriodHolder) {
+ loadingPeriodHolder = lastValidPeriodHolder;
+ }
+ boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET
+ && readingPeriodHolderIndex <= lastValidPeriodHolder.index;
+ if (!seenReadingPeriodHolder && playingPeriodHolder != null) {
+ // Renderers may have read from a period that's been removed. Seek back to the current
+ // position of the playing period to make sure none of the removed period is played.
+ MediaPeriodId periodId = playingPeriodHolder.info.id;
+ long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs);
+ playbackInfo = new PlaybackInfo(periodId, newPositionUs);
+ }
+ }
+
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
standaloneMediaClock.start();
@@ -451,8 +530,7 @@ import java.io.IOException;
long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
: playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
- ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs()
- : bufferedPositionUs;
+ ? playingPeriodHolder.info.durationUs : bufferedPositionUs;
}
private void doSomeWork() throws ExoPlaybackException, IOException {
@@ -504,17 +582,17 @@ import java.io.IOException;
}
}
- long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
- .getDurationUs();
+ long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
if (allRenderersEnded
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playingPeriodDurationUs <= playbackInfo.positionUs)
- && playingPeriodHolder.isLast) {
+ && playingPeriodHolder.info.isFinal) {
setState(ExoPlayer.STATE_ENDED);
stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING) {
boolean isNewlyReady = enabledRenderers.length > 0
- ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering))
+ ? (allRenderersReadyOrEnded
+ && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs))
: isTimelineReady(playingPeriodDurationUs);
if (isNewlyReady) {
setState(ExoPlayer.STATE_READY);
@@ -540,7 +618,7 @@ import java.io.IOException;
if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
- } else if (enabledRenderers.length != 0) {
+ } else if (enabledRenderers.length != 0 && state != ExoPlayer.STATE_ENDED) {
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else {
handler.removeMessages(MSG_DO_SOME_WORK);
@@ -585,24 +663,30 @@ import java.io.IOException;
boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
int periodIndex = periodPosition.first;
long periodPositionUs = periodPosition.second;
-
+ MediaPeriodId periodId =
+ mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs);
+ if (periodId.isAd()) {
+ seekPositionAdjusted = true;
+ // TODO: Resume content at periodPositionUs after the ad plays.
+ periodPositionUs = 0;
+ }
try {
- if (periodIndex == playbackInfo.periodIndex
+ if (periodId.equals(playbackInfo.periodId)
&& ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
// Seek position equals the current position. Do nothing.
return;
}
- long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
+ long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs);
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
periodPositionUs = newPeriodPositionUs;
} finally {
- playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
+ playbackInfo = new PlaybackInfo(periodId, periodPositionUs);
eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
.sendToTarget();
}
}
- private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
+ private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs)
throws ExoPlaybackException {
stopRenderers();
rebuffering = false;
@@ -618,7 +702,7 @@ import java.io.IOException;
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder periodHolder = playingPeriodHolder;
while (periodHolder != null) {
- if (periodHolder.index == periodIndex && periodHolder.prepared) {
+ if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) {
newPlayingPeriodHolder = periodHolder;
} else {
periodHolder.release();
@@ -662,6 +746,19 @@ import java.io.IOException;
return periodPositionUs;
}
+ private boolean shouldKeepPeriodHolder(MediaPeriodId seekPeriodId, long positionUs,
+ MediaPeriodHolder holder) {
+ if (seekPeriodId.equals(holder.info.id) && holder.prepared) {
+ timeline.getPeriod(holder.info.id.periodIndex, period);
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
+ if (nextAdGroupIndex == C.INDEX_UNSET
+ || period.getAdGroupTimeUs(nextAdGroupIndex) == holder.info.endPositionUs) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
rendererPositionUs = playingPeriodHolder == null
? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
@@ -724,6 +821,7 @@ import java.io.IOException;
mediaSource.releaseSource();
mediaSource = null;
}
+ mediaPeriodInfoSequence.setTimeline(null);
timeline = null;
}
}
@@ -733,7 +831,7 @@ import java.io.IOException;
for (ExoPlayerMessage message : messages) {
message.target.handleMessage(message.messageType, message.message);
}
- if (mediaSource != null) {
+ if (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) {
// The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -834,7 +932,7 @@ import java.io.IOException;
}
loadingPeriodHolder.next = null;
if (loadingPeriodHolder.prepared) {
- long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
+ long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs,
loadingPeriodHolder.toPeriodTime(rendererPositionUs));
loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
}
@@ -847,23 +945,8 @@ import java.io.IOException;
private boolean isTimelineReady(long playingPeriodDurationUs) {
return playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
- || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
- }
-
- private boolean haveSufficientBuffer(boolean rebuffering) {
- long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared
- ? loadingPeriodHolder.startPositionUs
- : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs();
- if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) {
- if (loadingPeriodHolder.isLast) {
- return true;
- }
- loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period)
- .getDurationUs();
- }
- return loadControl.shouldStartPlayback(
- loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs),
- rebuffering);
+ || (playingPeriodHolder.next != null
+ && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd()));
}
private void maybeThrowPeriodPrepareError() throws IOException {
@@ -882,6 +965,7 @@ import java.io.IOException;
throws ExoPlaybackException {
Timeline oldTimeline = timeline;
timeline = timelineAndManifest.first;
+ mediaPeriodInfoSequence.setTimeline(timeline);
Object manifest = timelineAndManifest.second;
int processedInitialSeekCount = 0;
@@ -897,14 +981,20 @@ import java.io.IOException;
handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
return;
}
- playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
+ int periodIndex = periodPosition.first;
+ long positionUs = periodPosition.second;
+ MediaPeriodId periodId =
+ mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs);
+ playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs);
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
if (timeline.isEmpty()) {
handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
return;
}
Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
- playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
+ MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(
+ defaultPosition.first, defaultPosition.second);
+ playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : defaultPosition.second);
}
}
@@ -920,7 +1010,8 @@ import java.io.IOException;
if (periodIndex == C.INDEX_UNSET) {
// We didn't find the current period in the new timeline. Attempt to resolve a subsequent
// period whose window we can restart from.
- int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline);
+ int newPeriodIndex = resolveSubsequentPeriod(periodHolder.info.id.periodIndex, oldTimeline,
+ timeline);
if (newPeriodIndex == C.INDEX_UNSET) {
// We failed to resolve a suitable restart position.
handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
@@ -935,26 +1026,29 @@ import java.io.IOException;
// Clear the index of each holder that doesn't contain the default position. If a holder
// contains the default position then update its index so it can be re-used when seeking.
Object newPeriodUid = period.uid;
- periodHolder.index = C.INDEX_UNSET;
+ periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET);
while (periodHolder.next != null) {
periodHolder = periodHolder.next;
- periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET;
+ if (periodHolder.uid.equals(newPeriodUid)) {
+ periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info,
+ newPeriodIndex);
+ } else {
+ periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET);
+ }
}
// Actually do the seek.
- newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs);
- playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs);
+ MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex);
+ newPositionUs = seekToPeriodPosition(periodId, newPositionUs);
+ playbackInfo = new PlaybackInfo(periodId, newPositionUs);
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
return;
}
// The current period is in the new timeline. Update the holder and playbackInfo.
- timeline.getPeriod(periodIndex, period);
- boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
- && !timeline.getWindow(period.windowIndex, window).isDynamic;
- periodHolder.setIndex(periodIndex, isLastPeriod);
- boolean seenReadingPeriod = periodHolder == readingPeriodHolder;
- if (periodIndex != playbackInfo.periodIndex) {
- playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
+ periodHolder = updatePeriodInfo(periodHolder, periodIndex);
+ if (periodIndex != playbackInfo.periodId.periodIndex) {
+ playbackInfo =
+ playbackInfo.copyWithPeriodId(playbackInfo.periodId.copyWithPeriodIndex(periodIndex));
}
// If there are subsequent holders, update the index for each of them. If we find a holder
@@ -962,22 +1056,21 @@ import java.io.IOException;
while (periodHolder.next != null) {
MediaPeriodHolder previousPeriodHolder = periodHolder;
periodHolder = periodHolder.next;
- periodIndex++;
- timeline.getPeriod(periodIndex, period, true);
- isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
- && !timeline.getWindow(period.windowIndex, window).isDynamic;
- if (periodHolder.uid.equals(period.uid)) {
+ periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode);
+ if (periodIndex != C.INDEX_UNSET
+ && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
// The holder is consistent with the new timeline. Update its index and continue.
- periodHolder.setIndex(periodIndex, isLastPeriod);
- seenReadingPeriod |= (periodHolder == readingPeriodHolder);
+ periodHolder = updatePeriodInfo(periodHolder, periodIndex);
} else {
// The holder is inconsistent with the new timeline.
- if (!seenReadingPeriod) {
+ boolean seenReadingPeriodHolder =
+ readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index;
+ if (!seenReadingPeriodHolder) {
// Renderers may have read from a period that's been removed. Seek back to the current
// position of the playing period to make sure none of the removed period is played.
- periodIndex = playingPeriodHolder.index;
- long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs);
- playbackInfo = new PlaybackInfo(periodIndex, newPositionUs);
+ long newPositionUs =
+ seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs);
+ playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, newPositionUs);
} else {
// Update the loading period to be the last period that's still valid, and release all
// subsequent periods.
@@ -993,6 +1086,17 @@ import java.io.IOException;
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
}
+ private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) {
+ while (true) {
+ periodHolder.info =
+ mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
+ if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) {
+ return periodHolder;
+ }
+ periodHolder = periodHolder.next;
+ }
+ }
+
private void handleSourceInfoRefreshEndedPlayback(Object manifest,
int processedInitialSeekCount) {
// Set the playback position to (0,0) for notifying the eventHandler.
@@ -1023,9 +1127,15 @@ import java.io.IOException;
private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
Timeline newTimeline) {
int newPeriodIndex = C.INDEX_UNSET;
- while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) {
+ int maxIterations = oldTimeline.getPeriodCount();
+ for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
+ oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode);
+ if (oldPeriodIndex == C.INDEX_UNSET) {
+ // We've reached the end of the old timeline.
+ break;
+ }
newPeriodIndex = newTimeline.getIndexOfPeriod(
- oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid);
+ oldTimeline.getPeriod(oldPeriodIndex, period, true).uid);
}
return newPeriodIndex;
}
@@ -1049,7 +1159,7 @@ import java.io.IOException;
// Map the SeekPosition to a position in the corresponding timeline.
Pair periodPosition;
try {
- periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex,
+ periodPosition = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex,
seekPosition.windowPositionUs);
} catch (IndexOutOfBoundsException e) {
// The window index of the seek position was outside the bounds of the timeline.
@@ -1078,53 +1188,11 @@ import java.io.IOException;
}
/**
- * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline.
+ * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the
+ * current timeline.
*/
private Pair getPeriodPosition(int windowIndex, long windowPositionUs) {
- return getPeriodPosition(timeline, windowIndex, windowPositionUs);
- }
-
- /**
- * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position
- * projection.
- */
- private Pair getPeriodPosition(Timeline timeline, int windowIndex,
- long windowPositionUs) {
- return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0);
- }
-
- /**
- * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
- *
- * @param timeline The timeline containing the window.
- * @param windowIndex The window index.
- * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
- * start position.
- * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
- * duration into the future by which the window's position should be projected.
- * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
- * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
- * position could not be projected by {@code defaultPositionProjectionUs}.
- */
- private Pair getPeriodPosition(Timeline timeline, int windowIndex,
- long windowPositionUs, long defaultPositionProjectionUs) {
- Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
- timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs);
- if (windowPositionUs == C.TIME_UNSET) {
- windowPositionUs = window.getDefaultPositionUs();
- if (windowPositionUs == C.TIME_UNSET) {
- return null;
- }
- }
- int periodIndex = window.firstPeriodIndex;
- long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
- long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
- while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
- && periodIndex < window.lastPeriodIndex) {
- periodPositionUs -= periodDurationUs;
- periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
- }
- return Pair.create(periodIndex, periodPositionUs);
+ return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
}
private void updatePeriods() throws ExoPlaybackException, IOException {
@@ -1138,7 +1206,7 @@ import java.io.IOException;
maybeUpdateLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
- } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) {
+ } else if (loadingPeriodHolder != null && !isLoading) {
maybeContinueLoading();
}
@@ -1154,13 +1222,13 @@ import java.io.IOException;
// the end of the playing period, so advance playback to the next period.
playingPeriodHolder.release();
setPlayingPeriodHolder(playingPeriodHolder.next);
- playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
- playingPeriodHolder.startPositionUs);
+ playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id,
+ playingPeriodHolder.info.startPositionUs);
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
- if (readingPeriodHolder.isLast) {
+ if (readingPeriodHolder.info.isFinal) {
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
@@ -1224,76 +1292,41 @@ import java.io.IOException;
}
private void maybeUpdateLoadingPeriod() throws IOException {
- int newLoadingPeriodIndex;
+ MediaPeriodInfo info;
if (loadingPeriodHolder == null) {
- newLoadingPeriodIndex = playbackInfo.periodIndex;
+ info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo);
} else {
- int loadingPeriodIndex = loadingPeriodHolder.index;
- if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered()
- || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
- // Either the existing loading period is the last period, or we are not ready to advance to
- // loading the next period because it hasn't been fully buffered or its duration is unknown.
+ if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered()
+ || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) {
return;
}
- if (playingPeriodHolder != null
- && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) {
- // We are already buffering the maximum number of periods ahead.
- return;
+ if (playingPeriodHolder != null) {
+ int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index;
+ if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) {
+ // We are already buffering the maximum number of periods ahead.
+ return;
+ }
}
- newLoadingPeriodIndex = loadingPeriodHolder.index + 1;
+ info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info,
+ loadingPeriodHolder.getRendererOffset(), rendererPositionUs);
}
-
- if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
- // The next period is not available yet.
+ if (info == null) {
mediaSource.maybeThrowSourceInfoRefreshError();
return;
}
- long newLoadingPeriodStartPositionUs;
- if (loadingPeriodHolder == null) {
- newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
- } else {
- int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
- if (newLoadingPeriodIndex
- != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) {
- // We're starting to buffer a new period in the current window. Always start from the
- // beginning of the period.
- newLoadingPeriodStartPositionUs = 0;
- } else {
- // We're starting to buffer a new window. When playback transitions to this window we'll
- // want it to be from its default start position. The expected delay until playback
- // transitions is equal the duration of media that's currently buffered (assuming no
- // interruptions). Hence we project the default start position forward by the duration of
- // the buffer, and start buffering from this point.
- long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset()
- + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
- - rendererPositionUs;
- Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex,
- C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
- if (defaultPosition == null) {
- return;
- }
-
- newLoadingPeriodIndex = defaultPosition.first;
- newLoadingPeriodStartPositionUs = defaultPosition.second;
- }
- }
-
long rendererPositionOffsetUs = loadingPeriodHolder == null
- ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US
- : (loadingPeriodHolder.getRendererOffset()
- + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
- timeline.getPeriod(newLoadingPeriodIndex, period, true);
- boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1
- && !timeline.getWindow(period.windowIndex, window).isDynamic;
+ ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US)
+ : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs);
+ int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1;
+ Object uid = timeline.getPeriod(info.id.periodIndex, period, true).uid;
MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
- rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid,
- newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs);
+ rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info);
if (loadingPeriodHolder != null) {
loadingPeriodHolder.next = newPeriodHolder;
}
loadingPeriodHolder = newPeriodHolder;
- loadingPeriodHolder.mediaPeriod.prepare(this);
+ loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
setIsLoading(true);
}
@@ -1306,7 +1339,7 @@ import java.io.IOException;
if (playingPeriodHolder == null) {
// This is the first prepared period, so start playing it.
readingPeriodHolder = loadingPeriodHolder;
- resetRendererPosition(readingPeriodHolder.startPositionUs);
+ resetRendererPosition(readingPeriodHolder.info.startPositionUs);
setPlayingPeriodHolder(readingPeriodHolder);
}
maybeContinueLoading();
@@ -1321,21 +1354,10 @@ import java.io.IOException;
}
private void maybeContinueLoading() {
- long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0
- : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs();
- if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
- setIsLoading(false);
- } else {
- long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs);
- long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
- boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs);
- setIsLoading(continueLoading);
- if (continueLoading) {
- loadingPeriodHolder.needsContinueLoading = false;
- loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs);
- } else {
- loadingPeriodHolder.needsContinueLoading = true;
- }
+ boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs);
+ setIsLoading(continueLoading);
+ if (continueLoading) {
+ loadingPeriodHolder.continueLoading(rendererPositionUs);
}
}
@@ -1432,17 +1454,15 @@ import java.io.IOException;
public final MediaPeriod mediaPeriod;
public final Object uid;
+ public final int index;
public final SampleStream[] sampleStreams;
public final boolean[] mayRetainStreamFlags;
public final long rendererPositionOffsetUs;
- public int index;
- public long startPositionUs;
- public boolean isLast;
+ public MediaPeriodInfo info;
public boolean prepared;
public boolean hasEnabledTracks;
public MediaPeriodHolder next;
- public boolean needsContinueLoading;
public TrackSelectorResult trackSelectorResult;
private final Renderer[] renderers;
@@ -1455,8 +1475,7 @@ import java.io.IOException;
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
- MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod,
- long startPositionUs) {
+ MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) {
this.renderers = renderers;
this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
@@ -1464,13 +1483,17 @@ import java.io.IOException;
this.loadControl = loadControl;
this.mediaSource = mediaSource;
this.uid = Assertions.checkNotNull(periodUid);
- this.index = periodIndex;
- this.isLast = isLastPeriod;
- this.startPositionUs = startPositionUs;
+ this.index = index;
+ this.info = info;
sampleStreams = new SampleStream[renderers.length];
mayRetainStreamFlags = new boolean[renderers.length];
- mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(),
- startPositionUs);
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, loadControl.getAllocator());
+ if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true);
+ clippingMediaPeriod.setClipping(0, info.endPositionUs);
+ mediaPeriod = clippingMediaPeriod;
+ }
+ this.mediaPeriod = mediaPeriod;
}
public long toRendererTime(long periodTimeUs) {
@@ -1482,12 +1505,7 @@ import java.io.IOException;
}
public long getRendererOffset() {
- return rendererPositionOffsetUs - startPositionUs;
- }
-
- public void setIndex(int index, boolean isLast) {
- this.index = index;
- this.isLast = isLast;
+ return rendererPositionOffsetUs - info.startPositionUs;
}
public boolean isFullyBuffered() {
@@ -1495,10 +1513,40 @@ import java.io.IOException;
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
}
+ public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) {
+ long bufferedPositionUs = !prepared ? info.startPositionUs
+ : mediaPeriod.getBufferedPositionUs();
+ if (bufferedPositionUs == C.TIME_END_OF_SOURCE) {
+ if (info.isFinal) {
+ return true;
+ }
+ bufferedPositionUs = info.durationUs;
+ }
+ return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs),
+ rebuffering);
+ }
+
public void handlePrepared() throws ExoPlaybackException {
prepared = true;
selectTracks();
- startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
+ long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false);
+ info = info.copyWithStartPositionUs(newStartPositionUs);
+ }
+
+ public boolean shouldContinueLoading(long rendererPositionUs) {
+ long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ return false;
+ } else {
+ long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
+ long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
+ return loadControl.shouldContinueLoading(bufferedDurationUs);
+ }
+ }
+
+ public void continueLoading(long rendererPositionUs) {
+ long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
+ mediaPeriod.continueLoading(loadingPeriodPositionUs);
}
public boolean selectTracks() throws ExoPlaybackException {
@@ -1547,7 +1595,11 @@ import java.io.IOException;
public void release() {
try {
- mediaSource.releasePeriod(mediaPeriod);
+ if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ } else {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Period release failed.", e);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index 13cf35d449..650ce727cd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo {
* 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.
- String VERSION = "2.4.0";
+ String VERSION = "2.4.3";
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- String VERSION_SLASHY = "ExoPlayerLib/2.4.0";
+ String VERSION_SLASHY = "ExoPlayerLib/2.4.3";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- int VERSION_INT = 2004000;
+ int VERSION_INT = 2004003;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
new file mode 100644
index 0000000000..953736d58b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
@@ -0,0 +1,327 @@
+/*
+ * 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;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.ExoPlayer.RepeatMode;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+
+/**
+ * Provides a sequence of {@link MediaPeriodInfo}s to the player, determining the order and
+ * start/end positions for {@link MediaPeriod}s to load and play.
+ */
+/* package */ final class MediaPeriodInfoSequence {
+
+ // TODO: Consider merging this class with the MediaPeriodHolder queue in ExoPlayerImplInternal.
+
+ /**
+ * Stores the information required to load and play a {@link MediaPeriod}.
+ */
+ public static final class MediaPeriodInfo {
+
+ /**
+ * The media period's identifier.
+ */
+ public final MediaPeriodId id;
+ /**
+ * The start position of the media to play within the media period, in microseconds.
+ */
+ public final long startPositionUs;
+ /**
+ * The end position of the media to play within the media period, in microseconds, or
+ * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period.
+ */
+ public final long endPositionUs;
+ /**
+ * The duration of the media to play within the media period, in microseconds, or
+ * {@link C#TIME_UNSET} if not known.
+ */
+ public final long durationUs;
+ /**
+ * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media
+ * period corresponding to a timeline period without ads).
+ */
+ public final boolean isLastInTimelinePeriod;
+ /**
+ * Whether this is the last media period in the entire timeline. If true,
+ * {@link #isLastInTimelinePeriod} will also be true.
+ */
+ public final boolean isFinal;
+
+ private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs,
+ long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) {
+ this.id = id;
+ this.startPositionUs = startPositionUs;
+ this.endPositionUs = endPositionUs;
+ this.durationUs = durationUs;
+ this.isLastInTimelinePeriod = isLastInTimelinePeriod;
+ this.isFinal = isFinal;
+ }
+
+ /**
+ * Returns a copy of this instance with the period identifier's period index set to the
+ * specified value.
+ */
+ public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) {
+ return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs,
+ endPositionUs, durationUs, isLastInTimelinePeriod, isFinal);
+ }
+
+ /**
+ * Returns a copy of this instance with the start position set to the specified value.
+ */
+ public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
+ return new MediaPeriodInfo(id, startPositionUs, endPositionUs, durationUs,
+ isLastInTimelinePeriod, isFinal);
+ }
+
+ }
+
+ private final Timeline.Period period;
+ private final Timeline.Window window;
+
+ private Timeline timeline;
+ @RepeatMode
+ private int repeatMode;
+
+ /**
+ * Creates a new media period info sequence.
+ */
+ public MediaPeriodInfoSequence() {
+ period = new Timeline.Period();
+ window = new Timeline.Window();
+ }
+
+ /**
+ * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information
+ * taking into account the new timeline.
+ */
+ public void setTimeline(Timeline timeline) {
+ this.timeline = timeline;
+ }
+
+ /**
+ * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period
+ * information taking into account the new repeat mode.
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ this.repeatMode = repeatMode;
+ }
+
+ /**
+ * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.
+ */
+ public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {
+ return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.startPositionUs);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}.
+ *
+ * @param currentMediaPeriodInfo The current media period info.
+ * @param rendererOffsetUs The current renderer offset in microseconds.
+ * @param rendererPositionUs The current renderer position in microseconds.
+ * @return The following media period info, or {@code null} if it is not yet possible to get the
+ * next media period info.
+ */
+ public MediaPeriodInfo getNextMediaPeriodInfo(MediaPeriodInfo currentMediaPeriodInfo,
+ long rendererOffsetUs, long rendererPositionUs) {
+ // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod
+ // but if the timeline is not ready to provide the next period it can't return a non-null value
+ // until the timeline is updated. Store whether the next timeline period is ready when the
+ // timeline is updated, to avoid repeatedly checking the same timeline.
+ if (currentMediaPeriodInfo.isLastInTimelinePeriod) {
+ int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex,
+ period, window, repeatMode);
+ if (nextPeriodIndex == C.INDEX_UNSET) {
+ // We can't create a next period yet.
+ return null;
+ }
+
+ long startPositionUs;
+ int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex;
+ if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
+ // We're starting to buffer a new window. When playback transitions to this window we'll
+ // want it to be from its default start position. The expected delay until playback
+ // transitions is equal the duration of media that's currently buffered (assuming no
+ // interruptions). Hence we project the default start position forward by the duration of
+ // the buffer, and start buffering from this point.
+ long defaultPositionProjectionUs =
+ rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs;
+ Pair defaultPosition = timeline.getPeriodPosition(window, period,
+ nextWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
+ if (defaultPosition == null) {
+ return null;
+ }
+ nextPeriodIndex = defaultPosition.first;
+ startPositionUs = defaultPosition.second;
+ } else {
+ startPositionUs = 0;
+ }
+ return getMediaPeriodInfo(resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs),
+ startPositionUs);
+ }
+
+ MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id;
+ if (currentPeriodId.isAd()) {
+ int currentAdGroupIndex = currentPeriodId.adGroupIndex;
+ timeline.getPeriod(currentPeriodId.periodIndex, period);
+ int adCountInCurrentAdGroup = period.getAdGroupCount() == C.LENGTH_UNSET ? C.LENGTH_UNSET
+ : period.getAdCountInAdGroup(currentAdGroupIndex);
+ if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {
+ return null;
+ }
+ int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1;
+ if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {
+ // Play the next ad in the ad group if it's available.
+ return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null
+ : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex,
+ nextAdIndexInAdGroup);
+ } else {
+ // Play content from the ad group position.
+ return getMediaPeriodInfo(new MediaPeriodId(currentPeriodId.periodIndex),
+ period.getAdGroupTimeUs(currentAdGroupIndex));
+ }
+ } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) {
+ // Play the next ad group if it's available.
+ int nextAdGroupIndex =
+ period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs);
+ return !period.isAdAvailable(nextAdGroupIndex, 0) ? null
+ : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0);
+ } else {
+ // Check if the postroll ad should be played.
+ int adGroupCount = period.getAdGroupCount();
+ if (adGroupCount == C.LENGTH_UNSET || adGroupCount == 0
+ || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE
+ || period.hasPlayedAdGroup(adGroupCount - 1)
+ || !period.isAdAvailable(adGroupCount - 1, 0)) {
+ return null;
+ }
+ return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0);
+ }
+ }
+
+ /**
+ * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
+ * played, returning an identifier for an ad group if one needs to be played before the specified
+ * position, or an identifier for a content media period if not.
+ */
+ public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) {
+ timeline.getPeriod(periodIndex, period);
+ int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
+ return adGroupIndex == C.INDEX_UNSET ? new MediaPeriodId(periodIndex)
+ : new MediaPeriodId(periodIndex, adGroupIndex, 0);
+ }
+
+ /**
+ * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline.
+ */
+ public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) {
+ return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id);
+ }
+
+ /**
+ * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline,
+ * resetting the identifier of the media period to the specified {@code newPeriodIndex}.
+ */
+ public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo,
+ int newPeriodIndex) {
+ return getUpdatedMediaPeriodInfo(mediaPeriodInfo,
+ mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex));
+ }
+
+ // Internal methods.
+
+ private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) {
+ long startPositionUs = info.startPositionUs;
+ long endPositionUs = info.endPositionUs;
+ boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs);
+ boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod);
+ timeline.getPeriod(newId.periodIndex, period);
+ long durationUs = newId.isAd()
+ ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup)
+ : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs);
+ return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, durationUs, isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long startPositionUs) {
+ timeline.getPeriod(id.periodIndex, period);
+ if (id.isAd()) {
+ if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {
+ return null;
+ }
+ return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup);
+ } else {
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
+ long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE
+ : period.getAdGroupTimeUs(nextAdGroupIndex);
+ return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs);
+ }
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex,
+ int adIndexInAdGroup) {
+ MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup);
+ boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ long durationUs = timeline.getPeriod(id.periodIndex, period)
+ .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);
+ return new MediaPeriodInfo(id, 0, C.TIME_END_OF_SOURCE, durationUs, isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs,
+ long endUs) {
+ MediaPeriodId id = new MediaPeriodId(periodIndex);
+ boolean isLastInPeriod = isLastInPeriod(id, endUs);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ timeline.getPeriod(id.periodIndex, period);
+ long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs;
+ return new MediaPeriodInfo(id, startPositionUs, endUs, durationUs, isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) {
+ int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount();
+ if (adGroupCount == 0) {
+ return true;
+ }
+ if (adGroupCount == C.LENGTH_UNSET) {
+ return false;
+ }
+ int lastAdGroupIndex = adGroupCount - 1;
+ boolean periodHasPostrollAd = period.getAdGroupTimeUs(lastAdGroupIndex) == C.TIME_END_OF_SOURCE;
+ if (!id.isAd()) {
+ return !periodHasPostrollAd && endPositionUs == C.TIME_END_OF_SOURCE;
+ } else if (periodHasPostrollAd && id.adGroupIndex == lastAdGroupIndex) {
+ int adCountInLastAdGroup = period.getAdCountInAdGroup(lastAdGroupIndex);
+ return adCountInLastAdGroup != C.LENGTH_UNSET
+ && id.adIndexInAdGroup == adCountInLastAdGroup - 1;
+ }
+ return false;
+ }
+
+ private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
+ int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex;
+ return !timeline.getWindow(windowIndex, window).isDynamic
+ && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode)
+ && isLastMediaPeriodInPeriod;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 28ba8cf9d7..054d3e38b9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -20,12 +20,14 @@ import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.PlaybackParams;
import android.os.Handler;
+import android.os.Looper;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
+import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -36,6 +38,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.util.List;
@@ -104,15 +107,16 @@ public class SimpleExoPlayer implements ExoPlayer {
private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters;
private int audioSessionId;
- @C.StreamType
- private int audioStreamType;
+ private AudioAttributes audioAttributes;
private float audioVolume;
protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
LoadControl loadControl) {
componentListener = new ComponentListener();
- renderers = renderersFactory.createRenderers(new Handler(), componentListener,
- componentListener, componentListener, componentListener);
+ Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
+ Handler eventHandler = new Handler(eventLooper);
+ renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
+ componentListener, componentListener);
// Obtain counts of video and audio renderers.
int videoRendererCount = 0;
@@ -133,7 +137,7 @@ public class SimpleExoPlayer implements ExoPlayer {
// Set initial values.
audioVolume = 1;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
- audioStreamType = C.STREAM_TYPE_DEFAULT;
+ audioAttributes = AudioAttributes.DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects.
@@ -218,8 +222,9 @@ public class SimpleExoPlayer implements ExoPlayer {
if (surfaceHolder == null) {
setVideoSurfaceInternal(null, false);
} else {
- setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
surfaceHolder.addCallback(componentListener);
+ Surface surface = surfaceHolder.getSurface();
+ setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false);
}
}
@@ -270,9 +275,9 @@ public class SimpleExoPlayer implements ExoPlayer {
if (textureView.getSurfaceTextureListener() != null) {
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
}
+ textureView.setSurfaceTextureListener(componentListener);
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
- textureView.setSurfaceTextureListener(componentListener);
}
}
@@ -289,33 +294,70 @@ public class SimpleExoPlayer implements ExoPlayer {
}
/**
- * Sets the stream type for audio playback (see {@link C.StreamType} and
- * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type
- * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}.
+ * Sets the stream type for audio playback, used by the underlying audio track.
*
- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
- * introduce a brief gap in audio output. Note also that tracks in the same audio session must
- * share the same routing, so a new audio session id will be generated.
+ * Setting the stream type during playback may introduce a short gap in audio output as the audio
+ * track is recreated. A new audio session id will also be generated.
+ *
+ * Calling this method overwrites any attributes set previously by calling
+ * {@link #setAudioAttributes(AudioAttributes)}.
*
- * @param audioStreamType The stream type for audio playback.
+ * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}.
+ * @param streamType The stream type for audio playback.
*/
- public void setAudioStreamType(@C.StreamType int audioStreamType) {
- this.audioStreamType = audioStreamType;
+ @Deprecated
+ public void setAudioStreamType(@C.StreamType int streamType) {
+ @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType);
+ @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType);
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build();
+ setAudioAttributes(audioAttributes);
+ }
+
+ /**
+ * Returns the stream type for audio playback.
+ *
+ * @deprecated Use {@link #getAudioAttributes()}.
+ */
+ @Deprecated
+ public @C.StreamType int getAudioStreamType() {
+ return Util.getStreamTypeForAudioUsage(audioAttributes.usage);
+ }
+
+ /**
+ * Sets the attributes for audio playback, used by the underlying audio track. If not set, the
+ * default audio attributes will be used. They are suitable for general media playback.
+ *
+ * Setting the audio attributes during playback may introduce a short gap in audio output as the
+ * audio track is recreated. A new audio session id will also be generated.
+ *
+ * If tunneling is enabled by the track selector, the specified audio attributes will be ignored,
+ * but they will take effect if audio is later played without tunneling.
+ *
+ * If the device is running a build before platform API version 21, audio attributes cannot be set
+ * directly on the underlying audio track. In this case, the usage will be mapped onto an
+ * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
+ *
+ * @param audioAttributes The attributes to use for audio playback.
+ */
+ public void setAudioAttributes(AudioAttributes audioAttributes) {
+ this.audioAttributes = audioAttributes;
ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType);
+ messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES,
+ audioAttributes);
}
}
player.sendMessages(messages);
}
/**
- * Returns the stream type for audio playback.
+ * Returns the attributes for audio playback.
*/
- public @C.StreamType int getAudioStreamType() {
- return audioStreamType;
+ public AudioAttributes getAudioAttributes() {
+ return audioAttributes;
}
/**
@@ -476,6 +518,11 @@ public class SimpleExoPlayer implements ExoPlayer {
// ExoPlayer implementation
+ @Override
+ public Looper getPlaybackLooper() {
+ return player.getPlaybackLooper();
+ }
+
@Override
public void addListener(EventListener listener) {
player.addListener(listener);
@@ -511,6 +558,16 @@ public class SimpleExoPlayer implements ExoPlayer {
return player.getPlayWhenReady();
}
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return player.getRepeatMode();
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ player.setRepeatMode(repeatMode);
+ }
+
@Override
public boolean isLoading() {
return player.isLoading();
@@ -643,6 +700,21 @@ public class SimpleExoPlayer implements ExoPlayer {
return player.isCurrentWindowSeekable();
}
+ @Override
+ public boolean isPlayingAd() {
+ return player.isPlayingAd();
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return player.getCurrentAdGroupIndex();
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return player.getCurrentAdIndexInAdGroup();
+ }
+
// Internal methods.
private void removeSurfaceCallbacks() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
index eb3966ae4d..19e66f9031 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2;
+import android.util.Pair;
+import com.google.android.exoplayer2.util.Assertions;
+
/**
* A representation of media currently available for playback.
*
@@ -91,128 +94,6 @@ package com.google.android.exoplayer2;
*/
public abstract class Timeline {
- /**
- * An empty timeline.
- */
- public static final Timeline EMPTY = new Timeline() {
-
- @Override
- public int getWindowCount() {
- return 0;
- }
-
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- throw new IndexOutOfBoundsException();
- }
-
- @Override
- public int getPeriodCount() {
- return 0;
- }
-
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- throw new IndexOutOfBoundsException();
- }
-
- @Override
- public int getIndexOfPeriod(Object uid) {
- return C.INDEX_UNSET;
- }
-
- };
-
- /**
- * Returns whether the timeline is empty.
- */
- public final boolean isEmpty() {
- return getWindowCount() == 0;
- }
-
- /**
- * Returns the number of windows in the timeline.
- */
- public abstract int getWindowCount();
-
- /**
- * Populates a {@link Window} with data for the window at the specified index. Does not populate
- * {@link Window#id}.
- *
- * @param windowIndex The index of the window.
- * @param window The {@link Window} to populate. Must not be null.
- * @return The populated {@link Window}, for convenience.
- */
- public final Window getWindow(int windowIndex, Window window) {
- return getWindow(windowIndex, window, false);
- }
-
- /**
- * Populates a {@link Window} with data for the window at the specified index.
- *
- * @param windowIndex The index of the window.
- * @param window The {@link Window} to populate. Must not be null.
- * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
- * null. The caller should pass false for efficiency reasons unless the field is required.
- * @return The populated {@link Window}, for convenience.
- */
- public Window getWindow(int windowIndex, Window window, boolean setIds) {
- return getWindow(windowIndex, window, setIds, 0);
- }
-
- /**
- * Populates a {@link Window} with data for the window at the specified index.
- *
- * @param windowIndex The index of the window.
- * @param window The {@link Window} to populate. Must not be null.
- * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
- * null. The caller should pass false for efficiency reasons unless the field is required.
- * @param defaultPositionProjectionUs A duration into the future that the populated window's
- * default start position should be projected.
- * @return The populated {@link Window}, for convenience.
- */
- public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs);
-
- /**
- * Returns the number of periods in the timeline.
- */
- public abstract int getPeriodCount();
-
- /**
- * Populates a {@link Period} with data for the period at the specified index. Does not populate
- * {@link Period#id} and {@link Period#uid}.
- *
- * @param periodIndex The index of the period.
- * @param period The {@link Period} to populate. Must not be null.
- * @return The populated {@link Period}, for convenience.
- */
- public final Period getPeriod(int periodIndex, Period period) {
- return getPeriod(periodIndex, period, false);
- }
-
- /**
- * Populates a {@link Period} with data for the period at the specified index.
- *
- * @param periodIndex The index of the period.
- * @param period The {@link Period} to populate. Must not be null.
- * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,
- * the fields will be set to null. The caller should pass false for efficiency reasons unless
- * the fields are required.
- * @return The populated {@link Period}, for convenience.
- */
- public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);
-
- /**
- * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET}
- * if the period is not in the timeline.
- *
- * @param uid A unique identifier for a period.
- * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.
- */
- public abstract int getIndexOfPeriod(Object uid);
-
/**
* Holds information about a window in a {@link Timeline}. A window defines a region of media
* currently available for playback along with additional information such as whether seeking is
@@ -354,7 +235,7 @@ public abstract class Timeline {
/**
* Holds information about a period in a {@link Timeline}. A period defines a single logical piece
- * of media, for example a a media file. See {@link Timeline} for more details. The figure below
+ * of media, for example a media file. See {@link Timeline} for more details. The figure below
* shows some of the information defined by a period, as well as how this information relates to a
* corresponding {@link Window} in the timeline.
*
@@ -383,24 +264,67 @@ public abstract class Timeline {
*/
public long durationUs;
- /**
- * Whether this period contains an ad.
- */
- public boolean isAd;
-
private long positionInWindowUs;
+ private long[] adGroupTimesUs;
+ private boolean[] hasPlayedAdGroup;
+ private int[] adCounts;
+ private boolean[][] isAdAvailable;
+ private long[][] adDurationsUs;
/**
* Sets the data held by this period.
+ *
+ * @param id An identifier for the period. Not necessarily unique.
+ * @param uid A unique identifier for the period.
+ * @param windowIndex The index of the window to which this period belongs.
+ * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if
+ * unknown.
+ * @param positionInWindowUs The position of the start of this period relative to the start of
+ * the window to which it belongs, in milliseconds. May be negative if the start of the
+ * period is not within the window.
+ * @return This period, for convenience.
*/
public Period set(Object id, Object uid, int windowIndex, long durationUs,
- long positionInWindowUs, boolean isAd) {
+ long positionInWindowUs) {
+ return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null,
+ null);
+ }
+
+ /**
+ * Sets the data held by this period.
+ *
+ * @param id An identifier for the period. Not necessarily unique.
+ * @param uid A unique identifier for the period.
+ * @param windowIndex The index of the window to which this period belongs.
+ * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if
+ * unknown.
+ * @param positionInWindowUs The position of the start of this period relative to the start of
+ * the window to which it belongs, in milliseconds. May be negative if the start of the
+ * period is not within the window.
+ * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
+ * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
+ * the period has a postroll ad.
+ * @param hasPlayedAdGroup Whether each ad group has been played.
+ * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
+ * if the number of ads is not yet known.
+ * @param isAdAvailable Whether each ad in each ad group is available.
+ * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
+ * may be {@link C#TIME_UNSET} if the duration is not yet known.
+ * @return This period, for convenience.
+ */
+ public Period set(Object id, Object uid, int windowIndex, long durationUs,
+ long positionInWindowUs, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, int[] adCounts,
+ boolean[][] isAdAvailable, long[][] adDurationsUs) {
this.id = id;
this.uid = uid;
this.windowIndex = windowIndex;
this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs;
- this.isAd = isAd;
+ this.adGroupTimesUs = adGroupTimesUs;
+ this.hasPlayedAdGroup = hasPlayedAdGroup;
+ this.adCounts = adCounts;
+ this.isAdAvailable = isAdAvailable;
+ this.adDurationsUs = adDurationsUs;
return this;
}
@@ -436,6 +360,398 @@ public abstract class Timeline {
return positionInWindowUs;
}
+ /**
+ * Returns the number of ad groups in the period.
+ */
+ public int getAdGroupCount() {
+ return adGroupTimesUs == null ? 0 : adGroupTimesUs.length;
+ }
+
+ /**
+ * Returns the time of the ad group at index {@code adGroupIndex} in the period, in
+ * microseconds.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return The time of the ad group at the index, in microseconds.
+ */
+ public long getAdGroupTimeUs(int adGroupIndex) {
+ if (adGroupTimesUs == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return adGroupTimesUs[adGroupIndex];
+ }
+
+ /**
+ * Returns whether the ad group at index {@code adGroupIndex} has been played.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return Whether the ad group at index {@code adGroupIndex} has been played.
+ */
+ public boolean hasPlayedAdGroup(int adGroupIndex) {
+ if (hasPlayedAdGroup == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return hasPlayedAdGroup[adGroupIndex];
+ }
+
+ /**
+ * Returns the index of the ad group at or before {@code positionUs}, if that ad group is
+ * unplayed. Returns {@link C#INDEX_UNSET} if the ad group before {@code positionUs} has been
+ * played, or if there is no such ad group.
+ *
+ * @param positionUs The position at or before which to find an ad group, in microseconds.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
+ */
+ public int getAdGroupIndexForPositionUs(long positionUs) {
+ if (adGroupTimesUs == null) {
+ return C.INDEX_UNSET;
+ }
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = adGroupTimesUs.length - 1;
+ while (index >= 0 && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE
+ || adGroupTimesUs[index] > positionUs)) {
+ index--;
+ }
+ return index >= 0 && !hasPlayedAdGroup(index) ? index : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the index of the next unplayed ad group after {@code positionUs}. Returns
+ * {@link C#INDEX_UNSET} if there is no such ad group.
+ *
+ * @param positionUs The position after which to find an ad group, in microseconds.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
+ */
+ public int getAdGroupIndexAfterPositionUs(long positionUs) {
+ if (adGroupTimesUs == null) {
+ return C.INDEX_UNSET;
+ }
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = 0;
+ while (index < adGroupTimesUs.length && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
+ && (positionUs >= adGroupTimesUs[index] || hasPlayedAdGroup(index))) {
+ index++;
+ }
+ return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the number of ads in the ad group at index {@code adGroupIndex}, or
+ * {@link C#LENGTH_UNSET} if not yet known.
+ *
+ * @param adGroupIndex The ad group index.
+ * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known.
+ */
+ public int getAdCountInAdGroup(int adGroupIndex) {
+ if (adCounts == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return adCounts[adGroupIndex];
+ }
+
+ /**
+ * Returns whether the URL for the specified ad is known.
+ *
+ * @param adGroupIndex The ad group index.
+ * @param adIndexInAdGroup The ad index in the ad group.
+ * @return Whether the URL for the specified ad is known.
+ */
+ public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) {
+ return isAdAvailable != null && adGroupIndex < isAdAvailable.length
+ && adIndexInAdGroup < isAdAvailable[adGroupIndex].length
+ && isAdAvailable[adGroupIndex][adIndexInAdGroup];
+ }
+
+ /**
+ * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at
+ * {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known.
+ *
+ * @param adGroupIndex The ad group index.
+ * @param adIndexInAdGroup The ad index in the ad group.
+ * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known.
+ */
+ public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) {
+ if (adDurationsUs == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) {
+ return C.TIME_UNSET;
+ }
+ return adDurationsUs[adGroupIndex][adIndexInAdGroup];
+ }
+
}
+ /**
+ * An empty timeline.
+ */
+ public static final Timeline EMPTY = new Timeline() {
+
+ @Override
+ public int getWindowCount() {
+ return 0;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 0;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return C.INDEX_UNSET;
+ }
+
+ };
+
+ /**
+ * Returns whether the timeline is empty.
+ */
+ public final boolean isEmpty() {
+ return getWindowCount() == 0;
+ }
+
+ /**
+ * Returns the number of windows in the timeline.
+ */
+ public abstract int getWindowCount();
+
+ /**
+ * Returns the index of the window after the window at index {@code windowIndex} depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex Index of a window in the timeline.
+ * @param repeatMode A repeat mode.
+ * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window.
+ */
+ public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ switch (repeatMode) {
+ case ExoPlayer.REPEAT_MODE_OFF:
+ return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1;
+ case ExoPlayer.REPEAT_MODE_ONE:
+ return windowIndex;
+ case ExoPlayer.REPEAT_MODE_ALL:
+ return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Returns the index of the window before the window at index {@code windowIndex} depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex Index of a window in the timeline.
+ * @param repeatMode A repeat mode.
+ * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window.
+ */
+ public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ switch (repeatMode) {
+ case ExoPlayer.REPEAT_MODE_OFF:
+ return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1;
+ case ExoPlayer.REPEAT_MODE_ONE:
+ return windowIndex;
+ case ExoPlayer.REPEAT_MODE_ALL:
+ return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Returns whether the given window is the last window of the timeline depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex A window index.
+ * @param repeatMode A repeat mode.
+ * @return Whether the window of the given index is the last window of the timeline.
+ */
+ public final boolean isLastWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns whether the given window is the first window of the timeline depending on the
+ * {@code repeatMode}.
+ *
+ * @param windowIndex A window index.
+ * @param repeatMode A repeat mode.
+ * @return Whether the window of the given index is the first window of the timeline.
+ */
+ public final boolean isFirstWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET;
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index. Does not populate
+ * {@link Window#id}.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public final Window getWindow(int windowIndex, Window window) {
+ return getWindow(windowIndex, window, false);
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+ * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public Window getWindow(int windowIndex, Window window, boolean setIds) {
+ return getWindow(windowIndex, window, setIds, 0);
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+ * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @param defaultPositionProjectionUs A duration into the future that the populated window's
+ * default start position should be projected.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs);
+
+ /**
+ * Returns the number of periods in the timeline.
+ */
+ public abstract int getPeriodCount();
+
+ /**
+ * Returns the index of the period after the period at index {@code periodIndex} depending on the
+ * {@code repeatMode}.
+ *
+ * @param periodIndex Index of a period in the timeline.
+ * @param period A {@link Period} to be used internally. Must not be null.
+ * @param window A {@link Window} to be used internally. Must not be null.
+ * @param repeatMode A repeat mode.
+ * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period.
+ */
+ public final int getNextPeriodIndex(int periodIndex, Period period, Window window,
+ @ExoPlayer.RepeatMode int repeatMode) {
+ int windowIndex = getPeriod(periodIndex, period).windowIndex;
+ if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) {
+ int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode);
+ if (nextWindowIndex == C.INDEX_UNSET) {
+ return C.INDEX_UNSET;
+ }
+ return getWindow(nextWindowIndex, window).firstPeriodIndex;
+ }
+ return periodIndex + 1;
+ }
+
+ /**
+ * Returns whether the given period is the last period of the timeline depending on the
+ * {@code repeatMode}.
+ *
+ * @param periodIndex A period index.
+ * @param period A {@link Period} to be used internally. Must not be null.
+ * @param window A {@link Window} to be used internally. Must not be null.
+ * @param repeatMode A repeat mode.
+ * @return Whether the period of the given index is the last period of the timeline.
+ */
+ public final boolean isLastPeriod(int periodIndex, Period period, Window window,
+ @ExoPlayer.RepeatMode int repeatMode) {
+ return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET;
+ }
+
+ /**
+ * Populates a {@link Period} with data for the period at the specified index. Does not populate
+ * {@link Period#id} and {@link Period#uid}.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public final Period getPeriod(int periodIndex, Period period) {
+ return getPeriod(periodIndex, period, false);
+ }
+
+ /**
+ * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position
+ * projection.
+ */
+ public final Pair getPeriodPosition(Window window, Period period, int windowIndex,
+ long windowPositionUs) {
+ return getPeriodPosition(window, period, windowIndex, windowPositionUs, 0);
+ }
+
+ /**
+ * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
+ *
+ * @param window A {@link Window} that may be overwritten.
+ * @param period A {@link Period} that may be overwritten.
+ * @param windowIndex The window index.
+ * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
+ * start position.
+ * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
+ * duration into the future by which the window's position should be projected.
+ * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
+ * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
+ * position could not be projected by {@code defaultPositionProjectionUs}.
+ */
+ public final Pair getPeriodPosition(Window window, Period period, int windowIndex,
+ long windowPositionUs, long defaultPositionProjectionUs) {
+ Assertions.checkIndex(windowIndex, 0, getWindowCount());
+ getWindow(windowIndex, window, false, defaultPositionProjectionUs);
+ if (windowPositionUs == C.TIME_UNSET) {
+ windowPositionUs = window.getDefaultPositionUs();
+ if (windowPositionUs == C.TIME_UNSET) {
+ return null;
+ }
+ }
+ int periodIndex = window.firstPeriodIndex;
+ long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
+ long periodDurationUs = getPeriod(periodIndex, period).getDurationUs();
+ while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+ && periodIndex < window.lastPeriodIndex) {
+ periodPositionUs -= periodDurationUs;
+ periodDurationUs = getPeriod(++periodIndex, period).getDurationUs();
+ }
+ return Pair.create(periodIndex, periodPositionUs);
+ }
+
+ /**
+ * Populates a {@link Period} with data for the period at the specified index.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,
+ * the fields will be set to null. The caller should pass false for efficiency reasons unless
+ * the fields are required.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);
+
+ /**
+ * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET}
+ * if the period is not in the timeline.
+ *
+ * @param uid A unique identifier for a period.
+ * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.
+ */
+ public abstract int getIndexOfPeriod(Object uid);
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
new file mode 100644
index 0000000000..337200da8f
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
@@ -0,0 +1,143 @@
+/*
+ * 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.audio;
+
+import android.annotation.TargetApi;
+import com.google.android.exoplayer2.C;
+
+/**
+ * Attributes for audio playback, which configure the underlying platform
+ * {@link android.media.AudioTrack}.
+ *
+ * To set the audio attributes, create an instance using the {@link Builder} and either pass it to
+ * {@link com.google.android.exoplayer2.SimpleExoPlayer#setAudioAttributes(AudioAttributes)} or
+ * send a message of type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to the audio renderers.
+ *
+ * This class is based on {@link android.media.AudioAttributes}, but can be used on all supported
+ * API versions.
+ */
+public final class AudioAttributes {
+
+ public static final AudioAttributes DEFAULT = new Builder().build();
+
+ /**
+ * Builder for {@link AudioAttributes}.
+ */
+ public static final class Builder {
+
+ @C.AudioContentType
+ private int contentType;
+ @C.AudioFlags
+ private int flags;
+ @C.AudioUsage
+ private int usage;
+
+ /**
+ * Creates a new builder for {@link AudioAttributes}.
+ *
+ * By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is
+ * {@link C#USAGE_MEDIA}, and no flags are set.
+ */
+ public Builder() {
+ contentType = C.CONTENT_TYPE_UNKNOWN;
+ flags = 0;
+ usage = C.USAGE_MEDIA;
+ }
+
+ /**
+ * @see android.media.AudioAttributes.Builder#setContentType(int)
+ */
+ public Builder setContentType(@C.AudioContentType int contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ /**
+ * @see android.media.AudioAttributes.Builder#setFlags(int)
+ */
+ public Builder setFlags(@C.AudioFlags int flags) {
+ this.flags = flags;
+ return this;
+ }
+
+ /**
+ * @see android.media.AudioAttributes.Builder#setUsage(int)
+ */
+ public Builder setUsage(@C.AudioUsage int usage) {
+ this.usage = usage;
+ return this;
+ }
+
+ /**
+ * Creates an {@link AudioAttributes} instance from this builder.
+ */
+ public AudioAttributes build() {
+ return new AudioAttributes(contentType, flags, usage);
+ }
+
+ }
+
+ @C.AudioContentType
+ public final int contentType;
+ @C.AudioFlags
+ public final int flags;
+ @C.AudioUsage
+ public final int usage;
+
+ private android.media.AudioAttributes audioAttributesV21;
+
+ private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags,
+ @C.AudioUsage int usage) {
+ this.contentType = contentType;
+ this.flags = flags;
+ this.usage = usage;
+ }
+
+ @TargetApi(21)
+ /* package */ android.media.AudioAttributes getAudioAttributesV21() {
+ if (audioAttributesV21 == null) {
+ audioAttributesV21 = new android.media.AudioAttributes.Builder()
+ .setContentType(contentType)
+ .setFlags(flags)
+ .setUsage(usage)
+ .build();
+ }
+ return audioAttributesV21;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ AudioAttributes other = (AudioAttributes) obj;
+ return this.contentType == other.contentType && this.flags == other.flags
+ && this.usage == other.usage;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + contentType;
+ result = 31 * result + flags;
+ result = 31 * result + usage;
+ return result;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
index 44a96373f3..79cb26bf39 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.audio;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
-import android.media.AudioAttributes;
import android.media.AudioFormat;
+import android.media.AudioManager;
import android.media.AudioTimestamp;
import android.os.ConditionVariable;
import android.os.SystemClock;
@@ -40,9 +40,9 @@ import java.util.LinkedList;
*
* Before starting playback, specify the input format by calling
* {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
- * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
- * to configure audio playback. These methods may be called after writing data to the track, in
- * which case it will be reinitialized as required.
+ * {@link #setAudioAttributes(AudioAttributes)}, {@link #enableTunnelingV21(int)} and
+ * {@link #disableTunneling()} to configure audio playback. These methods may be called after
+ * writing data to the track, in which case it will be reinitialized as required.
*
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
@@ -299,8 +299,7 @@ public final class AudioTrack {
private int encoding;
@C.Encoding
private int outputEncoding;
- @C.StreamType
- private int streamType;
+ private AudioAttributes audioAttributes;
private boolean passthrough;
private int bufferSize;
private long bufferSizeUs;
@@ -384,7 +383,7 @@ public final class AudioTrack {
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
- streamType = C.STREAM_TYPE_DEFAULT;
+ audioAttributes = AudioAttributes.DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
playbackParameters = PlaybackParameters.DEFAULT;
drainingAudioProcessorIndex = C.INDEX_UNSET;
@@ -634,19 +633,7 @@ public final class AudioTrack {
// initialization of the audio track to fail.
releasingConditionVariable.block();
- if (tunneling) {
- audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding,
- bufferSize, audioSessionId);
- } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
- audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
- outputEncoding, bufferSize, MODE_STREAM);
- } else {
- // Re-attach to the same audio session.
- audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
- outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
- }
- checkAudioTrackInitialized();
-
+ audioTrack = initializeAudioTrack();
int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) {
@@ -657,12 +644,7 @@ public final class AudioTrack {
releaseKeepSessionIdAudioTrack();
}
if (keepSessionIdAudioTrack == null) {
- int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
- int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
- @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
- int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
- keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
- channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
+ keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId);
}
}
}
@@ -1021,23 +1003,23 @@ public final class AudioTrack {
}
/**
- * Sets the stream type for audio track. If the stream type has changed and if the audio track
+ * Sets the attributes for audio playback. If the attributes have changed and if the audio track
* is not configured for use with tunneling, then the audio track is reset and the audio session
* id is cleared.
*
- * If the audio track is configured for use with tunneling then the stream type is ignored, the
- * audio track is not reset and the audio session id is not cleared. The passed stream type will
- * be used if the audio track is later re-configured into non-tunneled mode.
+ * If the audio track is configured for use with tunneling then the audio attributes are ignored.
+ * The audio track is not reset and the audio session id is not cleared. The passed attributes
+ * will be used if the audio track is later re-configured into non-tunneled mode.
*
- * @param streamType The {@link C.StreamType} to use for audio output.
+ * @param audioAttributes The attributes for audio playback.
*/
- public void setStreamType(@C.StreamType int streamType) {
- if (this.streamType == streamType) {
+ public void setAudioAttributes(AudioAttributes audioAttributes) {
+ if (this.audioAttributes.equals(audioAttributes)) {
return;
}
- this.streamType = streamType;
+ this.audioAttributes = audioAttributes;
if (tunneling) {
- // The stream type is ignored in tunneling mode, so no need to reset.
+ // The audio attributes are ignored in tunneling mode, so no need to reset.
return;
}
reset();
@@ -1292,7 +1274,7 @@ public final class AudioTrack {
// The timestamp time base is probably wrong.
String message = "Spurious audio timestamp (system clock mismatch): "
+ audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
- + playbackPositionUs;
+ + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames();
if (failOnSpuriousAudioTimestamp) {
throw new InvalidAudioTrackTimestampException(message);
}
@@ -1303,7 +1285,7 @@ public final class AudioTrack {
// The timestamp frame position is probably wrong.
String message = "Spurious audio timestamp (frame position mismatch): "
+ audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
- + playbackPositionUs;
+ + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames();
if (failOnSpuriousAudioTimestamp) {
throw new InvalidAudioTrackTimestampException(message);
}
@@ -1333,31 +1315,6 @@ public final class AudioTrack {
}
}
- /**
- * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
- * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
- * exception is thrown.
- *
- * @throws InitializationException If {@link #audioTrack} has not been successfully initialized.
- */
- private void checkAudioTrackInitialized() throws InitializationException {
- int state = audioTrack.getState();
- if (state == STATE_INITIALIZED) {
- return;
- }
- // The track is not successfully initialized. Release and null the track.
- try {
- audioTrack.release();
- } catch (Exception e) {
- // The track has already failed to initialize, so it wouldn't be that surprising if release
- // were to fail too. Swallow the exception.
- } finally {
- audioTrack = null;
- }
-
- throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
- }
-
private boolean isInitialized() {
return audioTrack != null;
}
@@ -1408,24 +1365,65 @@ public final class AudioTrack {
&& audioTrack.getPlaybackHeadPosition() == 0;
}
- /**
- * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
- */
+ private android.media.AudioTrack initializeAudioTrack() throws InitializationException {
+ android.media.AudioTrack audioTrack;
+ if (Util.SDK_INT >= 21) {
+ audioTrack = createAudioTrackV21();
+ } else {
+ int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);
+ if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+ audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+ outputEncoding, bufferSize, MODE_STREAM);
+ } else {
+ // Re-attach to the same audio session.
+ audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+ outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
+ }
+ }
+
+ int state = audioTrack.getState();
+ if (state != STATE_INITIALIZED) {
+ try {
+ audioTrack.release();
+ } catch (Exception e) {
+ // The track has already failed to initialize, so it wouldn't be that surprising if release
+ // were to fail too. Swallow the exception.
+ }
+ throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
+ }
+ return audioTrack;
+ }
+
@TargetApi(21)
- private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
- int channelConfig, int encoding, int bufferSize, int sessionId) {
- AudioAttributes attributesBuilder = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
- .setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
- .build();
+ private android.media.AudioTrack createAudioTrackV21() {
+ android.media.AudioAttributes attributes;
+ if (tunneling) {
+ attributes = new android.media.AudioAttributes.Builder()
+ .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC)
+ .setUsage(android.media.AudioAttributes.USAGE_MEDIA)
+ .build();
+ } else {
+ attributes = audioAttributes.getAudioAttributesV21();
+ }
AudioFormat format = new AudioFormat.Builder()
.setChannelMask(channelConfig)
- .setEncoding(encoding)
+ .setEncoding(outputEncoding)
.setSampleRate(sampleRate)
.build();
- return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
- sessionId);
+ int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId
+ : AudioManager.AUDIO_SESSION_ID_GENERATE;
+ return new android.media.AudioTrack(attributes, format, bufferSize, MODE_STREAM,
+ audioSessionId);
+ }
+
+ private android.media.AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) {
+ int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
+ int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+ @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
+ int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
+ return new android.media.AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding,
+ bufferSize, MODE_STATIC, audioSessionId);
}
@C.Encoding
@@ -1465,7 +1463,7 @@ public final class AudioTrack {
@TargetApi(21)
private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
ByteBuffer buffer, int size, long presentationTimeUs) {
- // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
+ // TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed.
// if (Util.SDK_INT >= 23) {
// // The underlying platform AudioTrack writes AV sync headers directly.
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index 48c7462b03..4d97c292ac 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -399,9 +399,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
case C.MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
- case C.MSG_SET_STREAM_TYPE:
- @C.StreamType int streamType = (Integer) message;
- audioTrack.setStreamType(streamType);
+ case C.MSG_SET_AUDIO_ATTRIBUTES:
+ AudioAttributes audioAttributes = (AudioAttributes) message;
+ audioTrack.setAudioAttributes(audioAttributes);
break;
default:
super.handleMessage(messageType, message);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index ddb870f6ff..c4a55eeb02 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
@@ -376,15 +377,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null) {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
- && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
private void processEndOfStream() throws ExoPlaybackException {
@@ -514,13 +514,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
- @DrmSession.State int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- } else if (drmSessionState == DrmSession.STATE_OPENED
- || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
- mediaCrypto = drmSession.getMediaCrypto();
- } else {
+ mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ throw ExoPlaybackException.createForRenderer(drmError, getIndex());
+ }
// The drm session isn't open yet.
return;
}
@@ -595,9 +594,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
case C.MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
- case C.MSG_SET_STREAM_TYPE:
- @C.StreamType int streamType = (Integer) message;
- audioTrack.setStreamType(streamType);
+ case C.MSG_SET_AUDIO_ATTRIBUTES:
+ AudioAttributes audioAttributes = (AudioAttributes) message;
+ audioTrack.setAudioAttributes(audioAttributes);
break;
default:
super.handleMessage(messageType, message);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
index 5d6f01b6e0..ef7877ae1e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
@@ -374,8 +374,8 @@ import java.util.Arrays;
}
private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {
- short left = in[inPos * numChannels];
- short right = in[inPos * numChannels + numChannels];
+ short left = in[inPos];
+ short right = in[inPos + numChannels];
int position = newRatePosition * oldSampleRate;
int leftPosition = oldRatePosition * newSampleRate;
int rightPosition = (oldRatePosition + 1) * newSampleRate;
@@ -402,7 +402,7 @@ import java.util.Arrays;
enlargeOutputBufferIfNeeded(1);
for (int i = 0; i < numChannels; i++) {
outputBuffer[numOutputSamples * numChannels + i] =
- interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate);
+ interpolate(pitchBuffer, position * numChannels + i, oldSampleRate, newSampleRate);
}
newRatePosition++;
numOutputSamples++;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
index 0d143cdf49..ec17de8d74 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
@@ -52,11 +52,11 @@ public final class CryptoInfo {
/**
* @see android.media.MediaCodec.CryptoInfo.Pattern
*/
- public int patternBlocksToEncrypt;
+ public int encryptedBlocks;
/**
* @see android.media.MediaCodec.CryptoInfo.Pattern
*/
- public int patternBlocksToSkip;
+ public int clearBlocks;
private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
private final PatternHolderV24 patternHolder;
@@ -70,28 +70,20 @@ public final class CryptoInfo {
* @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
*/
public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
- byte[] key, byte[] iv, @C.CryptoMode int mode) {
+ byte[] key, byte[] iv, @C.CryptoMode int mode, int encryptedBlocks, int clearBlocks) {
this.numSubSamples = numSubSamples;
this.numBytesOfClearData = numBytesOfClearData;
this.numBytesOfEncryptedData = numBytesOfEncryptedData;
this.key = key;
this.iv = iv;
this.mode = mode;
- patternBlocksToEncrypt = 0;
- patternBlocksToSkip = 0;
+ this.encryptedBlocks = encryptedBlocks;
+ this.clearBlocks = clearBlocks;
if (Util.SDK_INT >= 16) {
updateFrameworkCryptoInfoV16();
}
}
- public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) {
- this.patternBlocksToEncrypt = patternBlocksToEncrypt;
- this.patternBlocksToSkip = patternBlocksToSkip;
- if (Util.SDK_INT >= 24) {
- patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip);
- }
- }
-
/**
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
*
@@ -122,7 +114,7 @@ public final class CryptoInfo {
frameworkCryptoInfo.iv = iv;
frameworkCryptoInfo.mode = mode;
if (Util.SDK_INT >= 24) {
- patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip);
+ patternHolder.set(encryptedBlocks, clearBlocks);
}
}
@@ -137,8 +129,8 @@ public final class CryptoInfo {
pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0);
}
- private void set(int blocksToEncrypt, int blocksToSkip) {
- pattern.set(blocksToEncrypt, blocksToSkip);
+ private void set(int encryptedBlocks, int clearBlocks) {
+ pattern.set(encryptedBlocks, clearBlocks);
frameworkCryptoInfo.setPattern(pattern);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
index 309c7fd144..49c7dafbd6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.decoder;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
/**
* Buffer for {@link SimpleDecoder} output.
@@ -40,7 +41,7 @@ public class SimpleOutputBuffer extends OutputBuffer {
public ByteBuffer init(long timeUs, int size) {
this.timeUs = timeUs;
if (data == null || data.capacity() < size) {
- data = ByteBuffer.allocateDirect(size);
+ data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
data.position(0);
data.limit(size);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index 6fc149ba32..68eba76b11 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -222,7 +222,6 @@ public class DefaultDrmSessionManager implements DrmSe
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mediaDrm.setOnEventListener(new MediaDrmEventListener());
- state = STATE_CLOSED;
mode = MODE_PLAYBACK;
}
@@ -358,7 +357,7 @@ public class DefaultDrmSessionManager implements DrmSe
if (--openCount != 0) {
return;
}
- state = STATE_CLOSED;
+ state = STATE_RELEASED;
provisioningInProgress = false;
mediaDrmHandler.removeCallbacksAndMessages(null);
postResponseHandler.removeCallbacksAndMessages(null);
@@ -384,35 +383,19 @@ public class DefaultDrmSessionManager implements DrmSe
return state;
}
- @Override
- public final T getMediaCrypto() {
- if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
- throw new IllegalStateException();
- }
- return mediaCrypto;
- }
-
- @Override
- public boolean requiresSecureDecoderComponent(String mimeType) {
- if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
- throw new IllegalStateException();
- }
- return mediaCrypto.requiresSecureDecoderComponent(mimeType);
- }
-
@Override
public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null;
}
+ @Override
+ public final T getMediaCrypto() {
+ return mediaCrypto;
+ }
+
@Override
public Map queryKeyStatus() {
- // User may call this method rightfully even if state == STATE_ERROR. So only check if there is
- // a sessionId
- if (sessionId == null) {
- throw new IllegalStateException();
- }
- return mediaDrm.queryKeyStatus(sessionId);
+ return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);
}
@Override
@@ -513,6 +496,8 @@ public class DefaultDrmSessionManager implements DrmSe
}
break;
case MODE_RELEASE:
+ // It's not necessary to restore the key (and open a session to do that) before releasing it
+ // but this serves as a good sanity/fast-failure check.
if (restoreKeys()) {
postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
index 5126628dd9..9fa6547a00 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.util.Assertions;
@@ -102,6 +103,33 @@ public final class DrmInitData implements Comparator, Parcelable {
return schemeDatas[index];
}
+ /**
+ * Returns a copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated
+ * to have the specified scheme type.
+ *
+ * @param schemeType A protection scheme type. May be null.
+ * @return A copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated
+ * to have the specified scheme type.
+ */
+ public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
+ boolean isCopyRequired = false;
+ for (SchemeData schemeData : schemeDatas) {
+ if (!Util.areEqual(schemeData.type, schemeType)) {
+ isCopyRequired = true;
+ break;
+ }
+ }
+ if (isCopyRequired) {
+ SchemeData[] schemeDatas = new SchemeData[this.schemeDatas.length];
+ for (int i = 0; i < schemeDatas.length; i++) {
+ schemeDatas[i] = this.schemeDatas[i].copyWithSchemeType(schemeType);
+ }
+ return new DrmInitData(schemeDatas);
+ } else {
+ return this;
+ }
+ }
+
@Override
public int hashCode() {
if (hashCode == 0) {
@@ -167,6 +195,10 @@ public final class DrmInitData implements Comparator, Parcelable {
* applies to all schemes).
*/
private final UUID uuid;
+ /**
+ * The protection scheme type, or null if not applicable or unknown.
+ */
+ @Nullable public final String type;
/**
* The mimeType of {@link #data}.
*/
@@ -183,22 +215,26 @@ public final class DrmInitData implements Comparator, Parcelable {
/**
* @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
* universal (i.e. applies to all schemes).
+ * @param type The type of the protection scheme, or null if not applicable or unknown.
* @param mimeType The mimeType of the initialization data.
* @param data The initialization data.
*/
- public SchemeData(UUID uuid, String mimeType, byte[] data) {
- this(uuid, mimeType, data, false);
+ public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data) {
+ this(uuid, type, mimeType, data, false);
}
/**
* @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
* universal (i.e. applies to all schemes).
+ * @param type The type of the protection scheme, or null if not applicable or unknown.
* @param mimeType The mimeType of the initialization data.
* @param data The initialization data.
* @param requiresSecureDecryption Whether secure decryption is required.
*/
- public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
+ public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data,
+ boolean requiresSecureDecryption) {
this.uuid = Assertions.checkNotNull(uuid);
+ this.type = type;
this.mimeType = Assertions.checkNotNull(mimeType);
this.data = Assertions.checkNotNull(data);
this.requiresSecureDecryption = requiresSecureDecryption;
@@ -206,6 +242,7 @@ public final class DrmInitData implements Comparator, Parcelable {
/* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong());
+ type = in.readString();
mimeType = in.readString();
data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0;
@@ -221,6 +258,19 @@ public final class DrmInitData implements Comparator, Parcelable {
return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid);
}
+ /**
+ * Returns a copy of the {@link SchemeData} instance with the given scheme type.
+ *
+ * @param type A protection scheme type.
+ * @return A copy of the {@link SchemeData} instance with the given scheme type.
+ */
+ public SchemeData copyWithSchemeType(String type) {
+ if (Util.areEqual(this.type, type)) {
+ return this;
+ }
+ return new SchemeData(uuid, type, mimeType, data, requiresSecureDecryption);
+ }
+
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SchemeData)) {
@@ -231,13 +281,14 @@ public final class DrmInitData implements Comparator, Parcelable {
}
SchemeData other = (SchemeData) obj;
return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid)
- && Arrays.equals(data, other.data);
+ && Util.areEqual(type, other.type) && Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
if (hashCode == 0) {
int result = uuid.hashCode();
+ result = 31 * result + (type == null ? 0 : type.hashCode());
result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data);
hashCode = result;
@@ -256,6 +307,7 @@ public final class DrmInitData implements Comparator, Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(uuid.getMostSignificantBits());
dest.writeLong(uuid.getLeastSignificantBits());
+ dest.writeString(type);
dest.writeString(mimeType);
dest.writeByteArray(data);
dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
index 538db9e1d9..0c17b102fd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
@@ -28,11 +28,11 @@ import java.util.Map;
@TargetApi(16)
public interface DrmSession {
- /** Wraps the exception which is the cause of the error state. */
+ /** Wraps the throwable which is the cause of the error state. */
class DrmSessionException extends Exception {
- public DrmSessionException(Exception e) {
- super(e);
+ public DrmSessionException(Throwable cause) {
+ super(cause);
}
}
@@ -41,16 +41,16 @@ public interface DrmSession {
* The state of the DRM session.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
- @interface State {}
+ @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
+ public @interface State {}
+ /**
+ * The session has been released.
+ */
+ int STATE_RELEASED = 0;
/**
* The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
*/
- int STATE_ERROR = 0;
- /**
- * The session is closed.
- */
- int STATE_CLOSED = 1;
+ int STATE_ERROR = 1;
/**
* The session is being opened.
*/
@@ -65,66 +65,40 @@ public interface DrmSession {
int STATE_OPENED_WITH_KEYS = 4;
/**
- * Returns the current state of the session.
- *
- * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
- * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
+ * Returns the current state of the session, which is one of {@link #STATE_ERROR},
+ * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and
+ * {@link #STATE_OPENED_WITH_KEYS}.
*/
@State int getState();
- /**
- * Returns a {@link ExoMediaCrypto} for the open session.
- *
- * This method may be called when the session is in the following states:
- * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
- *
- * @return A {@link ExoMediaCrypto} for the open session.
- * @throws IllegalStateException If called when a session isn't opened.
- */
- T getMediaCrypto();
-
- /**
- * Whether the session requires a secure decoder for the specified mime type.
- *
- * Normally this method should return
- * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases
- * implementations may wish to modify the return value (i.e. to force a secure decoder even when
- * one is not required).
- *
- * This method may be called when the session is in the following states:
- * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
- *
- * @return Whether the open session requires a secure decoder for the specified mime type.
- * @throws IllegalStateException If called when a session isn't opened.
- */
- boolean requiresSecureDecoderComponent(String mimeType);
-
/**
* Returns the cause of the error state.
- *
- * This method may be called when the session is in any state.
- *
- * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
*/
DrmSessionException getError();
/**
- * Returns an informative description of the key status for the session. The status is in the form
- * of {name, value} pairs.
- *
- *
Since DRM license policies vary by vendor, the specific status field names are determined by
+ * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has
+ * been opened or after it's been released.
+ */
+ T getMediaCrypto();
+
+ /**
+ * Returns a map describing the key status for the session, or null if called before the session
+ * has been opened or after it's been released.
+ *
+ * Since DRM license policies vary by vendor, the specific status field names are determined by
* each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
* for a particular DRM engine plugin.
*
- * @return A map of key status.
- * @throws IllegalStateException If called when the session isn't opened.
+ * @return A map describing the key status for the session, or null if called before the session
+ * has been opened or after it's been released.
* @see MediaDrm#queryKeyStatus(byte[])
*/
Map queryKeyStatus();
/**
- * Returns the key set id of the offline license loaded into this session, if there is one. Null
- * otherwise.
+ * Returns the key set id of the offline license loaded into this session, or null if there isn't
+ * one.
*/
byte[] getOfflineLicenseKeySetId();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
index dd441a022f..5bee85f449 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
@@ -26,9 +26,12 @@ import com.google.android.exoplayer2.util.Assertions;
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
private final MediaCrypto mediaCrypto;
+ private final boolean forceAllowInsecureDecoderComponents;
- /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
+ /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto,
+ boolean forceAllowInsecureDecoderComponents) {
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
+ this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
}
public MediaCrypto getWrappedMediaCrypto() {
@@ -37,7 +40,8 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto {
@Override
public boolean requiresSecureDecoderComponent(String mimeType) {
- return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+ return !forceAllowInsecureDecoderComponents
+ && mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
index e6887af6da..ed4494559a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -24,7 +24,9 @@ import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -163,7 +165,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm getLicenseDurationRemainingSec(DrmSession> drmSession) {
Map keyStatus = drmSession.queryKeyStatus();
- return new Pair<>(
- getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
+ if (keyStatus == null) {
+ return null;
+ }
+ return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
index 022ca1277d..c47a91b176 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -26,7 +26,9 @@ import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader;
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.lang.reflect.Constructor;
/**
@@ -67,8 +69,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
private @MatroskaExtractor.Flags int matroskaFlags;
private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
private @Mp3Extractor.Flags int mp3Flags;
+ private @TsExtractor.Mode int tsMode;
private @DefaultTsPayloadReaderFactory.Flags int tsFlags;
+ public DefaultExtractorsFactory() {
+ tsMode = TsExtractor.MODE_SINGLE_PMT;
+ }
+
/**
* Sets flags for {@link MatroskaExtractor} instances created by the factory.
*
@@ -107,6 +114,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
return this;
}
+ /**
+ * Sets the mode for {@link TsExtractor} instances created by the factory.
+ *
+ * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory).
+ * @param mode The mode to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) {
+ tsMode = mode;
+ return this;
+ }
+
/**
* Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances
* created by the factory.
@@ -130,7 +149,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors[3] = new Mp3Extractor(mp3Flags);
extractors[4] = new AdtsExtractor();
extractors[5] = new Ac3Extractor();
- extractors[6] = new TsExtractor(tsFlags);
+ extractors[6] = new TsExtractor(tsMode, tsFlags);
extractors[7] = new FlvExtractor();
extractors[8] = new OggExtractor();
extractors[9] = new PsExtractor();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
deleted file mode 100644
index 1c9a148226..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
+++ /dev/null
@@ -1,997 +0,0 @@
-/*
- * Copyright (C) 2016 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.extractor;
-
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.FormatHolder;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.upstream.Allocation;
-import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.ParsableByteArray;
-import com.google.android.exoplayer2.util.Util;
-import java.io.EOFException;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from
- * that queue.
- */
-public final class DefaultTrackOutput implements TrackOutput {
-
- /**
- * A listener for changes to the upstream format.
- */
- public interface UpstreamFormatChangedListener {
-
- /**
- * Called on the loading thread when an upstream format change occurs.
- *
- * @param format The new upstream format.
- */
- void onUpstreamFormatChanged(Format format);
-
- }
-
- private static final int INITIAL_SCRATCH_SIZE = 32;
-
- private static final int STATE_ENABLED = 0;
- private static final int STATE_ENABLED_WRITING = 1;
- private static final int STATE_DISABLED = 2;
-
- private final Allocator allocator;
- private final int allocationLength;
-
- private final InfoQueue infoQueue;
- private final LinkedBlockingDeque dataQueue;
- private final BufferExtrasHolder extrasHolder;
- private final ParsableByteArray scratch;
- private final AtomicInteger state;
-
- // Accessed only by the consuming thread.
- private long totalBytesDropped;
- private Format downstreamFormat;
-
- // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
- private boolean pendingFormatAdjustment;
- private Format lastUnadjustedFormat;
- private long sampleOffsetUs;
- private long totalBytesWritten;
- private Allocation lastAllocation;
- private int lastAllocationOffset;
- private boolean pendingSplice;
- private UpstreamFormatChangedListener upstreamFormatChangeListener;
-
- /**
- * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
- */
- public DefaultTrackOutput(Allocator allocator) {
- this.allocator = allocator;
- allocationLength = allocator.getIndividualAllocationLength();
- infoQueue = new InfoQueue();
- dataQueue = new LinkedBlockingDeque<>();
- extrasHolder = new BufferExtrasHolder();
- scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
- state = new AtomicInteger();
- lastAllocationOffset = allocationLength;
- }
-
- // Called by the consuming thread, but only when there is no loading thread.
-
- /**
- * Resets the output.
- *
- * @param enable Whether the output should be enabled. False if it should be disabled.
- */
- public void reset(boolean enable) {
- int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED);
- clearSampleData();
- infoQueue.resetLargestParsedTimestamps();
- if (previousState == STATE_DISABLED) {
- downstreamFormat = null;
- }
- }
-
- /**
- * Sets a source identifier for subsequent samples.
- *
- * @param sourceId The source identifier.
- */
- public void sourceId(int sourceId) {
- infoQueue.sourceId(sourceId);
- }
-
- /**
- * Indicates that samples subsequently queued to the buffer should be spliced into those already
- * queued.
- */
- public void splice() {
- pendingSplice = true;
- }
-
- /**
- * Returns the current absolute write index.
- */
- public int getWriteIndex() {
- return infoQueue.getWriteIndex();
- }
-
- /**
- * Discards samples from the write side of the buffer.
- *
- * @param discardFromIndex The absolute index of the first sample to be discarded.
- */
- public void discardUpstreamSamples(int discardFromIndex) {
- totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex);
- dropUpstreamFrom(totalBytesWritten);
- }
-
- /**
- * Discards data from the write side of the buffer. Data is discarded from the specified absolute
- * position. Any allocations that are fully discarded are returned to the allocator.
- *
- * @param absolutePosition The absolute position (inclusive) from which to discard data.
- */
- private void dropUpstreamFrom(long absolutePosition) {
- int relativePosition = (int) (absolutePosition - totalBytesDropped);
- // Calculate the index of the allocation containing the position, and the offset within it.
- int allocationIndex = relativePosition / allocationLength;
- int allocationOffset = relativePosition % allocationLength;
- // We want to discard any allocations after the one at allocationIdnex.
- int allocationDiscardCount = dataQueue.size() - allocationIndex - 1;
- if (allocationOffset == 0) {
- // If the allocation at allocationIndex is empty, we should discard that one too.
- allocationDiscardCount++;
- }
- // Discard the allocations.
- for (int i = 0; i < allocationDiscardCount; i++) {
- allocator.release(dataQueue.removeLast());
- }
- // Update lastAllocation and lastAllocationOffset to reflect the new position.
- lastAllocation = dataQueue.peekLast();
- lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset;
- }
-
- // Called by the consuming thread.
-
- /**
- * Disables buffering of sample data and metadata.
- */
- public void disable() {
- if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) {
- clearSampleData();
- }
- }
-
- /**
- * Returns whether the buffer is empty.
- */
- public boolean isEmpty() {
- return infoQueue.isEmpty();
- }
-
- /**
- * Returns the current absolute read index.
- */
- public int getReadIndex() {
- return infoQueue.getReadIndex();
- }
-
- /**
- * Peeks the source id of the next sample, or the current upstream source id if the buffer is
- * empty.
- *
- * @return The source id.
- */
- public int peekSourceId() {
- return infoQueue.peekSourceId();
- }
-
- /**
- * Returns the upstream {@link Format} in which samples are being queued.
- */
- public Format getUpstreamFormat() {
- return infoQueue.getUpstreamFormat();
- }
-
- /**
- * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
- *
- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
- * considered as having been queued. Samples that were dequeued from the front of the queue are
- * considered as having been queued.
- *
- * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
- * samples have been queued.
- */
- public long getLargestQueuedTimestampUs() {
- return infoQueue.getLargestQueuedTimestampUs();
- }
-
- /**
- * Skips all samples currently in the buffer.
- */
- public void skipAll() {
- long nextOffset = infoQueue.skipAll();
- if (nextOffset != C.POSITION_UNSET) {
- dropDownstreamTo(nextOffset);
- }
- }
-
- /**
- * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
- * contains a keyframe with a timestamp of {@code timeUs} or earlier. If
- * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
- * falls within the buffer.
- *
- * @param timeUs The seek time.
- * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
- * of the buffer.
- * @return Whether the skip was successful.
- */
- public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
- long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
- if (nextOffset == C.POSITION_UNSET) {
- return false;
- }
- dropDownstreamTo(nextOffset);
- return true;
- }
-
- /**
- * Attempts to read from the queue.
- *
- * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
- * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
- * end of the stream. If the end of the stream has been reached, the
- * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
- * @param formatRequired Whether the caller requires that the format of the stream be read even if
- * it's not changing. A sample will never be read if set to true, however it is still possible
- * for the end of stream or nothing to be read.
- * @param loadingFinished True if an empty queue should be considered the end of the stream.
- * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
- * be set if the buffer's timestamp is less than this value.
- * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
- * {@link C#RESULT_BUFFER_READ}.
- */
- public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
- boolean loadingFinished, long decodeOnlyUntilUs) {
- int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
- downstreamFormat, extrasHolder);
- switch (result) {
- case C.RESULT_FORMAT_READ:
- downstreamFormat = formatHolder.format;
- return C.RESULT_FORMAT_READ;
- case C.RESULT_BUFFER_READ:
- if (!buffer.isEndOfStream()) {
- if (buffer.timeUs < decodeOnlyUntilUs) {
- buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
- }
- // Read encryption data if the sample is encrypted.
- if (buffer.isEncrypted()) {
- readEncryptionData(buffer, extrasHolder);
- }
- // Write the sample data into the holder.
- buffer.ensureSpaceForWrite(extrasHolder.size);
- readData(extrasHolder.offset, buffer.data, extrasHolder.size);
- // Advance the read head.
- dropDownstreamTo(extrasHolder.nextOffset);
- }
- return C.RESULT_BUFFER_READ;
- case C.RESULT_NOTHING_READ:
- return C.RESULT_NOTHING_READ;
- default:
- throw new IllegalStateException();
- }
- }
-
- /**
- * Reads encryption data for the current sample.
- *
- * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and
- * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The
- * same value is added to {@link BufferExtrasHolder#offset}.
- *
- * @param buffer The buffer into which the encryption data should be written.
- * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
- */
- private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) {
- long offset = extrasHolder.offset;
-
- // Read the signal byte.
- scratch.reset(1);
- readData(offset, scratch.data, 1);
- offset++;
- byte signalByte = scratch.data[0];
- boolean subsampleEncryption = (signalByte & 0x80) != 0;
- int ivSize = signalByte & 0x7F;
-
- // Read the initialization vector.
- if (buffer.cryptoInfo.iv == null) {
- buffer.cryptoInfo.iv = new byte[16];
- }
- readData(offset, buffer.cryptoInfo.iv, ivSize);
- offset += ivSize;
-
- // Read the subsample count, if present.
- int subsampleCount;
- if (subsampleEncryption) {
- scratch.reset(2);
- readData(offset, scratch.data, 2);
- offset += 2;
- subsampleCount = scratch.readUnsignedShort();
- } else {
- subsampleCount = 1;
- }
-
- // Write the clear and encrypted subsample sizes.
- int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
- if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
- clearDataSizes = new int[subsampleCount];
- }
- int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
- if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
- encryptedDataSizes = new int[subsampleCount];
- }
- if (subsampleEncryption) {
- int subsampleDataLength = 6 * subsampleCount;
- scratch.reset(subsampleDataLength);
- readData(offset, scratch.data, subsampleDataLength);
- offset += subsampleDataLength;
- scratch.setPosition(0);
- for (int i = 0; i < subsampleCount; i++) {
- clearDataSizes[i] = scratch.readUnsignedShort();
- encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
- }
- } else {
- clearDataSizes[0] = 0;
- encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
- }
-
- // Populate the cryptoInfo.
- buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
- extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
-
- // Adjust the offset and size to take into account the bytes read.
- int bytesRead = (int) (offset - extrasHolder.offset);
- extrasHolder.offset += bytesRead;
- extrasHolder.size -= bytesRead;
- }
-
- /**
- * Reads data from the front of the rolling buffer.
- *
- * @param absolutePosition The absolute position from which data should be read.
- * @param target The buffer into which data should be written.
- * @param length The number of bytes to read.
- */
- private void readData(long absolutePosition, ByteBuffer target, int length) {
- int remaining = length;
- while (remaining > 0) {
- dropDownstreamTo(absolutePosition);
- int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
- int toCopy = Math.min(remaining, allocationLength - positionInAllocation);
- Allocation allocation = dataQueue.peek();
- target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy);
- absolutePosition += toCopy;
- remaining -= toCopy;
- }
- }
-
- /**
- * Reads data from the front of the rolling buffer.
- *
- * @param absolutePosition The absolute position from which data should be read.
- * @param target The array into which data should be written.
- * @param length The number of bytes to read.
- */
- private void readData(long absolutePosition, byte[] target, int length) {
- int bytesRead = 0;
- while (bytesRead < length) {
- dropDownstreamTo(absolutePosition);
- int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
- int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation);
- Allocation allocation = dataQueue.peek();
- System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target,
- bytesRead, toCopy);
- absolutePosition += toCopy;
- bytesRead += toCopy;
- }
- }
-
- /**
- * Discard any allocations that hold data prior to the specified absolute position, returning
- * them to the allocator.
- *
- * @param absolutePosition The absolute position up to which allocations can be discarded.
- */
- private void dropDownstreamTo(long absolutePosition) {
- int relativePosition = (int) (absolutePosition - totalBytesDropped);
- int allocationIndex = relativePosition / allocationLength;
- for (int i = 0; i < allocationIndex; i++) {
- allocator.release(dataQueue.remove());
- totalBytesDropped += allocationLength;
- }
- }
-
- // Called by the loading thread.
-
- /**
- * Sets a listener to be notified of changes to the upstream format.
- *
- * @param listener The listener.
- */
- public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
- upstreamFormatChangeListener = listener;
- }
-
- /**
- * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
- * subsequently queued to the buffer.
- *
- * @param sampleOffsetUs The timestamp offset in microseconds.
- */
- public void setSampleOffsetUs(long sampleOffsetUs) {
- if (this.sampleOffsetUs != sampleOffsetUs) {
- this.sampleOffsetUs = sampleOffsetUs;
- pendingFormatAdjustment = true;
- }
- }
-
- @Override
- public void format(Format format) {
- Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
- boolean formatChanged = infoQueue.format(adjustedFormat);
- lastUnadjustedFormat = format;
- pendingFormatAdjustment = false;
- if (upstreamFormatChangeListener != null && formatChanged) {
- upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
- }
- }
-
- @Override
- public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
- throws IOException, InterruptedException {
- if (!startWriteOperation()) {
- int bytesSkipped = input.skip(length);
- if (bytesSkipped == C.RESULT_END_OF_INPUT) {
- if (allowEndOfInput) {
- return C.RESULT_END_OF_INPUT;
- }
- throw new EOFException();
- }
- return bytesSkipped;
- }
- try {
- length = prepareForAppend(length);
- int bytesAppended = input.read(lastAllocation.data,
- lastAllocation.translateOffset(lastAllocationOffset), length);
- if (bytesAppended == C.RESULT_END_OF_INPUT) {
- if (allowEndOfInput) {
- return C.RESULT_END_OF_INPUT;
- }
- throw new EOFException();
- }
- lastAllocationOffset += bytesAppended;
- totalBytesWritten += bytesAppended;
- return bytesAppended;
- } finally {
- endWriteOperation();
- }
- }
-
- @Override
- public void sampleData(ParsableByteArray buffer, int length) {
- if (!startWriteOperation()) {
- buffer.skipBytes(length);
- return;
- }
- while (length > 0) {
- int thisAppendLength = prepareForAppend(length);
- buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset),
- thisAppendLength);
- lastAllocationOffset += thisAppendLength;
- totalBytesWritten += thisAppendLength;
- length -= thisAppendLength;
- }
- endWriteOperation();
- }
-
- @Override
- public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey) {
- if (pendingFormatAdjustment) {
- format(lastUnadjustedFormat);
- }
- if (!startWriteOperation()) {
- infoQueue.commitSampleTimestamp(timeUs);
- return;
- }
- try {
- if (pendingSplice) {
- if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) {
- return;
- }
- pendingSplice = false;
- }
- timeUs += sampleOffsetUs;
- long absoluteOffset = totalBytesWritten - size - offset;
- infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey);
- } finally {
- endWriteOperation();
- }
- }
-
- // Private methods.
-
- private boolean startWriteOperation() {
- return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING);
- }
-
- private void endWriteOperation() {
- if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) {
- clearSampleData();
- }
- }
-
- private void clearSampleData() {
- infoQueue.clearSampleData();
- allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()]));
- dataQueue.clear();
- allocator.trim();
- totalBytesDropped = 0;
- totalBytesWritten = 0;
- lastAllocation = null;
- lastAllocationOffset = allocationLength;
- }
-
- /**
- * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the
- * number of bytes that can actually be appended.
- */
- private int prepareForAppend(int length) {
- if (lastAllocationOffset == allocationLength) {
- lastAllocationOffset = 0;
- lastAllocation = allocator.allocate();
- dataQueue.add(lastAllocation);
- }
- return Math.min(length, allocationLength - lastAllocationOffset);
- }
-
- /**
- * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
- *
- * @param format The {@link Format} to adjust.
- * @param sampleOffsetUs The offset to apply.
- * @return The adjusted {@link Format}.
- */
- private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
- if (format == null) {
- return null;
- }
- if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
- format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
- }
- return format;
- }
-
- /**
- * Holds information about the samples in the rolling buffer.
- */
- private static final class InfoQueue {
-
- private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
-
- private int capacity;
-
- private int[] sourceIds;
- private long[] offsets;
- private int[] sizes;
- private int[] flags;
- private long[] timesUs;
- private byte[][] encryptionKeys;
- private Format[] formats;
-
- private int queueSize;
- private int absoluteReadIndex;
- private int relativeReadIndex;
- private int relativeWriteIndex;
-
- private long largestDequeuedTimestampUs;
- private long largestQueuedTimestampUs;
- private boolean upstreamKeyframeRequired;
- private boolean upstreamFormatRequired;
- private Format upstreamFormat;
- private int upstreamSourceId;
-
- public InfoQueue() {
- capacity = SAMPLE_CAPACITY_INCREMENT;
- sourceIds = new int[capacity];
- offsets = new long[capacity];
- timesUs = new long[capacity];
- flags = new int[capacity];
- sizes = new int[capacity];
- encryptionKeys = new byte[capacity][];
- formats = new Format[capacity];
- largestDequeuedTimestampUs = Long.MIN_VALUE;
- largestQueuedTimestampUs = Long.MIN_VALUE;
- upstreamFormatRequired = true;
- upstreamKeyframeRequired = true;
- }
-
- public void clearSampleData() {
- absoluteReadIndex = 0;
- relativeReadIndex = 0;
- relativeWriteIndex = 0;
- queueSize = 0;
- upstreamKeyframeRequired = true;
- }
-
- // Called by the consuming thread, but only when there is no loading thread.
-
- public void resetLargestParsedTimestamps() {
- largestDequeuedTimestampUs = Long.MIN_VALUE;
- largestQueuedTimestampUs = Long.MIN_VALUE;
- }
-
- /**
- * Returns the current absolute write index.
- */
- public int getWriteIndex() {
- return absoluteReadIndex + queueSize;
- }
-
- /**
- * Discards samples from the write side of the buffer.
- *
- * @param discardFromIndex The absolute index of the first sample to be discarded.
- * @return The reduced total number of bytes written, after the samples have been discarded.
- */
- public long discardUpstreamSamples(int discardFromIndex) {
- int discardCount = getWriteIndex() - discardFromIndex;
- Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize);
-
- if (discardCount == 0) {
- if (absoluteReadIndex == 0) {
- // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue.
- return 0;
- }
- int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1;
- return offsets[lastWriteIndex] + sizes[lastWriteIndex];
- }
-
- queueSize -= discardCount;
- relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity;
- // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are
- // always less than the timestamp of the keyframe itself, and of subsequent frames.
- largestQueuedTimestampUs = Long.MIN_VALUE;
- for (int i = queueSize - 1; i >= 0; i--) {
- int sampleIndex = (relativeReadIndex + i) % capacity;
- largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]);
- if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
- break;
- }
- }
- return offsets[relativeWriteIndex];
- }
-
- public void sourceId(int sourceId) {
- upstreamSourceId = sourceId;
- }
-
- // Called by the consuming thread.
-
- /**
- * Returns the current absolute read index.
- */
- public int getReadIndex() {
- return absoluteReadIndex;
- }
-
- /**
- * Peeks the source id of the next sample, or the current upstream source id if the queue is
- * empty.
- */
- public int peekSourceId() {
- return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex];
- }
-
- /**
- * Returns whether the queue is empty.
- */
- public synchronized boolean isEmpty() {
- return queueSize == 0;
- }
-
- /**
- * Returns the upstream {@link Format} in which samples are being queued.
- */
- public synchronized Format getUpstreamFormat() {
- return upstreamFormatRequired ? null : upstreamFormat;
- }
-
- /**
- * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
- *
- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
- * considered as having been queued. Samples that were dequeued from the front of the queue are
- * considered as having been queued.
- *
- * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
- * samples have been queued.
- */
- public synchronized long getLargestQueuedTimestampUs() {
- return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs);
- }
-
- /**
- * Attempts to read from the queue.
- *
- * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
- * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
- * end of the stream. If a sample is read then the buffer is populated with information
- * about the sample, but not its data. The size and absolute position of the data in the
- * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
- * and the absolute position of the first byte that may still be required after the current
- * sample has been read. May be null if the caller requires that the format of the stream be
- * read even if it's not changing.
- * @param formatRequired Whether the caller requires that the format of the stream be read even
- * if it's not changing. A sample will never be read if set to true, however it is still
- * possible for the end of stream or nothing to be read.
- * @param loadingFinished True if an empty queue should be considered the end of the stream.
- * @param downstreamFormat The current downstream {@link Format}. If the format of the next
- * sample is different to the current downstream format then a format will be read.
- * @param extrasHolder The holder into which extra sample information should be written.
- * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
- * or {@link C#RESULT_BUFFER_READ}.
- */
- @SuppressWarnings("ReferenceEquality")
- public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
- boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
- BufferExtrasHolder extrasHolder) {
- if (queueSize == 0) {
- if (loadingFinished) {
- buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
- return C.RESULT_BUFFER_READ;
- } else if (upstreamFormat != null
- && (formatRequired || upstreamFormat != downstreamFormat)) {
- formatHolder.format = upstreamFormat;
- return C.RESULT_FORMAT_READ;
- } else {
- return C.RESULT_NOTHING_READ;
- }
- }
-
- if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
- formatHolder.format = formats[relativeReadIndex];
- return C.RESULT_FORMAT_READ;
- }
-
- if (buffer.isFlagsOnly()) {
- return C.RESULT_NOTHING_READ;
- }
-
- buffer.timeUs = timesUs[relativeReadIndex];
- buffer.setFlags(flags[relativeReadIndex]);
- extrasHolder.size = sizes[relativeReadIndex];
- extrasHolder.offset = offsets[relativeReadIndex];
- extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
-
- largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs);
- queueSize--;
- relativeReadIndex++;
- absoluteReadIndex++;
- if (relativeReadIndex == capacity) {
- // Wrap around.
- relativeReadIndex = 0;
- }
-
- extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex]
- : extrasHolder.offset + extrasHolder.size;
- return C.RESULT_BUFFER_READ;
- }
-
- /**
- * Skips all samples in the buffer.
- *
- * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no
- * dropping of data is required.
- */
- public synchronized long skipAll() {
- if (queueSize == 0) {
- return C.POSITION_UNSET;
- }
-
- int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity;
- relativeReadIndex = (relativeReadIndex + queueSize) % capacity;
- absoluteReadIndex += queueSize;
- queueSize = 0;
- return offsets[lastSampleIndex] + sizes[lastSampleIndex];
- }
-
- /**
- * Attempts to locate the keyframe before or at the specified time. If
- * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
- * falls within the buffer.
- *
- * @param timeUs The seek time.
- * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
- * of the buffer.
- * @return The offset of the keyframe's data if the keyframe was present.
- * {@link C#POSITION_UNSET} otherwise.
- */
- public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
- if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
- return C.POSITION_UNSET;
- }
-
- if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
- return C.POSITION_UNSET;
- }
-
- // This could be optimized to use a binary search, however in practice callers to this method
- // often pass times near to the start of the buffer. Hence it's unclear whether switching to
- // a binary search would yield any real benefit.
- int sampleCount = 0;
- int sampleCountToKeyframe = -1;
- int searchIndex = relativeReadIndex;
- while (searchIndex != relativeWriteIndex) {
- if (timesUs[searchIndex] > timeUs) {
- // We've gone too far.
- break;
- } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
- // We've found a keyframe, and we're still before the seek position.
- sampleCountToKeyframe = sampleCount;
- }
- searchIndex = (searchIndex + 1) % capacity;
- sampleCount++;
- }
-
- if (sampleCountToKeyframe == -1) {
- return C.POSITION_UNSET;
- }
-
- relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity;
- absoluteReadIndex += sampleCountToKeyframe;
- queueSize -= sampleCountToKeyframe;
- return offsets[relativeReadIndex];
- }
-
- // Called by the loading thread.
-
- public synchronized boolean format(Format format) {
- if (format == null) {
- upstreamFormatRequired = true;
- return false;
- }
- upstreamFormatRequired = false;
- if (Util.areEqual(format, upstreamFormat)) {
- // Suppress changes between equal formats so we can use referential equality in readData.
- return false;
- } else {
- upstreamFormat = format;
- return true;
- }
- }
-
- public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
- int size, byte[] encryptionKey) {
- if (upstreamKeyframeRequired) {
- if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
- return;
- }
- upstreamKeyframeRequired = false;
- }
- Assertions.checkState(!upstreamFormatRequired);
- commitSampleTimestamp(timeUs);
- timesUs[relativeWriteIndex] = timeUs;
- offsets[relativeWriteIndex] = offset;
- sizes[relativeWriteIndex] = size;
- flags[relativeWriteIndex] = sampleFlags;
- encryptionKeys[relativeWriteIndex] = encryptionKey;
- formats[relativeWriteIndex] = upstreamFormat;
- sourceIds[relativeWriteIndex] = upstreamSourceId;
- // Increment the write index.
- queueSize++;
- if (queueSize == capacity) {
- // Increase the capacity.
- int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
- int[] newSourceIds = new int[newCapacity];
- long[] newOffsets = new long[newCapacity];
- long[] newTimesUs = new long[newCapacity];
- int[] newFlags = new int[newCapacity];
- int[] newSizes = new int[newCapacity];
- byte[][] newEncryptionKeys = new byte[newCapacity][];
- Format[] newFormats = new Format[newCapacity];
- int beforeWrap = capacity - relativeReadIndex;
- System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap);
- System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap);
- System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap);
- System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap);
- System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap);
- System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap);
- System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap);
- int afterWrap = relativeReadIndex;
- System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
- System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
- System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
- System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
- System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap);
- System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
- System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
- offsets = newOffsets;
- timesUs = newTimesUs;
- flags = newFlags;
- sizes = newSizes;
- encryptionKeys = newEncryptionKeys;
- formats = newFormats;
- sourceIds = newSourceIds;
- relativeReadIndex = 0;
- relativeWriteIndex = capacity;
- queueSize = capacity;
- capacity = newCapacity;
- } else {
- relativeWriteIndex++;
- if (relativeWriteIndex == capacity) {
- // Wrap around.
- relativeWriteIndex = 0;
- }
- }
- }
-
- public synchronized void commitSampleTimestamp(long timeUs) {
- largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
- }
-
- /**
- * Attempts to discard samples from the tail of the queue to allow samples starting from the
- * specified timestamp to be spliced in.
- *
- * @param timeUs The timestamp at which the splice occurs.
- * @return Whether the splice was successful.
- */
- public synchronized boolean attemptSplice(long timeUs) {
- if (largestDequeuedTimestampUs >= timeUs) {
- return false;
- }
- int retainCount = queueSize;
- while (retainCount > 0
- && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) {
- retainCount--;
- }
- discardUpstreamSamples(absoluteReadIndex + retainCount);
- return true;
- }
-
- }
-
- /**
- * Holds additional buffer information not held by {@link DecoderInputBuffer}.
- */
- private static final class BufferExtrasHolder {
-
- public int size;
- public long offset;
- public long nextOffset;
- public byte[] encryptionKeyId;
-
- }
-
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
index 61f97887be..c023b0de95 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -51,7 +51,7 @@ public final class DummyTrackOutput implements TrackOutput {
@Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey) {
+ CryptoData cryptoData) {
// Do nothing.
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
index c4dee4b6a7..a12a0315a4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -20,12 +20,78 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
+import java.util.Arrays;
/**
* Receives track level data extracted by an {@link Extractor}.
*/
public interface TrackOutput {
+ /**
+ * Holds data required to decrypt a sample.
+ */
+ final class CryptoData {
+
+ /**
+ * The encryption mode used for the sample.
+ */
+ @C.CryptoMode public final int cryptoMode;
+
+ /**
+ * The encryption key associated with the sample. Its contents must not be modified.
+ */
+ public final byte[] encryptionKey;
+
+ /**
+ * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not
+ * apply.
+ */
+ public final int encryptedBlocks;
+
+ /**
+ * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not
+ * apply.
+ */
+ public final int clearBlocks;
+
+ /**
+ * @param cryptoMode See {@link #cryptoMode}.
+ * @param encryptionKey See {@link #encryptionKey}.
+ * @param encryptedBlocks See {@link #encryptedBlocks}.
+ * @param clearBlocks See {@link #clearBlocks}.
+ */
+ public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks,
+ int clearBlocks) {
+ this.cryptoMode = cryptoMode;
+ this.encryptionKey = encryptionKey;
+ this.encryptedBlocks = encryptedBlocks;
+ this.clearBlocks = clearBlocks;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ CryptoData other = (CryptoData) obj;
+ return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks
+ && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = cryptoMode;
+ result = 31 * result + Arrays.hashCode(encryptionKey);
+ result = 31 * result + encryptedBlocks;
+ result = 31 * result + clearBlocks;
+ return result;
+ }
+
+ }
+
/**
* Called when the {@link Format} of the track has been extracted from the stream.
*
@@ -70,9 +136,9 @@ public interface TrackOutput {
* {@link #sampleData(ExtractorInput, int, boolean)} or
* {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
* whose metadata is being passed.
- * @param encryptionKey The encryption key associated with the sample. May be null.
+ * @param encryptionData The encryption data required to decrypt the sample. May be null.
*/
void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey);
+ CryptoData encryptionData);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
index 8e3bd08375..2f21898007 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
@@ -67,7 +67,7 @@ import java.util.Collections;
hasOutputFormat = true;
} else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
- : MimeTypes.AUDIO_ULAW;
+ : MimeTypes.AUDIO_MLAW;
int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;
Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,
Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 8f3abf4688..9f438d0977 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mkv;
import android.support.annotation.IntDef;
+import android.util.Log;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -84,6 +85,8 @@ public final class MatroskaExtractor implements Extractor {
*/
public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1;
+ private static final String TAG = "MatroskaExtractor";
+
private static final int UNSET_ENTRY_ID = -1;
private static final int BLOCK_STATE_START = 0;
@@ -580,11 +583,11 @@ public final class MatroskaExtractor implements Extractor {
break;
case ID_CONTENT_ENCODING:
if (currentTrack.hasContentEncryption) {
- if (currentTrack.encryptionKeyId == null) {
+ if (currentTrack.cryptoData == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
- currentTrack.drmInitData = new DrmInitData(
- new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId));
+ currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, null,
+ MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey));
}
break;
case ID_CONTENT_ENCODINGS:
@@ -888,8 +891,10 @@ public final class MatroskaExtractor implements Extractor {
input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize);
break;
case ID_CONTENT_ENCRYPTION_KEY_ID:
- currentTrack.encryptionKeyId = new byte[contentSize];
- input.readFully(currentTrack.encryptionKeyId, 0, contentSize);
+ byte[] encryptionKey = new byte[contentSize];
+ input.readFully(encryptionKey, 0, contentSize);
+ currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey,
+ 0, 0); // We assume patternless AES-CTR.
break;
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
@@ -1033,7 +1038,7 @@ public final class MatroskaExtractor implements Extractor {
if (CODEC_ID_SUBRIP.equals(track.codecId)) {
writeSubripSample(track);
}
- track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId);
+ track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData);
sampleRead = true;
resetSample();
}
@@ -1470,7 +1475,7 @@ public final class MatroskaExtractor implements Extractor {
public int defaultSampleDurationNs;
public boolean hasContentEncryption;
public byte[] sampleStrippedBytes;
- public byte[] encryptionKeyId;
+ public TrackOutput.CryptoData cryptoData;
public byte[] codecPrivate;
public DrmInitData drmInitData;
@@ -1558,7 +1563,12 @@ public final class MatroskaExtractor implements Extractor {
break;
case CODEC_ID_FOURCC:
initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate));
- mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1;
+ if (initializationData != null) {
+ mimeType = MimeTypes.VIDEO_VC1;
+ } else {
+ Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN);
+ mimeType = MimeTypes.VIDEO_UNKNOWN;
+ }
break;
case CODEC_ID_THEORA:
// TODO: This can be set to the real mimeType if/when we work out what initializationData
@@ -1614,19 +1624,27 @@ public final class MatroskaExtractor implements Extractor {
break;
case CODEC_ID_ACM:
mimeType = MimeTypes.AUDIO_RAW;
- if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {
- throw new ParserException("Non-PCM MS/ACM is unsupported");
- }
- pcmEncoding = Util.getPcmEncoding(audioBitDepth);
- if (pcmEncoding == C.ENCODING_INVALID) {
- throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
+ if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {
+ pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+ if (pcmEncoding == C.ENCODING_INVALID) {
+ pcmEncoding = Format.NO_VALUE;
+ mimeType = MimeTypes.AUDIO_UNKNOWN;
+ Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ + mimeType);
+ }
+ } else {
+ mimeType = MimeTypes.AUDIO_UNKNOWN;
+ Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType);
}
break;
case CODEC_ID_PCM_INT_LIT:
mimeType = MimeTypes.AUDIO_RAW;
pcmEncoding = Util.getPcmEncoding(audioBitDepth);
if (pcmEncoding == C.ENCODING_INVALID) {
- throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
+ pcmEncoding = Format.NO_VALUE;
+ mimeType = MimeTypes.AUDIO_UNKNOWN;
+ Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ + mimeType);
}
break;
case CODEC_ID_SUBRIP:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
index c5de8d8284..df7748a910 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Util;
/**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
@@ -41,8 +42,11 @@ import com.google.android.exoplayer2.C;
@Override
public long getPosition(long timeUs) {
- return durationUs == C.TIME_UNSET ? 0
- : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
+ if (durationUs == C.TIME_UNSET) {
+ return 0;
+ }
+ timeUs = Util.constrainValue(timeUs, 0, durationUs);
+ return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
index b0faad71c0..8d33f95640 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -78,7 +78,7 @@ public final class Mp3Extractor implements Extractor {
/**
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/
- private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ private static final int MAX_SNIFF_BYTES = 16 * 1024;
/**
* Maximum length of data read into {@link #scratch}.
*/
@@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor {
/**
* Mask that includes the audio header values that must match between frames.
*/
- private static final int HEADER_MASK = 0xFFFE0C00;
- private static final int XING_HEADER = Util.getIntegerCodeForString("Xing");
- private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
- private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
+ private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00;
+
+ private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing");
+ private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info");
+ private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI");
+ private static final int SEEK_HEADER_UNSET = 0;
@Flags private final int flags;
private final long forcedFirstSampleTimestampUs;
@@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor {
}
}
if (seeker == null) {
- seeker = setupSeeker(input);
+ seeker = maybeReadSeekFrame(input);
+ if (seeker == null
+ || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
+ seeker = getConstantBitrateSeeker(input);
+ }
extractorOutput.seekMap(seeker);
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
@@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor {
}
scratch.setPosition(0);
int sampleHeaderData = scratch.readInt();
- if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)
+ if (!headersMatch(sampleHeaderData, synchronizedHeaderData)
|| MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {
// We have lost synchronization, so attempt to resynchronize starting at the next byte.
extractorInput.skipFully(1);
@@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor {
int headerData = scratch.readInt();
int frameSize;
if ((candidateSynchronizedHeaderData != 0
- && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
+ && !headersMatch(headerData, candidateSynchronizedHeaderData))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {
// The header doesn't match the candidate header or is invalid. Try the next byte offset.
if (searchedBytes++ == searchLimitBytes) {
@@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor {
}
/**
- * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
- * data from the start of the first frame in the stream. On returning, the input's position will
- * be set to the start of the first frame of audio.
+ * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,
+ * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.
+ * After this method returns, the input position is the start of the first frame of audio.
*
* @param input The {@link ExtractorInput} from which to read.
+ * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise.
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
* next two frames were already peeked during synchronization.
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
* the next two frames were already peeked during synchronization.
- * @return a {@link Seeker}.
*/
- private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
- // Read the first frame which may contain a Xing or VBRI header with seeking metadata.
+ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException {
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
-
- long position = input.getPosition();
- long length = input.getLength();
- int headerData = 0;
- Seeker seeker = null;
-
- // Check if there is a Xing header.
int xingBase = (synchronizedHeader.version & 1) != 0
? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
: (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
- if (frame.limit() >= xingBase + 4) {
- frame.setPosition(xingBase);
- headerData = frame.readInt();
- }
- if (headerData == XING_HEADER || headerData == INFO_HEADER) {
- seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
+ int seekHeader = getSeekFrameHeader(frame, xingBase);
+ Seeker seeker;
+ if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {
+ seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition();
@@ -377,28 +373,60 @@ public final class Mp3Extractor implements Extractor {
gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
}
input.skipFully(synchronizedHeader.frameSize);
- } else if (frame.limit() >= 40) {
- // Check if there is a VBRI header.
- frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
- headerData = frame.readInt();
- if (headerData == VBRI_HEADER) {
- seeker = VbriSeeker.create(synchronizedHeader, frame, position, length);
- input.skipFully(synchronizedHeader.frameSize);
+ if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) {
+ // Fall back to constant bitrate seeking for Info headers missing a table of contents.
+ return getConstantBitrateSeeker(input);
+ }
+ } else if (seekHeader == SEEK_HEADER_VBRI) {
+ seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
+ input.skipFully(synchronizedHeader.frameSize);
+ } else { // seekerHeader == SEEK_HEADER_UNSET
+ // This frame doesn't contain seeking information, so reset the peek position.
+ seeker = null;
+ input.resetPeekPosition();
+ }
+ return seeker;
+ }
+
+ /**
+ * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate.
+ */
+ private Seeker getConstantBitrateSeeker(ExtractorInput input)
+ throws IOException, InterruptedException {
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
+ return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate,
+ input.getLength());
+ }
+
+ /**
+ * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}.
+ */
+ private static boolean headersMatch(int headerA, long headerB) {
+ return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK);
+ }
+
+ /**
+ * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if
+ * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise.
+ * If seeking metadata is present, {@code frame}'s position is advanced past the header.
+ */
+ private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) {
+ if (frame.limit() >= xingBase + 4) {
+ frame.setPosition(xingBase);
+ int headerData = frame.readInt();
+ if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) {
+ return headerData;
}
}
-
- if (seeker == null || (!seeker.isSeekable()
- && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
- // Repopulate the synchronized header in case we had to skip an invalid seeking header, which
- // would give an invalid CBR bitrate.
- input.resetPeekPosition();
- input.peekFully(scratch.data, 0, 4);
- scratch.setPosition(0);
- MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
- seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length);
+ if (frame.limit() >= 40) {
+ frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
+ if (frame.readInt() == SEEK_HEADER_VBRI) {
+ return SEEK_HEADER_VBRI;
+ }
}
-
- return seeker;
+ return SEEK_HEADER_UNSET;
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index 474ba65d86..ba190351c3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -615,10 +615,10 @@ import java.util.List;
|| childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp
|| childAtomType == Atom.TYPE_c608) {
parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
- language, drmInitData, out);
+ language, out);
} else if (childAtomType == Atom.TYPE_camm) {
out.format = Format.createSampleFormat(Integer.toString(trackId),
- MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData);
+ MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null);
}
stsd.setPosition(childStartPosition + childAtomSize);
}
@@ -626,8 +626,7 @@ import java.util.List;
}
private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,
- int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out)
- throws ParserException {
+ int atomSize, int trackId, String language, StsdData out) throws ParserException {
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
// Default values.
@@ -658,8 +657,7 @@ import java.util.List;
}
out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
- Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs,
- initializationData);
+ Format.NO_VALUE, 0, language, Format.NO_VALUE, null, subsampleOffsetUs, initializationData);
}
private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,
@@ -676,9 +674,19 @@ import java.util.List;
int childPosition = parent.getPosition();
if (atomType == Atom.TYPE_encv) {
- atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+ Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData(
+ parent, position, size);
+ if (sampleEntryEncryptionData != null) {
+ atomType = sampleEntryEncryptionData.first;
+ drmInitData = drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
+ out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
+ }
parent.setPosition(childPosition);
}
+ // TODO: Uncomment when [Internal: b/63092960] is fixed.
+ // else {
+ // drmInitData = null;
+ // }
List initializationData = null;
String mimeType = null;
@@ -845,9 +853,19 @@ import java.util.List;
int childPosition = parent.getPosition();
if (atomType == Atom.TYPE_enca) {
- atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+ Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData(
+ parent, position, size);
+ if (sampleEntryEncryptionData != null) {
+ atomType = sampleEntryEncryptionData.first;
+ drmInitData = drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
+ out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
+ }
parent.setPosition(childPosition);
}
+ // TODO: Uncomment when [Internal: b/63092960] is fixed.
+ // else {
+ // drmInitData = null;
+ // }
// If the atom type determines a MIME type, set it immediately.
String mimeType = null;
@@ -1023,11 +1041,12 @@ import java.util.List;
}
/**
- * Parses encryption data from an audio/video sample entry, populating {@code out} and returning
- * the unencrypted atom type, or 0 if no common encryption sinf atom was present.
+ * Parses encryption data from an audio/video sample entry, returning a pair consisting of the
+ * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common
+ * encryption sinf atom was present.
*/
- private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position,
- int size, StsdData out, int entryIndex) {
+ private static Pair parseSampleEntryEncryptionData(
+ ParsableByteArray parent, int position, int size) {
int childPosition = parent.getPosition();
while (childPosition - position < size) {
parent.setPosition(childPosition);
@@ -1038,22 +1057,20 @@ import java.util.List;
Pair result = parseSinfFromParent(parent, childPosition,
childAtomSize);
if (result != null) {
- out.trackEncryptionBoxes[entryIndex] = result.second;
- return result.first;
+ return result;
}
}
childPosition += childAtomSize;
}
- // This enca/encv box does not have a data format so return an invalid atom type.
- return 0;
+ return null;
}
private static Pair parseSinfFromParent(ParsableByteArray parent,
int position, int size) {
int childPosition = position + Atom.HEADER_SIZE;
-
- boolean isCencScheme = false;
- TrackEncryptionBox trackEncryptionBox = null;
+ int schemeInformationBoxPosition = C.POSITION_UNSET;
+ int schemeInformationBoxSize = 0;
+ String schemeType = null;
Integer dataFormat = null;
while (childPosition - position < size) {
parent.setPosition(childPosition);
@@ -1063,36 +1080,60 @@ import java.util.List;
dataFormat = parent.readInt();
} else if (childAtomType == Atom.TYPE_schm) {
parent.skipBytes(4);
- isCencScheme = parent.readInt() == TYPE_cenc;
+ // scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1.
+ schemeType = parent.readString(4);
} else if (childAtomType == Atom.TYPE_schi) {
- trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
+ schemeInformationBoxPosition = childPosition;
+ schemeInformationBoxSize = childAtomSize;
}
childPosition += childAtomSize;
}
- if (isCencScheme) {
+ if (schemeType != null) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
- Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory");
- return Pair.create(dataFormat, trackEncryptionBox);
+ Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET,
+ "schi atom is mandatory");
+ TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition,
+ schemeInformationBoxSize, schemeType);
+ Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory");
+ return Pair.create(dataFormat, encryptionBox);
} else {
return null;
}
}
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
- int size) {
+ int size, String schemeType) {
int childPosition = position + Atom.HEADER_SIZE;
while (childPosition - position < size) {
parent.setPosition(childPosition);
int childAtomSize = parent.readInt();
int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_tenc) {
- parent.skipBytes(6);
- boolean defaultIsEncrypted = parent.readUnsignedByte() == 1;
- int defaultInitVectorSize = parent.readUnsignedByte();
+ int fullAtom = parent.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ parent.skipBytes(1); // reserved = 0.
+ int defaultCryptByteBlock = 0;
+ int defaultSkipByteBlock = 0;
+ if (version == 0) {
+ parent.skipBytes(1); // reserved = 0.
+ } else /* version 1 or greater */ {
+ int patternByte = parent.readUnsignedByte();
+ defaultCryptByteBlock = (patternByte & 0xF0) >> 4;
+ defaultSkipByteBlock = patternByte & 0x0F;
+ }
+ boolean defaultIsProtected = parent.readUnsignedByte() == 1;
+ int defaultPerSampleIvSize = parent.readUnsignedByte();
byte[] defaultKeyId = new byte[16];
parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
- return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId);
+ byte[] constantIv = null;
+ if (defaultIsProtected && defaultPerSampleIvSize == 0) {
+ int constantIvSize = parent.readUnsignedByte();
+ constantIv = new byte[constantIvSize];
+ parent.readBytes(constantIv, 0, constantIvSize);
+ }
+ return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize,
+ defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv);
}
childPosition += childAtomSize;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index a228a9b775..a756edf0a4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -128,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray nalPrefix;
private final ParsableByteArray nalBuffer;
private final ParsableByteArray encryptionSignalByte;
+ private final ParsableByteArray defaultInitializationVector;
// Adjusts sample timestamps.
private final TimestampAdjuster timestampAdjuster;
@@ -197,6 +198,7 @@ public final class FragmentedMp4Extractor implements Extractor {
nalPrefix = new ParsableByteArray(5);
nalBuffer = new ParsableByteArray();
encryptionSignalByte = new ParsableByteArray(1);
+ defaultInitializationVector = new ParsableByteArray();
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>();
pendingMetadataSampleInfos = new LinkedList<>();
@@ -559,11 +561,12 @@ public final class FragmentedMp4Extractor implements Extractor {
parseTruns(traf, trackBundle, decodeTime, flags);
+ TrackEncryptionBox encryptionBox = trackBundle.track
+ .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
- TrackEncryptionBox trackEncryptionBox = trackBundle.track
- .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex];
- parseSaiz(trackEncryptionBox, saiz.data, fragment);
+ parseSaiz(encryptionBox, saiz.data, fragment);
}
LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
@@ -579,7 +582,8 @@ public final class FragmentedMp4Extractor implements Extractor {
LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp);
LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd);
if (sbgp != null && sgpd != null) {
- parseSgpd(sbgp.data, sgpd.data, fragment);
+ parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null,
+ fragment);
}
int leafChildrenSize = traf.leafChildren.size();
@@ -868,8 +872,8 @@ public final class FragmentedMp4Extractor implements Extractor {
out.fillEncryptionData(senc);
}
- private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, TrackFragment out)
- throws ParserException {
+ private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType,
+ TrackFragment out) throws ParserException {
sbgp.setPosition(Atom.HEADER_SIZE);
int sbgpFullAtom = sbgp.readInt();
if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) {
@@ -877,9 +881,9 @@ public final class FragmentedMp4Extractor implements Extractor {
return;
}
if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) {
- sbgp.skipBytes(4);
+ sbgp.skipBytes(4); // default_length.
}
- if (sbgp.readInt() != 1) {
+ if (sbgp.readInt() != 1) { // entry_count.
throw new ParserException("Entry count in sbgp != 1 (unsupported).");
}
@@ -892,25 +896,35 @@ public final class FragmentedMp4Extractor implements Extractor {
int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom);
if (sgpdVersion == 1) {
if (sgpd.readUnsignedInt() == 0) {
- throw new ParserException("Variable length decription in sgpd found (unsupported)");
+ throw new ParserException("Variable length description in sgpd found (unsupported)");
}
} else if (sgpdVersion >= 2) {
- sgpd.skipBytes(4);
+ sgpd.skipBytes(4); // default_sample_description_index.
}
- if (sgpd.readUnsignedInt() != 1) {
+ if (sgpd.readUnsignedInt() != 1) { // entry_count.
throw new ParserException("Entry count in sgpd != 1 (unsupported).");
}
// CencSampleEncryptionInformationGroupEntry
- sgpd.skipBytes(2);
+ sgpd.skipBytes(1); // reserved = 0.
+ int patternByte = sgpd.readUnsignedByte();
+ int cryptByteBlock = (patternByte & 0xF0) >> 4;
+ int skipByteBlock = patternByte & 0x0F;
boolean isProtected = sgpd.readUnsignedByte() == 1;
if (!isProtected) {
return;
}
- int initVectorSize = sgpd.readUnsignedByte();
+ int perSampleIvSize = sgpd.readUnsignedByte();
byte[] keyId = new byte[16];
sgpd.readBytes(keyId, 0, keyId.length);
+ byte[] constantIv = null;
+ if (isProtected && perSampleIvSize == 0) {
+ int constantIvSize = sgpd.readUnsignedByte();
+ constantIv = new byte[constantIvSize];
+ sgpd.readBytes(constantIv, 0, constantIvSize);
+ }
out.definesEncryptionData = true;
- out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId);
+ out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId,
+ cryptByteBlock, skipByteBlock, constantIv);
}
/**
@@ -1122,19 +1136,24 @@ public final class FragmentedMp4Extractor implements Extractor {
}
long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
- @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
- | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
- int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
- byte[] encryptionKey = null;
- if (fragment.definesEncryptionData) {
- encryptionKey = fragment.trackEncryptionBox != null
- ? fragment.trackEncryptionBox.keyId
- : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId;
- }
if (timestampAdjuster != null) {
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
}
- output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
+
+ @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex]
+ ? C.BUFFER_FLAG_KEY_FRAME : 0;
+
+ // Encryption data.
+ TrackOutput.CryptoData cryptoData = null;
+ if (fragment.definesEncryptionData) {
+ sampleFlags |= C.BUFFER_FLAG_ENCRYPTED;
+ TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null
+ ? fragment.trackEncryptionBox
+ : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+ cryptoData = encryptionBox.cryptoData;
+ }
+
+ output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData);
while (!pendingMetadataSampleInfos.isEmpty()) {
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
@@ -1190,12 +1209,24 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
private int appendSampleEncryptionData(TrackBundle trackBundle) {
TrackFragment trackFragment = trackBundle.fragment;
- ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData;
int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex;
TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null
? trackFragment.trackEncryptionBox
- : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
- int vectorSize = encryptionBox.initializationVectorSize;
+ : trackBundle.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);
+
+ ParsableByteArray initializationVectorData;
+ int vectorSize;
+ if (encryptionBox.initializationVectorSize != 0) {
+ initializationVectorData = trackFragment.sampleEncryptionData;
+ vectorSize = encryptionBox.initializationVectorSize;
+ } else {
+ // The default initialization vector should be used.
+ byte[] initVectorData = encryptionBox.defaultInitializationVector;
+ defaultInitializationVector.reset(initVectorData, initVectorData.length);
+ initializationVectorData = defaultInitializationVector;
+ vectorSize = initVectorData.length;
+ }
+
boolean subsampleEncryption = trackFragment
.sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex];
@@ -1205,20 +1236,20 @@ public final class FragmentedMp4Extractor implements Extractor {
TrackOutput output = trackBundle.output;
output.sampleData(encryptionSignalByte, 1);
// Write the vector.
- output.sampleData(sampleEncryptionData, vectorSize);
+ output.sampleData(initializationVectorData, vectorSize);
// If we don't have subsample encryption data, we're done.
if (!subsampleEncryption) {
return 1 + vectorSize;
}
// Write the subsample encryption data.
- int subsampleCount = sampleEncryptionData.readUnsignedShort();
- sampleEncryptionData.skipBytes(-2);
+ ParsableByteArray subsampleEncryptionData = trackFragment.sampleEncryptionData;
+ int subsampleCount = subsampleEncryptionData.readUnsignedShort();
+ subsampleEncryptionData.skipBytes(-2);
int subsampleDataLength = 2 + 6 * subsampleCount;
- output.sampleData(sampleEncryptionData, subsampleDataLength);
+ output.sampleData(subsampleEncryptionData, subsampleDataLength);
return 1 + vectorSize + subsampleDataLength;
}
-
/** Returns DrmInitData from leaf atoms. */
private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) {
ArrayList schemeDatas = null;
@@ -1234,7 +1265,7 @@ public final class FragmentedMp4Extractor implements Extractor {
if (uuid == null) {
Log.w(TAG, "Skipped pssh atom (failed to extract uuid)");
} else {
- schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData));
+ schemeDatas.add(new SchemeData(uuid, null, MimeTypes.VIDEO_MP4, psshData));
}
}
}
@@ -1308,8 +1339,12 @@ public final class FragmentedMp4Extractor implements Extractor {
}
public void updateDrmInitData(DrmInitData drmInitData) {
- output.format(track.format.copyWithDrmInitData(drmInitData));
+ TrackEncryptionBox encryptionBox =
+ track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+ String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;
+ output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType)));
}
+
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java
index f1c4e99ec1..7ac3158794 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.lang.annotation.Retention;
@@ -77,11 +78,6 @@ public final class Track {
*/
@Transformation public final int sampleTransformation;
- /**
- * Track encryption boxes for the different track sample descriptions. Entries may be null.
- */
- public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
-
/**
* Durations of edit list segments in the movie timescale. Null if there is no edit list.
*/
@@ -98,9 +94,11 @@ public final class Track {
*/
public final int nalUnitLengthFieldLength;
+ @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
+
public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
Format format, @Transformation int sampleTransformation,
- TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
+ @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
long[] editListDurations, long[] editListMediaTimes) {
this.id = id;
this.type = type;
@@ -115,4 +113,16 @@ public final class Track {
this.editListMediaTimes = editListMediaTimes;
}
+ /**
+ * Returns the {@link TrackEncryptionBox} for the given sample description index.
+ *
+ * @param sampleDescriptionIndex The given sample description index
+ * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no
+ * such entry exists.
+ */
+ public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {
+ return sampleDescriptionEncryptionBoxes == null ? null
+ : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
index dde03a8507..b987dad7fb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
@@ -15,37 +15,86 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.Assertions;
+
/**
* Encapsulates information parsed from a track encryption (tenc) box or sample group description
* (sgpd) box in an MP4 stream.
*/
public final class TrackEncryptionBox {
+ private static final String TAG = "TrackEncryptionBox";
+
/**
* Indicates the encryption state of the samples in the sample group.
*/
public final boolean isEncrypted;
+ /**
+ * The protection scheme type, as defined by the 'schm' box, or null if unknown.
+ */
+ @Nullable public final String schemeType;
+
+ /**
+ * A {@link TrackOutput.CryptoData} instance containing the encryption information from this
+ * {@link TrackEncryptionBox}.
+ */
+ public final TrackOutput.CryptoData cryptoData;
+
/**
* The initialization vector size in bytes for the samples in the corresponding sample group.
*/
public final int initializationVectorSize;
/**
- * The key identifier for the samples in the corresponding sample group.
+ * If {@link #initializationVectorSize} is 0, holds the default initialization vector as defined
+ * in the track encryption box or sample group description box. Null otherwise.
*/
- public final byte[] keyId;
+ public final byte[] defaultInitializationVector;
/**
- * @param isEncrypted Indicates the encryption state of the samples in the sample group.
- * @param initializationVectorSize The initialization vector size in bytes for the samples in the
- * corresponding sample group.
- * @param keyId The key identifier for the samples in the corresponding sample group.
+ * @param isEncrypted See {@link #isEncrypted}.
+ * @param schemeType See {@link #schemeType}.
+ * @param initializationVectorSize See {@link #initializationVectorSize}.
+ * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}.
+ * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}.
+ * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}.
+ * @param defaultInitializationVector See {@link #defaultInitializationVector}.
*/
- public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) {
+ public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType,
+ int initializationVectorSize, byte[] keyId, int defaultEncryptedBlocks,
+ int defaultClearBlocks, @Nullable byte[] defaultInitializationVector) {
+ Assertions.checkArgument(initializationVectorSize == 0 ^ defaultInitializationVector == null);
this.isEncrypted = isEncrypted;
+ this.schemeType = schemeType;
this.initializationVectorSize = initializationVectorSize;
- this.keyId = keyId;
+ this.defaultInitializationVector = defaultInitializationVector;
+ cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId,
+ defaultEncryptedBlocks, defaultClearBlocks);
+ }
+
+ @C.CryptoMode
+ private static int schemeToCryptoMode(@Nullable String schemeType) {
+ if (schemeType == null) {
+ // If unknown, assume cenc.
+ return C.CRYPTO_MODE_AES_CTR;
+ }
+ switch (schemeType) {
+ case "cenc":
+ case "cens":
+ return C.CRYPTO_MODE_AES_CTR;
+ case "cbc1":
+ case "cbcs":
+ return C.CRYPTO_MODE_AES_CBC;
+ default:
+ Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR "
+ + "crypto mode.");
+ return C.CRYPTO_MODE_AES_CTR;
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
index 892f0a68af..c7f4e9489b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
+import java.util.Arrays;
/**
* OGG packet class.
@@ -27,8 +28,8 @@ import java.io.IOException;
/* package */ final class OggPacket {
private final OggPageHeader pageHeader = new OggPageHeader();
- private final ParsableByteArray packetArray =
- new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
+ private final ParsableByteArray packetArray = new ParsableByteArray(
+ new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
private int currentSegmentIndex = C.INDEX_UNSET;
private int segmentCount;
@@ -85,6 +86,9 @@ import java.io.IOException;
int size = calculatePacketSize(currentSegmentIndex);
int segmentIndex = currentSegmentIndex + segmentCount;
if (size > 0) {
+ if (packetArray.capacity() < packetArray.limit() + size) {
+ packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size);
+ }
input.readFully(packetArray.data, packetArray.limit(), size);
packetArray.setLimit(packetArray.limit() + size);
populated = pageHeader.laces[segmentIndex - 1] != 255;
@@ -118,6 +122,17 @@ import java.io.IOException;
return packetArray;
}
+ /**
+ * Trims the packet data array.
+ */
+ public void trimPayload() {
+ if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) {
+ return;
+ }
+ packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD,
+ packetArray.limit()));
+ }
+
/**
* Calculates the size of the packet starting from {@code startSegmentIndex}.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
index 6424155bd9..c203b0c6bd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
@@ -103,15 +103,12 @@ import java.io.IOException;
switch (state) {
case STATE_READ_HEADERS:
return readHeaders(input);
-
case STATE_SKIP_HEADERS:
input.skipFully((int) payloadStartPosition);
state = STATE_READ_PAYLOAD;
return Extractor.RESULT_CONTINUE;
-
case STATE_READ_PAYLOAD:
return readPayload(input, seekPosition);
-
default:
// Never happens.
throw new IllegalStateException();
@@ -152,6 +149,8 @@ import java.io.IOException;
setupData = null;
state = STATE_READ_PAYLOAD;
+ // First payload packet. Trim the payload array of the ogg packet after headers have been read.
+ oggPacket.trimPayload();
return Extractor.RESULT_CONTINUE;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
index ae0a69ef7d..31ac6858be 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
@@ -101,7 +101,7 @@ import java.util.ArrayList;
codecInitialisationData.add(vorbisSetup.setupHeaderData);
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,
- this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD,
+ this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE,
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null, 0, null);
return true;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
index df6efb722c..7b63ce813c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -65,13 +65,13 @@ public final class TsExtractor implements Extractor {
* Modes for the extractor.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS})
+ @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS})
public @interface Mode {}
/**
* Behave as defined in ISO/IEC 13818-1.
*/
- public static final int MODE_NORMAL = 0;
+ public static final int MODE_MULTI_PMT = 0;
/**
* Assume only one PMT will be contained in the stream, even if more are declared by the PAT.
*/
@@ -132,12 +132,23 @@ public final class TsExtractor implements Extractor {
* {@code FLAG_*} values that control the behavior of the payload readers.
*/
public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
- this(MODE_NORMAL, new TimestampAdjuster(0),
- new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
+ this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags);
}
/**
- * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT}
+ * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
+ * and {@link #MODE_HLS}.
+ * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
+ * {@code FLAG_*} values that control the behavior of the payload readers.
+ */
+ public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) {
+ this(mode, new TimestampAdjuster(0),
+ new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
+ }
+
+
+ /**
+ * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
* and {@link #MODE_HLS}.
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
* @param payloadReaderFactory Factory for injecting a custom set of payload readers.
@@ -371,10 +382,14 @@ public final class TsExtractor implements Extractor {
private static final int TS_PMT_DESC_DVBSUBS = 0x59;
private final ParsableBitArray pmtScratch;
+ private final SparseArray trackIdToReaderScratch;
+ private final SparseIntArray trackIdToPidScratch;
private final int pid;
public PmtReader(int pid) {
pmtScratch = new ParsableBitArray(new byte[5]);
+ trackIdToReaderScratch = new SparseArray<>();
+ trackIdToPidScratch = new SparseIntArray();
this.pid = pid;
}
@@ -425,6 +440,8 @@ public final class TsExtractor implements Extractor {
new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
}
+ trackIdToReaderScratch.clear();
+ trackIdToPidScratch.clear();
int remainingEntriesLength = sectionData.bytesLeft();
while (remainingEntriesLength > 0) {
sectionData.readBytes(pmtScratch, 5);
@@ -443,23 +460,30 @@ public final class TsExtractor implements Extractor {
if (trackIds.get(trackId)) {
continue;
}
- trackIds.put(trackId, true);
- TsPayloadReader reader;
- if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) {
- reader = id3Reader;
- } else {
- reader = payloadReaderFactory.createPayloadReader(streamType, esInfo);
- if (reader != null) {
+ TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader
+ : payloadReaderFactory.createPayloadReader(streamType, esInfo);
+ if (mode != MODE_HLS
+ || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {
+ trackIdToPidScratch.put(trackId, elementaryPid);
+ trackIdToReaderScratch.put(trackId, reader);
+ }
+ }
+
+ int trackIdCount = trackIdToPidScratch.size();
+ for (int i = 0; i < trackIdCount; i++) {
+ int trackId = trackIdToPidScratch.keyAt(i);
+ trackIds.put(trackId, true);
+ TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
+ if (reader != null) {
+ if (reader != id3Reader) {
reader.init(timestampAdjuster, output,
new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));
}
- }
-
- if (reader != null) {
- tsPayloadReaders.put(elementaryPid, reader);
+ tsPayloadReaders.put(trackIdToPidScratch.valueAt(i), reader);
}
}
+
if (mode == MODE_HLS) {
if (!tracksEnded) {
output.endTracks();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index 6914b2f52c..17ef2c4456 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -61,6 +61,14 @@ public final class MediaCodecInfo {
*/
public final boolean tunneling;
+ /**
+ * Whether the decoder is secure.
+ *
+ * @see CodecCapabilities#isFeatureRequired(String)
+ * @see CodecCapabilities#FEATURE_SecurePlayback
+ */
+ public final boolean secure;
+
private final String mimeType;
private final CodecCapabilities capabilities;
@@ -71,7 +79,7 @@ public final class MediaCodecInfo {
* @return The created instance.
*/
public static MediaCodecInfo newPassthroughInstance(String name) {
- return new MediaCodecInfo(name, null, null);
+ return new MediaCodecInfo(name, null, null, false, false);
}
/**
@@ -84,19 +92,32 @@ public final class MediaCodecInfo {
*/
public static MediaCodecInfo newInstance(String name, String mimeType,
CodecCapabilities capabilities) {
- return new MediaCodecInfo(name, mimeType, capabilities);
+ return new MediaCodecInfo(name, mimeType, capabilities, false, false);
}
/**
- * @param name The name of the decoder.
- * @param capabilities The capabilities of the decoder.
+ * Creates an instance.
+ *
+ * @param name The name of the {@link MediaCodec}.
+ * @param mimeType A mime type supported by the {@link MediaCodec}.
+ * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
+ * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
+ * @param forceSecure Whether {@link #secure} should be forced to {@code true}.
+ * @return The created instance.
*/
- private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) {
+ public static MediaCodecInfo newInstance(String name, String mimeType,
+ CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) {
+ return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure);
+ }
+
+ private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities,
+ boolean forceDisableAdaptive, boolean forceSecure) {
this.name = Assertions.checkNotNull(name);
this.mimeType = mimeType;
this.capabilities = capabilities;
- adaptive = capabilities != null && isAdaptive(capabilities);
+ adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
tunneling = capabilities != null && isTunneling(capabilities);
+ secure = forceSecure || (capabilities != null && isSecure(capabilities));
}
/**
@@ -165,12 +186,12 @@ public final class MediaCodecInfo {
logNoSupport("sizeAndRate.vCaps");
return false;
}
- if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) {
+ if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) {
// Capabilities are known to be inaccurately reported for vertical resolutions on some devices
// (b/31387661). If the video is vertical and the capabilities indicate support if the width
// and height are swapped, we assume that the vertical resolution is also supported.
if (width >= height
- || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) {
+ || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) {
logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate);
return false;
}
@@ -253,7 +274,9 @@ public final class MediaCodecInfo {
logNoSupport("channelCount.aCaps");
return false;
}
- if (audioCapabilities.getMaxInputChannelCount() < channelCount) {
+ int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType,
+ audioCapabilities.getMaxInputChannelCount());
+ if (maxInputChannelCount < channelCount) {
logNoSupport("channelCount.support, " + channelCount);
return false;
}
@@ -270,6 +293,40 @@ public final class MediaCodecInfo {
+ Util.DEVICE_DEBUG_INFO + "]");
}
+ private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) {
+ if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) {
+ // The maximum channel count looks like it's been set correctly.
+ return maxChannelCount;
+ }
+ if (MimeTypes.AUDIO_MPEG.equals(mimeType)
+ || MimeTypes.AUDIO_AMR_NB.equals(mimeType)
+ || MimeTypes.AUDIO_AMR_WB.equals(mimeType)
+ || MimeTypes.AUDIO_AAC.equals(mimeType)
+ || MimeTypes.AUDIO_VORBIS.equals(mimeType)
+ || MimeTypes.AUDIO_OPUS.equals(mimeType)
+ || MimeTypes.AUDIO_RAW.equals(mimeType)
+ || MimeTypes.AUDIO_FLAC.equals(mimeType)
+ || MimeTypes.AUDIO_ALAW.equals(mimeType)
+ || MimeTypes.AUDIO_MLAW.equals(mimeType)
+ || MimeTypes.AUDIO_MSGSM.equals(mimeType)) {
+ // Platform code should have set a default.
+ return maxChannelCount;
+ }
+ // The maximum channel count looks incorrect. Adjust it to an assumed default.
+ int assumedMaxChannelCount;
+ if (MimeTypes.AUDIO_AC3.equals(mimeType)) {
+ assumedMaxChannelCount = 6;
+ } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) {
+ assumedMaxChannelCount = 16;
+ } else {
+ // Default to the platform limit, which is 30.
+ assumedMaxChannelCount = 30;
+ }
+ Log.w(TAG, "AssumedMaxChannelAdjustment: " + name + ", [" + maxChannelCount + " to "
+ + assumedMaxChannelCount + "]");
+ return assumedMaxChannelCount;
+ }
+
private static boolean isAdaptive(CodecCapabilities capabilities) {
return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities);
}
@@ -279,14 +336,6 @@ public final class MediaCodecInfo {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
}
- @TargetApi(21)
- private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width,
- int height, double frameRate) {
- return frameRate == Format.NO_VALUE || frameRate <= 0
- ? capabilities.isSizeSupported(width, height)
- : capabilities.areSizeAndRateSupported(width, height, frameRate);
- }
-
private static boolean isTunneling(CodecCapabilities capabilities) {
return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
}
@@ -296,4 +345,21 @@ public final class MediaCodecInfo {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
}
+ private static boolean isSecure(CodecCapabilities capabilities) {
+ return Util.SDK_INT >= 21 && isSecureV21(capabilities);
+ }
+
+ @TargetApi(21)
+ private static boolean isSecureV21(CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
+ }
+
+ @TargetApi(21)
+ private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width,
+ int height, double frameRate) {
+ return frameRate == Format.NO_VALUE || frameRate <= 0
+ ? capabilities.isSizeSupported(width, height)
+ : capabilities.areSizeAndRateSupported(width, height, frameRate);
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 8fb9bc9271..49b221d5b4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
@@ -175,10 +176,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final MediaCodec.BufferInfo outputBufferInfo;
private Format format;
- private MediaCodec codec;
private DrmSession drmSession;
private DrmSession pendingDrmSession;
- private boolean codecIsAdaptive;
+ private MediaCodec codec;
+ private MediaCodecInfo codecInfo;
private boolean codecNeedsDiscardToSpsWorkaround;
private boolean codecNeedsFlushWorkaround;
private boolean codecNeedsAdaptationWorkaround;
@@ -237,7 +238,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
@Override
- public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+ public final int supportsMixedMimeTypeAdaptation() {
return ADAPTIVE_NOT_SEAMLESS;
}
@@ -291,55 +292,60 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@SuppressWarnings("deprecation")
protected final void maybeInitCodec() throws ExoPlaybackException {
- if (!shouldInitCodec()) {
+ if (codec != null || format == null) {
+ // We have a codec already, or we don't have a format with which to instantiate one.
return;
}
drmSession = pendingDrmSession;
String mimeType = format.sampleMimeType;
- MediaCrypto mediaCrypto = null;
+ MediaCrypto wrappedMediaCrypto = null;
boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) {
- @DrmSession.State int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- } else if (drmSessionState == DrmSession.STATE_OPENED
- || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
- mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto();
- drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType);
- } else {
+ FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ throw ExoPlaybackException.createForRenderer(drmError, getIndex());
+ }
// The drm session isn't open yet.
return;
}
+ wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
+ drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
- MediaCodecInfo decoderInfo = null;
- try {
- decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
- if (decoderInfo == null && drmSessionRequiresSecureDecoder) {
- // The drm session indicates that a secure decoder is required, but the device does not have
- // one. Assuming that supportsFormat indicated support for the media being played, we know
- // that it does not require a secure output path. Most CDM implementations allow playback to
- // proceed with a non-secure decoder in this case, so we try our luck.
- decoderInfo = getDecoderInfo(mediaCodecSelector, format, false);
- if (decoderInfo != null) {
- Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but "
- + "no secure decoder available. Trying to proceed with " + decoderInfo.name + ".");
+ if (codecInfo == null) {
+ try {
+ codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
+ if (codecInfo == null && drmSessionRequiresSecureDecoder) {
+ // The drm session indicates that a secure decoder is required, but the device does not
+ // have one. Assuming that supportsFormat indicated support for the media being played, we
+ // know that it does not require a secure output path. Most CDM implementations allow
+ // playback to proceed with a non-secure decoder in this case, so we try our luck.
+ codecInfo = getDecoderInfo(mediaCodecSelector, format, false);
+ if (codecInfo != null) {
+ Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but "
+ + "no secure decoder available. Trying to proceed with " + codecInfo.name + ".");
+ }
}
+ } catch (DecoderQueryException e) {
+ throwDecoderInitError(new DecoderInitializationException(format, e,
+ drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
+ }
+
+ if (codecInfo == null) {
+ throwDecoderInitError(new DecoderInitializationException(format, null,
+ drmSessionRequiresSecureDecoder,
+ DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
}
- } catch (DecoderQueryException e) {
- throwDecoderInitError(new DecoderInitializationException(format, e,
- drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
}
- if (decoderInfo == null) {
- throwDecoderInitError(new DecoderInitializationException(format, null,
- drmSessionRequiresSecureDecoder,
- DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
+ if (!shouldInitCodec(codecInfo)) {
+ return;
}
- String codecName = decoderInfo.name;
- codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName);
+ String codecName = codecInfo.name;
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
@@ -353,7 +359,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codec = MediaCodec.createByCodecName(codecName);
TraceUtil.endSection();
TraceUtil.beginSection("configureCodec");
- configureCodec(decoderInfo, codec, format, mediaCrypto);
+ configureCodec(codecInfo, codec, format, wrappedMediaCrypto);
TraceUtil.endSection();
TraceUtil.beginSection("startCodec");
codec.start();
@@ -380,14 +386,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
- protected boolean shouldInitCodec() {
- return codec == null && format != null;
+ protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
+ return true;
}
protected final MediaCodec getCodec() {
return codec;
}
+ protected final MediaCodecInfo getCodecInfo() {
+ return codecInfo;
+ }
+
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
@@ -426,31 +436,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
protected void releaseCodec() {
+ codecHotswapDeadlineMs = C.TIME_UNSET;
+ inputIndex = C.INDEX_UNSET;
+ outputIndex = C.INDEX_UNSET;
+ waitingForKeys = false;
+ shouldSkipOutputBuffer = false;
+ decodeOnlyPresentationTimestamps.clear();
+ inputBuffers = null;
+ outputBuffers = null;
+ codecInfo = null;
+ codecReconfigured = false;
+ codecReceivedBuffers = false;
+ codecNeedsDiscardToSpsWorkaround = false;
+ codecNeedsFlushWorkaround = false;
+ codecNeedsAdaptationWorkaround = false;
+ codecNeedsEosPropagationWorkaround = false;
+ codecNeedsEosFlushWorkaround = false;
+ codecNeedsMonoChannelCountWorkaround = false;
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ shouldSkipAdaptationWorkaroundOutputBuffer = false;
+ codecReceivedEos = false;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ codecReinitializationState = REINITIALIZATION_STATE_NONE;
+ buffer.data = null;
if (codec != null) {
- codecHotswapDeadlineMs = C.TIME_UNSET;
- inputIndex = C.INDEX_UNSET;
- outputIndex = C.INDEX_UNSET;
- waitingForKeys = false;
- shouldSkipOutputBuffer = false;
- decodeOnlyPresentationTimestamps.clear();
- inputBuffers = null;
- outputBuffers = null;
- codecReconfigured = false;
- codecReceivedBuffers = false;
- codecIsAdaptive = false;
- codecNeedsDiscardToSpsWorkaround = false;
- codecNeedsFlushWorkaround = false;
- codecNeedsAdaptationWorkaround = false;
- codecNeedsEosPropagationWorkaround = false;
- codecNeedsEosFlushWorkaround = false;
- codecNeedsMonoChannelCountWorkaround = false;
- codecNeedsAdaptationWorkaroundBuffer = false;
- shouldSkipAdaptationWorkaroundOutputBuffer = false;
- codecReceivedEos = false;
- codecReconfigurationState = RECONFIGURATION_STATE_NONE;
- codecReinitializationState = REINITIALIZATION_STATE_NONE;
decoderCounters.decoderReleaseCount++;
- buffer.data = null;
try {
codec.stop();
} finally {
@@ -488,7 +498,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
if (format == null) {
// We don't have a format yet, so try and read one.
- buffer.clear();
+ flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
@@ -727,15 +737,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null) {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
- && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
/**
@@ -781,7 +790,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
if (pendingDrmSession == drmSession && codec != null
- && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
+ && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround
@@ -1188,18 +1197,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
}
- /**
- * Returns whether the decoder is known to fail when adapting, despite advertising itself as an
- * adaptive decoder.
- *
- * If true is returned then we explicitly disable adaptation for the decoder.
- *
- * @param name The decoder name.
- * @return True if the decoder is known to fail when adapting.
- */
- private static boolean codecNeedsDisableAdaptationWorkaround(String name) {
- return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3")
- && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name));
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
index bb946d76f9..1823c3a7ff 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
@@ -55,8 +55,7 @@ public interface MediaCodecSelector {
/**
* Selects a decoder to instantiate for audio passthrough.
*
- * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
- * exists.
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index a09f6e26dd..d3f3dae344 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -56,8 +56,10 @@ public final class MediaCodecUtil {
}
private static final String TAG = "MediaCodecUtil";
+ private static final String GOOGLE_RAW_DECODER_NAME = "OMX.google.raw.decoder";
+ private static final String MTK_RAW_DECODER_NAME = "OMX.MTK.AUDIO.DECODER.RAW";
private static final MediaCodecInfo PASSTHROUGH_DECODER_INFO =
- MediaCodecInfo.newPassthroughInstance("OMX.google.raw.decoder");
+ MediaCodecInfo.newPassthroughInstance(GOOGLE_RAW_DECODER_NAME);
private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
private static final HashMap> decoderInfosCache = new HashMap<>();
@@ -99,7 +101,7 @@ public final class MediaCodecUtil {
/**
* Returns information about a decoder suitable for audio passthrough.
- **
+ *
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
* exists.
*/
@@ -155,11 +157,61 @@ public final class MediaCodecUtil {
+ ". Assuming: " + decoderInfos.get(0).name);
}
}
+ applyWorkarounds(decoderInfos);
decoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, decoderInfos);
return decoderInfos;
}
+ /**
+ * Returns the maximum frame size supported by the default H264 decoder.
+ *
+ * @return The maximum frame size for an H264 stream that can be decoded on the device.
+ */
+ public static int maxH264DecodableFrameSize() throws DecoderQueryException {
+ if (maxH264DecodableFrameSize == -1) {
+ int result = 0;
+ MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
+ if (decoderInfo != null) {
+ for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
+ result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
+ }
+ // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
+ // the levels mandated by the Android CDD.
+ result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));
+ }
+ maxH264DecodableFrameSize = result;
+ }
+ return maxH264DecodableFrameSize;
+ }
+
+ /**
+ * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
+ * codec description string (as defined by RFC 6381).
+ *
+ * @param codec A codec description string, as defined by RFC 6381.
+ * @return A pair (profile constant, level constant) if {@code codec} is well-formed and
+ * recognized, or null otherwise
+ */
+ public static Pair getCodecProfileAndLevel(String codec) {
+ if (codec == null) {
+ return null;
+ }
+ String[] parts = codec.split("\\.");
+ switch (parts[0]) {
+ case CODEC_ID_HEV1:
+ case CODEC_ID_HVC1:
+ return getHevcProfileAndLevel(codec, parts);
+ case CODEC_ID_AVC1:
+ case CODEC_ID_AVC2:
+ return getAvcProfileAndLevel(codec, parts);
+ default:
+ return null;
+ }
+ }
+
+ // Internal methods.
+
private static List getDecoderInfosInternal(
CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
try {
@@ -177,12 +229,14 @@ public final class MediaCodecUtil {
try {
CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType);
boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities);
+ boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(codecName);
if ((secureDecodersExplicit && key.secure == secure)
|| (!secureDecodersExplicit && !key.secure)) {
- decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities));
+ decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities,
+ forceDisableAdaptive, false));
} else if (!secureDecodersExplicit && secure) {
decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType,
- capabilities));
+ capabilities, forceDisableAdaptive, true));
// It only makes sense to have one synthesized secure decoder, return immediately.
return decoderInfos;
}
@@ -289,50 +343,37 @@ public final class MediaCodecUtil {
}
/**
- * Returns the maximum frame size supported by the default H264 decoder.
+ * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the
+ * platform.
*
- * @return The maximum frame size for an H264 stream that can be decoded on the device.
+ * @param decoderInfos The list to modify.
*/
- public static int maxH264DecodableFrameSize() throws DecoderQueryException {
- if (maxH264DecodableFrameSize == -1) {
- int result = 0;
- MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
- if (decoderInfo != null) {
- for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
- result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
+ private static void applyWorkarounds(List decoderInfos) {
+ if (Util.SDK_INT < 26 && decoderInfos.size() > 1
+ && MTK_RAW_DECODER_NAME.equals(decoderInfos.get(0).name)) {
+ // Prefer the Google raw decoder over the MediaTek one [Internal: b/62337687].
+ for (int i = 1; i < decoderInfos.size(); i++) {
+ MediaCodecInfo decoderInfo = decoderInfos.get(i);
+ if (GOOGLE_RAW_DECODER_NAME.equals(decoderInfo.name)) {
+ decoderInfos.remove(i);
+ decoderInfos.add(0, decoderInfo);
+ break;
}
- // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
- // the levels mandated by the Android CDD.
- result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));
}
- maxH264DecodableFrameSize = result;
}
- return maxH264DecodableFrameSize;
}
/**
- * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
- * codec description string (as defined by RFC 6381).
+ * Returns whether the decoder is known to fail when adapting, despite advertising itself as an
+ * adaptive decoder.
*
- * @param codec A codec description string, as defined by RFC 6381.
- * @return A pair (profile constant, level constant) if {@code codec} is well-formed and
- * recognized, or null otherwise
+ * @param name The decoder name.
+ * @return True if the decoder is known to fail when adapting.
*/
- public static Pair getCodecProfileAndLevel(String codec) {
- if (codec == null) {
- return null;
- }
- String[] parts = codec.split("\\.");
- switch (parts[0]) {
- case CODEC_ID_HEV1:
- case CODEC_ID_HVC1:
- return getHevcProfileAndLevel(codec, parts);
- case CODEC_ID_AVC1:
- case CODEC_ID_AVC2:
- return getAvcProfileAndLevel(codec, parts);
- default:
- return null;
- }
+ private static boolean codecNeedsDisableAdaptationWorkaround(String name) {
+ return Util.SDK_INT <= 22
+ && (Util.MODEL.equals("ODROID-XU3") || Util.MODEL.equals("Nexus 10"))
+ && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name));
}
private static Pair getHevcProfileAndLevel(String codec, String[] parts) {
@@ -429,6 +470,7 @@ public final class MediaCodecUtil {
case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16;
case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16;
case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16;
+ case CodecProfileLevel.AVCLevel52: return 36864 * 16 * 16;
default: return -1;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
index 814238970b..7ff426e2df 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -104,7 +104,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
}
@Override
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
decoder = decoderFactory.createDecoder(formats[0]);
}
@@ -153,7 +153,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
protected void onDisabled() {
flushPendingMetadata();
decoder = null;
- super.onDisabled();
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
new file mode 100644
index 0000000000..714d72104b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
@@ -0,0 +1,185 @@
+/*
+ * 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;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+
+/**
+ * Abstract base class for the concatenation of one or more {@link Timeline}s.
+ */
+/* package */ abstract class AbstractConcatenatedTimeline extends Timeline {
+
+ /**
+ * Meta data of a child timeline.
+ */
+ protected static final class ChildDataHolder {
+
+ /**
+ * Child timeline.
+ */
+ public Timeline timeline;
+
+ /**
+ * First period index belonging to the child timeline.
+ */
+ public int firstPeriodIndexInChild;
+
+ /**
+ * First window index belonging to the child timeline.
+ */
+ public int firstWindowIndexInChild;
+
+ /**
+ * UID of child timeline.
+ */
+ public Object uid;
+
+ /**
+ * Set child holder data.
+ *
+ * @param timeline Child timeline.
+ * @param firstPeriodIndexInChild First period index belonging to the child timeline.
+ * @param firstWindowIndexInChild First window index belonging to the child timeline.
+ * @param uid UID of child timeline.
+ */
+ public void setData(Timeline timeline, int firstPeriodIndexInChild, int firstWindowIndexInChild,
+ Object uid) {
+ this.timeline = timeline;
+ this.firstPeriodIndexInChild = firstPeriodIndexInChild;
+ this.firstWindowIndexInChild = firstWindowIndexInChild;
+ this.uid = uid;
+ }
+
+ }
+
+ private final ChildDataHolder childDataHolder;
+
+ public AbstractConcatenatedTimeline() {
+ childDataHolder = new ChildDataHolder();
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ getChildDataByWindowIndex(windowIndex, childDataHolder);
+ int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild;
+ int nextWindowIndexInChild = childDataHolder.timeline.getNextWindowIndex(
+ windowIndex - firstWindowIndexInChild,
+ repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode);
+ if (nextWindowIndexInChild != C.INDEX_UNSET) {
+ return firstWindowIndexInChild + nextWindowIndexInChild;
+ } else {
+ firstWindowIndexInChild += childDataHolder.timeline.getWindowCount();
+ if (firstWindowIndexInChild < getWindowCount()) {
+ return firstWindowIndexInChild;
+ } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) {
+ return 0;
+ } else {
+ return C.INDEX_UNSET;
+ }
+ }
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ getChildDataByWindowIndex(windowIndex, childDataHolder);
+ int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild;
+ int previousWindowIndexInChild = childDataHolder.timeline.getPreviousWindowIndex(
+ windowIndex - firstWindowIndexInChild,
+ repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode);
+ if (previousWindowIndexInChild != C.INDEX_UNSET) {
+ return firstWindowIndexInChild + previousWindowIndexInChild;
+ } else {
+ if (firstWindowIndexInChild > 0) {
+ return firstWindowIndexInChild - 1;
+ } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) {
+ return getWindowCount() - 1;
+ } else {
+ return C.INDEX_UNSET;
+ }
+ }
+ }
+
+ @Override
+ public final Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ getChildDataByWindowIndex(windowIndex, childDataHolder);
+ int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild;
+ int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild;
+ childDataHolder.timeline.getWindow(windowIndex - firstWindowIndexInChild, window, setIds,
+ defaultPositionProjectionUs);
+ window.firstPeriodIndex += firstPeriodIndexInChild;
+ window.lastPeriodIndex += firstPeriodIndexInChild;
+ return window;
+ }
+
+ @Override
+ public final Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ getChildDataByPeriodIndex(periodIndex, childDataHolder);
+ int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild;
+ int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild;
+ childDataHolder.timeline.getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);
+ period.windowIndex += firstWindowIndexInChild;
+ if (setIds) {
+ period.uid = Pair.create(childDataHolder.uid, period.uid);
+ }
+ return period;
+ }
+
+ @Override
+ public final int getIndexOfPeriod(Object uid) {
+ if (!(uid instanceof Pair)) {
+ return C.INDEX_UNSET;
+ }
+ Pair, ?> childUidAndPeriodUid = (Pair, ?>) uid;
+ Object childUid = childUidAndPeriodUid.first;
+ Object periodUid = childUidAndPeriodUid.second;
+ if (!getChildDataByChildUid(childUid, childDataHolder)) {
+ return C.INDEX_UNSET;
+ }
+ int periodIndexInChild = childDataHolder.timeline.getIndexOfPeriod(periodUid);
+ return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET
+ : childDataHolder.firstPeriodIndexInChild + periodIndexInChild;
+ }
+
+ /**
+ * Populates {@link ChildDataHolder} for the child timeline containing the given period index.
+ *
+ * @param periodIndex A valid period index within the bounds of the timeline.
+ * @param childData A data holder to be populated.
+ */
+ protected abstract void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData);
+
+ /**
+ * Populates {@link ChildDataHolder} for the child timeline containing the given window index.
+ *
+ * @param windowIndex A valid window index within the bounds of the timeline.
+ * @param childData A data holder to be populated.
+ */
+ protected abstract void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData);
+
+ /**
+ * Populates {@link ChildDataHolder} for the child timeline with the given UID.
+ *
+ * @param childUid A child UID.
+ * @param childData A data holder to be populated.
+ * @return Whether a child with the given UID was found.
+ */
+ protected abstract boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
index e14930c7b8..12f58d9a21 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -16,10 +16,12 @@
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
/**
@@ -45,14 +47,20 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
*
* The clipping start/end positions must be specified by calling {@link #setClipping(long, long)}
* on the playback thread before preparation completes.
+ *
+ * If the start point is guaranteed to be a key frame, pass {@code false} to {@code
+ * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is
+ * first read from.
*
* @param mediaPeriod The media period to clip.
+ * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.
*/
- public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
+ public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) {
this.mediaPeriod = mediaPeriod;
startUs = C.TIME_UNSET;
endUs = C.TIME_UNSET;
sampleStreams = new ClippingSampleStream[0];
+ pendingInitialDiscontinuity = enableInitialDiscontinuity;
}
/**
@@ -68,9 +76,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
- public void prepare(MediaPeriod.Callback callback) {
+ public void prepare(MediaPeriod.Callback callback, long positionUs) {
this.callback = callback;
- mediaPeriod.prepare(this);
+ mediaPeriod.prepare(this, startUs + positionUs);
}
@Override
@@ -94,6 +102,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
internalStreams, streamResetFlags, positionUs + startUs);
+ if (pendingInitialDiscontinuity) {
+ pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections);
+ }
Assertions.checkState(enablePositionUs == positionUs + startUs
|| (enablePositionUs >= startUs
&& (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
@@ -179,6 +190,15 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selections) {
// If the clipping start position is non-zero, the clipping sample streams will adjust
// timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
// timestamps can be negative, because sample streams provide buffers starting at a key-frame,
@@ -186,13 +206,17 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
// negative timestamp, its offset timestamp can jump backwards compared to the last timestamp
// read in the previous period. Renderer implementations may not allow this, so we signal a
// discontinuity which resets the renderers before they read the clipping sample stream.
- pendingInitialDiscontinuity = startUs != 0;
- callback.onPrepared(this);
- }
-
- @Override
- public void onContinueLoadingRequested(MediaPeriod source) {
- callback.onContinueLoadingRequested(this);
+ // However, for audio-only track selections we assume to have random access seek behaviour and
+ // do not need an initial discontinuity to reset the renderer.
+ for (TrackSelection trackSelection : selections) {
+ if (trackSelection != null) {
+ Format selectedFormat = trackSelection.getSelectedFormat();
+ if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) {
+ return true;
+ }
+ }
+ }
+ return false;
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index be15a07726..99a8033589 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayer.RepeatMode;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
@@ -33,6 +34,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
private final MediaSource mediaSource;
private final long startUs;
private final long endUs;
+ private final boolean enableInitialDiscontinuity;
private final ArrayList mediaPeriods;
private MediaSource.Listener sourceListener;
@@ -49,10 +51,31 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
* from the specified start point up to the end of the source.
*/
public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {
+ this(mediaSource, startPositionUs, endPositionUs, true);
+ }
+
+ /**
+ * Creates a new clipping source that wraps the specified source.
+ *
+ * If the start point is guaranteed to be a key frame, pass {@code false} to
+ * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period
+ * is first read from.
+ *
+ * @param mediaSource The single-period, non-dynamic source to wrap.
+ * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to
+ * start providing samples, in microseconds.
+ * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop
+ * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples
+ * from the specified start point up to the end of the source.
+ * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.
+ */
+ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs,
+ boolean enableInitialDiscontinuity) {
Assertions.checkArgument(startPositionUs >= 0);
this.mediaSource = Assertions.checkNotNull(mediaSource);
startUs = startPositionUs;
endUs = endPositionUs;
+ this.enableInitialDiscontinuity = enableInitialDiscontinuity;
mediaPeriods = new ArrayList<>();
}
@@ -68,9 +91,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod(
- mediaSource.createPeriod(index, allocator, startUs + positionUs));
+ mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity);
mediaPeriods.add(mediaPeriod);
mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs);
return mediaPeriod;
@@ -142,6 +165,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
return 1;
}
+ @Override
+ public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
+ return timeline.getNextWindowIndex(windowIndex, repeatMode);
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
+ return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
+ }
+
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
index 9fc499f251..cb939fd14a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -15,8 +15,6 @@
*/
package com.google.android.exoplayer2.source;
-import android.util.Pair;
-import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -38,6 +36,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
private final Object[] manifests;
private final Map sourceIndexByMediaPeriod;
private final boolean[] duplicateFlags;
+ private final boolean isRepeatOneAtomic;
private Listener listener;
private ConcatenatedTimeline timeline;
@@ -47,7 +46,19 @@ public final class ConcatenatingMediaSource implements MediaSource {
* {@link MediaSource} instance to be present more than once in the array.
*/
public ConcatenatingMediaSource(MediaSource... mediaSources) {
+ this(false, mediaSources);
+ }
+
+ /**
+ * @param isRepeatOneAtomic Whether the concatenated media source shall be treated as atomic
+ * (i.e., repeated in its entirety) when repeat mode is set to
+ * {@code ExoPlayer.REPEAT_MODE_ONE}.
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
+ * {@link MediaSource} instance to be present more than once in the array.
+ */
+ public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) {
this.mediaSources = mediaSources;
+ this.isRepeatOneAtomic = isRepeatOneAtomic;
timelines = new Timeline[mediaSources.length];
manifests = new Object[mediaSources.length];
sourceIndexByMediaPeriod = new HashMap<>();
@@ -80,11 +91,11 @@ public final class ConcatenatingMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- int sourceIndex = timeline.getSourceIndexForPeriod(index);
- int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex);
- MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator,
- positionUs);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex);
+ MediaPeriodId periodIdInSource =
+ new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexInChild(sourceIndex));
+ MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator);
sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
return mediaPeriod;
}
@@ -123,7 +134,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
return;
}
}
- timeline = new ConcatenatedTimeline(timelines.clone());
+ timeline = new ConcatenatedTimeline(timelines.clone(), isRepeatOneAtomic);
listener.onSourceInfoRefreshed(timeline, manifests.clone());
}
@@ -144,13 +155,14 @@ public final class ConcatenatingMediaSource implements MediaSource {
/**
* A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
*/
- private static final class ConcatenatedTimeline extends Timeline {
+ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
private final Timeline[] timelines;
private final int[] sourcePeriodOffsets;
private final int[] sourceWindowOffsets;
+ private final boolean isRepeatOneAtomic;
- public ConcatenatedTimeline(Timeline[] timelines) {
+ public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) {
int[] sourcePeriodOffsets = new int[timelines.length];
int[] sourceWindowOffsets = new int[timelines.length];
long periodCount = 0;
@@ -167,6 +179,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
this.timelines = timelines;
this.sourcePeriodOffsets = sourcePeriodOffsets;
this.sourceWindowOffsets = sourceWindowOffsets;
+ this.isRepeatOneAtomic = isRepeatOneAtomic;
}
@Override
@@ -174,70 +187,64 @@ public final class ConcatenatingMediaSource implements MediaSource {
return sourceWindowOffsets[sourceWindowOffsets.length - 1];
}
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- int sourceIndex = getSourceIndexForWindow(windowIndex);
- int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
- int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
- timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds,
- defaultPositionProjectionUs);
- window.firstPeriodIndex += firstPeriodIndexInSource;
- window.lastPeriodIndex += firstPeriodIndexInSource;
- return window;
- }
-
@Override
public int getPeriodCount() {
return sourcePeriodOffsets[sourcePeriodOffsets.length - 1];
}
@Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- int sourceIndex = getSourceIndexForPeriod(periodIndex);
- int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
- int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
- timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds);
- period.windowIndex += firstWindowIndexInSource;
- if (setIds) {
- period.uid = Pair.create(sourceIndex, period.uid);
+ public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ if (isRepeatOneAtomic && repeatMode == ExoPlayer.REPEAT_MODE_ONE) {
+ repeatMode = ExoPlayer.REPEAT_MODE_ALL;
}
- return period;
+ return super.getNextWindowIndex(windowIndex, repeatMode);
}
@Override
- public int getIndexOfPeriod(Object uid) {
- if (!(uid instanceof Pair)) {
- return C.INDEX_UNSET;
+ public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ if (isRepeatOneAtomic && repeatMode == ExoPlayer.REPEAT_MODE_ONE) {
+ repeatMode = ExoPlayer.REPEAT_MODE_ALL;
}
- Pair, ?> sourceIndexAndPeriodId = (Pair, ?>) uid;
- if (!(sourceIndexAndPeriodId.first instanceof Integer)) {
- return C.INDEX_UNSET;
- }
- int sourceIndex = (Integer) sourceIndexAndPeriodId.first;
- Object periodId = sourceIndexAndPeriodId.second;
- if (sourceIndex < 0 || sourceIndex >= timelines.length) {
- return C.INDEX_UNSET;
- }
- int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId);
- return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET
- : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource;
+ return super.getPreviousWindowIndex(windowIndex, repeatMode);
}
- private int getSourceIndexForPeriod(int periodIndex) {
+ @Override
+ protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) {
+ int childIndex = getChildIndexByPeriodIndex(periodIndex);
+ getChildDataByChildIndex(childIndex, childData);
+ }
+
+ @Override
+ protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) {
+ int childIndex = Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1;
+ getChildDataByChildIndex(childIndex, childData);
+ }
+
+ @Override
+ protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) {
+ if (!(childUid instanceof Integer)) {
+ return false;
+ }
+ int childIndex = (Integer) childUid;
+ getChildDataByChildIndex(childIndex, childData);
+ return true;
+ }
+
+ private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) {
+ childData.setData(timelines[childIndex], getFirstPeriodIndexInChild(childIndex),
+ getFirstWindowIndexInChild(childIndex), childIndex);
+ }
+
+ private int getChildIndexByPeriodIndex(int periodIndex) {
return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1;
}
- private int getFirstPeriodIndexInSource(int sourceIndex) {
- return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1];
+ private int getFirstPeriodIndexInChild(int childIndex) {
+ return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1];
}
- private int getSourceIndexForWindow(int windowIndex) {
- return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1;
- }
-
- private int getFirstWindowIndexInSource(int sourceIndex) {
- return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1];
+ private int getFirstWindowIndexInChild(int childIndex) {
+ return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1];
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
new file mode 100644
index 0000000000..a9e478a67f
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
@@ -0,0 +1,587 @@
+/*
+ * 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;
+
+import android.util.Pair;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
+ * during playback. Access to this class is thread-safe.
+ */
+public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent {
+
+ private static final int MSG_ADD = 0;
+ private static final int MSG_ADD_MULTIPLE = 1;
+ private static final int MSG_REMOVE = 2;
+
+ // Accessed on the app thread.
+ private final List mediaSourcesPublic;
+
+ // Accessed on the playback thread.
+ private final List mediaSourceHolders;
+ private final MediaSourceHolder query;
+ private final Map mediaSourceByMediaPeriod;
+ private final List deferredMediaPeriods;
+
+ private ExoPlayer player;
+ private Listener listener;
+ private boolean preventListenerNotification;
+ private int windowCount;
+ private int periodCount;
+
+ public DynamicConcatenatingMediaSource() {
+ this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
+ this.mediaSourcesPublic = new ArrayList<>();
+ this.mediaSourceHolders = new ArrayList<>();
+ this.deferredMediaPeriods = new ArrayList<>(1);
+ this.query = new MediaSourceHolder(null, null, -1, -1, -1);
+ }
+
+ /**
+ * Appends a {@link MediaSource} to the playlist.
+ *
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ */
+ public synchronized void addMediaSource(MediaSource mediaSource) {
+ addMediaSource(mediaSourcesPublic.size(), mediaSource);
+ }
+
+ /**
+ * Adds a {@link MediaSource} to the playlist.
+ *
+ * @param index The index at which the new {@link MediaSource} will be inserted. This index must
+ * be in the range of 0 <= index <= {@link #getSize()}.
+ * @param mediaSource The {@link MediaSource} to be added to the list.
+ */
+ public synchronized void addMediaSource(int index, MediaSource mediaSource) {
+ Assertions.checkNotNull(mediaSource);
+ Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
+ mediaSourcesPublic.add(index, mediaSource);
+ if (player != null) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource)));
+ }
+ }
+
+ /**
+ * Appends multiple {@link MediaSource}s to the playlist.
+ *
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ */
+ public synchronized void addMediaSources(Collection mediaSources) {
+ addMediaSources(mediaSourcesPublic.size(), mediaSources);
+ }
+
+ /**
+ * Adds multiple {@link MediaSource}s to the playlist.
+ *
+ * @param index The index at which the new {@link MediaSource}s will be inserted. This index must
+ * be in the range of 0 <= index <= {@link #getSize()}.
+ * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
+ * sources are added in the order in which they appear in this collection.
+ */
+ public synchronized void addMediaSources(int index, Collection mediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ Assertions.checkNotNull(mediaSource);
+ Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
+ }
+ mediaSourcesPublic.addAll(index, mediaSources);
+ if (player != null && !mediaSources.isEmpty()) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE,
+ Pair.create(index, mediaSources)));
+ }
+ }
+
+ /**
+ * Removes a {@link MediaSource} from the playlist.
+ *
+ * @param index The index at which the media source will be removed.
+ */
+ public synchronized void removeMediaSource(int index) {
+ mediaSourcesPublic.remove(index);
+ if (player != null) {
+ player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index));
+ }
+ }
+
+ /**
+ * Returns the number of media sources in the playlist.
+ */
+ public synchronized int getSize() {
+ return mediaSourcesPublic.size();
+ }
+
+ /**
+ * Returns the {@link MediaSource} at a specified index.
+ *
+ * @param index A index in the range of 0 <= index <= {@link #getSize()}.
+ * @return The {@link MediaSource} at this index.
+ */
+ public synchronized MediaSource getMediaSource(int index) {
+ return mediaSourcesPublic.get(index);
+ }
+
+ @Override
+ public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource,
+ Listener listener) {
+ this.player = player;
+ this.listener = listener;
+ preventListenerNotification = true;
+ addMediaSourcesInternal(0, mediaSourcesPublic);
+ preventListenerNotification = false;
+ maybeNotifyListener();
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex);
+ MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex);
+ MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild);
+ MediaPeriod mediaPeriod;
+ if (!holder.isPrepared) {
+ mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator);
+ deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod);
+ } else {
+ mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator);
+ }
+ mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod);
+ mediaSourceByMediaPeriod.remove(mediaPeriod);
+ if (mediaPeriod instanceof DeferredMediaPeriod) {
+ deferredMediaPeriods.remove(mediaPeriod);
+ ((DeferredMediaPeriod) mediaPeriod).releasePeriod();
+ } else {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void releaseSource() {
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ mediaSourceHolder.mediaSource.releaseSource();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ preventListenerNotification = true;
+ switch (messageType) {
+ case MSG_ADD: {
+ Pair messageData = (Pair) message;
+ addMediaSourceInternal(messageData.first, messageData.second);
+ break;
+ }
+ case MSG_ADD_MULTIPLE: {
+ Pair> messageData =
+ (Pair>) message;
+ addMediaSourcesInternal(messageData.first, messageData.second);
+ break;
+ }
+ case MSG_REMOVE: {
+ removeMediaSourceInternal((Integer) message);
+ break;
+ }
+ default: {
+ throw new IllegalStateException();
+ }
+ }
+ preventListenerNotification = false;
+ maybeNotifyListener();
+ }
+
+ private void maybeNotifyListener() {
+ if (!preventListenerNotification) {
+ listener.onSourceInfoRefreshed(
+ new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null);
+ }
+ }
+
+ private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) {
+ final MediaSourceHolder newMediaSourceHolder;
+ Object newUid = System.identityHashCode(newMediaSource);
+ DeferredTimeline newTimeline = new DeferredTimeline();
+ if (newIndex > 0) {
+ MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
+ newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline,
+ previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
+ previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount(),
+ newUid);
+ } else {
+ newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, newUid);
+ }
+ correctOffsets(newIndex, newTimeline.getWindowCount(), newTimeline.getPeriodCount());
+ mediaSourceHolders.add(newIndex, newMediaSourceHolder);
+ newMediaSourceHolder.mediaSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
+ updateMediaSourceInternal(newMediaSourceHolder, newTimeline);
+ }
+ });
+ }
+
+ private void addMediaSourcesInternal(int index, Collection mediaSources) {
+ for (MediaSource mediaSource : mediaSources) {
+ addMediaSourceInternal(index++, mediaSource);
+ }
+ }
+
+ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
+ if (mediaSourceHolder == null) {
+ throw new IllegalArgumentException();
+ }
+ DeferredTimeline deferredTimeline = mediaSourceHolder.timeline;
+ if (deferredTimeline.getTimeline() == timeline) {
+ return;
+ }
+ int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();
+ int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();
+ if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {
+ int index = findMediaSourceHolderByPeriodIndex(mediaSourceHolder.firstPeriodIndexInChild);
+ correctOffsets(index + 1, windowOffsetUpdate, periodOffsetUpdate);
+ }
+ mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline);
+ if (!mediaSourceHolder.isPrepared) {
+ for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) {
+ if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) {
+ deferredMediaPeriods.get(i).createPeriod();
+ deferredMediaPeriods.remove(i);
+ }
+ }
+ }
+ mediaSourceHolder.isPrepared = true;
+ maybeNotifyListener();
+ }
+
+ private void removeMediaSourceInternal(int index) {
+ MediaSourceHolder holder = mediaSourceHolders.get(index);
+ mediaSourceHolders.remove(index);
+ Timeline oldTimeline = holder.timeline;
+ correctOffsets(index, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount());
+ holder.mediaSource.releaseSource();
+ }
+
+ private void correctOffsets(int startIndex, int windowOffsetUpdate, int periodOffsetUpdate) {
+ windowCount += windowOffsetUpdate;
+ periodCount += periodOffsetUpdate;
+ for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
+ mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;
+ mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;
+ }
+ }
+
+ private int findMediaSourceHolderByPeriodIndex(int periodIndex) {
+ query.firstPeriodIndexInChild = periodIndex;
+ int index = Collections.binarySearch(mediaSourceHolders, query);
+ return index >= 0 ? index : -index - 2;
+ }
+
+ private static final class MediaSourceHolder implements Comparable {
+
+ public final MediaSource mediaSource;
+ public final Object uid;
+
+ public DeferredTimeline timeline;
+ public int firstWindowIndexInChild;
+ public int firstPeriodIndexInChild;
+ public boolean isPrepared;
+
+ public MediaSourceHolder(MediaSource mediaSource, DeferredTimeline timeline, int window,
+ int period, Object uid) {
+ this.mediaSource = mediaSource;
+ this.timeline = timeline;
+ this.firstWindowIndexInChild = window;
+ this.firstPeriodIndexInChild = period;
+ this.uid = uid;
+ }
+
+ @Override
+ public int compareTo(MediaSourceHolder other) {
+ return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild;
+ }
+ }
+
+ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
+
+ private final int windowCount;
+ private final int periodCount;
+ private final int[] firstPeriodInChildIndices;
+ private final int[] firstWindowInChildIndices;
+ private final Timeline[] timelines;
+ private final int[] uids;
+ private final SparseIntArray childIndexByUid;
+
+ public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount,
+ int periodCount) {
+ this.windowCount = windowCount;
+ this.periodCount = periodCount;
+ int childCount = mediaSourceHolders.size();
+ firstPeriodInChildIndices = new int[childCount];
+ firstWindowInChildIndices = new int[childCount];
+ timelines = new Timeline[childCount];
+ uids = new int[childCount];
+ childIndexByUid = new SparseIntArray();
+ int index = 0;
+ for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
+ timelines[index] = mediaSourceHolder.timeline;
+ firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;
+ firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;
+ uids[index] = (int) mediaSourceHolder.uid;
+ childIndexByUid.put(uids[index], index++);
+ }
+ }
+
+ @Override
+ protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childDataHolder) {
+ int index = Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false);
+ setChildData(index, childDataHolder);
+ }
+
+ @Override
+ protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childDataHolder) {
+ int index = Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false);
+ setChildData(index, childDataHolder);
+ }
+
+ @Override
+ protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childDataHolder) {
+ if (!(childUid instanceof Integer)) {
+ return false;
+ }
+ int index = childIndexByUid.get((int) childUid, -1);
+ if (index == -1) {
+ return false;
+ }
+ setChildData(index, childDataHolder);
+ return true;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return windowCount;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return periodCount;
+ }
+
+ private void setChildData(int srcIndex, ChildDataHolder dest) {
+ dest.setData(timelines[srcIndex], firstPeriodInChildIndices[srcIndex],
+ firstWindowInChildIndices[srcIndex], uids[srcIndex]);
+ }
+ }
+
+ private static final class DeferredTimeline extends Timeline {
+
+ private static final Object DUMMY_ID = new Object();
+ private static final Period period = new Period();
+
+ private final Timeline timeline;
+ private final Object replacedID;
+
+ public DeferredTimeline() {
+ timeline = null;
+ replacedID = null;
+ }
+
+ private DeferredTimeline(Timeline timeline, Object replacedID) {
+ this.timeline = timeline;
+ this.replacedID = replacedID;
+ }
+
+ public DeferredTimeline cloneWithNewTimeline(Timeline timeline) {
+ return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0
+ ? timeline.getPeriod(0, period, true).uid : replacedID);
+ }
+
+ public Timeline getTimeline() {
+ return timeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return timeline == null ? 1 : timeline.getWindowCount();
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return timeline == null
+ // Dynamic window to indicate pending timeline updates.
+ ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0,
+ C.TIME_UNSET, 0, 0, 0)
+ : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return timeline == null ? 1 : timeline.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ if (timeline == null) {
+ return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET,
+ C.TIME_UNSET);
+ }
+ timeline.getPeriod(periodIndex, period, setIds);
+ if (period.uid == replacedID) {
+ period.uid = DUMMY_ID;
+ }
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET)
+ : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid);
+ }
+
+ }
+
+ private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ public final MediaSource mediaSource;
+
+ private final MediaPeriodId id;
+ private final Allocator allocator;
+
+ private MediaPeriod mediaPeriod;
+ private Callback callback;
+ private long preparePositionUs;
+
+ public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
+ this.id = id;
+ this.allocator = allocator;
+ this.mediaSource = mediaSource;
+ }
+
+ public void createPeriod() {
+ mediaPeriod = mediaSource.createPeriod(id, allocator);
+ if (callback != null) {
+ mediaPeriod.prepare(this, preparePositionUs);
+ }
+ }
+
+ public void releasePeriod() {
+ if (mediaPeriod != null) {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ }
+
+ @Override
+ public void prepare(Callback callback, long preparePositionUs) {
+ this.callback = callback;
+ this.preparePositionUs = preparePositionUs;
+ if (mediaPeriod != null) {
+ mediaPeriod.prepare(this, preparePositionUs);
+ }
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ if (mediaPeriod != null) {
+ mediaPeriod.maybeThrowPrepareError();
+ } else {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return mediaPeriod.getTrackGroups();
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
+ positionUs);
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ mediaPeriod.discardBuffer(positionUs);
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ return mediaPeriod.readDiscontinuity();
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return mediaPeriod.getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ return mediaPeriod.seekToUs(positionUs);
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return mediaPeriod.getNextLoadPositionUs();
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ callback.onPrepared(this);
+ }
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
index f247d4dd37..79bc753241 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -17,20 +17,18 @@ package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.os.Handler;
-import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -43,12 +41,14 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
+import java.util.Arrays;
/**
* A {@link MediaPeriod} that extracts data using an {@link Extractor}.
*/
/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
- Loader.Callback, UpstreamFormatChangedListener {
+ Loader.Callback, Loader.ReleaseCallback,
+ UpstreamFormatChangedListener {
/**
* When the source's duration is unknown, it is calculated by adding this value to the largest
@@ -64,17 +64,19 @@ import java.io.IOException;
private final MediaSource.Listener sourceListener;
private final Allocator allocator;
private final String customCacheKey;
+ private final long continueLoadingCheckIntervalBytes;
private final Loader loader;
private final ExtractorHolder extractorHolder;
private final ConditionVariable loadCondition;
private final Runnable maybeFinishPrepareRunnable;
private final Runnable onContinueLoadingRequestedRunnable;
private final Handler handler;
- private final SparseArray sampleQueues;
private Callback callback;
private SeekMap seekMap;
- private boolean tracksBuilt;
+ private SampleQueue[] sampleQueues;
+ private int[] sampleQueueTrackIds;
+ private boolean sampleQueuesBuilt;
private boolean prepared;
private boolean seenFirstTrackSelection;
@@ -105,11 +107,13 @@ import java.io.IOException;
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
+ * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
*/
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
int minLoadableRetryCount, Handler eventHandler,
ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
- Allocator allocator, String customCacheKey) {
+ Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) {
this.uri = uri;
this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount;
@@ -118,6 +122,7 @@ import java.io.IOException;
this.sourceListener = sourceListener;
this.allocator = allocator;
this.customCacheKey = customCacheKey;
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("Loader:ExtractorMediaPeriod");
extractorHolder = new ExtractorHolder(extractors, this);
loadCondition = new ConditionVariable();
@@ -136,30 +141,35 @@ import java.io.IOException;
}
};
handler = new Handler();
-
+ sampleQueueTrackIds = new int[0];
+ sampleQueues = new SampleQueue[0];
pendingResetPositionUs = C.TIME_UNSET;
- sampleQueues = new SparseArray<>();
length = C.LENGTH_UNSET;
}
public void release() {
- final ExtractorHolder extractorHolder = this.extractorHolder;
- loader.release(new Runnable() {
- @Override
- public void run() {
- extractorHolder.release();
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).disable();
- }
+ boolean releasedSynchronously = loader.release(this);
+ if (prepared && !releasedSynchronously) {
+ // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
+ // sampleQueues may still be being modified by the loading thread.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.discardToEnd();
}
- });
+ }
handler.removeCallbacksAndMessages(null);
released = true;
}
@Override
- public void prepare(Callback callback) {
+ public void onLoaderReleased() {
+ extractorHolder.release();
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
this.callback = callback;
loadCondition.open();
startLoading();
@@ -179,19 +189,21 @@ import java.io.IOException;
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
Assertions.checkState(prepared);
- // Disable old tracks.
+ int oldEnabledTrackCount = enabledTrackCount;
+ // Deselect old tracks.
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
int track = ((SampleStreamImpl) streams[i]).track;
Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--;
trackEnabledStates[track] = false;
- sampleQueues.valueAt(track).disable();
streams[i] = null;
}
}
- // Enable new tracks.
- boolean selectedNewTracks = false;
+ // We'll always need to seek if this is a first selection to a non-zero position, or if we're
+ // making a selection having previously disabled all tracks.
+ boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0;
+ // Select new tracks.
for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i];
@@ -203,25 +215,29 @@ import java.io.IOException;
trackEnabledStates[track] = true;
streams[i] = new SampleStreamImpl(track);
streamResetFlags[i] = true;
- selectedNewTracks = true;
- }
- }
- if (!seenFirstTrackSelection) {
- // At the time of the first track selection all queues will be enabled, so we need to disable
- // any that are no longer required.
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- if (!trackEnabledStates[i]) {
- sampleQueues.valueAt(i).disable();
+ // If there's still a chance of avoiding a seek, try and seek within the sample queue.
+ if (!seekRequired) {
+ SampleQueue sampleQueue = sampleQueues[track];
+ sampleQueue.rewind();
+ seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
+ && sampleQueue.getReadIndex() != 0;
}
}
}
if (enabledTrackCount == 0) {
notifyReset = false;
if (loader.isLoading()) {
+ // Discard as much as we can synchronously.
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.discardToEnd();
+ }
loader.cancelLoading();
+ } else {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
}
- } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) {
+ } else if (seekRequired) {
positionUs = seekToUs(positionUs);
// We'll need to reset renderers consuming from all streams due to the seek.
for (int i = 0; i < streams.length; i++) {
@@ -236,7 +252,10 @@ import java.io.IOException;
@Override
public void discardBuffer(long positionUs) {
- // Do nothing.
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues[i].discardTo(positionUs, false, trackEnabledStates[i]);
+ }
}
@Override
@@ -277,11 +296,11 @@ import java.io.IOException;
if (haveAudioVideoTracks) {
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
largestQueuedTimestampUs = Long.MAX_VALUE;
- int trackCount = sampleQueues.size();
+ int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
if (trackIsAudioVideoFlags[i]) {
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
- sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ sampleQueues[i].getLargestQueuedTimestampUs());
}
}
} else {
@@ -296,13 +315,17 @@ import java.io.IOException;
// Treat all seeks into non-seekable media as being to t=0.
positionUs = seekMap.isSeekable() ? positionUs : 0;
lastSeekPositionUs = positionUs;
- int trackCount = sampleQueues.size();
// If we're not pending a reset, see if we can seek within the sample queues.
boolean seekInsideBuffer = !isPendingReset();
+ int trackCount = sampleQueues.length;
for (int i = 0; seekInsideBuffer && i < trackCount; i++) {
- if (trackEnabledStates[i]) {
- seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false);
- }
+ SampleQueue sampleQueue = sampleQueues[i];
+ sampleQueue.rewind();
+ // TODO: For sparse tracks (e.g. text, metadata) this may return false when an in-buffer
+ // seek should be allowed. If there are non-sparse tracks (e.g. video, audio) for which
+ // in-buffer seeking is successful, we should perform an in-buffer seek unconditionally.
+ seekInsideBuffer = sampleQueue.advanceTo(positionUs, true, false);
+ sampleQueue.discardToRead();
}
// If we failed to seek within the sample queues, we need to restart.
if (!seekInsideBuffer) {
@@ -312,7 +335,7 @@ import java.io.IOException;
loader.cancelLoading();
} else {
for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
+ sampleQueues[i].reset();
}
}
}
@@ -323,7 +346,7 @@ import java.io.IOException;
// SampleStream methods.
/* package */ boolean isReady(int track) {
- return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
+ return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample());
}
/* package */ void maybeThrowError() throws IOException {
@@ -335,17 +358,16 @@ import java.io.IOException;
if (notifyReset || isPendingReset()) {
return C.RESULT_NOTHING_READ;
}
-
- return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired,
- loadingFinished, lastSeekPositionUs);
+ return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished,
+ lastSeekPositionUs);
}
/* package */ void skipData(int track, long positionUs) {
- DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track);
+ SampleQueue sampleQueue = sampleQueues[track];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.skipAll();
+ sampleQueue.advanceToEnd();
} else {
- sampleQueue.skipToKeyframeBefore(positionUs, true);
+ sampleQueue.advanceTo(positionUs, true, true);
}
}
@@ -369,12 +391,14 @@ import java.io.IOException;
@Override
public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
+ if (released) {
+ return;
+ }
copyLengthFromLoader(loadable);
- if (!released && enabledTrackCount > 0) {
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
- }
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this);
}
}
@@ -398,18 +422,24 @@ import java.io.IOException;
@Override
public TrackOutput track(int id, int type) {
- DefaultTrackOutput trackOutput = sampleQueues.get(id);
- if (trackOutput == null) {
- trackOutput = new DefaultTrackOutput(allocator);
- trackOutput.setUpstreamFormatChangeListener(this);
- sampleQueues.put(id, trackOutput);
+ int trackCount = sampleQueues.length;
+ for (int i = 0; i < trackCount; i++) {
+ if (sampleQueueTrackIds[i] == id) {
+ return sampleQueues[i];
+ }
}
+ SampleQueue trackOutput = new SampleQueue(allocator);
+ trackOutput.setUpstreamFormatChangeListener(this);
+ sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
+ sampleQueueTrackIds[trackCount] = id;
+ sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1);
+ sampleQueues[trackCount] = trackOutput;
return trackOutput;
}
@Override
public void endTracks() {
- tracksBuilt = true;
+ sampleQueuesBuilt = true;
handler.post(maybeFinishPrepareRunnable);
}
@@ -429,22 +459,22 @@ import java.io.IOException;
// Internal methods.
private void maybeFinishPrepare() {
- if (released || prepared || seekMap == null || !tracksBuilt) {
+ if (released || prepared || seekMap == null || !sampleQueuesBuilt) {
return;
}
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue.getUpstreamFormat() == null) {
return;
}
}
loadCondition.close();
+ int trackCount = sampleQueues.length;
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackIsAudioVideoFlags = new boolean[trackCount];
trackEnabledStates = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
- Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat();
+ Format trackFormat = sampleQueues[i].getUpstreamFormat();
trackArray[i] = new TrackGroup(trackFormat);
String mimeType = trackFormat.sampleMimeType;
boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
@@ -503,9 +533,8 @@ import java.io.IOException;
// a new load.
lastSeekPositionUs = 0;
notifyReset = prepared;
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
}
loadable.setLoadPosition(0, 0);
}
@@ -513,19 +542,17 @@ import java.io.IOException;
private int getExtractedSamplesCount() {
int extractedSamplesCount = 0;
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
- extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
+ for (SampleQueue sampleQueue : sampleQueues) {
+ extractedSamplesCount += sampleQueue.getWriteIndex();
}
return extractedSamplesCount;
}
private long getLargestQueuedTimestampUs() {
long largestQueuedTimestampUs = Long.MIN_VALUE;
- int trackCount = sampleQueues.size();
- for (int i = 0; i < trackCount; i++) {
+ for (SampleQueue sampleQueue : sampleQueues) {
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
- sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ sampleQueue.getLargestQueuedTimestampUs());
}
return largestQueuedTimestampUs;
}
@@ -585,12 +612,6 @@ import java.io.IOException;
*/
/* package */ final class ExtractingLoadable implements Loadable {
- /**
- * The number of bytes that should be loaded between each each invocation of
- * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
- */
- private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
-
private final Uri uri;
private final DataSource dataSource;
private final ExtractorHolder extractorHolder;
@@ -650,7 +671,7 @@ import java.io.IOException;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
loadCondition.block();
result = extractor.read(input, positionHolder);
- if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) {
+ if (input.getPosition() > position + continueLoadingCheckIntervalBytes) {
position = input.getPosition();
loadCondition.close();
handler.post(onContinueLoadingRequestedRunnable);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
index c560616aae..1749e6abf2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -72,6 +72,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
*/
public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1;
+ /**
+ * The default number of bytes that should be loaded between each each invocation of
+ * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ */
+ public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
+
private final Uri uri;
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
@@ -80,6 +86,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
private final EventListener eventListener;
private final Timeline.Period period;
private final String customCacheKey;
+ private final int continueLoadingCheckIntervalBytes;
private MediaSource.Listener sourceListener;
private Timeline timeline;
@@ -96,8 +103,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
- this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
- eventListener, null);
+ this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);
}
/**
@@ -115,7 +121,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
String customCacheKey) {
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
- eventListener, customCacheKey);
+ eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES);
}
/**
@@ -129,10 +135,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
+ * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
+ * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
- EventListener eventListener, String customCacheKey) {
+ EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) {
this.uri = uri;
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
@@ -140,6 +148,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.customCacheKey = customCacheKey;
+ this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
period = new Timeline.Period();
}
@@ -156,11 +165,11 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- Assertions.checkArgument(index == 0);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ Assertions.checkArgument(id.periodIndex == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
- this, allocator, customCacheKey);
+ this, allocator, customCacheKey, continueLoadingCheckIntervalBytes);
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
index 8b14c78234..da2593ba15 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -15,8 +15,6 @@
*/
package com.google.android.exoplayer2.source;
-import android.util.Log;
-import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
@@ -25,26 +23,21 @@ import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
/**
- * Loops a {@link MediaSource}.
+ * Loops a {@link MediaSource} a specified number of times.
+ *
+ * Note: To loop a {@link MediaSource} indefinitely, it is usually better to use
+ * {@link ExoPlayer#setRepeatMode(int)}.
*/
public final class LoopingMediaSource implements MediaSource {
- /**
- * The maximum number of periods that can be exposed by the source. The value of this constant is
- * large enough to cause indefinite looping in practice (the total duration of the looping source
- * will be approximately five years if the duration of each period is one second).
- */
- public static final int MAX_EXPOSED_PERIODS = 157680000;
-
- private static final String TAG = "LoopingMediaSource";
-
private final MediaSource childSource;
private final int loopCount;
private int childPeriodCount;
/**
- * Loops the provided source indefinitely.
+ * Loops the provided source indefinitely. Note that it is usually better to use
+ * {@link ExoPlayer#setRepeatMode(int)}.
*
* @param childSource The {@link MediaSource} to loop.
*/
@@ -56,9 +49,7 @@ public final class LoopingMediaSource implements MediaSource {
* Loops the provided source a specified number of times.
*
* @param childSource The {@link MediaSource} to loop.
- * @param loopCount The desired number of loops. Must be strictly positive. The actual number of
- * loops will be capped at the maximum that can achieved without causing the number of
- * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}.
+ * @param loopCount The desired number of loops. Must be strictly positive.
*/
public LoopingMediaSource(MediaSource childSource, int loopCount) {
Assertions.checkArgument(loopCount > 0);
@@ -72,7 +63,9 @@ public final class LoopingMediaSource implements MediaSource {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
childPeriodCount = timeline.getPeriodCount();
- listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest);
+ Timeline loopingTimeline = loopCount != Integer.MAX_VALUE
+ ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline);
+ listener.onSourceInfoRefreshed(loopingTimeline, manifest);
}
});
}
@@ -83,8 +76,10 @@ public final class LoopingMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- return childSource.createPeriod(index % childPeriodCount, allocator, positionUs);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ return loopCount != Integer.MAX_VALUE
+ ? childSource.createPeriod(new MediaPeriodId(id.periodIndex % childPeriodCount), allocator)
+ : childSource.createPeriod(id, allocator);
}
@Override
@@ -97,7 +92,7 @@ public final class LoopingMediaSource implements MediaSource {
childSource.releaseSource();
}
- private static final class LoopingTimeline extends Timeline {
+ private static final class LoopingTimeline extends AbstractConcatenatedTimeline {
private final Timeline childTimeline;
private final int childPeriodCount;
@@ -108,17 +103,9 @@ public final class LoopingMediaSource implements MediaSource {
this.childTimeline = childTimeline;
childPeriodCount = childTimeline.getPeriodCount();
childWindowCount = childTimeline.getWindowCount();
- // This is the maximum number of loops that can be performed without exceeding
- // MAX_EXPOSED_PERIODS periods.
- int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount;
- if (loopCount > maxLoopCount) {
- if (loopCount != Integer.MAX_VALUE) {
- Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount);
- }
- this.loopCount = maxLoopCount;
- } else {
- this.loopCount = loopCount;
- }
+ this.loopCount = loopCount;
+ Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount,
+ "LoopingMediaSource contains too many periods");
}
@Override
@@ -126,45 +113,85 @@ public final class LoopingMediaSource implements MediaSource {
return childWindowCount * loopCount;
}
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- childTimeline.getWindow(windowIndex % childWindowCount, window, setIds,
- defaultPositionProjectionUs);
- int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount;
- window.firstPeriodIndex += periodIndexOffset;
- window.lastPeriodIndex += periodIndexOffset;
- return window;
- }
-
@Override
public int getPeriodCount() {
return childPeriodCount * loopCount;
}
@Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds);
- int loopCount = (periodIndex / childPeriodCount);
- period.windowIndex += loopCount * childWindowCount;
- if (setIds) {
- period.uid = Pair.create(loopCount, period.uid);
+ protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) {
+ int childIndex = periodIndex / childPeriodCount;
+ getChildDataByChildIndex(childIndex, childData);
+ }
+
+ @Override
+ protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) {
+ int childIndex = windowIndex / childWindowCount;
+ getChildDataByChildIndex(childIndex, childData);
+ }
+
+ @Override
+ protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) {
+ if (!(childUid instanceof Integer)) {
+ return false;
}
- return period;
+ int childIndex = (Integer) childUid;
+ getChildDataByChildIndex(childIndex, childData);
+ return true;
+ }
+
+ private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) {
+ childData.setData(childTimeline, childIndex * childPeriodCount, childIndex * childWindowCount,
+ childIndex);
+ }
+
+ }
+
+ private static final class InfinitelyLoopingTimeline extends Timeline {
+
+ private final Timeline childTimeline;
+
+ public InfinitelyLoopingTimeline(Timeline childTimeline) {
+ this.childTimeline = childTimeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return childTimeline.getWindowCount();
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode);
+ return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex;
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
+ int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode);
+ return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1
+ : childPreviousWindowIndex;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return childTimeline.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ return childTimeline.getPeriod(periodIndex, period, setIds);
}
@Override
public int getIndexOfPeriod(Object uid) {
- if (!(uid instanceof Pair)) {
- return C.INDEX_UNSET;
- }
- Pair, ?> loopCountAndChildUid = (Pair, ?>) uid;
- if (!(loopCountAndChildUid.first instanceof Integer)) {
- return C.INDEX_UNSET;
- }
- int loopCount = (Integer) loopCountAndChildUid.first;
- int periodIndexOffset = loopCount * childPeriodCount;
- return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset;
+ return childTimeline.getIndexOfPeriod(uid);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index 3b06542855..24b7fdc75f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -55,8 +55,10 @@ public interface MediaPeriod extends SequenceableLoader {
*
* @param callback Callback to receive updates from this period, including being notified when
* preparation completes.
+ * @param positionUs The position in microseconds relative to the start of the period at which to
+ * start loading data.
*/
- void prepare(Callback callback);
+ void prepare(Callback callback, long positionUs);
/**
* Throws an error that's preventing the period from becoming prepared. Does nothing if no such
@@ -106,6 +108,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Discards buffered media up to the specified position.
+ *
+ * This method should only be called after the period has been prepared.
*
* @param positionUs The position in microseconds.
*/
@@ -116,6 +120,8 @@ public interface MediaPeriod extends SequenceableLoader {
*
* After this method has returned a value other than {@link C#TIME_UNSET}, all
* {@link SampleStream}s provided by the period are guaranteed to start from a key frame.
+ *
+ * This method should only be called after the period has been prepared.
*
* @return If a discontinuity was read then the playback position in microseconds after the
* discontinuity. Else {@link C#TIME_UNSET}.
@@ -162,9 +168,9 @@ public interface MediaPeriod extends SequenceableLoader {
* This method may be called both during and after the period has been prepared.
*
* A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
- * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called
- * when the period is permitted to continue loading data. A period may do this both during and
- * after preparation.
+ * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be
+ * called when the period is permitted to continue loading data. A period may do this both during
+ * and after preparation.
*
* @param positionUs The current playback position.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
index f013e790f7..790620a80c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -34,9 +36,100 @@ public interface MediaSource {
* Called when manifest and/or timeline has been refreshed.
*
* @param timeline The source's timeline.
- * @param manifest The loaded manifest.
+ * @param manifest The loaded manifest. May be null.
*/
- void onSourceInfoRefreshed(Timeline timeline, Object manifest);
+ void onSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest);
+
+ }
+
+ /**
+ * Identifier for a {@link MediaPeriod}.
+ */
+ final class MediaPeriodId {
+
+ /**
+ * Value for unset media period identifiers.
+ */
+ public static final MediaPeriodId UNSET =
+ new MediaPeriodId(C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET);
+
+ /**
+ * The timeline period index.
+ */
+ public final int periodIndex;
+
+ /**
+ * If the media period is in an ad group, the index of the ad group in the period.
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adGroupIndex;
+
+ /**
+ * If the media period is in an ad group, the index of the ad in its ad group in the period.
+ * {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adIndexInAdGroup;
+
+ /**
+ * Creates a media period identifier for the specified period in the timeline.
+ *
+ * @param periodIndex The timeline period index.
+ */
+ public MediaPeriodId(int periodIndex) {
+ this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET);
+ }
+
+ /**
+ * Creates a media period identifier that identifies an ad within an ad group at the specified
+ * timeline period.
+ *
+ * @param periodIndex The index of the timeline period that contains the ad group.
+ * @param adGroupIndex The index of the ad group.
+ * @param adIndexInAdGroup The index of the ad in the ad group.
+ */
+ public MediaPeriodId(int periodIndex, int adGroupIndex, int adIndexInAdGroup) {
+ this.periodIndex = periodIndex;
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ }
+
+ /**
+ * Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index.
+ */
+ public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) {
+ return periodIndex == newPeriodIndex ? this
+ : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup);
+ }
+
+ /**
+ * Returns whether this period identifier identifies an ad in an ad group in a period.
+ */
+ public boolean isAd() {
+ return adGroupIndex != C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ MediaPeriodId periodId = (MediaPeriodId) obj;
+ return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex
+ && adIndexInAdGroup == periodId.adIndexInAdGroup;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + periodIndex;
+ result = 31 * result + adGroupIndex;
+ result = 31 * result + adIndexInAdGroup;
+ return result;
+ }
}
@@ -58,16 +151,15 @@ public interface MediaSource {
void maybeThrowSourceInfoRefreshError() throws IOException;
/**
- * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}.
- * This method may be called multiple times with the same index without an intervening call to
+ * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called
+ * multiple times with the same period identifier without an intervening call to
* {@link #releasePeriod(MediaPeriod)}.
*
- * @param index The index of the period.
+ * @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
- * @param positionUs The player's current playback position.
* @return A new {@link MediaPeriod}.
*/
- MediaPeriod createPeriod(int index, Allocator allocator, long positionUs);
+ MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator);
/**
* Releases the period.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
index 077b5576c1..cfb75b1b87 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -44,11 +44,11 @@ import java.util.IdentityHashMap;
}
@Override
- public void prepare(Callback callback) {
+ public void prepare(Callback callback, long positionUs) {
this.callback = callback;
pendingChildPrepareCount = periods.length;
for (MediaPeriod period : periods) {
- period.prepare(this);
+ period.prepare(this, positionUs);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
index 6f37165916..642752b35b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -116,10 +116,10 @@ public final class MergingMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
for (int i = 0; i < periods.length; i++) {
- periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs);
+ periods[i] = mediaSources[i].createPeriod(id, allocator);
}
return new MergingMediaPeriod(periods);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
new file mode 100644
index 0000000000..c9c44ab014
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
@@ -0,0 +1,534 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * A queue of metadata describing the contents of a media buffer.
+ */
+/* package */ final class SampleMetadataQueue {
+
+ /**
+ * A holder for sample metadata not held by {@link DecoderInputBuffer}.
+ */
+ public static final class SampleExtrasHolder {
+
+ public int size;
+ public long offset;
+ public CryptoData cryptoData;
+
+ }
+
+ private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
+
+ private int capacity;
+ private int[] sourceIds;
+ private long[] offsets;
+ private int[] sizes;
+ private int[] flags;
+ private long[] timesUs;
+ private CryptoData[] cryptoDatas;
+ private Format[] formats;
+
+ private int length;
+ private int absoluteStartIndex;
+ private int relativeStartIndex;
+ private int readPosition;
+
+ private long largestDiscardedTimestampUs;
+ private long largestQueuedTimestampUs;
+ private boolean upstreamKeyframeRequired;
+ private boolean upstreamFormatRequired;
+ private Format upstreamFormat;
+ private int upstreamSourceId;
+
+ public SampleMetadataQueue() {
+ capacity = SAMPLE_CAPACITY_INCREMENT;
+ sourceIds = new int[capacity];
+ offsets = new long[capacity];
+ timesUs = new long[capacity];
+ flags = new int[capacity];
+ sizes = new int[capacity];
+ cryptoDatas = new CryptoData[capacity];
+ formats = new Format[capacity];
+ largestDiscardedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ upstreamFormatRequired = true;
+ upstreamKeyframeRequired = true;
+ }
+
+ public void clearSampleData() {
+ length = 0;
+ absoluteStartIndex = 0;
+ relativeStartIndex = 0;
+ readPosition = 0;
+ upstreamKeyframeRequired = true;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ public void resetLargestParsedTimestamps() {
+ largestDiscardedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ }
+
+ /**
+ * Returns the current absolute write index.
+ */
+ public int getWriteIndex() {
+ return absoluteStartIndex + length;
+ }
+
+ /**
+ * Discards samples from the write side of the queue.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded.
+ * @return The reduced total number of bytes written after the samples have been discarded, or 0
+ * if the queue is now empty.
+ */
+ public long discardUpstreamSamples(int discardFromIndex) {
+ int discardCount = getWriteIndex() - discardFromIndex;
+ Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
+ length -= discardCount;
+ largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
+ if (length == 0) {
+ return 0;
+ } else {
+ int relativeLastWriteIndex = getRelativeIndex(length - 1);
+ return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex];
+ }
+ }
+
+ public void sourceId(int sourceId) {
+ upstreamSourceId = sourceId;
+ }
+
+ // Called by the consuming thread.
+
+ /**
+ * Returns the current absolute read index.
+ */
+ public int getReadIndex() {
+ return absoluteStartIndex + readPosition;
+ }
+
+ /**
+ * Peeks the source id of the next sample to be read, or the current upstream source id if the
+ * queue is empty or if the read position is at the end of the queue.
+ *
+ * @return The source id.
+ */
+ public int peekSourceId() {
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId;
+ }
+
+ /**
+ * Returns whether a sample is available to be read.
+ */
+ public synchronized boolean hasNextSample() {
+ return readPosition != length;
+ }
+
+ /**
+ * Returns the upstream {@link Format} in which samples are being queued.
+ */
+ public synchronized Format getUpstreamFormat() {
+ return upstreamFormatRequired ? null : upstreamFormat;
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last call to
+ * {@link #resetLargestParsedTimestamps()}.
+ *
+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public synchronized long getLargestQueuedTimestampUs() {
+ return largestQueuedTimestampUs;
+ }
+
+ /**
+ * Rewinds the read position to the first sample retained in the queue.
+ */
+ public synchronized void rewind() {
+ readPosition = 0;
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If a sample is read then the buffer is populated with information
+ * about the sample, but not its data. The size and absolute position of the data in the
+ * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
+ * and the absolute position of the first byte that may still be required after the current
+ * sample has been read. May be null if the caller requires that the format of the stream be
+ * read even if it's not changing.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even
+ * if it's not changing. A sample will never be read if set to true, however it is still
+ * possible for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param downstreamFormat The current downstream {@link Format}. If the format of the next
+ * sample is different to the current downstream format then a format will be read.
+ * @param extrasHolder The holder into which extra sample information should be written.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
+ * or {@link C#RESULT_BUFFER_READ}.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public synchronized int read(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
+ SampleExtrasHolder extrasHolder) {
+ if (!hasNextSample()) {
+ if (loadingFinished) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else if (upstreamFormat != null
+ && (formatRequired || upstreamFormat != downstreamFormat)) {
+ formatHolder.format = upstreamFormat;
+ return C.RESULT_FORMAT_READ;
+ } else {
+ return C.RESULT_NOTHING_READ;
+ }
+ }
+
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
+ formatHolder.format = formats[relativeReadIndex];
+ return C.RESULT_FORMAT_READ;
+ }
+
+ if (buffer.isFlagsOnly()) {
+ return C.RESULT_NOTHING_READ;
+ }
+
+ buffer.timeUs = timesUs[relativeReadIndex];
+ buffer.setFlags(flags[relativeReadIndex]);
+ extrasHolder.size = sizes[relativeReadIndex];
+ extrasHolder.offset = offsets[relativeReadIndex];
+ extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];
+
+ readPosition++;
+ return C.RESULT_BUFFER_READ;
+ }
+
+ /**
+ * Attempts to advance the read position to the sample before or at the specified time.
+ *
+ * @param timeUs The time to advance to.
+ * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified
+ * time, rather than to any sample before or at that time.
+ * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
+ * end of the queue, by advancing the read position to the last sample (or keyframe) in the
+ * queue.
+ * @return Whether the operation was a success. A successful advance is one in which the read
+ * position was unchanged or advanced, and is now at a sample meeting the specified criteria.
+ */
+ public synchronized boolean advanceTo(long timeUs, boolean toKeyframe,
+ boolean allowTimeBeyondBuffer) {
+ int relativeReadIndex = getRelativeIndex(readPosition);
+ if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]
+ || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
+ return false;
+ }
+ int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe);
+ if (offset == -1) {
+ return false;
+ }
+ readPosition += offset;
+ return true;
+ }
+
+ /**
+ * Advances the read position to the end of the queue.
+ */
+ public synchronized void advanceToEnd() {
+ if (!hasNextSample()) {
+ return;
+ }
+ readPosition = length;
+ }
+
+ /**
+ * Discards up to but not including the sample immediately before or at the specified time.
+ *
+ * @param timeUs The time to discard up to.
+ * @param toKeyframe If true then discards samples up to the keyframe before or at the specified
+ * time, rather than just any sample before or at that time.
+ * @param stopAtReadPosition If true then samples are only discarded if they're before the read
+ * position. If false then samples at and beyond the read position may be discarded, in which
+ * case the read position is advanced to the first remaining sample.
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
+ if (length == 0 || timeUs < timesUs[relativeStartIndex]) {
+ return C.POSITION_UNSET;
+ }
+ int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
+ int discardCount = findSampleBefore(relativeStartIndex, searchLength, timeUs, toKeyframe);
+ if (discardCount == -1) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(discardCount);
+ }
+
+ /**
+ * Discards samples up to but not including the read position.
+ *
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ public synchronized long discardToRead() {
+ if (readPosition == 0) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(readPosition);
+ }
+
+ /**
+ * Discards all samples in the queue. The read position is also advanced.
+ *
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ public synchronized long discardToEnd() {
+ if (length == 0) {
+ return C.POSITION_UNSET;
+ }
+ return discardSamples(length);
+ }
+
+ // Called by the loading thread.
+
+ public synchronized boolean format(Format format) {
+ if (format == null) {
+ upstreamFormatRequired = true;
+ return false;
+ }
+ upstreamFormatRequired = false;
+ if (Util.areEqual(format, upstreamFormat)) {
+ // Suppress changes between equal formats so we can use referential equality in readData.
+ return false;
+ } else {
+ upstreamFormat = format;
+ return true;
+ }
+ }
+
+ public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
+ int size, CryptoData cryptoData) {
+ if (upstreamKeyframeRequired) {
+ if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+ return;
+ }
+ upstreamKeyframeRequired = false;
+ }
+ Assertions.checkState(!upstreamFormatRequired);
+ commitSampleTimestamp(timeUs);
+
+ int relativeEndIndex = getRelativeIndex(length);
+ timesUs[relativeEndIndex] = timeUs;
+ offsets[relativeEndIndex] = offset;
+ sizes[relativeEndIndex] = size;
+ flags[relativeEndIndex] = sampleFlags;
+ cryptoDatas[relativeEndIndex] = cryptoData;
+ formats[relativeEndIndex] = upstreamFormat;
+ sourceIds[relativeEndIndex] = upstreamSourceId;
+
+ length++;
+ if (length == capacity) {
+ // Increase the capacity.
+ int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
+ int[] newSourceIds = new int[newCapacity];
+ long[] newOffsets = new long[newCapacity];
+ long[] newTimesUs = new long[newCapacity];
+ int[] newFlags = new int[newCapacity];
+ int[] newSizes = new int[newCapacity];
+ CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
+ Format[] newFormats = new Format[newCapacity];
+ int beforeWrap = capacity - relativeStartIndex;
+ System.arraycopy(offsets, relativeStartIndex, newOffsets, 0, beforeWrap);
+ System.arraycopy(timesUs, relativeStartIndex, newTimesUs, 0, beforeWrap);
+ System.arraycopy(flags, relativeStartIndex, newFlags, 0, beforeWrap);
+ System.arraycopy(sizes, relativeStartIndex, newSizes, 0, beforeWrap);
+ System.arraycopy(cryptoDatas, relativeStartIndex, newCryptoDatas, 0, beforeWrap);
+ System.arraycopy(formats, relativeStartIndex, newFormats, 0, beforeWrap);
+ System.arraycopy(sourceIds, relativeStartIndex, newSourceIds, 0, beforeWrap);
+ int afterWrap = relativeStartIndex;
+ System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
+ System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
+ System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
+ System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
+ System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);
+ System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
+ System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
+ offsets = newOffsets;
+ timesUs = newTimesUs;
+ flags = newFlags;
+ sizes = newSizes;
+ cryptoDatas = newCryptoDatas;
+ formats = newFormats;
+ sourceIds = newSourceIds;
+ relativeStartIndex = 0;
+ length = capacity;
+ capacity = newCapacity;
+ }
+ }
+
+ public synchronized void commitSampleTimestamp(long timeUs) {
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
+ }
+
+ /**
+ * Attempts to discard samples from the end of the queue to allow samples starting from the
+ * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
+ *
+ * @param timeUs The timestamp at which the splice occurs.
+ * @return Whether the splice was successful.
+ */
+ public synchronized boolean attemptSplice(long timeUs) {
+ if (length == 0) {
+ return timeUs > largestDiscardedTimestampUs;
+ }
+ long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs,
+ getLargestTimestamp(readPosition));
+ if (largestReadTimestampUs >= timeUs) {
+ return false;
+ }
+ int retainCount = length;
+ int relativeSampleIndex = getRelativeIndex(length - 1);
+ while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
+ retainCount--;
+ relativeSampleIndex--;
+ if (relativeSampleIndex == -1) {
+ relativeSampleIndex = capacity - 1;
+ }
+ }
+ discardUpstreamSamples(absoluteStartIndex + retainCount);
+ return true;
+ }
+
+ // Internal methods.
+
+ /**
+ * Finds the sample in the specified range that's before or at the specified time. If
+ * {@code keyframe} is {@code true} then the sample is additionally required to be a keyframe.
+ *
+ * @param relativeStartIndex The relative index from which to start searching.
+ * @param length The length of the range being searched.
+ * @param timeUs The specified time.
+ * @param keyframe Whether only keyframes should be considered.
+ * @return The offset from {@code relativeStartIndex} to the found sample, or -1 if no matching
+ * sample was found.
+ */
+ private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) {
+ // This could be optimized to use a binary search, however in practice callers to this method
+ // normally pass times near to the start of the search region. Hence it's unclear whether
+ // switching to a binary search would yield any real benefit.
+ int sampleCountToTarget = -1;
+ int searchIndex = relativeStartIndex;
+ for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) {
+ if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ // We've found a suitable sample.
+ sampleCountToTarget = i;
+ }
+ searchIndex++;
+ if (searchIndex == capacity) {
+ searchIndex = 0;
+ }
+ }
+ return sampleCountToTarget;
+ }
+
+ /**
+ * Discards the specified number of samples.
+ *
+ * @param discardCount The number of samples to discard.
+ * @return The corresponding offset up to which data should be discarded, or
+ * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ */
+ private long discardSamples(int discardCount) {
+ largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs,
+ getLargestTimestamp(discardCount));
+ length -= discardCount;
+ absoluteStartIndex += discardCount;
+ relativeStartIndex += discardCount;
+ if (relativeStartIndex >= capacity) {
+ relativeStartIndex -= capacity;
+ }
+ readPosition -= discardCount;
+ if (readPosition < 0) {
+ readPosition = 0;
+ }
+ if (length == 0) {
+ int relativeLastDiscardIndex = (relativeStartIndex == 0 ? capacity : relativeStartIndex) - 1;
+ return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
+ } else {
+ return offsets[relativeStartIndex];
+ }
+ }
+
+ /**
+ * Finds the largest timestamp of any sample from the start of the queue up to the specified
+ * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of
+ * the keyframe itself, and of subsequent frames.
+ *
+ * @param length The length of the range being searched.
+ * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}.
+ */
+ private long getLargestTimestamp(int length) {
+ if (length == 0) {
+ return Long.MIN_VALUE;
+ }
+ long largestTimestampUs = Long.MIN_VALUE;
+ int relativeSampleIndex = getRelativeIndex(length - 1);
+ for (int i = 0; i < length; i++) {
+ largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]);
+ if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ break;
+ }
+ relativeSampleIndex--;
+ if (relativeSampleIndex == -1) {
+ relativeSampleIndex = capacity - 1;
+ }
+ }
+ return largestTimestampUs;
+ }
+
+ /**
+ * Returns the relative index for a given offset from the start of the queue.
+ *
+ * @param offset The offset, which must be in the range [0, length].
+ */
+ private int getRelativeIndex(int offset) {
+ int relativeIndex = relativeStartIndex + offset;
+ return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
new file mode 100644
index 0000000000..1b70b03a29
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
@@ -0,0 +1,793 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder;
+import com.google.android.exoplayer2.upstream.Allocation;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A queue of media samples.
+ */
+public final class SampleQueue implements TrackOutput {
+
+ /**
+ * A listener for changes to the upstream format.
+ */
+ public interface UpstreamFormatChangedListener {
+
+ /**
+ * Called on the loading thread when an upstream format change occurs.
+ *
+ * @param format The new upstream format.
+ */
+ void onUpstreamFormatChanged(Format format);
+
+ }
+
+ private static final int INITIAL_SCRATCH_SIZE = 32;
+
+ private static final int STATE_ENABLED = 0;
+ private static final int STATE_ENABLED_WRITING = 1;
+ private static final int STATE_DISABLED = 2;
+
+ private final Allocator allocator;
+ private final int allocationLength;
+ private final SampleMetadataQueue metadataQueue;
+ private final SampleExtrasHolder extrasHolder;
+ private final ParsableByteArray scratch;
+ private final AtomicInteger state;
+
+ // References into the linked list of allocations.
+ private AllocationNode firstAllocationNode;
+ private AllocationNode readAllocationNode;
+ private AllocationNode writeAllocationNode;
+
+ // Accessed only by the consuming thread.
+ private Format downstreamFormat;
+
+ // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
+ private boolean pendingFormatAdjustment;
+ private Format lastUnadjustedFormat;
+ private long sampleOffsetUs;
+ private long totalBytesWritten;
+ private boolean pendingSplice;
+ private UpstreamFormatChangedListener upstreamFormatChangeListener;
+
+ /**
+ * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
+ */
+ public SampleQueue(Allocator allocator) {
+ this.allocator = allocator;
+ allocationLength = allocator.getIndividualAllocationLength();
+ metadataQueue = new SampleMetadataQueue();
+ extrasHolder = new SampleExtrasHolder();
+ scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
+ state = new AtomicInteger();
+ firstAllocationNode = new AllocationNode(0, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /**
+ * Resets the output.
+ */
+ public void reset() {
+ reset(true);
+ }
+
+ /**
+ * @deprecated Use {@link #reset()}. Don't disable sample queues.
+ */
+ @Deprecated
+ public void reset(boolean enable) {
+ int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED);
+ clearSampleData();
+ metadataQueue.resetLargestParsedTimestamps();
+ if (previousState == STATE_DISABLED) {
+ downstreamFormat = null;
+ }
+ }
+
+ /**
+ * Sets a source identifier for subsequent samples.
+ *
+ * @param sourceId The source identifier.
+ */
+ public void sourceId(int sourceId) {
+ metadataQueue.sourceId(sourceId);
+ }
+
+ /**
+ * Indicates samples that are subsequently queued should be spliced into those already queued.
+ */
+ public void splice() {
+ pendingSplice = true;
+ }
+
+ /**
+ * Returns the current absolute write index.
+ */
+ public int getWriteIndex() {
+ return metadataQueue.getWriteIndex();
+ }
+
+ /**
+ * Discards samples from the write side of the queue.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the
+ * range [{@link #getReadIndex()}, {@link #getWriteIndex()}].
+ */
+ public void discardUpstreamSamples(int discardFromIndex) {
+ totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex);
+ if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) {
+ clearAllocationNodes(firstAllocationNode);
+ firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ } else {
+ // Find the last node containing at least 1 byte of data that we need to keep.
+ AllocationNode lastNodeToKeep = firstAllocationNode;
+ while (totalBytesWritten > lastNodeToKeep.endPosition) {
+ lastNodeToKeep = lastNodeToKeep.next;
+ }
+ // Discard all subsequent nodes.
+ AllocationNode firstNodeToDiscard = lastNodeToKeep.next;
+ clearAllocationNodes(firstNodeToDiscard);
+ // Reset the successor of the last node to be an uninitialized node.
+ lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength);
+ // Update writeAllocationNode and readAllocationNode as necessary.
+ writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next
+ : lastNodeToKeep;
+ if (readAllocationNode == firstNodeToDiscard) {
+ readAllocationNode = lastNodeToKeep.next;
+ }
+ }
+ }
+
+ // Called by the consuming thread.
+
+ /**
+ * @deprecated Don't disable sample queues.
+ */
+ @Deprecated
+ public void disable() {
+ if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) {
+ clearSampleData();
+ }
+ }
+
+ /**
+ * Returns whether a sample is available to be read.
+ */
+ public boolean hasNextSample() {
+ return metadataQueue.hasNextSample();
+ }
+
+ /**
+ * Returns the current absolute read index.
+ */
+ public int getReadIndex() {
+ return metadataQueue.getReadIndex();
+ }
+
+ /**
+ * Peeks the source id of the next sample to be read, or the current upstream source id if the
+ * queue is empty or if the read position is at the end of the queue.
+ *
+ * @return The source id.
+ */
+ public int peekSourceId() {
+ return metadataQueue.peekSourceId();
+ }
+
+ /**
+ * Returns the upstream {@link Format} in which samples are being queued.
+ */
+ public Format getUpstreamFormat() {
+ return metadataQueue.getUpstreamFormat();
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+ *
+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public long getLargestQueuedTimestampUs() {
+ return metadataQueue.getLargestQueuedTimestampUs();
+ }
+
+ /**
+ * Rewinds the read position to the first sample in the queue.
+ */
+ public void rewind() {
+ metadataQueue.rewind();
+ readAllocationNode = firstAllocationNode;
+ }
+
+ /**
+ * Discards up to but not including the sample immediately before or at the specified time.
+ *
+ * @param timeUs The time to discard to.
+ * @param toKeyframe If true then discards samples up to the keyframe before or at the specified
+ * time, rather than any sample before or at that time.
+ * @param stopAtReadPosition If true then samples are only discarded if they're before the
+ * read position. If false then samples at and beyond the read position may be discarded, in
+ * which case the read position is advanced to the first remaining sample.
+ */
+ public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
+ discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition));
+ }
+
+ /**
+ * Discards up to but not including the read position.
+ */
+ public void discardToRead() {
+ discardDownstreamTo(metadataQueue.discardToRead());
+ }
+
+ /**
+ * Discards to the end of the queue. The read position is also advanced.
+ */
+ public void discardToEnd() {
+ discardDownstreamTo(metadataQueue.discardToEnd());
+ }
+
+ /**
+ * @deprecated Use {@link #advanceToEnd()} followed by {@link #discardToRead()}.
+ */
+ @Deprecated
+ public void skipAll() {
+ advanceToEnd();
+ discardToRead();
+ }
+
+ /**
+ * Advances the read position to the end of the queue.
+ */
+ public void advanceToEnd() {
+ metadataQueue.advanceToEnd();
+ }
+
+ /**
+ * @deprecated Use {@link #advanceTo(long, boolean, boolean)} followed by
+ * {@link #discardToRead()}.
+ */
+ @Deprecated
+ public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
+ boolean success = advanceTo(timeUs, true, allowTimeBeyondBuffer);
+ discardToRead();
+ return success;
+ }
+
+ /**
+ * Attempts to advance the read position to the sample before or at the specified time.
+ *
+ * @param timeUs The time to advance to.
+ * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified
+ * time, rather than to any sample before or at that time.
+ * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
+ * end of the queue, by advancing the read position to the last sample (or keyframe).
+ * @return Whether the operation was a success. A successful advance is one in which the read
+ * position was unchanged or advanced, and is now at a sample meeting the specified criteria.
+ */
+ public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {
+ return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
+ }
+
+ /**
+ * @deprecated Use {@link #read(FormatHolder, DecoderInputBuffer, boolean, boolean, long)}
+ * followed by {@link #discardToRead()}.
+ */
+ @Deprecated
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
+ boolean loadingFinished, long decodeOnlyUntilUs) {
+ int result = read(formatHolder, buffer, formatRequired, loadingFinished,
+ decodeOnlyUntilUs);
+ discardToRead();
+ return result;
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
+ * be set if the buffer's timestamp is less than this value.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ public int read(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
+ boolean loadingFinished, long decodeOnlyUntilUs) {
+ int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
+ downstreamFormat, extrasHolder);
+ switch (result) {
+ case C.RESULT_FORMAT_READ:
+ downstreamFormat = formatHolder.format;
+ return C.RESULT_FORMAT_READ;
+ case C.RESULT_BUFFER_READ:
+ if (!buffer.isEndOfStream()) {
+ if (buffer.timeUs < decodeOnlyUntilUs) {
+ buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ // Read encryption data if the sample is encrypted.
+ if (buffer.isEncrypted()) {
+ readEncryptionData(buffer, extrasHolder);
+ }
+ // Write the sample data into the holder.
+ buffer.ensureSpaceForWrite(extrasHolder.size);
+ readData(extrasHolder.offset, buffer.data, extrasHolder.size);
+ }
+ return C.RESULT_BUFFER_READ;
+ case C.RESULT_NOTHING_READ:
+ return C.RESULT_NOTHING_READ;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Reads encryption data for the current sample.
+ *
+ * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and
+ * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The
+ * same value is added to {@link SampleExtrasHolder#offset}.
+ *
+ * @param buffer The buffer into which the encryption data should be written.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ */
+ private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
+ long offset = extrasHolder.offset;
+
+ // Read the signal byte.
+ scratch.reset(1);
+ readData(offset, scratch.data, 1);
+ offset++;
+ byte signalByte = scratch.data[0];
+ boolean subsampleEncryption = (signalByte & 0x80) != 0;
+ int ivSize = signalByte & 0x7F;
+
+ // Read the initialization vector.
+ if (buffer.cryptoInfo.iv == null) {
+ buffer.cryptoInfo.iv = new byte[16];
+ }
+ readData(offset, buffer.cryptoInfo.iv, ivSize);
+ offset += ivSize;
+
+ // Read the subsample count, if present.
+ int subsampleCount;
+ if (subsampleEncryption) {
+ scratch.reset(2);
+ readData(offset, scratch.data, 2);
+ offset += 2;
+ subsampleCount = scratch.readUnsignedShort();
+ } else {
+ subsampleCount = 1;
+ }
+
+ // Write the clear and encrypted subsample sizes.
+ int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
+ if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+ clearDataSizes = new int[subsampleCount];
+ }
+ int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
+ if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+ encryptedDataSizes = new int[subsampleCount];
+ }
+ if (subsampleEncryption) {
+ int subsampleDataLength = 6 * subsampleCount;
+ scratch.reset(subsampleDataLength);
+ readData(offset, scratch.data, subsampleDataLength);
+ offset += subsampleDataLength;
+ scratch.setPosition(0);
+ for (int i = 0; i < subsampleCount; i++) {
+ clearDataSizes[i] = scratch.readUnsignedShort();
+ encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
+ }
+ } else {
+ clearDataSizes[0] = 0;
+ encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
+ }
+
+ // Populate the cryptoInfo.
+ CryptoData cryptoData = extrasHolder.cryptoData;
+ buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
+ cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode,
+ cryptoData.encryptedBlocks, cryptoData.clearBlocks);
+
+ // Adjust the offset and size to take into account the bytes read.
+ int bytesRead = (int) (offset - extrasHolder.offset);
+ extrasHolder.offset += bytesRead;
+ extrasHolder.size -= bytesRead;
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The buffer into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, ByteBuffer target, int length) {
+ advanceReadTo(absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
+ Allocation allocation = readAllocationNode.allocation;
+ target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The array into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, byte[] target, int length) {
+ advanceReadTo(absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
+ Allocation allocation = readAllocationNode.allocation;
+ System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition),
+ target, length - remaining, toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+ }
+
+ /**
+ * Advances {@link #readAllocationNode} to the specified absolute position.
+ *
+ * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced.
+ */
+ private void advanceReadTo(long absolutePosition) {
+ while (absolutePosition >= readAllocationNode.endPosition) {
+ readAllocationNode = readAllocationNode.next;
+ }
+ }
+
+ /**
+ * Advances {@link #firstAllocationNode} to the specified absolute position.
+ * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind
+ * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their
+ * underlying allocations are returned to the allocator.
+ *
+ * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced.
+ * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op.
+ */
+ private void discardDownstreamTo(long absolutePosition) {
+ if (absolutePosition == C.POSITION_UNSET) {
+ return;
+ }
+ while (absolutePosition >= firstAllocationNode.endPosition) {
+ allocator.release(firstAllocationNode.allocation);
+ firstAllocationNode = firstAllocationNode.clear();
+ }
+ // If we discarded the node referenced by readAllocationNode then we need to advance it to the
+ // first remaining node.
+ if (readAllocationNode.startPosition < firstAllocationNode.startPosition) {
+ readAllocationNode = firstAllocationNode;
+ }
+ }
+
+ // Called by the loading thread.
+
+ /**
+ * Sets a listener to be notified of changes to the upstream format.
+ *
+ * @param listener The listener.
+ */
+ public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
+ upstreamFormatChangeListener = listener;
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
+ * that are subsequently queued.
+ *
+ * @param sampleOffsetUs The timestamp offset in microseconds.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ if (this.sampleOffsetUs != sampleOffsetUs) {
+ this.sampleOffsetUs = sampleOffsetUs;
+ pendingFormatAdjustment = true;
+ }
+ }
+
+ @Override
+ public void format(Format format) {
+ Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
+ boolean formatChanged = metadataQueue.format(adjustedFormat);
+ lastUnadjustedFormat = format;
+ pendingFormatAdjustment = false;
+ if (upstreamFormatChangeListener != null && formatChanged) {
+ upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
+ }
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ if (!startWriteOperation()) {
+ int bytesSkipped = input.skip(length);
+ if (bytesSkipped == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesSkipped;
+ }
+ try {
+ length = preAppend(length);
+ int bytesAppended = input.read(writeAllocationNode.allocation.data,
+ writeAllocationNode.translateOffset(totalBytesWritten), length);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ postAppend(bytesAppended);
+ return bytesAppended;
+ } finally {
+ endWriteOperation();
+ }
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray buffer, int length) {
+ if (!startWriteOperation()) {
+ buffer.skipBytes(length);
+ return;
+ }
+ while (length > 0) {
+ int bytesAppended = preAppend(length);
+ buffer.readBytes(writeAllocationNode.allocation.data,
+ writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended);
+ length -= bytesAppended;
+ postAppend(bytesAppended);
+ }
+ endWriteOperation();
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ CryptoData cryptoData) {
+ if (pendingFormatAdjustment) {
+ format(lastUnadjustedFormat);
+ }
+ if (!startWriteOperation()) {
+ metadataQueue.commitSampleTimestamp(timeUs);
+ return;
+ }
+ try {
+ if (pendingSplice) {
+ if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {
+ return;
+ }
+ pendingSplice = false;
+ }
+ timeUs += sampleOffsetUs;
+ long absoluteOffset = totalBytesWritten - size - offset;
+ metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
+ } finally {
+ endWriteOperation();
+ }
+ }
+
+ // Private methods.
+
+ private boolean startWriteOperation() {
+ return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING);
+ }
+
+ private void endWriteOperation() {
+ if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) {
+ clearSampleData();
+ }
+ }
+
+ private void clearSampleData() {
+ metadataQueue.clearSampleData();
+ clearAllocationNodes(firstAllocationNode);
+ firstAllocationNode = new AllocationNode(0, allocationLength);
+ readAllocationNode = firstAllocationNode;
+ writeAllocationNode = firstAllocationNode;
+ totalBytesWritten = 0;
+ allocator.trim();
+ }
+
+ /**
+ * Clears allocation nodes starting from {@code fromNode}.
+ *
+ * @param fromNode The node from which to clear.
+ */
+ private void clearAllocationNodes(AllocationNode fromNode) {
+ if (!fromNode.wasInitialized) {
+ return;
+ }
+ // Bulk release allocations for performance (it's significantly faster when using
+ // DefaultAllocator because the allocator's lock only needs to be acquired and released once)
+ // [Internal: See b/29542039].
+ int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0)
+ + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength);
+ Allocation[] allocationsToRelease = new Allocation[allocationCount];
+ AllocationNode currentNode = fromNode;
+ for (int i = 0; i < allocationsToRelease.length; i++) {
+ allocationsToRelease[i] = currentNode.allocation;
+ currentNode = currentNode.clear();
+ }
+ allocator.release(allocationsToRelease);
+ }
+
+ /**
+ * Called before writing sample data to {@link #writeAllocationNode}. May cause
+ * {@link #writeAllocationNode} to be initialized.
+ *
+ * @param length The number of bytes that the caller wishes to write.
+ * @return The number of bytes that the caller is permitted to write, which may be less than
+ * {@code length}.
+ */
+ private int preAppend(int length) {
+ if (!writeAllocationNode.wasInitialized) {
+ writeAllocationNode.initialize(allocator.allocate(),
+ new AllocationNode(writeAllocationNode.endPosition, allocationLength));
+ }
+ return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));
+ }
+
+ /**
+ * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced.
+ *
+ * @param length The number of bytes that were written.
+ */
+ private void postAppend(int length) {
+ totalBytesWritten += length;
+ if (totalBytesWritten == writeAllocationNode.endPosition) {
+ writeAllocationNode = writeAllocationNode.next;
+ }
+ }
+
+ /**
+ * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
+ *
+ * @param format The {@link Format} to adjust.
+ * @param sampleOffsetUs The offset to apply.
+ * @return The adjusted {@link Format}.
+ */
+ private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
+ if (format == null) {
+ return null;
+ }
+ if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
+ }
+ return format;
+ }
+
+ /**
+ * A node in a linked list of {@link Allocation}s held by the output.
+ */
+ private static final class AllocationNode {
+
+ /**
+ * The absolute position of the start of the data (inclusive).
+ */
+ public final long startPosition;
+ /**
+ * The absolute position of the end of the data (exclusive).
+ */
+ public final long endPosition;
+ /**
+ * Whether the node has been initialized. Remains true after {@link #clear()}.
+ */
+ public boolean wasInitialized;
+ /**
+ * The {@link Allocation}, or {@code null} if the node is not initialized.
+ */
+ @Nullable public Allocation allocation;
+ /**
+ * The next {@link AllocationNode} in the list, or {@code null} if the node has not been
+ * initialized. Remains set after {@link #clear()}.
+ */
+ @Nullable public AllocationNode next;
+
+ /**
+ * @param startPosition See {@link #startPosition}.
+ * @param allocationLength The length of the {@link Allocation} with which this node will be
+ * initialized.
+ */
+ public AllocationNode(long startPosition, int allocationLength) {
+ this.startPosition = startPosition;
+ this.endPosition = startPosition + allocationLength;
+ }
+
+ /**
+ * Initializes the node.
+ *
+ * @param allocation The node's {@link Allocation}.
+ * @param next The next {@link AllocationNode}.
+ */
+ public void initialize(Allocation allocation, AllocationNode next) {
+ this.allocation = allocation;
+ this.next = next;
+ wasInitialized = true;
+ }
+
+ /**
+ * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to
+ * the specified absolute position.
+ *
+ * @param absolutePosition The absolute position.
+ * @return The corresponding offset into the allocation's data.
+ */
+ public int translateOffset(long absolutePosition) {
+ return (int) (absolutePosition - startPosition) + allocation.offset;
+ }
+
+ /**
+ * Clears {@link #allocation} and {@link #next}.
+ *
+ * @return The cleared next {@link AllocationNode}.
+ */
+ public AllocationNode clear() {
+ allocation = null;
+ AllocationNode temp = next;
+ next = null;
+ return temp;
+ }
+
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
index 447839392e..6f35438444 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -26,6 +26,8 @@ public final class SinglePeriodTimeline extends Timeline {
private static final Object ID = new Object();
+ private final long presentationStartTimeMs;
+ private final long windowStartTimeMs;
private final long periodDurationUs;
private final long windowDurationUs;
private final long windowPositionInPeriodUs;
@@ -45,8 +47,8 @@ public final class SinglePeriodTimeline extends Timeline {
}
/**
- * Creates a timeline with one period of known duration, and a window of known duration starting
- * at a specified position in the period.
+ * Creates a timeline with one period, and a window of known duration starting at a specified
+ * position in the period.
*
* @param periodDurationUs The duration of the period in microseconds.
* @param windowDurationUs The duration of the window in microseconds.
@@ -60,6 +62,31 @@ public final class SinglePeriodTimeline extends Timeline {
public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs,
long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable,
boolean isDynamic) {
+ this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs,
+ windowDefaultStartPositionUs, isSeekable, isDynamic);
+ }
+
+ /**
+ * Creates a timeline with one period, and a window of known duration starting at a specified
+ * position in the period.
+ *
+ * @param presentationStartTimeMs The start time of the presentation in milliseconds since the
+ * epoch.
+ * @param windowStartTimeMs The window's start time in milliseconds since the epoch.
+ * @param periodDurationUs The duration of the period in microseconds.
+ * @param windowDurationUs The duration of the window in microseconds.
+ * @param windowPositionInPeriodUs The position of the start of the window in the period, in
+ * microseconds.
+ * @param windowDefaultStartPositionUs The default position relative to the start of the window at
+ * which to begin playback, in microseconds.
+ * @param isSeekable Whether seeking is supported within the window.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ */
+ public SinglePeriodTimeline(long presentationStartTimeMs, long windowStartTimeMs,
+ long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs,
+ long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) {
+ this.presentationStartTimeMs = presentationStartTimeMs;
+ this.windowStartTimeMs = windowStartTimeMs;
this.periodDurationUs = periodDurationUs;
this.windowDurationUs = windowDurationUs;
this.windowPositionInPeriodUs = windowPositionInPeriodUs;
@@ -86,7 +113,7 @@ public final class SinglePeriodTimeline extends Timeline {
windowDefaultStartPositionUs = C.TIME_UNSET;
}
}
- return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic,
+ return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic,
windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs);
}
@@ -99,7 +126,7 @@ public final class SinglePeriodTimeline extends Timeline {
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Assertions.checkIndex(periodIndex, 0, 1);
Object id = setIds ? ID : null;
- return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false);
+ return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs);
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 8e38588e89..3435c01eeb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -79,7 +79,7 @@ import java.util.Arrays;
}
@Override
- public void prepare(Callback callback) {
+ public void prepare(Callback callback, long positionUs) {
callback.onPrepared(this);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
index f6ee84a6f4..99bc60d6fb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -95,8 +95,8 @@ public final class SingleSampleMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- Assertions.checkArgument(index == 0);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ Assertions.checkArgument(id.periodIndex == 0);
return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
eventHandler, eventListener, eventSourceId);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
index 393ac1988a..06410d5426 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
@@ -42,7 +42,7 @@ public final class TrackGroup {
private int hashCode;
/**
- * @param formats The track formats. Must not be null or contain null elements.
+ * @param formats The track formats. Must not be null, contain null elements or be of length 0.
*/
public TrackGroup(Format... formats) {
Assertions.checkState(formats.length > 0);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
index 3882a330f9..9531aaf32e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
@@ -16,9 +16,9 @@
package com.google.android.exoplayer2.source.chunk;
import android.util.Log;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
/**
@@ -29,22 +29,22 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut
private static final String TAG = "BaseMediaChunkOutput";
private final int[] trackTypes;
- private final DefaultTrackOutput[] trackOutputs;
+ private final SampleQueue[] sampleQueues;
/**
* @param trackTypes The track types of the individual track outputs.
- * @param trackOutputs The individual track outputs.
+ * @param sampleQueues The individual sample queues.
*/
- public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) {
+ public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) {
this.trackTypes = trackTypes;
- this.trackOutputs = trackOutputs;
+ this.sampleQueues = sampleQueues;
}
@Override
public TrackOutput track(int id, int type) {
for (int i = 0; i < trackTypes.length; i++) {
if (type == trackTypes[i]) {
- return trackOutputs[i];
+ return sampleQueues[i];
}
}
Log.e(TAG, "Unmatched track of type: " + type);
@@ -52,13 +52,13 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut
}
/**
- * Returns the current absolute write indices of the individual track outputs.
+ * Returns the current absolute write indices of the individual sample queues.
*/
public int[] getWriteIndices() {
- int[] writeIndices = new int[trackOutputs.length];
- for (int i = 0; i < trackOutputs.length; i++) {
- if (trackOutputs[i] != null) {
- writeIndices[i] = trackOutputs[i].getWriteIndex();
+ int[] writeIndices = new int[sampleQueues.length];
+ for (int i = 0; i < sampleQueues.length; i++) {
+ if (sampleQueues[i] != null) {
+ writeIndices[i] = sampleQueues[i].getWriteIndex();
}
}
return writeIndices;
@@ -66,12 +66,12 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut
/**
* Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
- * subsequently written to the track outputs.
+ * subsequently written to the sample queues.
*/
public void setSampleOffsetUs(long sampleOffsetUs) {
- for (DefaultTrackOutput trackOutput : trackOutputs) {
- if (trackOutput != null) {
- trackOutput.setSampleOffsetUs(sampleOffsetUs);
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue != null) {
+ sampleQueue.setSampleOffsetUs(sampleOffsetUs);
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
index 501f4998cf..07d1cce8cb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -186,8 +186,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
@Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- byte[] encryptionKey) {
- trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
+ CryptoData cryptoData) {
+ trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index c43f3d577a..0fc3d5881e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -19,8 +19,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -36,7 +36,7 @@ import java.util.List;
* May also be configured to expose additional embedded {@link SampleStream}s.
*/
public class ChunkSampleStream implements SampleStream, SequenceableLoader,
- Loader.Callback {
+ Loader.Callback, Loader.ReleaseCallback {
private final int primaryTrackType;
private final int[] embeddedTrackTypes;
@@ -49,8 +49,8 @@ public class ChunkSampleStream implements SampleStream, S
private final ChunkHolder nextChunkHolder;
private final LinkedList mediaChunks;
private final List readOnlyMediaChunks;
- private final DefaultTrackOutput primarySampleQueue;
- private final DefaultTrackOutput[] embeddedSampleQueues;
+ private final SampleQueue primarySampleQueue;
+ private final SampleQueue[] embeddedSampleQueues;
private final BaseMediaChunkOutput mediaChunkOutput;
private Format primaryDownstreamTrackFormat;
@@ -85,19 +85,19 @@ public class ChunkSampleStream implements SampleStream, S
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;
- embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount];
+ embeddedSampleQueues = new SampleQueue[embeddedTrackCount];
embeddedTracksSelected = new boolean[embeddedTrackCount];
int[] trackTypes = new int[1 + embeddedTrackCount];
- DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount];
+ SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];
- primarySampleQueue = new DefaultTrackOutput(allocator);
+ primarySampleQueue = new SampleQueue(allocator);
trackTypes[0] = primaryTrackType;
sampleQueues[0] = primarySampleQueue;
for (int i = 0; i < embeddedTrackCount; i++) {
- DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator);
- embeddedSampleQueues[i] = trackOutput;
- sampleQueues[i + 1] = trackOutput;
+ SampleQueue sampleQueue = new SampleQueue(allocator);
+ embeddedSampleQueues[i] = sampleQueue;
+ sampleQueues[i + 1] = sampleQueue;
trackTypes[i + 1] = embeddedTrackTypes[i];
}
@@ -106,17 +106,20 @@ public class ChunkSampleStream implements SampleStream, S
lastSeekPositionUs = positionUs;
}
+ // TODO: Generalize this method to also discard from the primary sample queue and stop discarding
+ // from this queue in readData and skipData. This will cause samples to be kept in the queue until
+ // they've been rendered, rather than being discarded as soon as they're read by the renderer.
+ // This will make in-buffer seeks more likely when seeking slightly forward from the current
+ // position. This change will need handling with care, in particular when considering removal of
+ // chunks from the front of the mediaChunks list.
/**
- * Discards buffered media for embedded tracks that are not currently selected, up to the
- * specified position.
+ * Discards buffered media for embedded tracks, up to the specified position.
*
* @param positionUs The position to discard up to, in microseconds.
*/
- public void discardUnselectedEmbeddedTracksTo(long positionUs) {
+ public void discardEmbeddedTracksTo(long positionUs) {
for (int i = 0; i < embeddedSampleQueues.length; i++) {
- if (!embeddedTracksSelected[i]) {
- embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
- }
+ embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]);
}
}
@@ -135,7 +138,8 @@ public class ChunkSampleStream implements SampleStream, S
if (embeddedTrackTypes[i] == trackType) {
Assertions.checkState(!embeddedTracksSelected[i]);
embeddedTracksSelected[i] = true;
- embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
+ embeddedSampleQueues[i].rewind();
+ embeddedSampleQueues[i].advanceTo(positionUs, true, true);
return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i);
}
}
@@ -181,19 +185,15 @@ public class ChunkSampleStream implements SampleStream, S
public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the primary sample queue.
- boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore(
- positionUs, positionUs < getNextLoadPositionUs());
+ boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true,
+ positionUs < getNextLoadPositionUs());
if (seekInsideBuffer) {
- // We succeeded. We need to discard any chunks that we've moved past and perform the seek for
- // any embedded streams as well.
- while (mediaChunks.size() > 1
- && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) {
- mediaChunks.removeFirst();
- }
- // TODO: For this to work correctly, the embedded streams must not discard anything from their
- // sample queues beyond the current read position of the primary stream.
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.skipToKeyframeBefore(positionUs, true);
+ // We succeeded. Discard samples and corresponding chunks prior to the seek position.
+ discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
+ primarySampleQueue.discardToRead();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.rewind();
+ embeddedSampleQueue.discardTo(positionUs, true, false);
}
} else {
// We failed, and need to restart.
@@ -203,9 +203,9 @@ public class ChunkSampleStream implements SampleStream, S
if (loader.isLoading()) {
loader.cancelLoading();
} else {
- primarySampleQueue.reset(true);
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.reset(true);
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
}
}
}
@@ -217,18 +217,29 @@ public class ChunkSampleStream implements SampleStream, S
* This method should be called when the stream is no longer required.
*/
public void release() {
- primarySampleQueue.disable();
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.disable();
+ boolean releasedSynchronously = loader.release(this);
+ if (!releasedSynchronously) {
+ // Discard as much as we can synchronously.
+ primarySampleQueue.discardToEnd();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.discardToEnd();
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReleased() {
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
}
- loader.release();
}
// SampleStream implementation.
@Override
public boolean isReady() {
- return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty());
+ return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample());
}
@Override
@@ -246,17 +257,22 @@ public class ChunkSampleStream implements SampleStream, S
return C.RESULT_NOTHING_READ;
}
discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
- return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
lastSeekPositionUs);
+ if (result == C.RESULT_BUFFER_READ) {
+ primarySampleQueue.discardToRead();
+ }
+ return result;
}
@Override
public void skipData(long positionUs) {
if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
- primarySampleQueue.skipAll();
+ primarySampleQueue.advanceToEnd();
} else {
- primarySampleQueue.skipToKeyframeBefore(positionUs, true);
+ primarySampleQueue.advanceTo(positionUs, true, true);
}
+ primarySampleQueue.discardToRead();
}
// Loader.Callback implementation.
@@ -279,9 +295,9 @@ public class ChunkSampleStream implements SampleStream, S
loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs,
loadable.bytesLoaded());
if (!released) {
- primarySampleQueue.reset(true);
- for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.reset(true);
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
}
callback.onContinueLoadingRequested(this);
}
@@ -336,6 +352,7 @@ public class ChunkSampleStream implements SampleStream, S
nextChunkHolder.clear();
if (endOfStream) {
+ pendingResetPositionUs = C.TIME_UNSET;
loadingFinished = true;
return true;
}
@@ -389,18 +406,20 @@ public class ChunkSampleStream implements SampleStream, S
}
private void discardDownstreamMediaChunks(int primaryStreamReadIndex) {
- while (mediaChunks.size() > 1
- && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) {
- mediaChunks.removeFirst();
+ if (!mediaChunks.isEmpty()) {
+ while (mediaChunks.size() > 1
+ && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) {
+ mediaChunks.removeFirst();
+ }
+ BaseMediaChunk currentChunk = mediaChunks.getFirst();
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ primaryDownstreamTrackFormat = trackFormat;
}
- BaseMediaChunk currentChunk = mediaChunks.getFirst();
- Format trackFormat = currentChunk.trackFormat;
- if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
- eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
- currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
- currentChunk.startTimeUs);
- }
- primaryDownstreamTrackFormat = trackFormat;
}
/**
@@ -413,18 +432,18 @@ public class ChunkSampleStream implements SampleStream, S
if (mediaChunks.size() <= queueLength) {
return false;
}
- long startTimeUs = 0;
+ BaseMediaChunk removed;
+ long startTimeUs;
long endTimeUs = mediaChunks.getLast().endTimeUs;
- BaseMediaChunk removed = null;
- while (mediaChunks.size() > queueLength) {
+ do {
removed = mediaChunks.removeLast();
startTimeUs = removed.startTimeUs;
- loadingFinished = false;
- }
+ } while (mediaChunks.size() > queueLength);
primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
}
+ loadingFinished = false;
eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
return true;
}
@@ -436,11 +455,10 @@ public class ChunkSampleStream implements SampleStream, S
public final ChunkSampleStream parent;
- private final DefaultTrackOutput sampleQueue;
+ private final SampleQueue sampleQueue;
private final int index;
- public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue,
- int index) {
+ public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) {
this.parent = parent;
this.sampleQueue = sampleQueue;
this.index = index;
@@ -448,15 +466,15 @@ public class ChunkSampleStream implements SampleStream, S
@Override
public boolean isReady() {
- return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty());
+ return loadingFinished || (!isPendingReset() && sampleQueue.hasNextSample());
}
@Override
public void skipData(long positionUs) {
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- sampleQueue.skipAll();
+ sampleQueue.advanceToEnd();
} else {
- sampleQueue.skipToKeyframeBefore(positionUs, true);
+ sampleQueue.advanceTo(positionUs, true, true);
}
}
@@ -471,7 +489,7 @@ public class ChunkSampleStream implements SampleStream, S
if (isPendingReset()) {
return C.RESULT_NOTHING_READ;
}
- return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
lastSeekPositionUs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
index cfbefc0c2e..cc39c88fd0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -93,7 +93,7 @@ public class ContainerMediaChunk extends BaseMediaChunk {
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public final void load() throws IOException, InterruptedException {
- DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
index 69474aa150..4acf0b8525 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -72,7 +72,7 @@ public final class InitializationChunk extends Chunk {
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
- DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
index a008c9cd84..02cf7dfd55 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -85,7 +85,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
- DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
try {
// Create and open the input.
long length = dataSource.open(loadDataSpec);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
index 2f07fe5294..1820d43e75 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java
@@ -130,7 +130,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
}
@Override
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
@@ -254,7 +254,6 @@ public final class TextRenderer extends BaseRenderer implements Callback {
streamFormat = null;
clearOutput();
releaseDecoder();
- super.onDisabled();
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java
index 96c8a89801..c0caf1e57a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java
@@ -667,13 +667,15 @@ import java.util.List;
int runLength = 0;
int clutIndex = 0;
int peek = data.readBits(2);
- if (!data.readBit()) {
+ if (peek != 0x00) {
runLength = 1;
clutIndex = peek;
} else if (data.readBit()) {
runLength = 3 + data.readBits(3);
clutIndex = data.readBits(2);
- } else if (!data.readBit()) {
+ } else if (data.readBit()) {
+ runLength = 1;
+ } else {
switch (data.readBits(2)) {
case 0x00:
endOfPixelCodeString = true;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
index 71ce17eeed..e438aa1837 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.ttml;
import android.text.Layout;
import android.util.Log;
-import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
@@ -100,7 +99,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map globalStyles = new HashMap<>();
Map regionMap = new HashMap<>();
- regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion());
+ regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null;
@@ -211,9 +210,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style);
}
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
- Pair ttmlRegionInfo = parseRegionAttributes(xmlParser);
- if (ttmlRegionInfo != null) {
- globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second);
+ TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser);
+ if (ttmlRegion != null) {
+ globalRegions.put(ttmlRegion.id, ttmlRegion);
}
}
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
@@ -221,41 +220,92 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
/**
- * Parses a region declaration. Supports origin and extent definition but only when defined in
- * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored.
+ * Parses a region declaration.
+ *
+ * If the region defines an origin and extent, it is required that they're defined as percentages
+ * of the viewport. Region declarations that define origin and extent in other formats are
+ * unsupported, and null is returned.
*/
- private Pair parseRegionAttributes(XmlPullParser xmlParser) {
+ private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
- String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
- String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
- if (regionOrigin == null || regionId == null) {
+ if (regionId == null) {
return null;
}
- float position = Cue.DIMEN_UNSET;
- float line = Cue.DIMEN_UNSET;
- Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
- if (originMatcher.matches()) {
- try {
- position = Float.parseFloat(originMatcher.group(1)) / 100.f;
- line = Float.parseFloat(originMatcher.group(2)) / 100.f;
- } catch (NumberFormatException e) {
- Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e);
- position = Cue.DIMEN_UNSET;
+
+ float position;
+ float line;
+ String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
+ if (regionOrigin != null) {
+ Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
+ if (originMatcher.matches()) {
+ try {
+ position = Float.parseFloat(originMatcher.group(1)) / 100f;
+ line = Float.parseFloat(originMatcher.group(2)) / 100f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
+ return null;
+ }
+ } else {
+ Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin);
+ return null;
}
+ } else {
+ Log.w(TAG, "Ignoring region without an origin");
+ return null;
+ // TODO: Should default to top left as below in this case, but need to fix
+ // https://github.com/google/ExoPlayer/issues/2953 first.
+ // Origin is omitted. Default to top left.
+ // position = 0;
+ // line = 0;
}
- float width = Cue.DIMEN_UNSET;
+
+ float width;
+ float height;
+ String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionExtent != null) {
Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
if (extentMatcher.matches()) {
try {
- width = Float.parseFloat(extentMatcher.group(1)) / 100.f;
+ width = Float.parseFloat(extentMatcher.group(1)) / 100f;
+ height = Float.parseFloat(extentMatcher.group(2)) / 100f;
} catch (NumberFormatException e) {
- Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e);
+ Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
+ return null;
}
+ } else {
+ Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin);
+ return null;
+ }
+ } else {
+ Log.w(TAG, "Ignoring region without an extent");
+ return null;
+ // TODO: Should default to extent of parent as below in this case, but need to fix
+ // https://github.com/google/ExoPlayer/issues/2953 first.
+ // Extent is omitted. Default to extent of parent.
+ // width = 1;
+ // height = 1;
+ }
+
+ @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START;
+ String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser,
+ TtmlNode.ATTR_TTS_DISPLAY_ALIGN);
+ if (displayAlign != null) {
+ switch (displayAlign.toLowerCase()) {
+ case "center":
+ lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+ line += height / 2;
+ break;
+ case "after":
+ lineAnchor = Cue.ANCHOR_TYPE_END;
+ line += height;
+ break;
+ default:
+ // Default "before" case. Do nothing.
+ break;
}
}
- return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line,
- Cue.LINE_TYPE_FRACTION, width)) : null;
+
+ return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width);
}
private String[] parseStyleIds(String parentStyleIds) {
@@ -277,7 +327,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try {
style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) {
- Log.w(TAG, "failed parsing background value: '" + attributeValue + "'");
+ Log.w(TAG, "Failed parsing background value: " + attributeValue);
}
break;
case TtmlNode.ATTR_TTS_COLOR:
@@ -285,7 +335,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try {
style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) {
- Log.w(TAG, "failed parsing color value: '" + attributeValue + "'");
+ Log.w(TAG, "Failed parsing color value: " + attributeValue);
}
break;
case TtmlNode.ATTR_TTS_FONT_FAMILY:
@@ -296,7 +346,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
style = createIfNull(style);
parseFontSize(attributeValue, style);
} catch (SubtitleDecoderException e) {
- Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'");
+ Log.w(TAG, "Failed parsing fontSize value: " + attributeValue);
}
break;
case TtmlNode.ATTR_TTS_FONT_WEIGHT:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
index 18378df445..43fa7a1bd9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -50,14 +50,15 @@ import java.util.TreeSet;
public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id";
- public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
+ public static final String ATTR_TTS_ORIGIN = "origin";
public static final String ATTR_TTS_EXTENT = "extent";
+ public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign";
+ public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
public static final String ATTR_TTS_FONT_SIZE = "fontSize";
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
public static final String ATTR_TTS_COLOR = "color";
- public static final String ATTR_TTS_ORIGIN = "origin";
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
@@ -179,7 +180,7 @@ import java.util.TreeSet;
for (Entry entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey());
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
- Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width));
+ region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width));
}
return cues;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
index 5f30834b4d..98823d7a84 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
@@ -22,20 +22,24 @@ import com.google.android.exoplayer2.text.Cue;
*/
/* package */ final class TtmlRegion {
+ public final String id;
public final float position;
public final float line;
- @Cue.LineType
- public final int lineType;
+ @Cue.LineType public final int lineType;
+ @Cue.AnchorType public final int lineAnchor;
public final float width;
- public TtmlRegion() {
- this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
+ public TtmlRegion(String id) {
+ this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
}
- public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) {
+ public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType,
+ @Cue.AnchorType int lineAnchor, float width) {
+ this.id = id;
this.position = position;
this.line = line;
this.lineType = lineType;
+ this.lineAnchor = lineAnchor;
this.width = width;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
index dc78e28e56..12f5952dd0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -154,23 +154,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
public void updateSelectedTrack(long bufferedDurationUs) {
long nowMs = SystemClock.elapsedRealtime();
- // Get the current and ideal selections.
+ // Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex;
- Format currentFormat = getSelectedFormat();
- int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
- Format idealFormat = getFormat(idealSelectedIndex);
- // Assume we can switch to the ideal selection.
- selectedIndex = idealSelectedIndex;
- // Revert back to the current selection if conditions are not suitable for switching.
- if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {
- if (idealFormat.bitrate > currentFormat.bitrate
+ selectedIndex = determineIdealSelectedIndex(nowMs);
+ if (selectedIndex == currentSelectedIndex) {
+ return;
+ }
+ if (!isBlacklisted(currentSelectedIndex, nowMs)) {
+ // Revert back to the current selection if conditions are not suitable for switching.
+ Format currentFormat = getFormat(currentSelectedIndex);
+ Format selectedFormat = getFormat(selectedIndex);
+ if (selectedFormat.bitrate > currentFormat.bitrate
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
- // The ideal track is a higher quality, but we have insufficient buffer to safely switch
+ // The selected track is a higher quality, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
selectedIndex = currentSelectedIndex;
- } else if (idealFormat.bitrate < currentFormat.bitrate
+ } else if (selectedFormat.bitrate < currentFormat.bitrate
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
- // The ideal track is a lower quality, but we have sufficient buffer to defer switching
+ // The selected track is a lower quality, but we have sufficient buffer to defer switching
// down for now.
selectedIndex = currentSelectedIndex;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index 9db77fd7ad..2a426c9c52 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
@@ -376,10 +377,21 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private final AtomicReference paramsReference;
/**
- * Constructs an instance that does not support adaptive tracks.
+ * Constructs an instance that does not support adaptive track selection.
*/
public DefaultTrackSelector() {
- this(null);
+ this((TrackSelection.Factory) null);
+ }
+
+ /**
+ * Constructs an instance that supports adaptive track selection. Adaptive track selections use
+ * the provided {@link BandwidthMeter} to determine which individual track should be used during
+ * playback.
+ *
+ * @param bandwidthMeter The {@link BandwidthMeter}.
+ */
+ public DefaultTrackSelector(BandwidthMeter bandwidthMeter) {
+ this(new AdaptiveTrackSelection.Factory(bandwidthMeter));
}
/**
@@ -424,35 +436,48 @@ public class DefaultTrackSelector extends MappingTrackSelector {
int rendererCount = rendererCapabilities.length;
TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount];
Parameters params = paramsReference.get();
- boolean videoTrackAndRendererPresent = false;
+ boolean seenVideoRendererWithMappedTracks = false;
+ boolean selectedVideoTracks = false;
for (int i = 0; i < rendererCount; i++) {
if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) {
- rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
- rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
- params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness,
- params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
- params.orientationMayChange, adaptiveTrackSelectionFactory,
- params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary);
- videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0;
+ if (!selectedVideoTracks) {
+ rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
+ rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
+ params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness,
+ params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
+ params.orientationMayChange, adaptiveTrackSelectionFactory,
+ params.exceedVideoConstraintsIfNecessary,
+ params.exceedRendererCapabilitiesIfNecessary);
+ selectedVideoTracks = rendererTrackSelections[i] != null;
+ }
+ seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0;
}
}
+ boolean selectedAudioTracks = false;
+ boolean selectedTextTracks = false;
for (int i = 0; i < rendererCount; i++) {
switch (rendererCapabilities[i].getTrackType()) {
case C.TRACK_TYPE_VIDEO:
// Already done. Do nothing.
break;
case C.TRACK_TYPE_AUDIO:
- rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
- rendererFormatSupports[i], params.preferredAudioLanguage,
- params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness,
- videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory);
+ if (!selectedAudioTracks) {
+ rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
+ rendererFormatSupports[i], params.preferredAudioLanguage,
+ params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness,
+ seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory);
+ selectedAudioTracks = rendererTrackSelections[i] != null;
+ }
break;
case C.TRACK_TYPE_TEXT:
- rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
- rendererFormatSupports[i], params.preferredTextLanguage,
- params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
+ if (!selectedTextTracks) {
+ rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
+ rendererFormatSupports[i], params.preferredTextLanguage,
+ params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
+ selectedTextTracks = rendererTrackSelections[i] != null;
+ }
break;
default:
rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(),
@@ -614,7 +639,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
continue;
}
int trackScore = isWithinConstraints ? 2 : 1;
- if (isSupported(trackFormatSupport[trackIndex], false)) {
+ boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false);
+ if (isWithinCapabilities) {
trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
}
boolean selectTrack = trackScore > selectedTrackScore;
@@ -630,7 +656,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} else {
comparisonResult = compareFormatValues(format.bitrate, selectedBitrate);
}
- selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0;
+ selectTrack = isWithinCapabilities && isWithinConstraints
+ ? comparisonResult > 0 : comparisonResult < 0;
}
if (selectTrack) {
selectedGroup = trackGroup;
@@ -867,7 +894,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
protected static boolean formatHasLanguage(Format format, String language) {
- return TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
+ return language != null
+ && TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
}
// Viewport size util methods.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
index 690723cf15..3499efdb16 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -304,10 +304,10 @@ public abstract class MappingTrackSelector extends TrackSelector {
trackSelections[i] = null;
} else {
TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i];
- Map overrides = selectionOverrides.get(i);
- SelectionOverride override = overrides == null ? null : overrides.get(rendererTrackGroup);
- if (override != null) {
- trackSelections[i] = override.createTrackSelection(rendererTrackGroup);
+ if (hasSelectionOverride(i, rendererTrackGroup)) {
+ SelectionOverride override = selectionOverrides.get(i).get(rendererTrackGroup);
+ trackSelections[i] = override == null ? null
+ : override.createTrackSelection(rendererTrackGroup);
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java
index 08b42533cc..f5aa81f325 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java
@@ -25,29 +25,22 @@ public final class Allocation {
/**
* The array containing the allocated space. The allocated space might not be at the start of the
- * array, and so {@link #translateOffset(int)} method must be used when indexing into it.
+ * array, and so {@link #offset} must be used when indexing into it.
*/
public final byte[] data;
- private final int offset;
+ /**
+ * The offset of the allocated space in {@link #data}.
+ */
+ public final int offset;
/**
* @param data The array containing the allocated space.
- * @param offset The offset of the allocated space within the array.
+ * @param offset The offset of the allocated space in {@code data}.
*/
public Allocation(byte[] data, int offset) {
this.data = data;
this.offset = offset;
}
- /**
- * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset.
- *
- * @param offset The zero-based offset to translate.
- * @return The corresponding offset in {@link #data}.
- */
- public int translateOffset(int offset) {
- return this.offset + offset;
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java
index f806f47410..d118b91378 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java
@@ -22,6 +22,7 @@ import android.net.Uri;
import com.google.android.exoplayer2.C;
import java.io.EOFException;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -71,9 +72,13 @@ public final class ContentDataSource implements DataSource {
try {
uri = dataSpec.uri;
assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r");
+ if (assetFileDescriptor == null) {
+ throw new FileNotFoundException("Could not open file descriptor for: " + uri);
+ }
inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
- long skipped = inputStream.skip(dataSpec.position);
- if (skipped < dataSpec.position) {
+ long assertStartOffset = assetFileDescriptor.getStartOffset();
+ long skipped = inputStream.skip(assertStartOffset + dataSpec.position) - assertStartOffset;
+ if (skipped != dataSpec.position) {
// We expect the skip to be satisfied in full. If it isn't then we're probably trying to
// skip beyond the end of the data.
throw new EOFException();
@@ -81,12 +86,16 @@ public final class ContentDataSource implements DataSource {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
- bytesRemaining = inputStream.available();
- if (bytesRemaining == 0) {
- // FileInputStream.available() returns 0 if the remaining length cannot be determined, or
- // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case,
- // so treat as unbounded.
- bytesRemaining = C.LENGTH_UNSET;
+ bytesRemaining = assetFileDescriptor.getLength();
+ if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) {
+ // The asset must extend to the end of the file.
+ bytesRemaining = inputStream.available();
+ if (bytesRemaining == 0) {
+ // FileInputStream.available() returns 0 if the remaining length cannot be determined,
+ // or if it's greater than Integer.MAX_VALUE. We don't know the true length in either
+ // case, so treat as unbounded.
+ bytesRemaining = C.LENGTH_UNSET;
+ }
}
}
} catch (IOException e) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
index d3c63b4454..ab1542c7a6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -68,7 +68,7 @@ public final class DataSpec {
* The position of the data when read from {@link #uri}.
*
* Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
- * of a subset of the underyling data.
+ * of a subset of the underlying data.
*/
public final long position;
/**
@@ -187,4 +187,31 @@ public final class DataSpec {
+ ", " + position + ", " + length + ", " + key + ", " + flags + "]";
}
+ /**
+ * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec. The
+ * subrange includes data from the offset up to the end of this DataSpec.
+ *
+ * @param offset The offset of the subrange.
+ * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec.
+ */
+ public DataSpec subrange(long offset) {
+ return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset);
+ }
+
+ /**
+ * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec.
+ *
+ * @param offset The offset of the subrange.
+ * @param length The length of the subrange.
+ * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec.
+ */
+ public DataSpec subrange(long offset, long length) {
+ if (offset == 0 && this.length == length) {
+ return this;
+ } else {
+ return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length,
+ key, flags);
+ }
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
index bca90ddc5c..02ccfafa89 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
@@ -33,11 +33,11 @@ import java.util.concurrent.ExecutorService;
public final class Loader implements LoaderErrorThrower {
/**
- * Thrown when an unexpected exception is encountered during loading.
+ * Thrown when an unexpected exception or error is encountered during loading.
*/
public static final class UnexpectedLoaderException extends IOException {
- public UnexpectedLoaderException(Exception cause) {
+ public UnexpectedLoaderException(Throwable cause) {
super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
}
@@ -119,17 +119,23 @@ public final class Loader implements LoaderErrorThrower {
}
+ /**
+ * A callback to be notified when a {@link Loader} has finished being released.
+ */
+ public interface ReleaseCallback {
+
+ /**
+ * Called when the {@link Loader} has finished being released.
+ */
+ void onLoaderReleased();
+
+ }
+
public static final int RETRY = 0;
public static final int RETRY_RESET_ERROR_COUNT = 1;
public static final int DONT_RETRY = 2;
public static final int DONT_RETRY_FATAL = 3;
- private static final int MSG_START = 0;
- private static final int MSG_CANCEL = 1;
- private static final int MSG_END_OF_SOURCE = 2;
- private static final int MSG_IO_EXCEPTION = 3;
- private static final int MSG_FATAL_ERROR = 4;
-
private final ExecutorService downloadExecutorService;
private LoadTask extends Loadable> currentTask;
@@ -150,7 +156,7 @@ public final class Loader implements LoaderErrorThrower {
*
* @param The type of the loadable.
* @param loadable The {@link Loadable} to load.
- * @param callback A callback to called when the load ends.
+ * @param callback A callback to be called when the load ends.
* @param defaultMinRetryCount The minimum number of times the load must be retried before
* {@link #maybeThrowError()} will propagate an error.
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
@@ -188,20 +194,28 @@ public final class Loader implements LoaderErrorThrower {
}
/**
- * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should
- * be called when the {@link Loader} is no longer required.
+ * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer
+ * required.
*
- * @param postLoadAction A {@link Runnable} to run on the loader's thread when
- * {@link Loadable#load()} is no longer running.
+ * @param callback A callback to be called when the release ends. Will be called synchronously
+ * from this method if no load is in progress, or asynchronously once the load has been
+ * canceled otherwise. May be null.
+ * @return True if {@code callback} was called synchronously. False if it will be called
+ * asynchronously or if {@code callback} is null.
*/
- public void release(Runnable postLoadAction) {
+ public boolean release(ReleaseCallback callback) {
+ boolean callbackInvoked = false;
if (currentTask != null) {
currentTask.cancel(true);
- }
- if (postLoadAction != null) {
- downloadExecutorService.execute(postLoadAction);
+ if (callback != null) {
+ downloadExecutorService.execute(new ReleaseTask(callback));
+ }
+ } else if (callback != null) {
+ callback.onLoaderReleased();
+ callbackInvoked = true;
}
downloadExecutorService.shutdown();
+ return callbackInvoked;
}
// LoaderErrorThrower implementation.
@@ -228,6 +242,12 @@ public final class Loader implements LoaderErrorThrower {
private static final String TAG = "LoadTask";
+ private static final int MSG_START = 0;
+ private static final int MSG_CANCEL = 1;
+ private static final int MSG_END_OF_SOURCE = 2;
+ private static final int MSG_IO_EXCEPTION = 3;
+ private static final int MSG_FATAL_ERROR = 4;
+
private final T loadable;
private final Loader.Callback callback;
public final int defaultMinRetryCount;
@@ -316,6 +336,14 @@ public final class Loader implements LoaderErrorThrower {
if (!released) {
obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
}
+ } catch (OutOfMemoryError e) {
+ // This can occur if a stream is malformed in a way that causes an extractor to think it
+ // needs to allocate a large amount of memory. We don't want the process to die in this
+ // case, but we do want the playback to fail.
+ Log.e(TAG, "OutOfMemory error loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
+ }
} catch (Error e) {
// We'd hope that the platform would kill the process if an Error is thrown here, but the
// executor may catch the error (b/20616433). Throw it here, but also pass and throw it from
@@ -382,4 +410,24 @@ public final class Loader implements LoaderErrorThrower {
}
+ private static final class ReleaseTask extends Handler implements Runnable {
+
+ private final ReleaseCallback callback;
+
+ public ReleaseTask(ReleaseCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ sendEmptyMessage(0);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ callback.onLoaderReleased();
+ }
+
+ }
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
index 86dc5cfedf..bb2a952b11 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -54,8 +54,8 @@ public final class CacheDataSource implements DataSource {
FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS})
public @interface Flags {}
/**
- * A flag indicating whether we will block reads if the cache key is locked. If this flag is
- * set, then we will read from upstream if the cache key is locked.
+ * A flag indicating whether we will block reads if the cache key is locked. If unset then data is
+ * read from upstream if the cache key is locked, regardless of whether the data is cached.
*/
public static final int FLAG_BLOCK_ON_CACHE = 1 << 0;
@@ -110,7 +110,23 @@ public final class CacheDataSource implements DataSource {
/**
* Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
- * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}.
+ * reading and writing the cache.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream) {
+ this(cache, upstream, 0, DEFAULT_MAX_CACHE_FILE_SIZE);
+ }
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
+ * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
*/
public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {
this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE);
@@ -123,8 +139,8 @@ public final class CacheDataSource implements DataSource {
*
* @param cache The cache.
* @param upstream A {@link DataSource} for reading data not in the cache.
- * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
- * #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
+ * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
* @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size
* exceeds this value, then the data will be fragmented into multiple cache files. The
* finer-grained this is the finer-grained the eviction policy can be.
@@ -145,8 +161,8 @@ public final class CacheDataSource implements DataSource {
* @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
* @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
* accessed read-only.
- * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
- * #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
+ * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
* @param eventListener An optional {@link EventListener} to receive events.
*/
public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
index b6fa3b4e2c..f0285da274 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
@@ -33,18 +33,26 @@ public final class CacheDataSourceFactory implements DataSource.Factory {
private final int flags;
private final EventListener eventListener;
+ /**
+ * @see CacheDataSource#CacheDataSource(Cache, DataSource)
+ */
+ public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) {
+ this(cache, upstreamFactory, 0);
+ }
+
/**
* @see CacheDataSource#CacheDataSource(Cache, DataSource, int)
*/
- public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) {
+ public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory,
+ @CacheDataSource.Flags int flags) {
this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
}
/**
* @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long)
*/
- public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags,
- long maxCacheFileSize) {
+ public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory,
+ @CacheDataSource.Flags int flags, long maxCacheFileSize) {
this(cache, upstreamFactory, new FileDataSourceFactory(),
new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null);
}
@@ -54,8 +62,8 @@ public final class CacheDataSourceFactory implements DataSource.Factory {
* EventListener)
*/
public CacheDataSourceFactory(Cache cache, Factory upstreamFactory,
- Factory cacheReadDataSourceFactory,
- DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) {
+ Factory cacheReadDataSourceFactory, DataSink.Factory cacheWriteDataSinkFactory,
+ @CacheDataSource.Flags int flags, EventListener eventListener) {
this.cache = cache;
this.upstreamFactory = upstreamFactory;
this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
index f6251dbbf1..22a7635564 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -22,28 +22,32 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
import java.io.IOException;
import java.util.NavigableSet;
/**
* Caching related utility methods.
*/
+@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public final class CacheUtil {
/** Holds the counters used during caching. */
public static class CachingCounters {
/** Total number of already cached bytes. */
- public long alreadyCachedBytes;
+ public volatile long alreadyCachedBytes;
+ /** Total number of downloaded bytes. */
+ public volatile long downloadedBytes;
/**
- * Total number of downloaded bytes.
- *
- * {@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing
- * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't
- * available in the {@code cache}.
+ * Total number of bytes. This is the sum of already cached, downloaded and missing bytes. If
+ * the length of the missing bytes is unknown this is set to {@link C#LENGTH_UNSET}.
*/
- public long downloadedBytes;
+ public volatile long totalBytes = C.LENGTH_UNSET;
}
+ /** Default buffer size to be used while caching. */
+ public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024;
+
/**
* Generates a cache key out of the given {@link Uri}.
*
@@ -64,7 +68,7 @@ public final class CacheUtil {
}
/**
- * Returns already cached and missing bytes in the {@cache} for the data defined by {@code
+ * Returns already cached and missing bytes in the {@code cache} for the data defined by {@code
* dataSpec}.
*
* @param dataSpec Defines the data to be checked.
@@ -76,14 +80,34 @@ public final class CacheUtil {
public static CachingCounters getCached(DataSpec dataSpec, Cache cache,
CachingCounters counters) {
try {
- return internalCache(dataSpec, cache, null, null, null, 0, counters);
+ return internalCache(dataSpec, cache, null, null, null, 0, counters, false);
} catch (IOException | InterruptedException e) {
throw new IllegalStateException(e);
}
}
/**
- * Caches the data defined by {@code dataSpec} while skipping already cached data.
+ * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops
+ * early if end of input is reached.
+ *
+ * @param dataSpec Defines the data to be cached.
+ * @param cache A {@link Cache} to store the data.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param counters The counters to be set during caching. If not null its values reset to
+ * zero before using. If null a new {@link CachingCounters} is created and used.
+ * @return The used {@link CachingCounters} instance.
+ * @throws IOException If an error occurs reading from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public static CachingCounters cache(DataSpec dataSpec, Cache cache,
+ DataSource upstream, CachingCounters counters) throws IOException, InterruptedException {
+ return cache(dataSpec, cache, new CacheDataSource(cache, upstream),
+ new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false);
+ }
+
+ /**
+ * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops
+ * early if end of input is reached and {@code enableEOFException} is false.
*
* @param dataSpec Defines the data to be cached.
* @param cache A {@link Cache} to store the data.
@@ -94,17 +118,20 @@ public final class CacheUtil {
* @param priority The priority of this task. Used with {@code priorityTaskManager}.
* @param counters The counters to be set during caching. If not null its values reset to
* zero before using. If null a new {@link CachingCounters} is created and used.
+ * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
+ * reached unexpectedly.
* @return The used {@link CachingCounters} instance.
* @throws IOException If an error occurs reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource,
byte[] buffer, PriorityTaskManager priorityTaskManager, int priority,
- CachingCounters counters) throws IOException, InterruptedException {
+ CachingCounters counters, boolean enableEOFException)
+ throws IOException, InterruptedException {
Assertions.checkNotNull(dataSource);
Assertions.checkNotNull(buffer);
return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority,
- counters);
+ counters, enableEOFException);
}
/**
@@ -121,21 +148,21 @@ public final class CacheUtil {
* @param priority The priority of this task. Used with {@code priorityTaskManager}.
* @param counters The counters to be set during caching. If not null its values reset to
* zero before using. If null a new {@link CachingCounters} is created and used.
+ * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
+ * reached unexpectedly.
* @return The used {@link CachingCounters} instance.
* @throws IOException If not dry run and an error occurs reading from the source.
* @throws InterruptedException If not dry run and the thread was interrupted.
*/
private static CachingCounters internalCache(DataSpec dataSpec, Cache cache,
CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager,
- int priority, CachingCounters counters) throws IOException, InterruptedException {
- long start = dataSpec.position;
+ int priority, CachingCounters counters, boolean enableEOFException)
+ throws IOException, InterruptedException {
+ long start = dataSpec.absoluteStreamPosition;
long left = dataSpec.length;
String key = getKey(dataSpec);
if (left == C.LENGTH_UNSET) {
left = cache.getContentLength(key);
- if (left == C.LENGTH_UNSET) {
- left = Long.MAX_VALUE;
- }
}
if (counters == null) {
counters = new CachingCounters();
@@ -143,8 +170,11 @@ public final class CacheUtil {
counters.alreadyCachedBytes = 0;
counters.downloadedBytes = 0;
}
- while (left > 0) {
- long blockLength = cache.getCachedBytes(key, start, left);
+ counters.totalBytes = left;
+
+ while (left != 0) {
+ long blockLength = cache.getCachedBytes(key, start,
+ left != C.LENGTH_UNSET ? left : Long.MAX_VALUE);
// Skip already cached data
if (blockLength > 0) {
counters.alreadyCachedBytes += blockLength;
@@ -152,24 +182,21 @@ public final class CacheUtil {
// There is a hole in the cache which is at least "-blockLength" long.
blockLength = -blockLength;
if (dataSource != null && buffer != null) {
- DataSpec subDataSpec = new DataSpec(dataSpec.uri, start,
- blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key);
- long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager,
- priority);
- counters.downloadedBytes += read;
+ long read = readAndDiscard(dataSpec, start, blockLength, dataSource, buffer,
+ priorityTaskManager, priority, counters);
if (read < blockLength) {
- // Reached end of data.
+ // Reached to the end of the data.
+ if (enableEOFException && left != C.LENGTH_UNSET) {
+ throw new EOFException();
+ }
break;
}
} else if (blockLength == Long.MAX_VALUE) {
- counters.downloadedBytes = C.LENGTH_UNSET;
break;
- } else {
- counters.downloadedBytes += blockLength;
}
}
start += blockLength;
- if (left != Long.MAX_VALUE) {
+ if (left != C.LENGTH_UNSET) {
left -= blockLength;
}
}
@@ -179,36 +206,56 @@ public final class CacheUtil {
/**
* Reads and discards all data specified by the {@code dataSpec}.
*
- * @param dataSpec Defines the data to be read.
+ * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length}
+ * fields are overwritten by the following parameters.
+ * @param absoluteStreamPosition The absolute position of the data to be read.
+ * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown.
* @param dataSource The {@link DataSource} to read the data from.
* @param buffer The buffer to be used while downloading.
* @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
* caching.
* @param priority The priority of this task.
+ * @param counters The counters to be set during reading.
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
- * has been reached.
+ * has been reached.
*/
- private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer,
- PriorityTaskManager priorityTaskManager, int priority)
- throws IOException, InterruptedException {
+ private static long readAndDiscard(DataSpec dataSpec, long absoluteStreamPosition, long length,
+ DataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority,
+ CachingCounters counters) throws IOException, InterruptedException {
while (true) {
if (priorityTaskManager != null) {
// Wait for any other thread with higher priority to finish its job.
priorityTaskManager.proceed(priority);
}
try {
- dataSource.open(dataSpec);
+ // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in
+ // case the given length exceeds the end of input.
+ dataSpec = new DataSpec(dataSpec.uri, dataSpec.postBody, absoluteStreamPosition,
+ dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition,
+ C.LENGTH_UNSET, dataSpec.key,
+ dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH);
+ long resolvedLength = dataSource.open(dataSpec);
+ if (counters.totalBytes == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) {
+ counters.totalBytes = dataSpec.absoluteStreamPosition + resolvedLength;
+ }
long totalRead = 0;
- while (true) {
+ while (totalRead != length) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
- int read = dataSource.read(buffer, 0, buffer.length);
+ int read = dataSource.read(buffer, 0,
+ length != C.LENGTH_UNSET ? (int) Math.min(buffer.length, length - totalRead)
+ : buffer.length);
if (read == C.RESULT_END_OF_INPUT) {
- return totalRead;
+ if (counters.totalBytes == C.LENGTH_UNSET) {
+ counters.totalBytes = dataSpec.absoluteStreamPosition + totalRead;
+ }
+ break;
}
totalRead += read;
+ counters.downloadedBytes += read;
}
+ return totalRead;
} catch (PriorityTaskManager.PriorityTooLowException exception) {
// catch and try again
} finally {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
index 14f006c850..2da6ba759b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -110,7 +110,8 @@ public final class SimpleCache implements Cache {
@Override
public synchronized NavigableSet getCachedSpans(String key) {
CachedContent cachedContent = index.get(key);
- return cachedContent == null ? null : new TreeSet(cachedContent.getSpans());
+ return cachedContent == null || cachedContent.isEmpty() ? null
+ : new TreeSet(cachedContent.getSpans());
}
@Override
@@ -286,7 +287,9 @@ public final class SimpleCache implements Cache {
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
CachedContent cachedContent = index.get(span.key);
- Assertions.checkState(cachedContent.removeSpan(span));
+ if (cachedContent == null || !cachedContent.removeSpan(span)) {
+ return;
+ }
totalSpace -= span.length;
if (removeEmptyCachedContent && cachedContent.isEmpty()) {
index.removeEmpty(cachedContent.key);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
index ea669e6f2a..db1122dbe7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
@@ -48,7 +48,7 @@ public final class MimeTypes {
public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2";
public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw";
public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw";
- public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw";
+ public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw";
public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3";
public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3";
public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd";
@@ -59,8 +59,10 @@ public final class MimeTypes {
public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus";
public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
- public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac";
+ public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
+ public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
+ public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
index 206349fa07..c00d7fa36c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -34,7 +34,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@@ -98,7 +97,7 @@ public final class Util {
private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(
"(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
- + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?");
+ + "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?");
private static final Pattern XS_DURATION_PATTERN =
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
@@ -680,25 +679,6 @@ public final class Util {
return intArray;
}
- /**
- * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec}
- * that represents the remainder of the data.
- *
- * @param dataSpec The original {@link DataSpec}.
- * @param bytesLoaded The number of bytes already loaded.
- * @return A {@link DataSpec} that represents the remainder of the data.
- */
- public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) {
- if (bytesLoaded == 0) {
- return dataSpec;
- } else {
- long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET
- : dataSpec.length - bytesLoaded;
- return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength,
- dataSpec.key, dataSpec.flags);
- }
- }
-
/**
* Returns the integer equal to the big-endian concatenation of the characters in {@code string}
* as bytes. The string must be no more than four characters long.
@@ -816,6 +796,85 @@ public final class Util {
}
}
+ /**
+ * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}.
+ */
+ @C.AudioUsage
+ public static int getAudioUsageForStreamType(@C.StreamType int streamType) {
+ switch (streamType) {
+ case C.STREAM_TYPE_ALARM:
+ return C.USAGE_ALARM;
+ case C.STREAM_TYPE_DTMF:
+ return C.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+ case C.STREAM_TYPE_NOTIFICATION:
+ return C.USAGE_NOTIFICATION;
+ case C.STREAM_TYPE_RING:
+ return C.USAGE_NOTIFICATION_RINGTONE;
+ case C.STREAM_TYPE_SYSTEM:
+ return C.USAGE_ASSISTANCE_SONIFICATION;
+ case C.STREAM_TYPE_VOICE_CALL:
+ return C.USAGE_VOICE_COMMUNICATION;
+ case C.STREAM_TYPE_USE_DEFAULT:
+ case C.STREAM_TYPE_MUSIC:
+ default:
+ return C.USAGE_MEDIA;
+ }
+ }
+
+ /**
+ * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}.
+ */
+ @C.AudioContentType
+ public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) {
+ switch (streamType) {
+ case C.STREAM_TYPE_ALARM:
+ case C.STREAM_TYPE_DTMF:
+ case C.STREAM_TYPE_NOTIFICATION:
+ case C.STREAM_TYPE_RING:
+ case C.STREAM_TYPE_SYSTEM:
+ return C.CONTENT_TYPE_SONIFICATION;
+ case C.STREAM_TYPE_VOICE_CALL:
+ return C.CONTENT_TYPE_SPEECH;
+ case C.STREAM_TYPE_USE_DEFAULT:
+ case C.STREAM_TYPE_MUSIC:
+ default:
+ return C.CONTENT_TYPE_MUSIC;
+ }
+ }
+
+ /**
+ * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}.
+ */
+ @C.StreamType
+ public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) {
+ switch (usage) {
+ case C.USAGE_MEDIA:
+ case C.USAGE_GAME:
+ case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+ return C.STREAM_TYPE_MUSIC;
+ case C.USAGE_ASSISTANCE_SONIFICATION:
+ return C.STREAM_TYPE_SYSTEM;
+ case C.USAGE_VOICE_COMMUNICATION:
+ return C.STREAM_TYPE_VOICE_CALL;
+ case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+ return C.STREAM_TYPE_DTMF;
+ case C.USAGE_ALARM:
+ return C.STREAM_TYPE_ALARM;
+ case C.USAGE_NOTIFICATION_RINGTONE:
+ return C.STREAM_TYPE_RING;
+ case C.USAGE_NOTIFICATION:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+ case C.USAGE_NOTIFICATION_EVENT:
+ return C.STREAM_TYPE_NOTIFICATION;
+ case C.USAGE_ASSISTANCE_ACCESSIBILITY:
+ case C.USAGE_UNKNOWN:
+ default:
+ return C.STREAM_TYPE_DEFAULT;
+ }
+ }
+
/**
* Makes a best guess to infer the type from a {@link Uri}.
*
@@ -977,15 +1036,15 @@ public final class Util {
int expectedLength = length - percentCharacterCount * 2;
StringBuilder builder = new StringBuilder(expectedLength);
Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
- int endOfLastMatch = 0;
+ int startOfNotEscaped = 0;
while (percentCharacterCount > 0 && matcher.find()) {
char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
- builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter);
- endOfLastMatch = matcher.end();
+ builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);
+ startOfNotEscaped = matcher.end();
percentCharacterCount--;
}
- if (endOfLastMatch < length) {
- builder.append(fileName, endOfLastMatch, length);
+ if (startOfNotEscaped < length) {
+ builder.append(fileName, startOfNotEscaped, length);
}
if (builder.length() != expectedLength) {
return null;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
new file mode 100644
index 0000000000..e32f23fed7
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
@@ -0,0 +1,336 @@
+/*
+ * 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.video;
+
+import static android.opengl.EGL14.EGL_ALPHA_SIZE;
+import static android.opengl.EGL14.EGL_BLUE_SIZE;
+import static android.opengl.EGL14.EGL_CONFIG_CAVEAT;
+import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION;
+import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY;
+import static android.opengl.EGL14.EGL_DEPTH_SIZE;
+import static android.opengl.EGL14.EGL_GREEN_SIZE;
+import static android.opengl.EGL14.EGL_HEIGHT;
+import static android.opengl.EGL14.EGL_NONE;
+import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT;
+import static android.opengl.EGL14.EGL_RED_SIZE;
+import static android.opengl.EGL14.EGL_RENDERABLE_TYPE;
+import static android.opengl.EGL14.EGL_SURFACE_TYPE;
+import static android.opengl.EGL14.EGL_TRUE;
+import static android.opengl.EGL14.EGL_WIDTH;
+import static android.opengl.EGL14.EGL_WINDOW_BIT;
+import static android.opengl.EGL14.eglChooseConfig;
+import static android.opengl.EGL14.eglCreateContext;
+import static android.opengl.EGL14.eglCreatePbufferSurface;
+import static android.opengl.EGL14.eglGetDisplay;
+import static android.opengl.EGL14.eglInitialize;
+import static android.opengl.EGL14.eglMakeCurrent;
+import static android.opengl.GLES20.glDeleteTextures;
+import static android.opengl.GLES20.glGenTextures;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.SurfaceTexture;
+import android.graphics.SurfaceTexture.OnFrameAvailableListener;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLSurface;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.view.Surface;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import javax.microedition.khronos.egl.EGL10;
+
+/**
+ * A dummy {@link Surface}.
+ */
+@TargetApi(17)
+public final class DummySurface extends Surface {
+
+ private static final String TAG = "DummySurface";
+
+ private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
+
+ private static boolean secureSupported;
+ private static boolean secureSupportedInitialized;
+
+ /**
+ * Whether the surface is secure.
+ */
+ public final boolean secure;
+
+ private final DummySurfaceThread thread;
+ private boolean threadReleased;
+
+ /**
+ * Returns whether the device supports secure dummy surfaces.
+ *
+ * @param context Any {@link Context}.
+ * @return Whether the device supports secure dummy surfaces.
+ */
+ public static synchronized boolean isSecureSupported(Context context) {
+ if (!secureSupportedInitialized) {
+ if (Util.SDK_INT >= 17) {
+ EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content")
+ && !deviceNeedsSecureDummySurfaceWorkaround(context);
+ }
+ secureSupportedInitialized = true;
+ }
+ return secureSupported;
+ }
+
+ /**
+ * Returns a newly created dummy surface. The surface must be released by calling {@link #release}
+ * when it's no longer required.
+ *
+ * Must only be called if {@link Util#SDK_INT} is 17 or higher.
+ *
+ * @param context Any {@link Context}.
+ * @param secure Whether a secure surface is required. Must only be requested if
+ * {@link #isSecureSupported(Context)} returns {@code true}.
+ * @throws IllegalStateException If a secure surface is requested on a device for which
+ * {@link #isSecureSupported(Context)} returns {@code false}.
+ */
+ public static DummySurface newInstanceV17(Context context, boolean secure) {
+ assertApiLevel17OrHigher();
+ Assertions.checkState(!secure || isSecureSupported(context));
+ DummySurfaceThread thread = new DummySurfaceThread();
+ return thread.init(secure);
+ }
+
+ private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) {
+ super(surfaceTexture);
+ this.thread = thread;
+ this.secure = secure;
+ }
+
+ @Override
+ public void release() {
+ super.release();
+ // The Surface may be released multiple times (explicitly and by Surface.finalize()). The
+ // implementation of super.release() has its own deduplication logic. Below we need to
+ // deduplicate ourselves. Synchronization is required as we don't control the thread on which
+ // Surface.finalize() is called.
+ synchronized (thread) {
+ if (!threadReleased) {
+ thread.release();
+ threadReleased = true;
+ }
+ }
+ }
+
+ private static void assertApiLevel17OrHigher() {
+ if (Util.SDK_INT < 17) {
+ throw new UnsupportedOperationException("Unsupported prior to API level 17");
+ }
+ }
+
+ /**
+ * Returns whether the device is known to advertise secure surface textures but not implement them
+ * correctly.
+ *
+ * @param context Any {@link Context}.
+ */
+ private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) {
+ return Util.SDK_INT == 24
+ && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955"))
+ && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager());
+ }
+
+ @TargetApi(24)
+ private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) {
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
+ }
+
+ private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
+ Callback {
+
+ private static final int MSG_INIT = 1;
+ private static final int MSG_UPDATE_TEXTURE = 2;
+ private static final int MSG_RELEASE = 3;
+
+ private final int[] textureIdHolder;
+ private Handler handler;
+ private SurfaceTexture surfaceTexture;
+
+ private Error initError;
+ private RuntimeException initException;
+ private DummySurface surface;
+
+ public DummySurfaceThread() {
+ super("dummySurface");
+ textureIdHolder = new int[1];
+ }
+
+ public DummySurface init(boolean secure) {
+ start();
+ handler = new Handler(getLooper(), this);
+ boolean wasInterrupted = false;
+ synchronized (this) {
+ handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget();
+ while (surface == null && initException == null && initError == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ if (initException != null) {
+ throw initException;
+ } else if (initError != null) {
+ throw initError;
+ } else {
+ return surface;
+ }
+ }
+
+ public void release() {
+ handler.sendEmptyMessage(MSG_RELEASE);
+ }
+
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ handler.sendEmptyMessage(MSG_UPDATE_TEXTURE);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INIT:
+ try {
+ initInternal(msg.arg1 != 0);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to initialize dummy surface", e);
+ initException = e;
+ } catch (Error e) {
+ Log.e(TAG, "Failed to initialize dummy surface", e);
+ initError = e;
+ } finally {
+ synchronized (this) {
+ notify();
+ }
+ }
+ return true;
+ case MSG_UPDATE_TEXTURE:
+ surfaceTexture.updateTexImage();
+ return true;
+ case MSG_RELEASE:
+ try {
+ releaseInternal();
+ } catch (Throwable e) {
+ Log.e(TAG, "Failed to release dummy surface", e);
+ } finally {
+ quit();
+ }
+ return true;
+ default:
+ return true;
+ }
+ }
+
+ private void initInternal(boolean secure) {
+ EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ Assertions.checkState(display != null, "eglGetDisplay failed");
+
+ int[] version = new int[2];
+ boolean eglInitialized = eglInitialize(display, version, 0, version, 1);
+ Assertions.checkState(eglInitialized, "eglInitialize failed");
+
+ int[] eglAttributes = new int[] {
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_ALPHA_SIZE, 8,
+ EGL_DEPTH_SIZE, 0,
+ EGL_CONFIG_CAVEAT, EGL_NONE,
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_NONE
+ };
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfigs = new int[1];
+ boolean eglChooseConfigSuccess = eglChooseConfig(display, eglAttributes, 0, configs, 0, 1,
+ numConfigs, 0);
+ Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null,
+ "eglChooseConfig failed");
+
+ EGLConfig config = configs[0];
+ int[] glAttributes;
+ if (secure) {
+ glAttributes = new int[] {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_PROTECTED_CONTENT_EXT, EGL_TRUE,
+ EGL_NONE};
+ } else {
+ glAttributes = new int[] {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_NONE};
+ }
+ EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT,
+ glAttributes, 0);
+ Assertions.checkState(context != null, "eglCreateContext failed");
+
+ int[] pbufferAttributes;
+ if (secure) {
+ pbufferAttributes = new int[] {
+ EGL_WIDTH, 1,
+ EGL_HEIGHT, 1,
+ EGL_PROTECTED_CONTENT_EXT, EGL_TRUE,
+ EGL_NONE};
+ } else {
+ pbufferAttributes = new int[] {
+ EGL_WIDTH, 1,
+ EGL_HEIGHT, 1,
+ EGL_NONE};
+ }
+ EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0);
+ Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed");
+
+ boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context);
+ Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed");
+
+ glGenTextures(1, textureIdHolder, 0);
+ surfaceTexture = new SurfaceTexture(textureIdHolder[0]);
+ surfaceTexture.setOnFrameAvailableListener(this);
+ surface = new DummySurface(this, surfaceTexture, secure);
+ }
+
+ private void releaseInternal() {
+ try {
+ surfaceTexture.release();
+ } finally {
+ surface = null;
+ surfaceTexture = null;
+ glDeleteTextures(1, textureIdHolder, 0);
+ }
+ }
+
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index ac4bb36035..07c45dcd25 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -40,6 +40,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
@@ -62,16 +63,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] {
1920, 1600, 1440, 1280, 960, 854, 640, 540, 480};
+ // Generally there is zero or one pending output stream offset. We track more offsets to allow for
+ // pending output streams that have fewer frames than the codec latency.
+ private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10;
+
+ private final Context context;
private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
private final EventDispatcher eventDispatcher;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
private final boolean deviceNeedsAutoFrcWorkaround;
+ private final long[] pendingOutputStreamOffsetsUs;
private Format[] streamFormats;
private CodecMaxValues codecMaxValues;
private Surface surface;
+ private Surface dummySurface;
@C.VideoScalingMode
private int scalingMode;
private boolean renderedFirstFrame;
@@ -95,6 +103,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private int tunnelingAudioSessionId;
/* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
+ private long outputStreamOffsetUs;
+ private int pendingOutputStreamOffsetCount;
+
/**
* @param context A context.
* @param mediaCodecSelector A decoder selector.
@@ -157,9 +168,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
+ this.context = context.getApplicationContext();
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
+ pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
+ outputStreamOffsetUs = C.TIME_UNSET;
joiningDeadlineMs = C.TIME_UNSET;
currentWidth = Format.NO_VALUE;
currentHeight = Format.NO_VALUE;
@@ -219,9 +233,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
@Override
- protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
streamFormats = formats;
- super.onStreamChanged(formats);
+ if (outputStreamOffsetUs == C.TIME_UNSET) {
+ outputStreamOffsetUs = offsetUs;
+ } else {
+ if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) {
+ Log.w(TAG, "Too many stream changes, so dropping offset: "
+ + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]);
+ } else {
+ pendingOutputStreamOffsetCount++;
+ }
+ pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs;
+ }
+ super.onStreamChanged(formats, offsetUs);
}
@Override
@@ -229,6 +254,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.onPositionReset(positionUs, joining);
clearRenderedFirstFrame();
consecutiveDroppedFrameCount = 0;
+ if (pendingOutputStreamOffsetCount != 0) {
+ outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1];
+ pendingOutputStreamOffsetCount = 0;
+ }
if (joining) {
setJoiningDeadlineMs();
} else {
@@ -238,7 +267,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
public boolean isReady() {
- if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) {
+ if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface)
+ || getCodec() == null || tunneling)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET;
return true;
@@ -260,11 +290,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.onStarted();
droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
- joiningDeadlineMs = C.TIME_UNSET;
}
@Override
protected void onStopped() {
+ joiningDeadlineMs = C.TIME_UNSET;
maybeNotifyDroppedFrames();
super.onStopped();
}
@@ -275,10 +305,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
currentHeight = Format.NO_VALUE;
currentPixelWidthHeightRatio = Format.NO_VALUE;
pendingPixelWidthHeightRatio = Format.NO_VALUE;
+ outputStreamOffsetUs = C.TIME_UNSET;
+ pendingOutputStreamOffsetCount = 0;
clearReportedVideoSize();
clearRenderedFirstFrame();
frameReleaseTimeHelper.disable();
tunnelingOnFrameRenderedListener = null;
+ tunneling = false;
try {
super.onDisabled();
} finally {
@@ -303,6 +336,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
private void setSurface(Surface surface) throws ExoPlaybackException {
+ if (surface == null) {
+ // Use a dummy surface if possible.
+ if (dummySurface != null) {
+ surface = dummySurface;
+ } else {
+ MediaCodecInfo codecInfo = getCodecInfo();
+ if (codecInfo != null && shouldUseDummySurface(codecInfo.secure)) {
+ dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);
+ surface = dummySurface;
+ }
+ }
+ }
// We only need to update the codec if the surface has changed.
if (this.surface != surface) {
this.surface = surface;
@@ -316,7 +361,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeInitCodec();
}
}
- if (surface != null) {
+ if (surface != null && surface != dummySurface) {
// If we know the video size, report it again immediately.
maybeRenotifyVideoSizeChanged();
// We haven't rendered to the new surface yet.
@@ -329,17 +374,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
}
- } else if (surface != null) {
- // The surface is unchanged and non-null. If we know the video size and/or have already
- // rendered to the surface, report these again immediately.
+ } else if (surface != null && surface != dummySurface) {
+ // The surface is set and unchanged. If we know the video size and/or have already rendered to
+ // the surface, report these again immediately.
maybeRenotifyVideoSizeChanged();
maybeRenotifyRenderedFirstFrame();
}
}
@Override
- protected boolean shouldInitCodec() {
- return super.shouldInitCodec() && surface != null && surface.isValid();
+ protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
+ return surface != null || shouldUseDummySurface(codecInfo.secure);
}
@Override
@@ -348,12 +393,34 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats);
MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
tunnelingAudioSessionId);
+ if (surface == null) {
+ Assertions.checkState(shouldUseDummySurface(codecInfo.secure));
+ if (dummySurface == null) {
+ dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);
+ }
+ surface = dummySurface;
+ }
codec.configure(mediaFormat, surface, crypto, 0);
if (Util.SDK_INT >= 23 && tunneling) {
tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
}
}
+ @Override
+ protected void releaseCodec() {
+ try {
+ super.releaseCodec();
+ } finally {
+ if (dummySurface != null) {
+ if (surface == dummySurface) {
+ surface = null;
+ }
+ dummySurface.release();
+ dummySurface = null;
+ }
+ }
+ }
+
@Override
protected void onCodecInitialized(String name, long initializedTimestampMs,
long initializationDurationMs) {
@@ -376,7 +443,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
@Override
- protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) {
+ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
&& outputFormat.containsKey(KEY_CROP_TOP);
@@ -408,27 +475,44 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
Format oldFormat, Format newFormat) {
- return areAdaptationCompatible(oldFormat, newFormat)
+ return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat)
&& newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height
- && newFormat.maxInputSize <= codecMaxValues.inputSize
- && (codecIsAdaptive
- || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
+ && newFormat.maxInputSize <= codecMaxValues.inputSize;
}
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
boolean shouldSkip) {
+ while (pendingOutputStreamOffsetCount != 0
+ && bufferPresentationTimeUs >= pendingOutputStreamOffsetsUs[0]) {
+ outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0];
+ pendingOutputStreamOffsetCount--;
+ System.arraycopy(pendingOutputStreamOffsetsUs, 1, pendingOutputStreamOffsetsUs, 0,
+ pendingOutputStreamOffsetCount);
+ }
+ long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
+
if (shouldSkip) {
- skipOutputBuffer(codec, bufferIndex);
+ skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
+ long earlyUs = bufferPresentationTimeUs - positionUs;
+ if (surface == dummySurface) {
+ // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
+ if (isBufferLate(earlyUs)) {
+ skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
+ return true;
+ }
+ return false;
+ }
+
if (!renderedFirstFrame) {
if (Util.SDK_INT >= 21) {
- renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
+ renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
} else {
- renderOutputBuffer(codec, bufferIndex);
+ renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
}
return true;
}
@@ -437,9 +521,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return false;
}
- // Compute how many microseconds it is until the buffer's presentation time.
+ // 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 earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
+ earlyUs -= elapsedSinceStartOfLoopUs;
// Compute the buffer's desired release time in nanoseconds.
long systemTimeNs = System.nanoTime();
@@ -451,15 +536,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
- // We're more than 30ms late rendering the frame.
- dropOutputBuffer(codec, bufferIndex);
+ dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
if (Util.SDK_INT >= 21) {
// Let the underlying framework time the release.
if (earlyUs < 50000) {
- renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);
+ renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);
return true;
}
} else {
@@ -475,7 +559,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Thread.currentThread().interrupt();
}
}
- renderOutputBuffer(codec, bufferIndex);
+ renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
}
@@ -493,20 +577,33 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* measured at the start of the current iteration of the rendering loop.
*/
protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
- // Drop the frame if we're more than 30ms late rendering the frame.
- return earlyUs < -30000;
+ return isBufferLate(earlyUs);
}
- private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
+ /**
+ * Skips the output buffer with the specified index.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to skip.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ */
+ protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {
TraceUtil.beginSection("skipVideoBuffer");
- codec.releaseOutputBuffer(bufferIndex, false);
+ codec.releaseOutputBuffer(index, false);
TraceUtil.endSection();
decoderCounters.skippedOutputBufferCount++;
}
- private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
+ /**
+ * Drops the output buffer with the specified index.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to drop.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ */
+ protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {
TraceUtil.beginSection("dropVideoBuffer");
- codec.releaseOutputBuffer(bufferIndex, false);
+ codec.releaseOutputBuffer(index, false);
TraceUtil.endSection();
decoderCounters.droppedOutputBufferCount++;
droppedFrames++;
@@ -518,27 +615,50 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
- private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
+ /**
+ * Renders the output buffer with the specified index. This method is only called if the platform
+ * API version of the device is less than 21.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to drop.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ */
+ protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
- codec.releaseOutputBuffer(bufferIndex, true);
+ codec.releaseOutputBuffer(index, true);
TraceUtil.endSection();
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
}
+ /**
+ * Renders the output buffer with the specified index. This method is only called if the platform
+ * API version of the device is 21 or later.
+ *
+ * @param codec The codec that owns the output buffer.
+ * @param index The index of the output buffer to drop.
+ * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+ * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.
+ */
@TargetApi(21)
- private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
+ protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs,
+ long releaseTimeNs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
- codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
+ codec.releaseOutputBuffer(index, releaseTimeNs);
TraceUtil.endSection();
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
}
+ private boolean shouldUseDummySurface(boolean codecIsSecure) {
+ return Util.SDK_INT >= 23 && !tunneling
+ && (!codecIsSecure || DummySurface.isSecureSupported(context));
+ }
+
private void setJoiningDeadlineMs() {
joiningDeadlineMs = allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
@@ -580,9 +700,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
private void maybeNotifyVideoSizeChanged() {
- if (reportedWidth != currentWidth || reportedHeight != currentHeight
+ if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE)
+ && (reportedWidth != currentWidth || reportedHeight != currentHeight
|| reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees
- || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) {
+ || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) {
eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,
currentPixelWidthHeightRatio);
reportedWidth = currentWidth;
@@ -594,8 +715,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private void maybeRenotifyVideoSizeChanged() {
if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {
- eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,
- currentPixelWidthHeightRatio);
+ eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight,
+ reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio);
}
}
@@ -609,6 +730,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
+ private static boolean isBufferLate(long earlyUs) {
+ // Class a buffer as late if it should have been presented more than 30ms ago.
+ return earlyUs < -30000;
+ }
+
@SuppressLint("InlinedApi")
private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues,
boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) {
@@ -652,7 +778,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @return Suitable {@link CodecMaxValues}.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
- private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format,
+ protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format,
Format[] streamFormats) throws DecoderQueryException {
int maxWidth = format.width;
int maxHeight = format.height;
@@ -664,7 +790,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
boolean haveUnknownDimensions = false;
for (Format streamFormat : streamFormats) {
- if (areAdaptationCompatible(format, streamFormat)) {
+ if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) {
haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE
|| streamFormat.height == Format.NO_VALUE);
maxWidth = Math.max(maxWidth, streamFormat.width);
@@ -817,17 +943,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
- * Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
- * between two {@link Format}s.
+ * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between
+ * two {@link Format}s.
*
+ * @param codecIsAdaptive Whether the codec supports seamless resolution switches.
* @param first The first format.
* @param second The second format.
- * @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
- * between two {@link Format}s.
+ * @return Whether the codec will support adaptation between the two {@link Format}s.
*/
- private static boolean areAdaptationCompatible(Format first, Format second) {
+ private static boolean areAdaptationCompatible(boolean codecIsAdaptive, Format first,
+ Format second) {
return first.sampleMimeType.equals(second.sampleMimeType)
- && getRotationDegrees(first) == getRotationDegrees(second);
+ && getRotationDegrees(first) == getRotationDegrees(second)
+ && (codecIsAdaptive || (first.width == second.width && first.height == second.height));
}
private static float getPixelWidthHeightRatio(Format format) {
@@ -838,7 +966,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees;
}
- private static final class CodecMaxValues {
+ protected static final class CodecMaxValues {
public final int width;
public final int height;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
index 4771f2572c..ad489c2312 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.C;
@TargetApi(16)
public final class VideoFrameReleaseTimeHelper {
+ private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1;
private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
@@ -52,26 +53,25 @@ public final class VideoFrameReleaseTimeHelper {
private long frameCount;
/**
- * Constructs an instance that smoothes frame release timestamps but does not align them with
+ * Constructs an instance that smooths frame release timestamps but does not align them with
* the default display's vsync signal.
*/
public VideoFrameReleaseTimeHelper() {
- this(-1 /* Value unused */, false);
+ this(DISPLAY_REFRESH_RATE_UNKNOWN);
}
/**
- * Constructs an instance that smoothes frame release timestamps and aligns them with the default
+ * Constructs an instance that smooths frame release timestamps and aligns them with the default
* display's vsync signal.
*
* @param context A context from which information about the default display can be retrieved.
*/
public VideoFrameReleaseTimeHelper(Context context) {
- this(getDefaultDisplayRefreshRate(context), true);
+ this(getDefaultDisplayRefreshRate(context));
}
- private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate,
- boolean useDefaultDisplayVsync) {
- this.useDefaultDisplayVsync = useDefaultDisplayVsync;
+ private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) {
+ useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN;
if (useDefaultDisplayVsync) {
vsyncSampler = VSyncSampler.getInstance();
vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate);
@@ -200,9 +200,10 @@ public final class VideoFrameReleaseTimeHelper {
return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
}
- private static float getDefaultDisplayRefreshRate(Context context) {
+ private static double getDefaultDisplayRefreshRate(Context context) {
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- return manager.getDefaultDisplay().getRefreshRate();
+ return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate()
+ : DISPLAY_REFRESH_RATE_UNKNOWN;
}
/**
diff --git a/library/dash/build.gradle b/library/dash/build.gradle
index ebad5a8603..aa8031467e 100644
--- a/library/dash/build.gradle
+++ b/library/dash/build.gradle
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
@@ -22,12 +23,6 @@ android {
targetSdkVersion project.ext.targetSdkVersion
}
- sourceSets {
- androidTest {
- java.srcDirs += "../../testutils/src/main/java/"
- }
- }
-
buildTypes {
debug {
testCoverageEnabled = true
@@ -36,9 +31,10 @@ android {
}
dependencies {
- compile project(':library-core')
+ compile project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion
compile 'com.android.support:support-core-utils:' + supportLibraryVersion
+ androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/androidTest/AndroidManifest.xml
index ac2511d3bd..a9b143253f 100644
--- a/library/dash/src/androidTest/AndroidManifest.xml
+++ b/library/dash/src/androidTest/AndroidManifest.xml
@@ -28,7 +28,6 @@
+ android:name="android.test.InstrumentationTestRunner"/>
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
index c9f1ca1030..bac1c272e8 100644
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
@@ -62,7 +62,7 @@ public final class DashUtilTest extends TestCase {
}
private static AdaptationSet newAdaptationSets(Representation... representations) {
- return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null);
+ return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null);
}
private static Representation newRepresentations(DrmInitData drmInitData) {
@@ -75,7 +75,7 @@ public final class DashUtilTest extends TestCase {
}
private static DrmInitData newDrmInitData() {
- return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
+ return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType",
new byte[]{1, 4, 7, 0, 3, 6}));
}
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
index 5b8760f929..3ce4b37ec6 100644
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
@@ -115,12 +115,12 @@ public class DashManifestParserTest extends InstrumentationTestCase {
buildCea708AccessibilityDescriptors("Wrong format")));
}
- private static List buildCea608AccessibilityDescriptors(String value) {
- return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value));
+ private static List buildCea608AccessibilityDescriptors(String value) {
+ return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null));
}
- private static List buildCea708AccessibilityDescriptors(String value) {
- return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value));
+ private static List buildCea708AccessibilityDescriptors(String value) {
+ return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null));
}
}
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
index c796025b08..7d77ae82d9 100644
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
+++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
@@ -30,8 +30,6 @@ import junit.framework.TestCase;
public class DashManifestTest extends TestCase {
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
- private static final List DUMMY_ACCESSIBILITY_DESCRIPTORS =
- Collections.emptyList();
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
@@ -190,8 +188,7 @@ public class DashManifestTest extends TestCase {
}
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
- return new AdaptationSet(++seed, ++seed, Arrays.asList(representations),
- DUMMY_ACCESSIBILITY_DESCRIPTORS);
+ return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null);
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
index 72f728092c..4e25c0e333 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
@@ -28,8 +28,8 @@ public interface DashChunkSource extends ChunkSource {
interface Factory {
DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
- DashManifest manifest, int periodIndex, int adaptationSetIndex,
- TrackSelection trackSelection, long elapsedRealtimeOffsetMs,
+ DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
+ TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack, boolean enableCea608Track);
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
index 5e0541cb31..d86cc8bae2 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash;
import android.util.Pair;
+import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
@@ -30,13 +31,14 @@ import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
-import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -55,7 +57,7 @@ import java.util.List;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Allocator allocator;
private final TrackGroupArray trackGroups;
- private final EmbeddedTrackInfo[] embeddedTrackInfos;
+ private final TrackGroupInfo[] trackGroupInfos;
private Callback callback;
private ChunkSampleStream[] sampleStreams;
@@ -80,9 +82,9 @@ import java.util.List;
sampleStreams = newSampleStreamArray(0);
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
adaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
- Pair result = buildTrackGroups(adaptationSets);
+ Pair result = buildTrackGroups(adaptationSets);
trackGroups = result.first;
- embeddedTrackInfos = result.second;
+ trackGroupInfos = result.second;
}
public void updateManifest(DashManifest manifest, int periodIndex) {
@@ -104,7 +106,7 @@ import java.util.List;
}
@Override
- public void prepare(Callback callback) {
+ public void prepare(Callback callback, long positionUs) {
this.callback = callback;
callback.onPrepared(this);
}
@@ -122,7 +124,6 @@ import java.util.List;
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
- int adaptationSetCount = adaptationSets.size();
HashMap> primarySampleStreams = new HashMap<>();
// First pass for primary tracks.
for (int i = 0; i < selections.length; i++) {
@@ -133,14 +134,15 @@ import java.util.List;
stream.release();
streams[i] = null;
} else {
- int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup());
- primarySampleStreams.put(adaptationSetIndex, stream);
+ int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
+ primarySampleStreams.put(trackGroupIndex, stream);
}
}
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
- if (trackGroupIndex < adaptationSetCount) {
- ChunkSampleStream stream = buildSampleStream(trackGroupIndex,
+ TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
+ if (trackGroupInfo.isPrimary) {
+ ChunkSampleStream stream = buildSampleStream(trackGroupInfo,
selections[i], positionUs);
primarySampleStreams.put(trackGroupIndex, stream);
streams[i] = stream;
@@ -160,11 +162,10 @@ import java.util.List;
// may have been replaced, selected or deselected.
if (selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
- if (trackGroupIndex >= adaptationSetCount) {
- int embeddedTrackIndex = trackGroupIndex - adaptationSetCount;
- EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex];
- int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex;
- ChunkSampleStream> primaryStream = primarySampleStreams.get(adaptationSetIndex);
+ TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
+ if (!trackGroupInfo.isPrimary) {
+ ChunkSampleStream> primaryStream = primarySampleStreams.get(
+ trackGroupInfo.primaryTrackGroupIndex);
SampleStream stream = streams[i];
boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream
: (stream instanceof EmbeddedSampleStream
@@ -172,7 +173,7 @@ import java.util.List;
if (!mayRetainStream) {
releaseIfEmbeddedSampleStream(stream);
streams[i] = primaryStream == null ? new EmptySampleStream()
- : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType);
+ : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);
streamResetFlags[i] = true;
}
}
@@ -187,7 +188,7 @@ import java.util.List;
@Override
public void discardBuffer(long positionUs) {
for (ChunkSampleStream sampleStream : sampleStreams) {
- sampleStream.discardUnselectedEmbeddedTracksTo(positionUs);
+ sampleStream.discardEmbeddedTracksTo(positionUs);
}
}
@@ -235,49 +236,114 @@ import java.util.List;
// Internal methods.
- private static Pair buildTrackGroups(
+ private static Pair buildTrackGroups(
List adaptationSets) {
- int adaptationSetCount = adaptationSets.size();
- int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets);
- TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount];
- EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount];
+ int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
- int embeddedTrackIndex = 0;
- for (int i = 0; i < adaptationSetCount; i++) {
- AdaptationSet adaptationSet = adaptationSets.get(i);
- List representations = adaptationSet.representations;
+ int primaryGroupCount = groupedAdaptationSetIndices.length;
+ boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
+ boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount];
+ int totalGroupCount = primaryGroupCount;
+ for (int i = 0; i < primaryGroupCount; i++) {
+ if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
+ primaryGroupHasEventMessageTrackFlags[i] = true;
+ totalGroupCount++;
+ }
+ if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) {
+ primaryGroupHasCea608TrackFlags[i] = true;
+ totalGroupCount++;
+ }
+ }
+
+ TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
+ TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];
+
+ int trackGroupCount = 0;
+ for (int i = 0; i < primaryGroupCount; i++) {
+ int[] adaptationSetIndices = groupedAdaptationSetIndices[i];
+ List representations = new ArrayList<>();
+ for (int adaptationSetIndex : adaptationSetIndices) {
+ representations.addAll(adaptationSets.get(adaptationSetIndex).representations);
+ }
Format[] formats = new Format[representations.size()];
for (int j = 0; j < formats.length; j++) {
formats[j] = representations.get(j).format;
}
- trackGroupArray[i] = new TrackGroup(formats);
- if (hasEventMessageTrack(adaptationSet)) {
- Format format = Format.createSampleFormat(adaptationSet.id + ":emsg",
+
+ AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
+ int primaryTrackGroupIndex = trackGroupCount;
+ boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i];
+ boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i];
+
+ trackGroups[trackGroupCount] = new TrackGroup(formats);
+ trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type,
+ adaptationSetIndices, primaryTrackGroupIndex, true, hasEventMessageTrack, hasCea608Track);
+ if (hasEventMessageTrack) {
+ Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg",
MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);
- trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format);
- embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA);
+ trackGroups[trackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_METADATA,
+ adaptationSetIndices, primaryTrackGroupIndex, false, false, false);
}
- if (hasCea608Track(adaptationSet)) {
- Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608",
+ if (hasCea608Track) {
+ Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608",
MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null);
- trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format);
- embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT);
+ trackGroups[trackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_TEXT,
+ adaptationSetIndices, primaryTrackGroupIndex, false, false, false);
}
}
- return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos);
+ return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
}
- private ChunkSampleStream buildSampleStream(int adaptationSetIndex,
+ private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) {
+ int adaptationSetCount = adaptationSets.size();
+ SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount);
+ for (int i = 0; i < adaptationSetCount; i++) {
+ idToIndexMap.put(adaptationSets.get(i).id, i);
+ }
+
+ int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][];
+ boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount];
+
+ int groupCount = 0;
+ for (int i = 0; i < adaptationSetCount; i++) {
+ if (adaptationSetUsedFlags[i]) {
+ // This adaptation set has already been included in a group.
+ continue;
+ }
+ adaptationSetUsedFlags[i] = true;
+ Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty(
+ adaptationSets.get(i).supplementalProperties);
+ if (adaptationSetSwitchingProperty == null) {
+ groupedAdaptationSetIndices[groupCount++] = new int[] {i};
+ } else {
+ String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(",");
+ int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length];
+ adaptationSetIndices[0] = i;
+ for (int j = 0; j < extraAdaptationSetIds.length; j++) {
+ int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j]));
+ adaptationSetUsedFlags[extraIndex] = true;
+ adaptationSetIndices[1 + j] = extraIndex;
+ }
+ groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
+ }
+ }
+
+ return groupCount < adaptationSetCount
+ ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices;
+ }
+
+ private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo,
TrackSelection selection, long positionUs) {
- AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex);
int embeddedTrackCount = 0;
int[] embeddedTrackTypes = new int[2];
- boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet);
+ boolean enableEventMessageTrack = trackGroupInfo.hasEmbeddedEventMessageTrack;
if (enableEventMessageTrack) {
embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA;
}
- boolean enableCea608Track = hasCea608Track(adaptationSet);
+ boolean enableCea608Track = trackGroupInfo.hasEmbeddedCea608Track;
if (enableCea608Track) {
embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT;
}
@@ -285,45 +351,48 @@ import java.util.List;
embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount);
}
DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(
- manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection,
- elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track);
- ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type,
+ manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices,
+ selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack,
+ enableCea608Track);
+ ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType,
embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount,
eventDispatcher);
return stream;
}
- private static int getEmbeddedTrackCount(List adaptationSets) {
- int embeddedTrackCount = 0;
- for (int i = 0; i < adaptationSets.size(); i++) {
- AdaptationSet adaptationSet = adaptationSets.get(i);
- if (hasEventMessageTrack(adaptationSet)) {
- embeddedTrackCount++;
- }
- if (hasCea608Track(adaptationSet)) {
- embeddedTrackCount++;
+ private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) {
+ for (int i = 0; i < descriptors.size(); i++) {
+ Descriptor descriptor = descriptors.get(i);
+ if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) {
+ return descriptor;
}
}
- return embeddedTrackCount;
+ return null;
}
- private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) {
- List representations = adaptationSet.representations;
- for (int i = 0; i < representations.size(); i++) {
- Representation representation = representations.get(i);
- if (!representation.inbandEventStreams.isEmpty()) {
- return true;
+ private static boolean hasEventMessageTrack(List adaptationSets,
+ int[] adaptationSetIndices) {
+ for (int i : adaptationSetIndices) {
+ List representations = adaptationSets.get(i).representations;
+ for (int j = 0; j < representations.size(); j++) {
+ Representation representation = representations.get(j);
+ if (!representation.inbandEventStreams.isEmpty()) {
+ return true;
+ }
}
}
return false;
}
- private static boolean hasCea608Track(AdaptationSet adaptationSet) {
- List descriptors = adaptationSet.accessibilityDescriptors;
- for (int i = 0; i < descriptors.size(); i++) {
- SchemeValuePair descriptor = descriptors.get(i);
- if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
- return true;
+ private static boolean hasCea608Track(List adaptationSets,
+ int[] adaptationSetIndices) {
+ for (int i : adaptationSetIndices) {
+ List descriptors = adaptationSets.get(i).accessibilityDescriptors;
+ for (int j = 0; j < descriptors.size(); j++) {
+ Descriptor descriptor = descriptors.get(j);
+ if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
+ return true;
+ }
}
}
return false;
@@ -340,14 +409,24 @@ import java.util.List;
}
}
- private static final class EmbeddedTrackInfo {
+ private static final class TrackGroupInfo {
- public final int adaptationSetIndex;
+ public final int[] adaptationSetIndices;
public final int trackType;
+ public final boolean isPrimary;
- public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) {
- this.adaptationSetIndex = adaptationSetIndex;
+ public final int primaryTrackGroupIndex;
+ public final boolean hasEmbeddedEventMessageTrack;
+ public final boolean hasEmbeddedCea608Track;
+
+ public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex,
+ boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) {
this.trackType = trackType;
+ this.adaptationSetIndices = adaptationSetIndices;
+ this.primaryTrackGroupIndex = primaryTrackGroupIndex;
+ this.isPrimary = isPrimary;
+ this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack;
+ this.hasEmbeddedCea608Track = hasEmbeddedCea608Track;
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index 5ab04ea7be..e17f1d26e7 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -281,7 +281,8 @@ public final class DashMediaSource implements MediaSource {
}
@Override
- public MediaPeriod createPeriod(int periodIndex, Allocator allocator, long positionUs) {
+ public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) {
+ int periodIndex = periodId.periodIndex;
EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs(
manifest.getPeriod(periodIndex).startMs);
DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest,
@@ -410,12 +411,14 @@ public final class DashMediaSource implements MediaSource {
private void resolveUtcTimingElement(UtcTimingElement timingElement) {
String scheme = timingElement.schemeIdUri;
- if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
+ if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2014")
+ || Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
resolveUtcTimingElementDirect(timingElement);
- } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) {
+ } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")
+ || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2012")) {
resolveUtcTimingElementHttp(timingElement, new Iso8601Parser());
- } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
- || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
+ } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")
+ || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) {
resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser());
} else {
// Unsupported scheme.
@@ -625,9 +628,9 @@ public final class DashMediaSource implements MediaSource {
private final long windowDefaultStartPositionUs;
private final DashManifest manifest;
- public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs,
- int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs,
- long windowDefaultStartPositionUs, DashManifest manifest) {
+ public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, int firstPeriodId,
+ long offsetInFirstPeriodUs, long windowDurationUs, long windowDefaultStartPositionUs,
+ DashManifest manifest) {
this.presentationStartTimeMs = presentationStartTimeMs;
this.windowStartTimeMs = windowStartTimeMs;
this.firstPeriodId = firstPeriodId;
@@ -650,7 +653,7 @@ public final class DashMediaSource implements MediaSource {
+ Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null;
return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),
C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)
- - offsetInFirstPeriodUs, false);
+ - offsetInFirstPeriodUs);
}
@Override
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
index e679ef635c..297052f65a 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
@@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -69,20 +70,21 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Override
public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
- DashManifest manifest, int periodIndex, int adaptationSetIndex,
- TrackSelection trackSelection, long elapsedRealtimeOffsetMs,
+ DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
+ TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack, boolean enableCea608Track) {
DataSource dataSource = dataSourceFactory.createDataSource();
return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex,
- adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs,
+ adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs,
maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track);
}
}
private final LoaderErrorThrower manifestLoaderErrorThrower;
- private final int adaptationSetIndex;
+ private final int[] adaptationSetIndices;
private final TrackSelection trackSelection;
+ private final int trackType;
private final RepresentationHolder[] representationHolders;
private final DataSource dataSource;
private final long elapsedRealtimeOffsetMs;
@@ -98,8 +100,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest.
* @param periodIndex The index of the period in the manifest.
- * @param adaptationSetIndex The index of the adaptation set in the period.
+ * @param adaptationSetIndices The indices of the adaptation sets in the period.
* @param trackSelection The track selection.
+ * @param trackType The type of the tracks in the selection.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
@@ -112,26 +115,27 @@ public class DefaultDashChunkSource implements DashChunkSource {
* @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track.
*/
public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
- DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection,
- DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad,
- boolean enableEventMessageTrack, boolean enableCea608Track) {
+ DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
+ TrackSelection trackSelection, int trackType, DataSource dataSource,
+ long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack,
+ boolean enableCea608Track) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
- this.adaptationSetIndex = adaptationSetIndex;
+ this.adaptationSetIndices = adaptationSetIndices;
this.trackSelection = trackSelection;
+ this.trackType = trackType;
this.dataSource = dataSource;
this.periodIndex = periodIndex;
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
- AdaptationSet adaptationSet = getAdaptationSet();
- List representations = adaptationSet.representations;
+ List representations = getRepresentations();
representationHolders = new RepresentationHolder[trackSelection.length()];
for (int i = 0; i < representationHolders.length; i++) {
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
representationHolders[i] = new RepresentationHolder(periodDurationUs, representation,
- enableEventMessageTrack, enableCea608Track, adaptationSet.type);
+ enableEventMessageTrack, enableCea608Track);
}
}
@@ -141,7 +145,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
manifest = newManifest;
periodIndex = newPeriodIndex;
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
- List representations = getAdaptationSet().representations;
+ List representations = getRepresentations();
for (int i = 0; i < representationHolders.length; i++) {
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
representationHolders[i].updateRepresentation(periodDurationUs, representation);
@@ -248,9 +252,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
- out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(),
- trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum,
- maxSegmentCount);
+ out.chunk = newMediaChunk(representationHolder, dataSource, trackType,
+ trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
+ trackSelection.getSelectionData(), segmentNum, maxSegmentCount);
}
@Override
@@ -298,8 +302,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
// Private methods.
- private AdaptationSet getAdaptationSet() {
- return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex);
+ private ArrayList getRepresentations() {
+ List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets;
+ ArrayList representations = new ArrayList<>();
+ for (int adaptationSetIndex : adaptationSetIndices) {
+ representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations);
+ }
+ return representations;
}
private long getNowUnixTimeUs() {
@@ -332,7 +341,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
private static Chunk newMediaChunk(RepresentationHolder representationHolder,
- DataSource dataSource, Format trackFormat, int trackSelectionReason,
+ DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason,
Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) {
Representation representation = representationHolder.representation;
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
@@ -343,8 +352,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),
segmentUri.start, segmentUri.length, representation.getCacheKey());
return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason,
- trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum,
- representationHolder.trackType, trackFormat);
+ trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat);
} else {
int segmentCount = 1;
for (int i = 1; i < maxSegmentCount; i++) {
@@ -371,7 +379,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
protected static final class RepresentationHolder {
- public final int trackType;
public final ChunkExtractorWrapper extractorWrapper;
public Representation representation;
@@ -381,10 +388,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
private int segmentNumShift;
public RepresentationHolder(long periodDurationUs, Representation representation,
- boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) {
+ boolean enableEventMessageTrack, boolean enableCea608Track) {
this.periodDurationUs = periodDurationUs;
this.representation = representation;
- this.trackType = trackType;
String containerMimeType = representation.format.containerMimeType;
if (mimeTypeIsRawText(containerMimeType)) {
extractorWrapper = null;
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
index 097676b89f..fd91a2f784 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
@@ -41,31 +41,40 @@ public class AdaptationSet {
public final int type;
/**
- * The {@link Representation}s in the adaptation set.
+ * {@link Representation}s in the adaptation set.
*/
public final List representations;
/**
- * The accessibility descriptors in the adaptation set.
+ * Accessibility descriptors in the adaptation set.
*/
- public final List accessibilityDescriptors;
+ public final List accessibilityDescriptors;
+
+ /**
+ * Supplemental properties in the adaptation set.
+ */
+ public final List supplementalProperties;
/**
* @param id A non-negative identifier for the adaptation set that's unique in the scope of its
* containing period, or {@link #ID_UNSET} if not specified.
* @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}
* {@code TRACK_TYPE_*} constants.
- * @param representations The {@link Representation}s in the adaptation set.
- * @param accessibilityDescriptors The accessibility descriptors in the adaptation set.
+ * @param representations {@link Representation}s in the adaptation set.
+ * @param accessibilityDescriptors Accessibility descriptors in the adaptation set.
+ * @param supplementalProperties Supplemental properties in the adaptation set.
*/
public AdaptationSet(int id, int type, List representations,
- List accessibilityDescriptors) {
+ List accessibilityDescriptors, List supplementalProperties) {
this.id = id;
this.type = type;
this.representations = Collections.unmodifiableList(representations);
this.accessibilityDescriptors = accessibilityDescriptors == null
- ? Collections.emptyList()
+ ? Collections.emptyList()
: Collections.unmodifiableList(accessibilityDescriptors);
+ this.supplementalProperties = supplementalProperties == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(supplementalProperties);
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
index eb51c8312d..cd02e27fce 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
@@ -134,7 +134,8 @@ public class DashManifest {
} while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex);
copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type,
- copyRepresentations, adaptationSet.accessibilityDescriptors));
+ copyRepresentations, adaptationSet.accessibilityDescriptors,
+ adaptationSet.supplementalProperties));
} while(key.periodIndex == periodIndex);
// Add back the last key which doesn't belong to the period being processed
keys.addFirst(key);
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index d4338fd812..53115a7a0e 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -239,8 +239,9 @@ public class DashManifestParser extends DefaultHandler
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE);
String language = xpp.getAttributeValue(null, "lang");
ArrayList drmSchemeDatas = new ArrayList<>();
- ArrayList inbandEventStreams = new ArrayList<>();
- ArrayList accessibilityDescriptors = new ArrayList<>();
+ ArrayList inbandEventStreams = new ArrayList<>();
+ ArrayList accessibilityDescriptors = new ArrayList<>();
+ ArrayList supplementalProperties = new ArrayList<>();
List representationInfos = new ArrayList<>();
@C.SelectionFlags int selectionFlags = 0;
@@ -265,7 +266,9 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) {
- accessibilityDescriptors.add(parseAccessibility(xpp));
+ accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility"));
+ } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) {
+ supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty"));
} else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs,
width, height, frameRate, audioChannels, audioSamplingRate, language,
@@ -280,7 +283,7 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
- inbandEventStreams.add(parseInbandEventStream(xpp));
+ inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp);
}
@@ -293,12 +296,15 @@ public class DashManifestParser extends DefaultHandler
drmSchemeDatas, inbandEventStreams));
}
- return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors);
+ return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors,
+ supplementalProperties);
}
protected AdaptationSet buildAdaptationSet(int id, int contentType,
- List representations, List accessibilityDescriptors) {
- return new AdaptationSet(id, contentType, representations, accessibilityDescriptors);
+ List representations, List accessibilityDescriptors,
+ List supplementalProperties) {
+ return new AdaptationSet(id, contentType, representations, accessibilityDescriptors,
+ supplementalProperties);
}
protected int parseContentType(XmlPullParser xpp) {
@@ -337,6 +343,7 @@ public class DashManifestParser extends DefaultHandler
IOException {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri);
+ String schemeType = xpp.getAttributeValue(null, "value");
byte[] data = null;
UUID uuid = null;
boolean requiresSecureDecoder = false;
@@ -362,34 +369,8 @@ public class DashManifestParser extends DefaultHandler
requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
- return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)
- : null;
- }
-
- /**
- * Parses an InbandEventStream element.
- *
- * @param xpp The parser from which to read.
- * @throws XmlPullParserException If an error occurs parsing the element.
- * @throws IOException If an error occurs reading the element.
- * @return A {@link SchemeValuePair} parsed from the element.
- */
- protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp)
- throws XmlPullParserException, IOException {
- return parseSchemeValuePair(xpp, "InbandEventStream");
- }
-
- /**
- * Parses an Accessibility element.
- *
- * @param xpp The parser from which to read.
- * @throws XmlPullParserException If an error occurs parsing the element.
- * @throws IOException If an error occurs reading the element.
- * @return A {@link SchemeValuePair} parsed from the element.
- */
- protected SchemeValuePair parseAccessibility(XmlPullParser xpp)
- throws XmlPullParserException, IOException {
- return parseSchemeValuePair(xpp, "Accessibility");
+ return data != null
+ ? new SchemeData(uuid, schemeType, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null;
}
/**
@@ -429,7 +410,7 @@ public class DashManifestParser extends DefaultHandler
int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels,
int adaptationSetAudioSamplingRate, String adaptationSetLanguage,
@C.SelectionFlags int adaptationSetSelectionFlags,
- List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase)
+ List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE);
@@ -442,7 +423,7 @@ public class DashManifestParser extends DefaultHandler
int audioChannels = adaptationSetAudioChannels;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate);
ArrayList drmSchemeDatas = new ArrayList<>();
- ArrayList inbandEventStreams = new ArrayList<>();
+ ArrayList inbandEventStreams = new ArrayList<>();
boolean seenFirstBaseUrl = false;
do {
@@ -466,7 +447,7 @@ public class DashManifestParser extends DefaultHandler
drmSchemeDatas.add(contentProtection);
}
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
- inbandEventStreams.add(parseInbandEventStream(xpp));
+ inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
}
} while (!XmlPullParserUtil.isEndTag(xpp, "Representation"));
@@ -480,7 +461,7 @@ public class DashManifestParser extends DefaultHandler
protected Format buildFormat(String id, String containerMimeType, int width, int height,
float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language,
- @C.SelectionFlags int selectionFlags, List accessibilityDescriptors,
+ @C.SelectionFlags int selectionFlags, List accessibilityDescriptors,
String codecs) {
String sampleMimeType = getSampleMimeType(containerMimeType, codecs);
if (sampleMimeType != null) {
@@ -509,14 +490,14 @@ public class DashManifestParser extends DefaultHandler
protected Representation buildRepresentation(RepresentationInfo representationInfo,
String contentId, ArrayList extraDrmSchemeDatas,
- ArrayList extraInbandEventStreams) {
+ ArrayList extraInbandEventStreams) {
Format format = representationInfo.format;
ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas));
}
- ArrayList inbandEventStremas = representationInfo.inbandEventStreams;
+ ArrayList inbandEventStremas = representationInfo.inbandEventStreams;
inbandEventStremas.addAll(extraInbandEventStreams);
return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format,
representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas);
@@ -809,28 +790,29 @@ public class DashManifestParser extends DefaultHandler
}
/**
- * Parses a {@link SchemeValuePair} from an element.
+ * Parses a {@link Descriptor} from an element.
*
* @param xpp The parser from which to read.
* @param tag The tag of the element being parsed.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
- * @return The parsed {@link SchemeValuePair}.
+ * @return The parsed {@link Descriptor}.
*/
- protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag)
+ protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag)
throws XmlPullParserException, IOException {
- String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+ String schemeIdUri = parseString(xpp, "schemeIdUri", "");
String value = parseString(xpp, "value", null);
+ String id = parseString(xpp, "id", null);
do {
xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, tag));
- return new SchemeValuePair(schemeIdUri, value);
+ return new Descriptor(schemeIdUri, value, id);
}
protected static int parseCea608AccessibilityChannel(
- List accessibilityDescriptors) {
+ List accessibilityDescriptors) {
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
- SchemeValuePair descriptor = accessibilityDescriptors.get(i);
+ Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)
&& descriptor.value != null) {
Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
@@ -845,9 +827,9 @@ public class DashManifestParser extends DefaultHandler
}
protected static int parseCea708AccessibilityChannel(
- List accessibilityDescriptors) {
+ List accessibilityDescriptors) {
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
- SchemeValuePair descriptor = accessibilityDescriptors.get(i);
+ Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri)
&& descriptor.value != null) {
Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
@@ -925,10 +907,10 @@ public class DashManifestParser extends DefaultHandler
public final String baseUrl;
public final SegmentBase segmentBase;
public final ArrayList drmSchemeDatas;
- public final ArrayList inbandEventStreams;
+ public final ArrayList inbandEventStreams;
public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase,
- ArrayList drmSchemeDatas, ArrayList inbandEventStreams) {
+ ArrayList drmSchemeDatas, ArrayList inbandEventStreams) {
this.format = format;
this.baseUrl = baseUrl;
this.segmentBase = segmentBase;
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java
similarity index 52%
rename from library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java
rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java
index 470bf0f989..18d0a937ab 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java
@@ -15,19 +15,37 @@
*/
package com.google.android.exoplayer2.source.dash.manifest;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
/**
- * A pair consisting of a scheme ID and value.
+ * A descriptor, as defined by ISO 23009-1, 2nd edition, 5.8.2.
*/
-public class SchemeValuePair {
+public final class Descriptor {
- public final String schemeIdUri;
- public final String value;
+ /**
+ * The scheme URI.
+ */
+ @NonNull public final String schemeIdUri;
+ /**
+ * The value, or null.
+ */
+ @Nullable public final String value;
+ /**
+ * The identifier, or null.
+ */
+ @Nullable public final String id;
- public SchemeValuePair(String schemeIdUri, String value) {
+ /**
+ * @param schemeIdUri The scheme URI.
+ * @param value The value, or null.
+ * @param id The identifier, or null.
+ */
+ public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) {
this.schemeIdUri = schemeIdUri;
this.value = value;
+ this.id = id;
}
@Override
@@ -38,14 +56,17 @@ public class SchemeValuePair {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
- SchemeValuePair other = (SchemeValuePair) obj;
- return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value);
+ Descriptor other = (Descriptor) obj;
+ return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value)
+ && Util.areEqual(id, other.id);
}
@Override
public int hashCode() {
- return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0)
- + (value != null ? value.hashCode() : 0);
+ int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ result = 31 * result + (id != null ? id.hashCode() : 0);
+ return result;
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
index 5960d4d7ba..81e4602c1d 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
@@ -65,7 +65,7 @@ public abstract class Representation {
/**
* The in-band event streams in the representation. Never null, but may be empty.
*/
- public final List inbandEventStreams;
+ public final List inbandEventStreams;
private final RangedUri initializationUri;
@@ -96,7 +96,7 @@ public abstract class Representation {
* @return The constructed instance.
*/
public static Representation newInstance(String contentId, long revisionId, Format format,
- String baseUrl, SegmentBase segmentBase, List inbandEventStreams) {
+ String baseUrl, SegmentBase segmentBase, List inbandEventStreams) {
return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams,
null);
}
@@ -115,7 +115,7 @@ public abstract class Representation {
* @return The constructed instance.
*/
public static Representation newInstance(String contentId, long revisionId, Format format,
- String baseUrl, SegmentBase segmentBase, List inbandEventStreams,
+ String baseUrl, SegmentBase segmentBase, List inbandEventStreams,
String customCacheKey) {
if (segmentBase instanceof SingleSegmentBase) {
return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl,
@@ -130,13 +130,12 @@ public abstract class Representation {
}
private Representation(String contentId, long revisionId, Format format, String baseUrl,
- SegmentBase segmentBase, List inbandEventStreams) {
+ SegmentBase segmentBase, List