emptyMap());
+ new CastTimeline(Collections.emptyList(), Collections.emptyMap());
private final SparseIntArray idsToIndex;
private final int[] ids;
@@ -108,6 +107,11 @@ import java.util.Map;
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
}
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ return ids[periodIndex];
+ }
+
// equals and hashCode implementations.
@Override
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
index d2154eec1b..997857f6b5 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
@@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack;
* @return The equivalent {@link Format}.
*/
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
- return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
- null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
+ return Format.createContainerFormat(
+ mediaTrack.getContentId(),
+ /* label= */ null,
+ mediaTrack.getContentType(),
+ /* sampleMimeType= */ null,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* selectionFlags= */ 0,
+ mediaTrack.getLanguage());
}
private CastUtils() {}
diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md
index ea84b602db..f1f6d68c81 100644
--- a/extensions/cronet/README.md
+++ b/extensions/cronet/README.md
@@ -5,37 +5,22 @@ The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
-## Build instructions ##
+## 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][]. In addition, it's necessary to get the Cronet libraries
-and enable the extension:
+The easiest way to use the extension is to add it as a gradle dependency:
-1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
- directory
-1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`,
- `cronet_impl_native_java.jar` and the `libs` directory
-1. Copy the three jar files into the `libs` directory of this extension
-1. Copy the content of the downloaded `libs` directory into the `jniLibs`
- directory of this extension
-1. In your `settings.gradle` file, add
- `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
- applies `core_settings.gradle`.
-1. In all `build.gradle` files where this extension is linked as a dependency,
- add
- ```
- android {
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- }
- ```
- to enable Java 8 features required by the Cronet library.
+```gradle
+implementation 'com.google.android.exoplayer:extension-cronet:2.X.X'
+```
+
+where `2.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+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
-[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
## Using the extension ##
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index 0a52344464..7d8c217b58 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -19,14 +19,10 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
- minSdkVersion project.ext.minSdkVersion
+ minSdkVersion 16
targetSdkVersion project.ext.targetSdkVersion
}
- sourceSets.main {
- jniLibs.srcDirs = ['jniLibs']
- }
-
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -34,9 +30,7 @@ android {
}
dependencies {
- api files('libs/cronet_api.jar')
- implementation files('libs/cronet_impl_common_java.jar')
- implementation files('libs/cronet_impl_native_java.jar')
+ api 'org.chromium.net:cronet-embedded:66.3359.158'
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'library')
@@ -47,3 +41,9 @@ ext {
javadocTitle = 'Cronet extension'
}
apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-cronet'
+ releaseDescription = 'Cronet extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/cronet/jniLibs/README.md b/extensions/cronet/jniLibs/README.md
deleted file mode 100644
index e9f0717ae6..0000000000
--- a/extensions/cronet/jniLibs/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Copy folders containing architecture specific .so files here.
diff --git a/extensions/cronet/libs/README.md b/extensions/cronet/libs/README.md
deleted file mode 100644
index 641a80db18..0000000000
--- a/extensions/cronet/libs/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Copy cronet.jar and cronet_api.jar here.
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index db980aa72b..fd6a3ce9ec 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -20,10 +20,10 @@ import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
-import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ConditionVariable;
@@ -32,6 +32,7 @@ import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -47,9 +48,10 @@ import org.chromium.net.UrlResponseInfo;
/**
* DataSource without intermediate buffer based on Cronet API set using UrlRequest.
+ *
* This class's methods are organized in the sequence of expected calls.
*/
-public class CronetDataSource extends UrlRequest.Callback implements HttpDataSource {
+public class CronetDataSource extends BaseDataSource implements HttpDataSource {
/**
* Thrown when an error is encountered when trying to open a {@link CronetDataSource}.
@@ -95,6 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
*/
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
+ /* package */ final UrlRequest.Callback urlRequestCallback;
+
private static final String TAG = "CronetDataSource";
private static final String CONTENT_TYPE = "Content-Type";
private static final String SET_COOKIE = "Set-Cookie";
@@ -108,7 +112,6 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
private final CronetEngine cronetEngine;
private final Executor executor;
private final Predicate contentTypePredicate;
- private final TransferListener super CronetDataSource> listener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
@@ -143,57 +146,73 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
/**
* @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses.
- * This may be a direct executor (i.e. executes tasks on the calling thread) in order
- * to avoid a thread hop from Cronet's internal network thread to the response handling
- * thread. However, to avoid slowing down overall network performance, care must be taken
- * to make sure response handling is a fast operation when using a direct executor.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from
- * {@link #open(DataSpec)}.
- * @param listener An optional listener.
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
*/
- public CronetDataSource(CronetEngine cronetEngine, Executor executor,
- Predicate contentTypePredicate, TransferListener super CronetDataSource> listener) {
- this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS, false, null, false);
+ public CronetDataSource(
+ CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) {
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ null,
+ false);
}
/**
* @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses.
- * This may be a direct executor (i.e. executes tasks on the calling thread) in order
- * to avoid a thread hop from Cronet's internal network thread to the response handling
- * thread. However, to avoid slowing down overall network performance, care must be taken
- * to make sure response handling is a fast operation when using a direct executor.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from
- * {@link #open(DataSpec)}.
- * @param listener An optional listener.
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
* @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 defaultRequestProperties The default request properties to be used.
*/
- public CronetDataSource(CronetEngine cronetEngine, Executor executor,
- Predicate contentTypePredicate, TransferListener super CronetDataSource> listener,
- int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
RequestProperties defaultRequestProperties) {
- this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
- readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, false);
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ Clock.DEFAULT,
+ defaultRequestProperties,
+ false);
}
/**
* @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses.
- * This may be a direct executor (i.e. executes tasks on the calling thread) in order
- * to avoid a thread hop from Cronet's internal network thread to the response handling
- * thread. However, to avoid slowing down overall network performance, care must be taken
- * to make sure response handling is a fast operation when using a direct executor.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from
- * {@link #open(DataSpec)}.
- * @param listener An optional listener.
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
* @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.
@@ -201,23 +220,42 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
* the redirect url in the "Cookie" header.
*/
- public CronetDataSource(CronetEngine cronetEngine, Executor executor,
- Predicate contentTypePredicate, TransferListener super CronetDataSource> listener,
- int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
- this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
- readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties,
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ RequestProperties defaultRequestProperties,
+ boolean handleSetCookieRequests) {
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ Clock.DEFAULT,
+ defaultRequestProperties,
handleSetCookieRequests);
}
- /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
- Predicate contentTypePredicate, TransferListener super CronetDataSource> listener,
- int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
- RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
+ /* package */ CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ Clock clock,
+ RequestProperties defaultRequestProperties,
+ boolean handleSetCookieRequests) {
+ super(/* isNetwork= */ true);
+ this.urlRequestCallback = new UrlRequestCallback();
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
this.contentTypePredicate = contentTypePredicate;
- this.listener = listener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
@@ -247,7 +285,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
@Override
public Map> getResponseHeaders() {
- return responseInfo == null ? null : responseInfo.getAllHeaders();
+ return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
}
@Override
@@ -270,6 +308,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
}
currentUrlRequest.start();
+ transferInitializing(dataSpec);
try {
boolean connectionOpened = blockUntilConnectTimeout();
if (exception != null) {
@@ -323,9 +362,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
}
opened = true;
- if (listener != null) {
- listener.onTransferStart(this, dataSpec);
- }
+ transferStarted(dataSpec);
return bytesRemaining;
}
@@ -391,9 +428,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
- if (listener != null) {
- listener.onBytesTransferred(this, bytesRead);
- }
+ bytesTransferred(bytesRead);
return bytesRead;
}
@@ -412,107 +447,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
finished = false;
if (opened) {
opened = false;
- if (listener != null) {
- listener.onTransferEnd(this);
- }
+ transferEnded();
}
}
- // UrlRequest.Callback implementation
-
- @Override
- public synchronized void onRedirectReceived(UrlRequest request, UrlResponseInfo info,
- String newLocationUrl) {
- if (request != currentUrlRequest) {
- return;
- }
- if (currentDataSpec.postBody != null) {
- int responseCode = info.getHttpStatusCode();
- // The industry standard is to disregard POST redirects when the status code is 307 or 308.
- // For other redirect response codes the POST request is converted to a GET request and the
- // redirect is followed.
- if (responseCode == 307 || responseCode == 308) {
- exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(),
- currentDataSpec);
- operation.open();
- return;
- }
- }
- if (resetTimeoutOnRedirects) {
- resetConnectTimeout();
- }
-
- Map> headers = info.getAllHeaders();
- if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
- request.followRedirect();
- } else {
- currentUrlRequest.cancel();
- DataSpec redirectUrlDataSpec = new DataSpec(Uri.parse(newLocationUrl),
- currentDataSpec.postBody, currentDataSpec.absoluteStreamPosition,
- currentDataSpec.position, currentDataSpec.length, currentDataSpec.key,
- currentDataSpec.flags);
- UrlRequest.Builder requestBuilder;
- try {
- requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
- } catch (IOException e) {
- exception = e;
- return;
- }
- String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
- attachCookies(requestBuilder, cookieHeadersValue);
- currentUrlRequest = requestBuilder.build();
- currentUrlRequest.start();
- }
- }
-
- @Override
- public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
- if (request != currentUrlRequest) {
- return;
- }
- responseInfo = info;
- operation.open();
- }
-
- @Override
- public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info,
- ByteBuffer buffer) {
- if (request != currentUrlRequest) {
- return;
- }
- operation.open();
- }
-
- @Override
- public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
- if (request != currentUrlRequest) {
- return;
- }
- finished = true;
- operation.open();
- }
-
- @Override
- public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
- CronetException error) {
- if (request != currentUrlRequest) {
- return;
- }
- if (error instanceof NetworkException
- && ((NetworkException) error).getErrorCode()
- == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
- exception = new UnknownHostException();
- } else {
- exception = error;
- }
- operation.open();
- }
-
// Internal methods.
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
- UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
- dataSpec.uri.toString(), this, executor).allowDirectExecutor();
+ UrlRequest.Builder requestBuilder =
+ cronetEngine
+ .newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
+ .allowDirectExecutor();
// Set the headers.
boolean isContentTypeHeaderSet = false;
if (defaultRequestProperties != null) {
@@ -528,8 +473,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, headerEntry.getValue());
}
- if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
- throw new IOException("POST request with non-empty body must set Content-Type");
+ if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
+ throw new IOException("HTTP request with non-empty body must set Content-Type");
}
// Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
@@ -549,12 +494,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
// requestBuilder.addHeader("Accept-Encoding", "identity");
// }
// Set the method and (if non-empty) the body.
- if (dataSpec.postBody != null) {
- requestBuilder.setHttpMethod("POST");
- if (dataSpec.postBody.length != 0) {
- requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
- executor);
- }
+ requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());
+ if (dataSpec.httpBody != null) {
+ requestBuilder.setUploadDataProvider(
+ new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);
}
return requestBuilder;
}
@@ -655,4 +598,91 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
return list == null || list.isEmpty();
}
+ private final class UrlRequestCallback extends UrlRequest.Callback {
+
+ @Override
+ public synchronized void onRedirectReceived(
+ UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
+ if (request != currentUrlRequest) {
+ return;
+ }
+ if (currentDataSpec.postBody != null) {
+ int responseCode = info.getHttpStatusCode();
+ // The industry standard is to disregard POST redirects when the status code is 307 or 308.
+ // For other redirect response codes the POST request is converted to a GET request and the
+ // redirect is followed.
+ if (responseCode == 307 || responseCode == 308) {
+ exception =
+ new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec);
+ operation.open();
+ return;
+ }
+ }
+ if (resetTimeoutOnRedirects) {
+ resetConnectTimeout();
+ }
+
+ Map> headers = info.getAllHeaders();
+ if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
+ request.followRedirect();
+ } else {
+ currentUrlRequest.cancel();
+ DataSpec redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
+ UrlRequest.Builder requestBuilder;
+ try {
+ requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
+ } catch (IOException e) {
+ exception = e;
+ return;
+ }
+ String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
+ attachCookies(requestBuilder, cookieHeadersValue);
+ currentUrlRequest = requestBuilder.build();
+ currentUrlRequest.start();
+ }
+ }
+
+ @Override
+ public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
+ if (request != currentUrlRequest) {
+ return;
+ }
+ responseInfo = info;
+ operation.open();
+ }
+
+ @Override
+ public synchronized void onReadCompleted(
+ UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) {
+ if (request != currentUrlRequest) {
+ return;
+ }
+ operation.open();
+ }
+
+ @Override
+ public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
+ if (request != currentUrlRequest) {
+ return;
+ }
+ finished = true;
+ operation.open();
+ }
+
+ @Override
+ public synchronized void onFailed(
+ UrlRequest request, UrlResponseInfo info, CronetException error) {
+ if (request != currentUrlRequest) {
+ return;
+ }
+ if (error instanceof NetworkException
+ && ((NetworkException) error).getErrorCode()
+ == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
+ exception = new UnknownHostException();
+ } else {
+ exception = error;
+ }
+ operation.open();
+ }
+ }
}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index d6237fc988..d832e4625d 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-import com.google.android.exoplayer2.upstream.DataSource;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
@@ -46,7 +46,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor;
private final Predicate contentTypePredicate;
- private final TransferListener super DataSource> transferListener;
+ private final @Nullable TransferListener transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
@@ -54,26 +54,176 @@ public final class CronetDataSourceFactory extends BaseFactory {
/**
* Constructs a CronetDataSourceFactory.
- *
- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
+ *
+ *
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
+ *
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.
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * CronetDataSource#open}.
+ * @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,
+ public CronetDataSourceFactory(
+ CronetEngineWrapper cronetEngineWrapper,
+ Executor executor,
+ Predicate contentTypePredicate,
+ HttpDataSource.Factory fallbackFactory) {
+ this(
+ cronetEngineWrapper,
+ executor,
+ contentTypePredicate,
+ /* transferListener= */ null,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ fallbackFactory);
+ }
+
+ /**
+ * 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 userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
+ public CronetDataSourceFactory(
+ CronetEngineWrapper cronetEngineWrapper,
+ Executor executor,
+ Predicate contentTypePredicate,
+ String userAgent) {
+ this(
+ cronetEngineWrapper,
+ executor,
+ contentTypePredicate,
+ /* transferListener= */ null,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ new DefaultHttpDataSourceFactory(
+ userAgent,
+ /* listener= */ null,
+ 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 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,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ String userAgent) {
+ this(
+ cronetEngineWrapper,
+ executor,
+ contentTypePredicate,
+ /* transferListener= */ null,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(
+ userAgent,
+ /* listener= */ null,
+ 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 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,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ HttpDataSource.Factory fallbackFactory) {
+ this(
+ cronetEngineWrapper,
+ executor,
+ contentTypePredicate,
+ /* transferListener= */ null,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ fallbackFactory);
+ }
+
+ /**
+ * 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,
+ @Nullable TransferListener transferListener,
HttpDataSource.Factory fallbackFactory) {
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
@@ -81,25 +231,28 @@ public final class CronetDataSourceFactory extends BaseFactory {
/**
* 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
+ *
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}.
+ * 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) {
+ public CronetDataSourceFactory(
+ CronetEngineWrapper cronetEngineWrapper,
+ Executor executor,
+ Predicate contentTypePredicate,
+ @Nullable TransferListener transferListener,
+ String userAgent) {
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
new DefaultHttpDataSourceFactory(userAgent, transferListener,
@@ -108,25 +261,30 @@ public final class CronetDataSourceFactory extends BaseFactory {
/**
* Constructs a CronetDataSourceFactory.
- *
- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
- * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ *
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}.
+ * 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, String userAgent) {
+ public CronetDataSourceFactory(
+ CronetEngineWrapper cronetEngineWrapper,
+ Executor executor,
+ Predicate contentTypePredicate,
+ @Nullable TransferListener transferListener,
+ int connectTimeoutMs,
+ 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,
@@ -135,26 +293,30 @@ public final class CronetDataSourceFactory extends BaseFactory {
/**
* Constructs a CronetDataSourceFactory.
- *
- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
+ *
+ *
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}.
+ * 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.
+ * @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,
+ public CronetDataSourceFactory(
+ CronetEngineWrapper cronetEngineWrapper,
+ Executor executor,
+ Predicate contentTypePredicate,
+ @Nullable TransferListener transferListener,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
HttpDataSource.Factory fallbackFactory) {
this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor;
@@ -173,8 +335,19 @@ public final class CronetDataSourceFactory extends BaseFactory {
if (cronetEngine == null) {
return fallbackFactory.createDataSource();
}
- return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
- connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
+ CronetDataSource dataSource =
+ new CronetDataSource(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ defaultRequestProperties);
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return dataSource;
}
}
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index 4e990cd027..3e2242826c 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -24,7 +24,6 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -81,13 +80,14 @@ public final class CronetDataSourceTest {
private DataSpec testDataSpec;
private DataSpec testPostDataSpec;
+ private DataSpec testHeadDataSpec;
private Map testResponseHeader;
private UrlResponseInfo testUrlResponseInfo;
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock private UrlRequest mockUrlRequest;
@Mock private Predicate mockContentTypePredicate;
- @Mock private TransferListener mockTransferListener;
+ @Mock private TransferListener mockTransferListener;
@Mock private Executor mockExecutor;
@Mock private NetworkException mockNetworkException;
@Mock private CronetEngine mockCronetEngine;
@@ -99,18 +99,17 @@ public final class CronetDataSourceTest {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
dataSourceUnderTest =
- spy(
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- mockContentTypePredicate,
- mockTransferListener,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
- Clock.DEFAULT,
- null,
- false));
+ new CronetDataSource(
+ mockCronetEngine,
+ mockExecutor,
+ mockContentTypePredicate,
+ TEST_CONNECT_TIMEOUT_MS,
+ TEST_READ_TIMEOUT_MS,
+ true, // resetTimeoutOnRedirects
+ Clock.DEFAULT,
+ null,
+ false);
+ dataSourceUnderTest.addTransferListener(mockTransferListener);
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
@@ -122,6 +121,9 @@ public final class CronetDataSourceTest {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
testPostDataSpec =
new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
+ testHeadDataSpec =
+ new DataSpec(
+ Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0);
testResponseHeader = new HashMap<>();
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
// This value can be anything since the DataSpec is unset.
@@ -172,9 +174,10 @@ public final class CronetDataSourceTest {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
// Invoke the callback for the previous request.
- dataSourceUnderTest.onFailed(
+ dataSourceUnderTest.urlRequestCallback.onFailed(
mockUrlRequest, testUrlResponseInfo, mockNetworkException);
- dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo);
+ dataSourceUnderTest.urlRequestCallback.onResponseStarted(
+ mockUrlRequest2, testUrlResponseInfo);
return null;
}
})
@@ -213,7 +216,8 @@ public final class CronetDataSourceTest {
public void testRequestOpen() throws HttpDataSourceException {
mockResponseStartSuccess();
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
- verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener)
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
@Test
@@ -225,7 +229,8 @@ public final class CronetDataSourceTest {
mockResponseStartSuccess();
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
- verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener)
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
@Test
@@ -239,7 +244,8 @@ public final class CronetDataSourceTest {
// Check for connection not automatically closed.
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
verify(mockUrlRequest, never()).cancel();
- verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
}
@@ -256,7 +262,8 @@ public final class CronetDataSourceTest {
// Check for connection not automatically closed.
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
verify(mockUrlRequest, never()).cancel();
- verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
}
@@ -272,7 +279,8 @@ public final class CronetDataSourceTest {
assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
- verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
}
@@ -298,7 +306,8 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
- verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
+ verify(mockTransferListener)
+ .onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true);
}
@Test
@@ -327,6 +336,15 @@ public final class CronetDataSourceTest {
}
}
+ @Test
+ public void testHeadRequestOpen() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ dataSourceUnderTest.open(testHeadDataSpec);
+ verify(mockTransferListener)
+ .onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true);
+ dataSourceUnderTest.close();
+ }
+
@Test
public void testRequestReadTwice() throws HttpDataSourceException {
mockResponseStartSuccess();
@@ -346,7 +364,8 @@ public final class CronetDataSourceTest {
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
- verify(mockTransferListener, times(2)).onBytesTransferred(dataSourceUnderTest, 8);
+ verify(mockTransferListener, times(2))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
}
@Test
@@ -386,7 +405,8 @@ public final class CronetDataSourceTest {
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
assertThat(bytesRead).isEqualTo(8);
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
- verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
}
@Test
@@ -402,7 +422,8 @@ public final class CronetDataSourceTest {
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertThat(bytesRead).isEqualTo(16);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
- verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
}
@Test
@@ -418,7 +439,8 @@ public final class CronetDataSourceTest {
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertThat(bytesRead).isEqualTo(16);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
- verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
}
@Test
@@ -433,7 +455,8 @@ public final class CronetDataSourceTest {
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
assertThat(bytesRead).isEqualTo(8);
- verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
}
@Test
@@ -447,7 +470,8 @@ public final class CronetDataSourceTest {
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
assertThat(bytesRead).isEqualTo(16);
- verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
}
@Test
@@ -464,7 +488,8 @@ public final class CronetDataSourceTest {
assertThat(bytesRead).isEqualTo(8);
dataSourceUnderTest.close();
- verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
+ verify(mockTransferListener)
+ .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
try {
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
@@ -505,9 +530,12 @@ public final class CronetDataSourceTest {
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
- verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 8);
- verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 6);
- verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 2);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
// Now we already returned the 16 bytes initially asked.
// Try to read again even though all requested 16 bytes are already returned.
@@ -518,7 +546,8 @@ public final class CronetDataSourceTest {
assertThat(returnedBuffer).isEqualTo(new byte[16]);
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
verify(mockTransferListener, never())
- .onBytesTransferred(dataSourceUnderTest, C.RESULT_END_OF_INPUT);
+ .onBytesTransferred(
+ dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
// There should still be only one call to read on cronet.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
// Check for connection not automatically closed.
@@ -559,7 +588,8 @@ public final class CronetDataSourceTest {
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
timedOutLatch.await();
- verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
@Test
@@ -597,11 +627,12 @@ public final class CronetDataSourceTest {
thread.interrupt();
timedOutLatch.await();
- verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
}
@Test
- public void testConnectResponseBeforeTimeout() throws InterruptedException {
+ public void testConnectResponseBeforeTimeout() throws Exception {
long startTimeMs = SystemClock.elapsedRealtime();
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final CountDownLatch openLatch = new CountDownLatch(1);
@@ -625,12 +656,12 @@ public final class CronetDataSourceTest {
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
assertNotCountedDown(openLatch);
// The response arrives just in time.
- dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
+ dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
openLatch.await();
}
@Test
- public void testRedirectIncreasesConnectionTimeout() throws InterruptedException {
+ public void testRedirectIncreasesConnectionTimeout() throws Exception {
long startTimeMs = SystemClock.elapsedRealtime();
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final CountDownLatch timedOutLatch = new CountDownLatch(1);
@@ -659,7 +690,7 @@ public final class CronetDataSourceTest {
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
assertNotCountedDown(timedOutLatch);
// A redirect arrives just in time.
- dataSourceUnderTest.onRedirectReceived(
+ dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
@@ -667,7 +698,7 @@ public final class CronetDataSourceTest {
// We should still be trying to open as we approach the new timeout.
assertNotCountedDown(timedOutLatch);
// A redirect arrives just in time.
- dataSourceUnderTest.onRedirectReceived(
+ dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
@@ -678,7 +709,8 @@ public final class CronetDataSourceTest {
ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
timedOutLatch.await();
- verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
assertThat(openExceptions.get()).isEqualTo(1);
}
@@ -700,18 +732,17 @@ public final class CronetDataSourceTest {
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
throws HttpDataSourceException {
dataSourceUnderTest =
- spy(
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- mockContentTypePredicate,
- mockTransferListener,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
- Clock.DEFAULT,
- null,
- true));
+ new CronetDataSource(
+ mockCronetEngine,
+ mockExecutor,
+ mockContentTypePredicate,
+ TEST_CONNECT_TIMEOUT_MS,
+ TEST_READ_TIMEOUT_MS,
+ true, // resetTimeoutOnRedirects
+ Clock.DEFAULT,
+ null,
+ true);
+ dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
mockSingleRedirectSuccess();
@@ -732,18 +763,17 @@ public final class CronetDataSourceTest {
throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
dataSourceUnderTest =
- spy(
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- mockContentTypePredicate,
- mockTransferListener,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
- Clock.DEFAULT,
- null,
- true));
+ new CronetDataSource(
+ mockCronetEngine,
+ mockExecutor,
+ mockContentTypePredicate,
+ TEST_CONNECT_TIMEOUT_MS,
+ TEST_READ_TIMEOUT_MS,
+ true, // resetTimeoutOnRedirects
+ Clock.DEFAULT,
+ null,
+ true);
+ dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
mockSingleRedirectSuccess();
@@ -772,18 +802,17 @@ public final class CronetDataSourceTest {
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
throws HttpDataSourceException {
dataSourceUnderTest =
- spy(
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- mockContentTypePredicate,
- mockTransferListener,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
- Clock.DEFAULT,
- null,
- true));
+ new CronetDataSource(
+ mockCronetEngine,
+ mockExecutor,
+ mockContentTypePredicate,
+ TEST_CONNECT_TIMEOUT_MS,
+ TEST_READ_TIMEOUT_MS,
+ true, // resetTimeoutOnRedirects
+ Clock.DEFAULT,
+ null,
+ true);
+ dataSourceUnderTest.addTransferListener(mockTransferListener);
mockSingleRedirectSuccess();
mockFollowRedirectSuccess();
@@ -800,7 +829,7 @@ public final class CronetDataSourceTest {
// the subsequent open() call succeeds.
doThrow(new NullPointerException())
.when(mockTransferListener)
- .onTransferEnd(dataSourceUnderTest);
+ .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
dataSourceUnderTest.open(testDataSpec);
try {
dataSourceUnderTest.close();
@@ -889,7 +918,8 @@ public final class CronetDataSourceTest {
new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
- dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
+ dataSourceUnderTest.urlRequestCallback.onResponseStarted(
+ mockUrlRequest, testUrlResponseInfo);
return null;
}
})
@@ -902,7 +932,7 @@ public final class CronetDataSourceTest {
new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
- dataSourceUnderTest.onRedirectReceived(
+ dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
mockUrlRequest,
createUrlResponseInfo(307), // statusCode
"http://redirect.location.com");
@@ -920,12 +950,13 @@ public final class CronetDataSourceTest {
public Object answer(InvocationOnMock invocation) throws Throwable {
if (!redirectCalled) {
redirectCalled = true;
- dataSourceUnderTest.onRedirectReceived(
+ dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
mockUrlRequest,
createUrlResponseInfoWithUrl("http://example.com/video", 300),
"http://example.com/video/redirect");
} else {
- dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
+ dataSourceUnderTest.urlRequestCallback.onResponseStarted(
+ mockUrlRequest, testUrlResponseInfo);
}
return null;
}
@@ -939,7 +970,8 @@ public final class CronetDataSourceTest {
new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
- dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
+ dataSourceUnderTest.urlRequestCallback.onResponseStarted(
+ mockUrlRequest, testUrlResponseInfo);
return null;
}
})
@@ -952,7 +984,7 @@ public final class CronetDataSourceTest {
new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
- dataSourceUnderTest.onFailed(
+ dataSourceUnderTest.urlRequestCallback.onFailed(
mockUrlRequest,
createUrlResponseInfo(500), // statusCode
mockNetworkException);
@@ -970,14 +1002,15 @@ public final class CronetDataSourceTest {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
if (positionAndRemaining[1] == 0) {
- dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
+ dataSourceUnderTest.urlRequestCallback.onSucceeded(
+ mockUrlRequest, testUrlResponseInfo);
} else {
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
positionAndRemaining[0] += readLength;
positionAndRemaining[1] -= readLength;
- dataSourceUnderTest.onReadCompleted(
+ dataSourceUnderTest.urlRequestCallback.onReadCompleted(
mockUrlRequest, testUrlResponseInfo, inputBuffer);
}
return null;
@@ -992,7 +1025,7 @@ public final class CronetDataSourceTest {
new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
- dataSourceUnderTest.onFailed(
+ dataSourceUnderTest.urlRequestCallback.onFailed(
mockUrlRequest,
createUrlResponseInfo(500), // statusCode
mockNetworkException);
diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md
index fa7ac6b9fa..d5a37db013 100644
--- a/extensions/ffmpeg/README.md
+++ b/extensions/ffmpeg/README.md
@@ -70,7 +70,8 @@ COMMON_OPTIONS="\
--enable-decoder=flac \
" && \
cd "${FFMPEG_EXT_PATH}/jni" && \
-git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \
+(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
+cd ffmpeg && \
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index e2d3a08e36..1630b6f775 100644
--- a/extensions/ffmpeg/build.gradle
+++ b/extensions/ffmpeg/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
@@ -32,6 +37,8 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
+ implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
index d7687e42ac..13e3964c71 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
@@ -26,29 +27,27 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.Collections;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Decodes and renders audio using FFmpeg.
*/
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
- /**
- * The number of input and output buffers.
- */
+ /** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
- /**
- * The initial input buffer size. Input buffers are reallocated dynamically if this value is
- * insufficient.
- */
- private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
+ /** The default input buffer size. */
+ private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
private final boolean enableFloatOutput;
- private FfmpegDecoder decoder;
+ private @MonotonicNonNull FfmpegDecoder decoder;
public FfmpegAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -57,9 +56,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
+ public FfmpegAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
- this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false);
+ this(
+ eventHandler,
+ eventListener,
+ new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
+ /* enableFloatOutput= */ false);
}
/**
@@ -72,8 +77,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
* adjustment.
*/
- public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- AudioSink audioSink, boolean enableFloatOutput) {
+ public FfmpegAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ AudioSink audioSink,
+ boolean enableFloatOutput) {
super(
eventHandler,
eventListener,
@@ -86,10 +94,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
Format format) {
- String sampleMimeType = format.sampleMimeType;
- if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
+ Assertions.checkNotNull(format.sampleMimeType);
+ if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
- } else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
+ } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
+ || !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
@@ -106,18 +115,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
- decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
- format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
+ int initialInputBufferSize =
+ format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
+ decoder =
+ new FfmpegDecoder(
+ NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
return decoder;
}
@Override
public Format getOutputFormat() {
+ Assertions.checkNotNull(decoder);
int channelCount = decoder.getChannelCount();
int sampleRate = decoder.getSampleRate();
@C.PcmEncoding int encoding = decoder.getEncoding();
- return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
- Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ Format.NO_VALUE,
+ Format.NO_VALUE,
+ channelCount,
+ sampleRate,
+ encoding,
+ Collections.emptyList(),
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
}
private boolean isOutputSupported(Format inputFormat) {
@@ -125,6 +149,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
private boolean shouldUseFloatOutput(Format inputFormat) {
+ Assertions.checkNotNull(inputFormat.sampleMimeType);
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
return false;
}
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
index 91bd82ab2a..6f3c623f3f 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
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.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
@@ -30,13 +33,12 @@ import java.util.List;
/* package */ final class FfmpegDecoder extends
SimpleDecoder {
- // Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
- private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
- // Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
+ // Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
+ private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
private final String codecName;
- private final byte[] extraData;
+ private final @Nullable byte[] extraData;
private final @C.Encoding int encoding;
private final int outputBufferSize;
@@ -45,18 +47,26 @@ import java.util.List;
private volatile int channelCount;
private volatile int sampleRate;
- public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
- String mimeType, List initializationData, boolean outputFloat)
+ public FfmpegDecoder(
+ int numInputBuffers,
+ int numOutputBuffers,
+ int initialInputBufferSize,
+ Format format,
+ boolean outputFloat)
throws FfmpegDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!FfmpegLibrary.isAvailable()) {
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
}
- codecName = FfmpegLibrary.getCodecName(mimeType);
- extraData = getExtraData(mimeType, initializationData);
+ Assertions.checkNotNull(format.sampleMimeType);
+ codecName =
+ Assertions.checkNotNull(
+ FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
+ extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
- nativeContext = ffmpegInitialize(codecName, extraData, outputFloat);
+ nativeContext =
+ ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);
if (nativeContext == 0) {
throw new FfmpegDecoderException("Initialization failed.");
}
@@ -84,7 +94,7 @@ import java.util.List;
}
@Override
- protected FfmpegDecoderException decode(
+ protected @Nullable FfmpegDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
nativeContext = ffmpegReset(nativeContext, extraData);
@@ -103,6 +113,7 @@ import java.util.List;
channelCount = ffmpegGetChannelCount(nativeContext);
sampleRate = ffmpegGetSampleRate(nativeContext);
if (sampleRate == 0 && "alac".equals(codecName)) {
+ Assertions.checkNotNull(extraData);
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
// See https://trac.ffmpeg.org/ticket/6096
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
@@ -148,7 +159,7 @@ import java.util.List;
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
* not required.
*/
- private static byte[] getExtraData(String mimeType, List initializationData) {
+ private static @Nullable byte[] getExtraData(String mimeType, List initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC:
@@ -173,12 +184,20 @@ import java.util.List;
}
}
- private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat);
+ private native long ffmpegInitialize(
+ String codecName,
+ @Nullable byte[] extraData,
+ boolean outputFloat,
+ int rawSampleRate,
+ int rawChannelCount);
+
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
ByteBuffer outputData, int outputSize);
private native int ffmpegGetChannelCount(long context);
private native int ffmpegGetSampleRate(long context);
- private native long ffmpegReset(long context, byte[] extraData);
+
+ private native long ffmpegReset(long context, @Nullable byte[] extraData);
+
private native void ffmpegRelease(long context);
}
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
index 9b3bbbb6ab..e5018a49b3 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -51,10 +53,8 @@ public final class FfmpegLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
- public static String getVersion() {
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ public static @Nullable String getVersion() {
return isAvailable() ? ffmpegGetVersion() : null;
}
@@ -62,19 +62,21 @@ public final class FfmpegLibrary {
* Returns whether the underlying library supports the specified MIME type.
*
* @param mimeType The MIME type to check.
+ * @param encoding The PCM encoding for raw audio.
*/
- public static boolean supportsFormat(String mimeType) {
+ public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
if (!isAvailable()) {
return false;
}
- String codecName = getCodecName(mimeType);
+ String codecName = getCodecName(mimeType, encoding);
return codecName != null && ffmpegHasDecoder(codecName);
}
/**
- * Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}.
+ * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
+ * if it's unsupported.
*/
- /* package */ static String getCodecName(String mimeType) {
+ /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
return "aac";
@@ -85,6 +87,7 @@ public final class FfmpegLibrary {
case MimeTypes.AUDIO_AC3:
return "ac3";
case MimeTypes.AUDIO_E_AC3:
+ case MimeTypes.AUDIO_E_AC3_JOC:
return "eac3";
case MimeTypes.AUDIO_TRUEHD:
return "truehd";
@@ -103,6 +106,14 @@ public final class FfmpegLibrary {
return "flac";
case MimeTypes.AUDIO_ALAC:
return "alac";
+ case MimeTypes.AUDIO_RAW:
+ if (encoding == C.ENCODING_PCM_MU_LAW) {
+ return "pcm_mulaw";
+ } else if (encoding == C.ENCODING_PCM_A_LAW) {
+ return "pcm_alaw";
+ } else {
+ return null;
+ }
default:
return null;
}
diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
index d077c819ab..87579ebb9a 100644
--- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
+++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
@@ -27,6 +27,7 @@ extern "C" {
#endif
#include
#include
+#include
#include
#include
}
@@ -72,8 +73,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
* provided extraData as initialization data for the decoder if it is non-NULL.
* Returns the created context.
*/
-AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
- jbyteArray extraData, jboolean outputFloat);
+AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
+ jboolean outputFloat, jint rawSampleRate,
+ jint rawChannelCount);
/**
* Decodes the packet into the output buffer, returning the number of bytes
@@ -110,13 +112,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
}
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
- jboolean outputFloat) {
+ jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
AVCodec *codec = getCodecByName(env, codecName);
if (!codec) {
LOGE("Codec not found.");
return 0L;
}
- return (jlong) createContext(env, codec, extraData, outputFloat);
+ return (jlong)createContext(env, codec, extraData, outputFloat, rawSampleRate,
+ rawChannelCount);
}
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
@@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
LOGE("Unexpected error finding codec %d.", codecId);
return 0L;
}
- return (jlong) createContext(env, codec, extraData,
- context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
+ jboolean outputFloat =
+ (jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
+ return (jlong)createContext(env, codec, extraData, outputFloat,
+ /* rawSampleRate= */ -1,
+ /* rawChannelCount= */ -1);
}
avcodec_flush_buffers(context);
@@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
return codec;
}
-AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
- jbyteArray extraData, jboolean outputFloat) {
+AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
+ jboolean outputFloat, jint rawSampleRate,
+ jint rawChannelCount) {
AVCodecContext *context = avcodec_alloc_context3(codec);
if (!context) {
LOGE("Failed to allocate context.");
@@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
}
env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata);
}
+ if (context->codec_id == AV_CODEC_ID_PCM_MULAW ||
+ context->codec_id == AV_CODEC_ID_PCM_ALAW) {
+ context->sample_rate = rawSampleRate;
+ context->channels = rawChannelCount;
+ context->channel_layout = av_get_default_channel_layout(rawChannelCount);
+ }
int result = avcodec_open2(context, codec, NULL);
if (result < 0) {
logError("avcodec_open2", result);
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index 609953130b..98b81d911a 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
index 8124f1958a..f8e61a0609 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
@@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
- assertThat(seeker.hasPendingSeek()).isTrue();
+ assertThat(seeker.isSeeking()).isTrue();
}
}
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 b236b706b8..07b7a0ccdb 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
@@ -64,8 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
}
}
- private static class TestPlaybackRunnable extends Player.DefaultEventListener
- implements Runnable {
+ private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
index 0bbee1ea30..b9c6ea06dd 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
@@ -15,15 +15,11 @@
*/
package com.google.android.exoplayer2.ext.flac;
-import android.support.annotation.Nullable;
-import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
-import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
-import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo;
-import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -33,111 +29,51 @@ import java.nio.ByteBuffer;
* This seeker performs seeking by using binary search within the stream, until it finds the
* frame that contains the target sample.
*/
-/* package */ final class FlacBinarySearchSeeker {
+/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
- /**
- * When seeking within the source, if the offset is smaller than or equal to this value, the seek
- * operation will be performed using a skip operation. Otherwise, the source will be reloaded at
- * the new seek position.
- */
- private static final long MAX_SKIP_BYTES = 256 * 1024;
-
- private final FlacStreamInfo streamInfo;
- private final FlacBinarySearchSeekMap seekMap;
private final FlacDecoderJni decoderJni;
- private final long firstFramePosition;
- private final long inputLength;
- private final long approxBytesPerFrame;
-
- private @Nullable SeekOperationParams pendingSeekOperationParams;
-
public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
- this.streamInfo = Assertions.checkNotNull(streamInfo);
+ super(
+ new FlacSeekTimestampConverter(streamInfo),
+ new FlacTimestampSeeker(decoderJni),
+ streamInfo.durationUs(),
+ /* floorTimePosition= */ 0,
+ /* ceilingTimePosition= */ streamInfo.totalSamples,
+ /* floorBytePosition= */ firstFramePosition,
+ /* ceilingBytePosition= */ inputLength,
+ /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
+ /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
- this.firstFramePosition = firstFramePosition;
- this.inputLength = inputLength;
- this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
-
- pendingSeekOperationParams = null;
- seekMap =
- new FlacBinarySearchSeekMap(
- streamInfo,
- firstFramePosition,
- inputLength,
- streamInfo.durationUs(),
- approxBytesPerFrame);
}
- /** Returns the seek map for the wrapped FLAC stream. */
- public SeekMap getSeekMap() {
- return seekMap;
+ @Override
+ protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
+ if (!foundTargetFrame) {
+ // If we can't find the target frame (sample), we need to reset the decoder jni so that
+ // it can continue from the result position.
+ decoderJni.reset(resultPosition);
+ }
}
- /** Sets the target time in microseconds within the stream to seek to. */
- public void setSeekTargetUs(long timeUs) {
- if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
- return;
+ private static final class FlacTimestampSeeker implements TimestampSeeker {
+
+ private final FlacDecoderJni decoderJni;
+
+ private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
+ this.decoderJni = decoderJni;
}
- pendingSeekOperationParams =
- new SeekOperationParams(
- timeUs,
- streamInfo.getSampleIndex(timeUs),
- /* floorSample= */ 0,
- /* ceilingSample= */ streamInfo.totalSamples,
- /* floorPosition= */ firstFramePosition,
- /* ceilingPosition= */ inputLength,
- approxBytesPerFrame);
- }
-
- /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
- public boolean hasPendingSeek() {
- return pendingSeekOperationParams != null;
- }
-
- /**
- * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
- * {@link Extractor}.
- *
- * @param input The {@link ExtractorInput} from which data should be read.
- * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
- * to hold the position of the required seek.
- * @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
- * updated to hold the extracted frame that contains the target sample. The caller needs to
- * check the byte buffer limit to see if an extracted frame is available.
- * @return One of the {@code RESULT_} values defined in {@link Extractor}.
- * @throws IOException If an error occurred reading from the input.
- * @throws InterruptedException If the thread was interrupted.
- */
- public int handlePendingSeek(
- ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
- throws InterruptedException, IOException {
- outputBuffer.position(0);
- outputBuffer.limit(0);
- while (true) {
- long floorPosition = pendingSeekOperationParams.floorPosition;
- long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
- long searchPosition = pendingSeekOperationParams.nextSearchPosition;
-
- // streamInfo may not contain minFrameSize, in which case this value will be 0.
- int minFrameSize = Math.max(1, streamInfo.minFrameSize);
- if (floorPosition + minFrameSize >= ceilingPosition) {
- // The seeking range is too small for more than 1 frame, so we can just continue from
- // the floor position.
- pendingSeekOperationParams = null;
- decoderJni.reset(floorPosition);
- return seekToPosition(input, floorPosition, seekPositionHolder);
- }
-
- if (!skipInputUntilPosition(input, searchPosition)) {
- return seekToPosition(input, searchPosition, seekPositionHolder);
- }
-
+ @Override
+ public TimestampSearchResult searchForTimestamp(
+ ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
+ throws IOException, InterruptedException {
+ ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
+ long searchPosition = input.getPosition();
decoderJni.reset(searchPosition);
try {
decoderJni.decodeSampleWithBacktrackPosition(
@@ -145,11 +81,10 @@ import java.nio.ByteBuffer;
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
// For some reasons, the extractor can't find a frame mid-stream.
// Stop the seeking and let it re-try playing at the last search position.
- pendingSeekOperationParams = null;
- throw new IOException("Cannot read frame at position " + searchPosition, e);
+ return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
}
if (outputBuffer.limit() == 0) {
- return Extractor.RESULT_END_OF_INPUT;
+ return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
}
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
@@ -157,184 +92,35 @@ import java.nio.ByteBuffer;
long nextFrameSamplePosition = decoderJni.getDecodePosition();
boolean targetSampleInLastFrame =
- lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
- && nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
+ lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex;
if (targetSampleInLastFrame) {
- pendingSeekOperationParams = null;
- return Extractor.RESULT_CONTINUE;
- }
-
- if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
- pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
+ // We are holding the target frame in outputFrameHolder. Set its presentation time now.
+ outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
+ return TimestampSearchResult.targetFoundResult(input.getPosition());
+ } else if (nextFrameSampleIndex <= targetSampleIndex) {
+ return TimestampSearchResult.underestimatedResult(
+ nextFrameSampleIndex, nextFrameSamplePosition);
} else {
- pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
+ return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition);
}
}
}
- private boolean skipInputUntilPosition(ExtractorInput input, long position)
- throws IOException, InterruptedException {
- long bytesToSkip = position - input.getPosition();
- if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
- input.skipFully((int) bytesToSkip);
- return true;
- }
- return false;
- }
-
- private int seekToPosition(
- ExtractorInput input, long position, PositionHolder seekPositionHolder) {
- if (position == input.getPosition()) {
- return Extractor.RESULT_CONTINUE;
- } else {
- seekPositionHolder.position = position;
- return Extractor.RESULT_SEEK;
- }
- }
-
/**
- * Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
- *
- *
This class holds parameters for a binary-search for the {@code targetSample} in the range
- * [floorPosition, ceilingPosition).
+ * A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
+ * the timestamp for a stream seek time position.
*/
- private static final class SeekOperationParams {
- private final long seekTimeUs;
- private final long targetSample;
- private final long approxBytesPerFrame;
- private long floorSample;
- private long ceilingSample;
- private long floorPosition;
- private long ceilingPosition;
- private long nextSearchPosition;
-
- private SeekOperationParams(
- long seekTimeUs,
- long targetSample,
- long floorSample,
- long ceilingSample,
- long floorPosition,
- long ceilingPosition,
- long approxBytesPerFrame) {
- this.seekTimeUs = seekTimeUs;
- this.floorSample = floorSample;
- this.ceilingSample = ceilingSample;
- this.floorPosition = floorPosition;
- this.ceilingPosition = ceilingPosition;
- this.targetSample = targetSample;
- this.approxBytesPerFrame = approxBytesPerFrame;
- updateNextSearchPosition();
- }
-
- /** Updates the floor constraints (inclusive) of the seek operation. */
- private void updateSeekFloor(long floorSample, long floorPosition) {
- this.floorSample = floorSample;
- this.floorPosition = floorPosition;
- updateNextSearchPosition();
- }
-
- /** Updates the ceiling constraints (exclusive) of the seek operation. */
- private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
- this.ceilingSample = ceilingSample;
- this.ceilingPosition = ceilingPosition;
- updateNextSearchPosition();
- }
-
- private void updateNextSearchPosition() {
- this.nextSearchPosition =
- getNextSearchPosition(
- targetSample,
- floorSample,
- ceilingSample,
- floorPosition,
- ceilingPosition,
- approxBytesPerFrame);
- }
-
- /**
- * Returns the next position in FLAC stream to search for target sample, given [floorPosition,
- * ceilingPosition).
- */
- private static long getNextSearchPosition(
- long targetSample,
- long floorSample,
- long ceilingSample,
- long floorPosition,
- long ceilingPosition,
- long approxBytesPerFrame) {
- if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
- return floorPosition;
- }
- long samplesToSkip = targetSample - floorSample;
- long estimatedBytesPerSample =
- Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
- // In the stream, the samples are accessed in a group of frame. Given a stream position, the
- // seeker will be able to find the first frame following that position.
- // Hence, if our target sample is in the middle of a frame, and our estimate position is
- // correct, or very near the actual sample position, the seeker will keep accessing the next
- // frame, rather than the frame that contains the target sample.
- // Moreover, it's better to under-estimate rather than over-estimate, because the extractor
- // input can skip forward easily, but cannot rewind easily (it may require a new connection
- // to be made).
- // Therefore, we should reduce the estimated position by some amount, so it will converge to
- // the correct frame earlier.
- long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
- long confidenceInterval = bytesToSkip / 20;
-
- long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
- long estimatedPosition = estimatedFramePosition - confidenceInterval;
-
- return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
- }
- }
-
- /**
- * A {@link SeekMap} implementation that returns the estimated byte location from {@link
- * SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
- * #getSeekPoints(long)} query.
- */
- private static final class FlacBinarySearchSeekMap implements SeekMap {
+ private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo;
- private final long firstFramePosition;
- private final long inputLength;
- private final long approxBytesPerFrame;
- private final long durationUs;
- private FlacBinarySearchSeekMap(
- FlacStreamInfo streamInfo,
- long firstFramePosition,
- long inputLength,
- long durationUs,
- long approxBytesPerFrame) {
+ public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
- this.firstFramePosition = firstFramePosition;
- this.inputLength = inputLength;
- this.approxBytesPerFrame = approxBytesPerFrame;
- this.durationUs = durationUs;
}
@Override
- public boolean isSeekable() {
- return true;
- }
-
- @Override
- public SeekPoints getSeekPoints(long timeUs) {
- long nextSearchPosition =
- SeekOperationParams.getNextSearchPosition(
- streamInfo.getSampleIndex(timeUs),
- /* floorSample= */ 0,
- /* ceilingSample= */ streamInfo.totalSamples,
- /* floorPosition= */ firstFramePosition,
- /* ceilingPosition= */ inputLength,
- approxBytesPerFrame);
- return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
- }
-
- @Override
- public long getDurationUs() {
- return durationUs;
+ public long timeUsToTargetTime(long timeUs) {
+ return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
}
}
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index e8a04e06ae..2d74bce5f1 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
@@ -37,11 +38,17 @@ import java.util.List;
*
* @param numInputBuffers The number of input buffers.
* @param numOutputBuffers The number of output buffers.
+ * @param maxInputBufferSize The maximum required input buffer size if known, or {@link
+ * Format#NO_VALUE} otherwise.
* @param initializationData Codec-specific initialization data. It should contain only one entry
- * which is the flac file header.
+ * which is the flac file header.
* @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder.
*/
- public FlacDecoder(int numInputBuffers, int numOutputBuffers, List initializationData)
+ public FlacDecoder(
+ int numInputBuffers,
+ int numOutputBuffers,
+ int maxInputBufferSize,
+ List initializationData)
throws FlacDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (initializationData.size() != 1) {
@@ -60,7 +67,9 @@ import java.util.List;
throw new FlacDecoderException("Metadata decoding failed");
}
- setInitialInputBufferSize(streamInfo.maxFrameSize);
+ int initialInputBufferSize =
+ maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
+ setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index a5efeb69f9..b6eec765d1 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -21,6 +21,7 @@ import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -46,24 +47,14 @@ import java.util.Arrays;
*/
public final class FlacExtractor implements Extractor {
- /**
- * Factory that returns one extractor which is a {@link FlacExtractor}.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new FlacExtractor()};
- }
-
- };
+ /** Factory that returns one extractor which is a {@link FlacExtractor}. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
/** Flags controlling the behavior of the extractor. */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- flag = true,
- value = {FLAG_DISABLE_ID3_METADATA}
- )
+ flag = true,
+ value = {FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
@@ -88,6 +79,7 @@ public final class FlacExtractor implements Extractor {
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
+ private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
@@ -140,7 +132,7 @@ public final class FlacExtractor implements Extractor {
decoderJni.setData(input);
readPastStreamInfo(input);
- if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
+ if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition);
}
@@ -224,6 +216,7 @@ public final class FlacExtractor implements Extractor {
outputFormat(streamInfo);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
+ outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
@@ -286,9 +279,10 @@ public final class FlacExtractor implements Extractor {
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
throws InterruptedException, IOException {
int seekResult =
- flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
+ flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
- writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
+ writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
index a72b03cd44..fa66abbdc6 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
@@ -65,7 +65,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
- return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
+ return new FlacDecoder(
+ NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
}
}
diff --git a/extensions/flac/src/test/AndroidManifest.xml b/extensions/flac/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..1d68b376ac
--- /dev/null
+++ b/extensions/flac/src/test/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
new file mode 100644
index 0000000000..e08f4dc28c
--- /dev/null
+++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.ext.flac;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
+import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
+import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
+import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
+import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import com.google.android.exoplayer2.extractor.ts.PsExtractor;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.extractor.wav.WavExtractor;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Unit test for {@link DefaultExtractorsFactory}. */
+@RunWith(RobolectricTestRunner.class)
+public final class DefaultExtractorsFactoryTest {
+
+ @Test
+ public void testCreateExtractors_returnExpectedClasses() {
+ DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
+
+ Extractor[] extractors = defaultExtractorsFactory.createExtractors();
+ List listCreatedExtractorClasses = new ArrayList<>();
+ for (Extractor extractor : extractors) {
+ listCreatedExtractorClasses.add(extractor.getClass());
+ }
+
+ Class[] expectedExtractorClassses =
+ new Class[] {
+ MatroskaExtractor.class,
+ FragmentedMp4Extractor.class,
+ Mp4Extractor.class,
+ Mp3Extractor.class,
+ AdtsExtractor.class,
+ Ac3Extractor.class,
+ TsExtractor.class,
+ FlvExtractor.class,
+ OggExtractor.class,
+ PsExtractor.class,
+ WavExtractor.class,
+ AmrExtractor.class,
+ FlacExtractor.class
+ };
+
+ assertThat(listCreatedExtractorClasses).containsNoDuplicates();
+ assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);
+ }
+}
diff --git a/extensions/flac/src/test/resources/robolectric.properties b/extensions/flac/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..2f3210368e
--- /dev/null
+++ b/extensions/flac/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+manifest=src/test/AndroidManifest.xml
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index 87e72939c5..af973e1345 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
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 1b595d6886..eca31c98e4 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
@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.vr.sdk.audio.GvrAudioSurround;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public void queueInput(ByteBuffer input) {
int position = input.position();
+ Assertions.checkNotNull(gvrAudioSurround);
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
input.position(position + readBytes);
}
@Override
public void queueEndOfStream() {
+ Assertions.checkNotNull(gvrAudioSurround);
inputEnded = true;
gvrAudioSurround.triggerProcessing();
}
@Override
public ByteBuffer getOutput() {
+ Assertions.checkNotNull(gvrAudioSurround);
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
@@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
+ Assertions.checkNotNull(gvrAudioSurround);
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
}
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index 3529e05380..cf6938a2b1 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
@@ -26,19 +31,25 @@ android {
}
dependencies {
- // This dependency is necessary to force the supportLibraryVersion of
- // com.android.support:support-v4 to be used. Else an older version (25.2.0)
- // is included via:
- // com.google.android.gms:play-services-ads:12.0.0
- // |-- com.google.android.gms:play-services-ads-lite:12.0.0
- // |-- com.google.android.gms:play-services-basement:12.0.0
- // |-- com.android.support:support-v4:26.1.0
- api 'com.android.support:support-v4:' + supportLibraryVersion
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4'
implementation project(modulePrefix + 'library-core')
- implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
+ implementation 'com.google.android.gms:play-services-ads:15.0.1'
+ // These dependencies are necessary to force the supportLibraryVersion of
+ // com.android.support:support-v4 and com.android.support:customtabs to be
+ // used. Else older versions are used, for example via:
+ // com.google.android.gms:play-services-ads:15.0.1
+ // |-- com.android.support:customtabs:26.1.0
+ implementation 'com.android.support:support-v4:' + supportLibraryVersion
+ implementation 'com.android.support:customtabs:' + supportLibraryVersion
+ testImplementation 'com.google.truth:truth:' + truthVersion
+ testImplementation 'junit:junit:' + junitVersion
+ testImplementation 'org.mockito:mockito-core:' + mockitoVersion
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ testImplementation project(modulePrefix + 'testutils-robolectric')
}
+apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
+
ext {
javadocTitle = 'IMA extension'
}
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
index 2d9ddfb288..bf1cdfe02c 100644
--- 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
@@ -22,7 +22,6 @@ import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.ViewGroup;
-import android.webkit.WebView;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError;
@@ -38,6 +37,7 @@ 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.CompanionAdSlot;
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;
@@ -53,6 +53,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -62,15 +63,20 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-/**
- * Loads ads using the IMA SDK. All methods are called on the main thread.
- */
-public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader,
- VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener {
+/** Loads ads using the IMA SDK. All methods are called on the main thread. */
+public final class ImaAdsLoader
+ implements Player.EventListener,
+ AdsLoader,
+ VideoAdPlayer,
+ ContentProgressProvider,
+ AdErrorListener,
+ AdsLoadedListener,
+ AdEventListener {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
@@ -85,6 +91,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private @Nullable AdEventListener adEventListener;
private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs;
+ private ImaFactory imaFactory;
/**
* Creates a new builder for {@link ImaAdsLoader}.
@@ -95,6 +102,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
this.context = Assertions.checkNotNull(context);
vastLoadTimeoutMs = TIMEOUT_UNSET;
mediaLoadTimeoutMs = TIMEOUT_UNSET;
+ imaFactory = new DefaultImaFactory();
}
/**
@@ -149,6 +157,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return this;
}
+ // @VisibleForTesting
+ /* package */ Builder setImaFactory(ImaFactory imaFactory) {
+ this.imaFactory = Assertions.checkNotNull(imaFactory);
+ return this;
+ }
+
/**
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
*
@@ -165,7 +179,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
null,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
- adEventListener);
+ adEventListener,
+ imaFactory);
}
/**
@@ -183,7 +198,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsResponse,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
- adEventListener);
+ adEventListener,
+ imaFactory);
}
}
@@ -210,14 +226,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
- /**
- * The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be
- * clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in
- * the WebView directly when an ad starts. See [Internal: b/62371030].
- */
- private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
- + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
-
private static final int TIMEOUT_UNSET = -1;
/** The state of ad playback. */
@@ -242,9 +250,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
private final @Nullable AdEventListener adEventListener;
+ private final ImaFactory imaFactory;
private final Timeline.Period period;
private final List adCallbacks;
- private final ImaSdkFactory imaSdkFactory;
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
@@ -252,9 +260,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private List supportedMimeTypes;
private EventListener eventListener;
private Player player;
- private ViewGroup adUiViewGroup;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
+ private int lastVolumePercentage;
private AdsManager adsManager;
private AdLoadException pendingAdLoadError;
@@ -267,13 +275,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/** The expected ad group index that IMA should load next. */
private int expectedAdGroupIndex;
- /**
- * The index of the current ad group that IMA is loading.
- */
+ /** The index of the current ad group that IMA is loading. */
private int adGroupIndex;
- /**
- * Whether IMA has sent an ad event to pause content since the last resume content event.
- */
+ /** Whether IMA has sent an ad event to pause content since the last resume content event. */
private boolean imaPausedContent;
/** The current ad playback state. */
private @ImaAdState int imaAdState;
@@ -285,9 +289,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking the player/loader state.
- /**
- * Whether the player is playing an ad.
- */
+ /** Whether the player is playing an ad. */
private boolean playingAd;
/**
* If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
@@ -310,13 +312,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* content progress should increase. {@link C#TIME_UNSET} otherwise.
*/
private long fakeContentProgressOffsetMs;
- /**
- * Stores the pending content position when a seek operation was intercepted to play an ad.
- */
+ /** 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.
- */
+ /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
private boolean sentPendingContentPositionMs;
/**
@@ -337,7 +335,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
- /* adEventListener= */ null);
+ /* adEventListener= */ null,
+ /* imaFactory= */ new DefaultImaFactory());
}
/**
@@ -360,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/* adsResponse= */ null,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
- /* adEventListener= */ null);
+ /* adEventListener= */ null,
+ /* imaFactory= */ new DefaultImaFactory());
}
private ImaAdsLoader(
@@ -370,26 +370,30 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Nullable String adsResponse,
int vastLoadTimeoutMs,
int mediaLoadTimeoutMs,
- @Nullable AdEventListener adEventListener) {
+ @Nullable AdEventListener adEventListener,
+ ImaFactory imaFactory) {
Assertions.checkArgument(adTagUri != null || adsResponse != null);
this.adTagUri = adTagUri;
this.adsResponse = adsResponse;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
this.adEventListener = adEventListener;
- period = new Timeline.Period();
- adCallbacks = new ArrayList<>(1);
- imaSdkFactory = ImaSdkFactory.getInstance();
- adDisplayContainer = imaSdkFactory.createAdDisplayContainer();
- adDisplayContainer.setPlayer(this);
+ this.imaFactory = imaFactory;
if (imaSdkSettings == null) {
- imaSdkSettings = imaSdkFactory.createImaSdkSettings();
+ imaSdkSettings = imaFactory.createImaSdkSettings();
+ if (DEBUG) {
+ imaSdkSettings.setDebugMode(true);
+ }
}
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
- adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings);
- adsLoader.addAdErrorListener(this);
- adsLoader.addAdsLoadedListener(this);
+ adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
+ period = new Timeline.Period();
+ adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
+ adDisplayContainer = imaFactory.createAdDisplayContainer();
+ adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
+ adsLoader.addAdErrorListener(/* adErrorListener= */ this);
+ adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
pendingContentPositionMs = C.TIME_UNSET;
@@ -405,6 +409,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return adsLoader;
}
+ /**
+ * Sets the slots for displaying companion ads. Individual slots can be created using {@link
+ * ImaSdkFactory#createCompanionAdSlot()}.
+ *
+ * @param companionSlots Slots for displaying companion ads.
+ * @see AdDisplayContainer#setCompanionSlots(Collection)
+ */
+ public void setCompanionSlots(Collection companionSlots) {
+ adDisplayContainer.setCompanionSlots(companionSlots);
+ }
+
/**
* Requests ads, if they have not already been requested. Must be called on the main thread.
*
@@ -421,7 +436,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
adDisplayContainer.setAdContainer(adUiViewGroup);
pendingAdRequestContext = new Object();
- AdsRequest request = imaSdkFactory.createAdsRequest();
+ AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) {
request.setAdTagUrl(adTagUri.toString());
} else /* adsResponse != null */ {
@@ -447,9 +462,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} else if (contentType == C.TYPE_HLS) {
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
} else if (contentType == C.TYPE_OTHER) {
- supportedMimeTypes.addAll(Arrays.asList(
- MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
- MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
+ supportedMimeTypes.addAll(
+ Arrays.asList(
+ MimeTypes.VIDEO_MP4,
+ MimeTypes.VIDEO_WEBM,
+ MimeTypes.VIDEO_H263,
+ MimeTypes.AUDIO_MP4,
+ MimeTypes.AUDIO_MPEG));
} else if (contentType == C.TYPE_SS) {
// IMA does not support Smooth Streaming ad media.
}
@@ -461,7 +480,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
this.player = player;
this.eventListener = eventListener;
- this.adUiViewGroup = adUiViewGroup;
+ lastVolumePercentage = 0;
lastAdProgress = null;
lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup);
@@ -490,12 +509,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
adsManager.pause();
}
+ lastVolumePercentage = getVolume();
lastAdProgress = getAdProgress();
lastContentProgress = getContentProgress();
player.removeListener(this);
player = null;
eventListener = null;
- adUiViewGroup = null;
}
@Override
@@ -505,6 +524,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsManager.destroy();
adsManager = null;
}
+ imaPausedContent = false;
+ imaAdState = IMA_AD_STATE_NONE;
+ pendingAdLoadError = null;
+ adPlaybackState = AdPlaybackState.NONE;
+ updateAdPlaybackState();
}
@Override
@@ -554,7 +578,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
- Log.w(TAG, "Dropping ad event after release: " + adEvent);
+ Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
return;
}
try {
@@ -607,8 +631,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
- expectedAdGroupIndex =
+ int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
+ if (adGroupIndexForPosition != C.INDEX_UNSET) {
+ expectedAdGroupIndex = adGroupIndexForPosition;
+ }
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = player.getCurrentPosition();
// Update the expected ad group index for the current content position. The update is delayed
@@ -647,9 +674,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
+ @Override
+ public int getVolume() {
+ if (player == null) {
+ return lastVolumePercentage;
+ }
+
+ Player.AudioComponent audioComponent = player.getAudioComponent();
+ if (audioComponent != null) {
+ return (int) (audioComponent.getVolume() * 100);
+ }
+
+ // Check for a selected track using an audio renderer.
+ TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
+ for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
+ if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
+ return 100;
+ }
+ }
+ return 0;
+ }
+
@Override
public void loadAd(String adUriString) {
try {
+ if (DEBUG) {
+ Log.d(TAG, "loadAd in ad group " + adGroupIndex);
+ }
+ if (adsManager == null) {
+ Log.w(TAG, "Ignoring loadAd after release");
+ return;
+ }
if (adGroupIndex == C.INDEX_UNSET) {
Log.w(
TAG,
@@ -658,9 +713,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adGroupIndex = expectedAdGroupIndex;
adsManager.start();
}
- if (DEBUG) {
- Log.d(TAG, "loadAd in ad group " + adGroupIndex);
- }
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
@@ -689,6 +741,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) {
Log.d(TAG, "playAd");
}
+ if (adsManager == null) {
+ Log.w(TAG, "Ignoring playAd after release");
+ return;
+ }
switch (imaAdState) {
case IMA_AD_STATE_PLAYING:
// IMA does not always call stopAd before resuming content.
@@ -732,6 +788,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) {
Log.d(TAG, "stopAd");
}
+ if (adsManager == null) {
+ Log.w(TAG, "Ignoring stopAd after release");
+ return;
+ }
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected stopAd while detached");
@@ -771,8 +831,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest,
- @Player.TimelineChangeReason int reason) {
+ public void onTimelineChanged(
+ Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
// The player is being reset and this source will be released.
return;
@@ -861,8 +921,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Internal methods.
private void startAdPlayback() {
- ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
- AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
+ AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
@@ -951,11 +1010,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
imaPausedContent = true;
pauseContentInternal();
break;
- case STARTED:
- if (ad.isSkippable()) {
- focusSkipButton();
- }
- break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
@@ -978,6 +1032,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
handleAdGroupLoadError(new IOException(message));
}
break;
+ case STARTED:
case ALL_ADS_COMPLETED:
default:
break;
@@ -1072,6 +1127,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
}
+ // Discard the ad break, which makes sure we don't receive duplicate load error events.
+ adsManager.discardAdBreak();
+ // Set the next expected ad group index so we can handle multiple load errors in a row.
+ adGroupIndex++;
+ if (adGroupIndex < adPlaybackState.adGroupCount) {
+ expectedAdGroupIndex = adGroupIndex;
+ } else {
+ expectedAdGroupIndex = C.INDEX_UNSET;
+ }
+ pendingContentPositionMs = C.TIME_UNSET;
}
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
@@ -1079,6 +1144,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
}
+ if (adsManager == null) {
+ Log.w(TAG, "Ignoring ad prepare error after release");
+ return;
+ }
if (imaAdState == IMA_AD_STATE_NONE) {
// Send IMA a content position at the ad group so that it will try to play it, at which point
// we can notify that it failed to load.
@@ -1125,15 +1194,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
- private void focusSkipButton() {
- if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0
- && adUiViewGroup.getChildAt(0) instanceof WebView) {
- WebView webView = (WebView) (adUiViewGroup.getChildAt(0));
- webView.requestFocus();
- webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS);
- }
- }
-
/**
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
* ads in the ad group have loaded.
@@ -1161,7 +1221,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) {
- adPlaybackState = new AdPlaybackState();
+ adPlaybackState = AdPlaybackState.NONE;
} else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
@@ -1214,4 +1274,49 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return true;
}
}
+
+ /** Factory for objects provided by the IMA SDK. */
+ // @VisibleForTesting
+ /* package */ interface ImaFactory {
+ /** @see ImaSdkSettings */
+ ImaSdkSettings createImaSdkSettings();
+ /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */
+ AdsRenderingSettings createAdsRenderingSettings();
+ /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */
+ AdDisplayContainer createAdDisplayContainer();
+ /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
+ AdsRequest createAdsRequest();
+ /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */
+ com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
+ Context context, ImaSdkSettings imaSdkSettings);
+ }
+
+ /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
+ private static final class DefaultImaFactory implements ImaFactory {
+ @Override
+ public ImaSdkSettings createImaSdkSettings() {
+ return ImaSdkFactory.getInstance().createImaSdkSettings();
+ }
+
+ @Override
+ public AdsRenderingSettings createAdsRenderingSettings() {
+ return ImaSdkFactory.getInstance().createAdsRenderingSettings();
+ }
+
+ @Override
+ public AdDisplayContainer createAdDisplayContainer() {
+ return ImaSdkFactory.getInstance().createAdDisplayContainer();
+ }
+
+ @Override
+ public AdsRequest createAdsRequest() {
+ return ImaSdkFactory.getInstance().createAdsRequest();
+ }
+
+ @Override
+ public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
+ Context context, ImaSdkSettings imaSdkSettings) {
+ return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings);
+ }
+ }
}
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
index d3e1d9725e..400061d019 100644
--- 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
@@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
/**
@@ -34,12 +36,10 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/
@Deprecated
-public final class ImaAdsMediaSource extends BaseMediaSource {
+public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
private final AdsMediaSource adsMediaSource;
- private SourceInfoRefreshListener adsMediaSourceListener;
-
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
@@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
}
@Override
- public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
- adsMediaSourceListener =
- new SourceInfoRefreshListener() {
- @Override
- public void onSourceInfoRefreshed(
- MediaSource source, Timeline timeline, @Nullable Object manifest) {
- refreshSourceInfo(timeline, manifest);
- }
- };
- adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
+ public void prepareSourceInternal(
+ final ExoPlayer player,
+ boolean isTopLevelSource,
+ @Nullable TransferListener mediaTransferListener) {
+ adsMediaSource.prepareSource(
+ player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
}
@Override
@@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
@Override
public void releaseSourceInternal() {
- adsMediaSource.releaseSource(adsMediaSourceListener);
+ adsMediaSource.releaseSource(/* listener= */ this);
+ }
+
+ @Override
+ public void onSourceInfoRefreshed(
+ MediaSource source, Timeline timeline, @Nullable Object manifest) {
+ refreshSourceInfo(timeline, manifest);
}
}
diff --git a/extensions/ima/src/test/AndroidManifest.xml b/extensions/ima/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..9a4e33189e
--- /dev/null
+++ b/extensions/ima/src/test/AndroidManifest.xml
@@ -0,0 +1,16 @@
+
+
+
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
new file mode 100644
index 0000000000..873a1b1d09
--- /dev/null
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.ads.interactivemedia.v3.api.Ad;
+import com.google.ads.interactivemedia.v3.api.AdPodInfo;
+import com.google.ads.interactivemedia.v3.api.CompanionAd;
+import com.google.ads.interactivemedia.v3.api.UiElement;
+import java.util.List;
+import java.util.Set;
+
+/** A fake ad for testing. */
+/* package */ final class FakeAd implements Ad {
+
+ private final boolean skippable;
+ private final AdPodInfo adPodInfo;
+
+ public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) {
+ this.skippable = skippable;
+ adPodInfo =
+ new AdPodInfo() {
+ @Override
+ public int getTotalAds() {
+ return totalAds;
+ }
+
+ @Override
+ public int getAdPosition() {
+ return adPosition;
+ }
+
+ @Override
+ public int getPodIndex() {
+ return podIndex;
+ }
+
+ @Override
+ public boolean isBumper() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getMaxDuration() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getTimeOffset() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ @Override
+ public boolean isSkippable() {
+ return skippable;
+ }
+
+ @Override
+ public AdPodInfo getAdPodInfo() {
+ return adPodInfo;
+ }
+
+ @Override
+ public String getAdId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getCreativeId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getCreativeAdId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getUniversalAdIdValue() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getUniversalAdIdRegistry() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getAdSystem() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getAdWrapperIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getAdWrapperSystems() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getAdWrapperCreativeIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isLinear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getSkipTimeOffset() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isUiDisabled() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getDescription() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getTitle() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getContentType() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getAdvertiserName() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getSurveyUrl() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getDealId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getWidth() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getHeight() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getTraffickingParameters() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getDuration() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set getUiElements() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List getCompanionAds() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java
new file mode 100644
index 0000000000..a8f3daae33
--- /dev/null
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
+import com.google.ads.interactivemedia.v3.api.AdsManager;
+import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
+import com.google.ads.interactivemedia.v3.api.AdsRequest;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.ads.interactivemedia.v3.api.StreamManager;
+import com.google.ads.interactivemedia.v3.api.StreamRequest;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayList;
+
+/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */
+public final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader {
+
+ private final ImaSdkSettings imaSdkSettings;
+ private final AdsManager adsManager;
+ private final ArrayList adsLoadedListeners;
+ private final ArrayList adErrorListeners;
+
+ public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) {
+ this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);
+ this.adsManager = Assertions.checkNotNull(adsManager);
+ adsLoadedListeners = new ArrayList<>();
+ adErrorListeners = new ArrayList<>();
+ }
+
+ @Override
+ public void contentComplete() {
+ // Do nothing.
+ }
+
+ @Override
+ public ImaSdkSettings getSettings() {
+ return imaSdkSettings;
+ }
+
+ @Override
+ public void requestAds(AdsRequest adsRequest) {
+ for (AdsLoadedListener listener : adsLoadedListeners) {
+ listener.onAdsManagerLoaded(
+ new AdsManagerLoadedEvent() {
+ @Override
+ public AdsManager getAdsManager() {
+ return adsManager;
+ }
+
+ @Override
+ public StreamManager getStreamManager() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object getUserRequestContext() {
+ return adsRequest.getUserRequestContext();
+ }
+ });
+ }
+ }
+
+ @Override
+ public String requestStream(StreamRequest streamRequest) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) {
+ adsLoadedListeners.add(adsLoadedListener);
+ }
+
+ @Override
+ public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) {
+ adsLoadedListeners.remove(adsLoadedListener);
+ }
+
+ @Override
+ public void addAdErrorListener(AdErrorListener adErrorListener) {
+ adErrorListeners.add(adErrorListener);
+ }
+
+ @Override
+ public void removeAdErrorListener(AdErrorListener adErrorListener) {
+ adErrorListeners.remove(adErrorListener);
+ }
+}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java
new file mode 100644
index 0000000000..7c2c8a6e0b
--- /dev/null
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdsRequest;
+import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
+import java.util.List;
+import java.util.Map;
+
+/** Fake {@link AdsRequest} implementation for tests. */
+public final class FakeAdsRequest implements AdsRequest {
+
+ private String adTagUrl;
+ private String adsResponse;
+ private Object userRequestContext;
+ private AdDisplayContainer adDisplayContainer;
+ private ContentProgressProvider contentProgressProvider;
+
+ @Override
+ public void setAdTagUrl(String adTagUrl) {
+ this.adTagUrl = adTagUrl;
+ }
+
+ @Override
+ public String getAdTagUrl() {
+ return adTagUrl;
+ }
+
+ @Override
+ public void setExtraParameter(String s, String s1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getExtraParameter(String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map getExtraParameters() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setUserRequestContext(Object userRequestContext) {
+ this.userRequestContext = userRequestContext;
+ }
+
+ @Override
+ public Object getUserRequestContext() {
+ return userRequestContext;
+ }
+
+ @Override
+ public AdDisplayContainer getAdDisplayContainer() {
+ return adDisplayContainer;
+ }
+
+ @Override
+ public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) {
+ this.adDisplayContainer = adDisplayContainer;
+ }
+
+ @Override
+ public ContentProgressProvider getContentProgressProvider() {
+ return contentProgressProvider;
+ }
+
+ @Override
+ public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) {
+ this.contentProgressProvider = contentProgressProvider;
+ }
+
+ @Override
+ public String getAdsResponse() {
+ return adsResponse;
+ }
+
+ @Override
+ public void setAdsResponse(String adsResponse) {
+ this.adsResponse = adsResponse;
+ }
+
+ @Override
+ public void setAdWillAutoPlay(boolean b) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setAdWillPlayMuted(boolean b) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setContentDuration(float v) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setContentKeywords(List list) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setContentTitle(String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setVastLoadTimeout(float v) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setLiveStreamPrefetchSeconds(float v) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
new file mode 100644
index 0000000000..11ed214279
--- /dev/null
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.testutil.StubExoPlayer;
+import java.util.ArrayList;
+
+/** A fake player for testing content/ad playback. */
+/* package */ final class FakePlayer extends StubExoPlayer {
+
+ private final ArrayList listeners;
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+
+ private boolean prepared;
+ private Timeline timeline;
+ private int state;
+ private boolean playWhenReady;
+ private long position;
+ private long contentPosition;
+ private boolean isPlayingAd;
+ private int adGroupIndex;
+ private int adIndexInAdGroup;
+
+ public FakePlayer() {
+ listeners = new ArrayList<>();
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ state = Player.STATE_IDLE;
+ playWhenReady = true;
+ timeline = Timeline.EMPTY;
+ }
+
+ /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
+ public void updateTimeline(Timeline timeline) {
+ for (Player.EventListener listener : listeners) {
+ listener.onTimelineChanged(
+ timeline,
+ null,
+ prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
+ }
+ prepared = true;
+ }
+
+ /**
+ * Sets the state of this player as if it were playing content at the given {@code position}. If
+ * an ad is currently playing, this will trigger a position discontinuity.
+ */
+ public void setPlayingContentPosition(long position) {
+ boolean notify = isPlayingAd;
+ isPlayingAd = false;
+ adGroupIndex = C.INDEX_UNSET;
+ adIndexInAdGroup = C.INDEX_UNSET;
+ this.position = position;
+ contentPosition = position;
+ if (notify) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
+ }
+ }
+ }
+
+ /**
+ * Sets the state of this player as if it were playing an ad with the given indices at the given
+ * {@code position}. If the player is playing a different ad or content, this will trigger a
+ * position discontinuity.
+ */
+ public void setPlayingAdPosition(
+ int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) {
+ boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
+ isPlayingAd = true;
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ this.position = position;
+ this.contentPosition = contentPosition;
+ if (notify) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
+ }
+ }
+ }
+
+ /** Sets the state of this player with the given {@code STATE} constant. */
+ public void setState(int state, boolean playWhenReady) {
+ boolean notify = this.state != state || this.playWhenReady != playWhenReady;
+ this.state = state;
+ this.playWhenReady = playWhenReady;
+ if (notify) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, state);
+ }
+ }
+ }
+
+ // ExoPlayer methods. Other methods are unsupported.
+
+ @Override
+ public void addListener(Player.EventListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(Player.EventListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public int getPlaybackState() {
+ return state;
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ return playWhenReady;
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ return timeline;
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getNextWindowIndex() {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousWindowIndex() {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public long getDuration() {
+ if (timeline.isEmpty()) {
+ return C.INDEX_UNSET;
+ }
+ if (isPlayingAd()) {
+ long adDurationUs =
+ timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup);
+ return C.usToMs(adDurationUs);
+ } else {
+ return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return position;
+ }
+
+ @Override
+ public boolean isPlayingAd() {
+ return isPlayingAd;
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return adGroupIndex;
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return adIndexInAdGroup;
+ }
+
+ @Override
+ public long getContentPosition() {
+ return contentPosition;
+ }
+}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
new file mode 100644
index 0000000000..b0fe731480
--- /dev/null
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.google.ads.interactivemedia.v3.api.Ad;
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdEvent;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
+import com.google.ads.interactivemedia.v3.api.AdsManager;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
+import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+/** Test for {@link ImaAdsLoader}. */
+@RunWith(RobolectricTestRunner.class)
+public class ImaAdsLoaderTest {
+
+ private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
+ private static final Timeline CONTENT_TIMELINE =
+ new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);
+ private static final Uri TEST_URI = Uri.EMPTY;
+ private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
+ private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
+ private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
+ private static final FakeAd UNSKIPPABLE_AD =
+ new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1);
+
+ private @Mock ImaSdkSettings imaSdkSettings;
+ private @Mock AdsRenderingSettings adsRenderingSettings;
+ private @Mock AdDisplayContainer adDisplayContainer;
+ private @Mock AdsManager adsManager;
+ private SingletonImaFactory testImaFactory;
+ private ViewGroup adUiViewGroup;
+ private TestAdsLoaderListener adsLoaderListener;
+ private FakePlayer fakeExoPlayer;
+ private ImaAdsLoader imaAdsLoader;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ FakeAdsRequest fakeAdsRequest = new FakeAdsRequest();
+ FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager);
+ testImaFactory =
+ new SingletonImaFactory(
+ imaSdkSettings,
+ adsRenderingSettings,
+ adDisplayContainer,
+ fakeAdsRequest,
+ fakeAdsLoader);
+ adUiViewGroup = new FrameLayout(RuntimeEnvironment.application);
+ }
+
+ @After
+ public void teardown() {
+ if (imaAdsLoader != null) {
+ imaAdsLoader.release();
+ }
+ }
+
+ @Test
+ public void testBuilder_overridesPlayerType() {
+ when(imaSdkSettings.getPlayerType()).thenReturn("test player type");
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+
+ verify(imaSdkSettings).setPlayerType("google/exo.ext.ima");
+ }
+
+ @Test
+ public void testAttachPlayer_setsAdUiViewGroup() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+ imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+
+ verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
+ }
+
+ @Test
+ public void testAttachPlayer_updatesAdPlaybackState() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+ imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+
+ assertThat(adsLoaderListener.adPlaybackState)
+ .isEqualTo(
+ new AdPlaybackState(/* adGroupTimesUs= */ 0)
+ .withAdDurationsUs(PREROLL_ADS_DURATIONS_US));
+ }
+
+ @Test
+ public void testAttachAfterRelease() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+ imaAdsLoader.release();
+ imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ }
+
+ @Test
+ public void testAttachAndCallbacksAfterRelease() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+ imaAdsLoader.release();
+ imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
+ fakeExoPlayer.setState(Player.STATE_READY, true);
+
+ // If callbacks are invoked there is no crash.
+ // Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown
+ // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
+ // SDK being proguarded.
+ imaAdsLoader.requestAds(adUiViewGroup);
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
+ imaAdsLoader.loadAd(TEST_URI.toString());
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
+ imaAdsLoader.playAd();
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD));
+ imaAdsLoader.pauseAd();
+ imaAdsLoader.stopAd();
+ imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
+ imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
+ imaAdsLoader.handlePrepareError(
+ /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
+ }
+
+ @Test
+ public void testPlayback_withPrerollAd_marksAdAsPlayed() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+
+ // Load the preroll ad.
+ imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
+ imaAdsLoader.loadAd(TEST_URI.toString());
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
+
+ // Play the preroll ad.
+ imaAdsLoader.playAd();
+ fakeExoPlayer.setPlayingAdPosition(
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* position= */ 0,
+ /* contentPosition= */ 0);
+ fakeExoPlayer.setState(Player.STATE_READY, true);
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD));
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, UNSKIPPABLE_AD));
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, UNSKIPPABLE_AD));
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, UNSKIPPABLE_AD));
+
+ // Play the content.
+ fakeExoPlayer.setPlayingContentPosition(0);
+ imaAdsLoader.stopAd();
+ imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
+
+ // Verify that the preroll ad has been marked as played.
+ assertThat(adsLoaderListener.adPlaybackState)
+ .isEqualTo(
+ new AdPlaybackState(/* adGroupTimesUs= */ 0)
+ .withContentDurationUs(CONTENT_DURATION_US)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
+ .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
+ .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
+ .withAdResumePositionUs(/* adResumePositionUs= */ 0));
+ }
+
+ private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
+ fakeExoPlayer = new FakePlayer();
+ adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
+ when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
+ imaAdsLoader =
+ new ImaAdsLoader.Builder(RuntimeEnvironment.application)
+ .setImaFactory(testImaFactory)
+ .setImaSdkSettings(imaSdkSettings)
+ .buildForAdTag(TEST_URI);
+ }
+
+ private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
+ return new AdEvent() {
+ @Override
+ public AdEventType getType() {
+ return adEventType;
+ }
+
+ @Override
+ public @Nullable Ad getAd() {
+ return ad;
+ }
+
+ @Override
+ public Map getAdData() {
+ return Collections.emptyMap();
+ }
+ };
+ }
+
+ /** Ad loader event listener that forwards ad playback state to a fake player. */
+ private static final class TestAdsLoaderListener implements AdsLoader.EventListener {
+
+ private final FakePlayer fakeExoPlayer;
+ private final Timeline contentTimeline;
+ private final long[][] adDurationsUs;
+
+ public AdPlaybackState adPlaybackState;
+
+ public TestAdsLoaderListener(
+ FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) {
+ this.fakeExoPlayer = fakeExoPlayer;
+ this.contentTimeline = contentTimeline;
+ this.adDurationsUs = adDurationsUs;
+ }
+
+ @Override
+ public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
+ adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
+ this.adPlaybackState = adPlaybackState;
+ fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
+ }
+
+ @Override
+ public void onAdLoadError(AdLoadException error, DataSpec dataSpec) {
+ assertThat(error.type).isNotEqualTo(AdLoadException.TYPE_UNEXPECTED);
+ }
+
+ @Override
+ public void onAdClicked() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAdTapped() {
+ // Do nothing.
+ }
+ }
+}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
new file mode 100644
index 0000000000..dd46d8a68b
--- /dev/null
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import android.content.Context;
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+import com.google.ads.interactivemedia.v3.api.AdsRequest;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+
+/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */
+final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
+
+ private final ImaSdkSettings imaSdkSettings;
+ private final AdsRenderingSettings adsRenderingSettings;
+ private final AdDisplayContainer adDisplayContainer;
+ private final AdsRequest adsRequest;
+ private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
+
+ public SingletonImaFactory(
+ ImaSdkSettings imaSdkSettings,
+ AdsRenderingSettings adsRenderingSettings,
+ AdDisplayContainer adDisplayContainer,
+ AdsRequest adsRequest,
+ com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
+ this.imaSdkSettings = imaSdkSettings;
+ this.adsRenderingSettings = adsRenderingSettings;
+ this.adDisplayContainer = adDisplayContainer;
+ this.adsRequest = adsRequest;
+ this.adsLoader = adsLoader;
+ }
+
+ @Override
+ public ImaSdkSettings createImaSdkSettings() {
+ return imaSdkSettings;
+ }
+
+ @Override
+ public AdsRenderingSettings createAdsRenderingSettings() {
+ return adsRenderingSettings;
+ }
+
+ @Override
+ public AdDisplayContainer createAdDisplayContainer() {
+ return adDisplayContainer;
+ }
+
+ @Override
+ public AdsRequest createAdsRequest() {
+ return adsRequest;
+ }
+
+ @Override
+ public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
+ Context context, ImaSdkSettings imaSdkSettings) {
+ return adsLoader;
+ }
+}
diff --git a/extensions/ima/src/test/resources/robolectric.properties b/extensions/ima/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..2f3210368e
--- /dev/null
+++ b/extensions/ima/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+manifest=src/test/AndroidManifest.xml
diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle
index f4a8751c67..a0e3f8e0c8 100644
--- a/extensions/jobdispatcher/build.gradle
+++ b/extensions/jobdispatcher/build.gradle
@@ -20,6 +20,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index c6701da964..f75607f268 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -29,6 +29,7 @@ import com.firebase.jobdispatcher.JobService;
import com.firebase.jobdispatcher.Lifetime;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
@@ -146,11 +147,14 @@ public final class JobDispatcherScheduler implements Scheduler {
public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras();
+ Assertions.checkNotNull(extras, "Service started without extras.");
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) {
logd("Requirements are met");
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
+ Assertions.checkNotNull(serviceAction, "Service action missing.");
+ Assertions.checkNotNull(servicePackage, "Service package missing.");
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(this, intent);
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index dc187a5709..10bfef8e7c 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
index 03f53c263f..0c9491bb1a 100644
--- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.video.VideoListener;
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
-public final class LeanbackPlayerAdapter extends PlayerAdapter {
+public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
@@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private final Player player;
private final Handler handler;
private final ComponentListener componentListener;
- private final Runnable updateProgressRunnable;
+ private final int updatePeriodMs;
private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private SurfaceHolderGlueHost surfaceHolderGlueHost;
+ private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
this.context = context;
this.player = player;
+ this.updatePeriodMs = updatePeriodMs;
handler = new Handler();
componentListener = new ComponentListener();
controlDispatcher = new DefaultControlDispatcher();
- updateProgressRunnable = new Runnable() {
- @Override
- public void run() {
- Callback callback = getCallback();
- callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
- callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
- handler.postDelayed(this, updatePeriodMs);
- }
- };
}
/**
@@ -138,7 +130,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
videoComponent.removeVideoListener(componentListener);
}
if (surfaceHolderGlueHost != null) {
- surfaceHolderGlueHost.setSurfaceHolderCallback(null);
+ removeSurfaceHolderCallback(surfaceHolderGlueHost);
surfaceHolderGlueHost = null;
}
hasSurface = false;
@@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override
public void setProgressUpdatingEnabled(boolean enabled) {
- handler.removeCallbacks(updateProgressRunnable);
+ handler.removeCallbacks(this);
if (enabled) {
- handler.post(updateProgressRunnable);
+ handler.post(this);
}
}
@@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
&& (surfaceHolderGlueHost == null || hasSurface);
}
+ // Runnable implementation.
+
+ @Override
+ public void run() {
+ Callback callback = getCallback();
+ callback.onCurrentPositionChanged(this);
+ callback.onBufferedPositionChanged(this);
+ handler.postDelayed(this, updatePeriodMs);
+ }
+
// Internal methods.
- /* package */ void setVideoSurface(Surface surface) {
+ /* package */ void setVideoSurface(@Nullable Surface surface) {
hasSurface = surface != null;
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
@@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
}
- private final class ComponentListener extends Player.DefaultEventListener
- implements SurfaceHolder.Callback, VideoListener {
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) {
+ surfaceHolderGlueHost.setSurfaceHolderCallback(null);
+ }
+
+ private final class ComponentListener
+ implements Player.EventListener, SurfaceHolder.Callback, VideoListener {
// SurfaceHolder.Callback implementation.
@@ -281,8 +288,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest,
- @TimelineChangeReason int reason) {
+ public void onTimelineChanged(
+ Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
index eaaf078b5c..da04b0aec3 100644
--- a/extensions/mediasession/build.gradle
+++ b/extensions/mediasession/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
index ce597b45cd..7d983e14e9 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017 The Android Open Source Project
+ * 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.
@@ -127,7 +127,7 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
@Override
public void onStop(Player player) {
- player.stop();
+ player.stop(true);
}
@Override
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 83fb16236d..c3d6a13f46 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -19,7 +19,6 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.os.Looper;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.support.annotation.NonNull;
@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil;
+import com.google.android.exoplayer2.util.Util;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -46,25 +46,26 @@ import java.util.Map;
/**
* Connects a {@link MediaSessionCompat} to a {@link Player}.
- *
- * The connector listens for actions sent by the media session's controller and implements these
+ *
+ *
The connector listens for actions sent by the media session's controller and implements these
* actions by calling appropriate player methods. The playback state of the media session is
* automatically synced with the player. The connector can also be optionally extended by providing
* various collaborators:
+ *
*
- * Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and
- * {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
- * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
- * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way.
- *
+ * Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
+ * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
+ * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
+ * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
+ * way.
* To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
- * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is
- * recommended for most use cases.
- * To enable editing of the media queue, you can set a {@link QueueEditor} by calling
- * {@link #setQueueEditor(QueueEditor)}.
+ * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
+ * is recommended for most use cases.
+ * To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
+ * #setQueueEditor(QueueEditor)}.
* An {@link ErrorMessageProvider} for providing human readable error messages and
- * corresponding error codes can be set by calling
- * {@link #setErrorMessageProvider(ErrorMessageProvider)}.
+ * corresponding error codes can be set by calling {@link
+ * #setErrorMessageProvider(ErrorMessageProvider)}.
*
*/
public final class MediaSessionConnector {
@@ -74,35 +75,30 @@ public final class MediaSessionConnector {
}
/**
- * The default repeat toggle modes which is the bitmask of
- * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and
- * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
+ * The default repeat toggle modes which is the bitmask of {@link
+ * RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
*/
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
- public static final String EXTRAS_PITCH = "EXO_PITCH";
- private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
- | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
- private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
- | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
- /**
- * Receiver of media commands sent by a media controller.
- */
+ public static final String EXTRAS_PITCH = "EXO_PITCH";
+ private static final int BASE_MEDIA_SESSION_FLAGS =
+ MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
+ private static final int EDITOR_MEDIA_SESSION_FLAGS =
+ BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
+
+ /** Receiver of media commands sent by a media controller. */
public interface CommandReceiver {
/**
* Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
*/
String[] getCommands();
- /**
- * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */
void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
}
- /**
- * Interface to which playback preparation actions are delegated.
- */
+ /** Interface to which playback preparation actions are delegated. */
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS =
@@ -127,96 +123,77 @@ public final class MediaSessionConnector {
* @return The bitmask of the supported media actions.
*/
long getSupportedPrepareActions();
- /**
- * See {@link MediaSessionCompat.Callback#onPrepare()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onPrepare()}. */
void onPrepare();
- /**
- * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
void onPrepareFromMediaId(String mediaId, Bundle extras);
- /**
- * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
void onPrepareFromSearch(String query, Bundle extras);
- /**
- * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
void onPrepareFromUri(Uri uri, Bundle extras);
}
- /**
- * Interface to which playback actions are delegated.
- */
+ /** Interface to which playback actions are delegated. */
public interface PlaybackController extends CommandReceiver {
- long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
- | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
- | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
- | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
- | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
+ long ACTIONS =
+ PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_SEEK_TO
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
/**
* Returns the actions which are supported by the controller. The supported actions must be a
- * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE},
- * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE},
- * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD},
- * {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP},
- * {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and
- * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
+ * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, {@link
+ * PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, {@link
+ * PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, {@link
+ * PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, {@link
+ * PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and {@link
+ * PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
*
* @param player The player.
* @return The bitmask of the supported media actions.
*/
long getSupportedPlaybackActions(@Nullable Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onPlay()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onPlay()}. */
void onPlay(Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onPause()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onPause()}. */
void onPause(Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onSeekTo(long)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */
void onSeekTo(Player player, long position);
- /**
- * See {@link MediaSessionCompat.Callback#onFastForward()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onFastForward()}. */
void onFastForward(Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onRewind()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onRewind()}. */
void onRewind(Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onStop()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onStop()}. */
void onStop(Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */
void onSetShuffleMode(Player player, int shuffleMode);
- /**
- * See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. */
void onSetRepeatMode(Player player, int repeatMode);
}
/**
- * Handles queue navigation actions, and updates the media session queue by calling
- * {@code MediaSessionCompat.setQueue()}.
+ * Handles queue navigation actions, and updates the media session queue by calling {@code
+ * MediaSessionCompat.setQueue()}.
*/
public interface QueueNavigator extends CommandReceiver {
- long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
- | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ long ACTIONS =
+ PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
/**
* Returns the actions which are supported by the navigator. The supported actions must be a
- * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM},
- * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT},
- * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
+ * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, {@link
+ * PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link
+ * PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
*
* @param player The {@link Player}.
* @return The bitmask of the supported media actions.
@@ -235,34 +212,26 @@ public final class MediaSessionConnector {
*/
void onCurrentWindowIndexChanged(Player player);
/**
- * Gets the id of the currently active queue item, or
- * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
- *
- * To let the connector publish metadata for the active queue item, the queue item with the
- * returned id must be available in the list of items returned by
- * {@link MediaControllerCompat#getQueue()}.
+ * Gets the id of the currently active queue item, or {@link
+ * MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
+ *
+ *
To let the connector publish metadata for the active queue item, the queue item with the
+ * returned id must be available in the list of items returned by {@link
+ * MediaControllerCompat#getQueue()}.
*
* @param player The player connected to the media session.
* @return The id of the active queue item.
*/
long getActiveQueueItemId(@Nullable Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */
void onSkipToPrevious(Player player);
- /**
- * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */
void onSkipToQueueItem(Player player, long id);
- /**
- * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
- */
+ /** See {@link MediaSessionCompat.Callback#onSkipToNext()}. */
void onSkipToNext(Player player);
}
- /**
- * Handles media session queue edits.
- */
+ /** Handles media session queue edits. */
public interface QueueEditor extends CommandReceiver {
/**
@@ -270,8 +239,8 @@ public final class MediaSessionConnector {
*/
void onAddQueueItem(Player player, MediaDescriptionCompat description);
/**
- * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description,
- * int index)}.
+ * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, int
+ * index)}.
*/
void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);
/**
@@ -279,9 +248,7 @@ public final class MediaSessionConnector {
* description)}.
*/
void onRemoveQueueItem(Player player, MediaDescriptionCompat description);
- /**
- * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}.
- */
+ /** See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. */
void onRemoveQueueItemAt(Player player, int index);
}
@@ -308,43 +275,49 @@ public final class MediaSessionConnector {
void onCustomAction(String action, Bundle extras);
/**
- * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the
- * media session by the connector or {@code null} if this action should not be published at the
- * given player state.
+ * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
+ * session by the connector or {@code null} if this action should not be published at the given
+ * player state.
*
* @return The custom action to be included in the session playback state or {@code null}.
*/
PlaybackStateCompat.CustomAction getCustomAction();
}
- /**
- * The wrapped {@link MediaSessionCompat}.
- */
+ /** Provides a {@link MediaMetadataCompat} for a given player state. */
+ public interface MediaMetadataProvider {
+ /**
+ * Gets the {@link MediaMetadataCompat} to be published to the session.
+ *
+ * @param player The player for which to provide metadata.
+ * @return The {@link MediaMetadataCompat} to be published to the session.
+ */
+ MediaMetadataCompat getMetadata(Player player);
+ }
+
+ /** The wrapped {@link MediaSessionCompat}. */
public final MediaSessionCompat mediaSession;
- private final MediaControllerCompat mediaController;
- private final Handler handler;
- private final boolean doMaintainMetadata;
+ private @Nullable final MediaMetadataProvider mediaMetadataProvider;
private final ExoPlayerEventListener exoPlayerEventListener;
private final MediaSessionCallback mediaSessionCallback;
private final PlaybackController playbackController;
- private final String metadataExtrasPrefix;
private final Map commandMap;
private Player player;
private CustomActionProvider[] customActionProviders;
private Map customActionMap;
private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ private @Nullable Pair customError;
private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator;
private QueueEditor queueEditor;
private RatingCallback ratingCallback;
/**
- * Creates an instance. Must be called on the same thread that is used to construct the player
- * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
- *
- * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
+ * Creates an instance.
+ *
+ *
Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
@@ -353,17 +326,46 @@ public final class MediaSessionConnector {
}
/**
- * Creates an instance. Must be called on the same thread that is used to construct the player
- * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
+ * Creates an instance.
*
- *
Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}.
+ *
Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, new
+ * DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions.
*/
public MediaSessionConnector(
MediaSessionCompat mediaSession, PlaybackController playbackController) {
- this(mediaSession, playbackController, true, null);
+ this(
+ mediaSession,
+ playbackController,
+ new DefaultMediaMetadataProvider(mediaSession.getController(), null));
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param mediaSession The {@link MediaSessionCompat} to connect to.
+ * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
+ * null} if the connector should handle playback actions directly.
+ * @param doMaintainMetadata Whether the connector should maintain the metadata of the session.
+ * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
+ * queue item to the session metadata.
+ * @deprecated Use {@link MediaSessionConnector#MediaSessionConnector(MediaSessionCompat,
+ * PlaybackController, MediaMetadataProvider)}.
+ */
+ @Deprecated
+ public MediaSessionConnector(
+ MediaSessionCompat mediaSession,
+ @Nullable PlaybackController playbackController,
+ boolean doMaintainMetadata,
+ @Nullable String metadataExtrasPrefix) {
+ this(
+ mediaSession,
+ playbackController,
+ doMaintainMetadata
+ ? new DefaultMediaMetadataProvider(mediaSession.getController(), metadataExtrasPrefix)
+ : null);
}
/**
@@ -373,26 +375,19 @@ public final class MediaSessionConnector {
* @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
* null} if the connector should handle playback actions directly.
- * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
- * {@code false}, you need to maintain the metadata of the media session yourself (provide at
- * least the duration to allow clients to show a progress bar).
- * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
- * queue item to the session metadata.
+ * @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
+ * object to be published to the media session, or {@code null} if metadata shouldn't be
+ * published.
*/
public MediaSessionConnector(
MediaSessionCompat mediaSession,
- PlaybackController playbackController,
- boolean doMaintainMetadata,
- @Nullable String metadataExtrasPrefix) {
+ @Nullable PlaybackController playbackController,
+ @Nullable MediaMetadataProvider mediaMetadataProvider) {
this.mediaSession = mediaSession;
- this.playbackController = playbackController != null ? playbackController
- : new DefaultPlaybackController();
- this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
- this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
- : Looper.getMainLooper());
- this.doMaintainMetadata = doMaintainMetadata;
+ this.playbackController =
+ playbackController != null ? playbackController : new DefaultPlaybackController();
+ this.mediaMetadataProvider = mediaMetadataProvider;
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
- mediaController = mediaSession.getController();
mediaSessionCallback = new MediaSessionCallback();
exoPlayerEventListener = new ExoPlayerEventListener();
customActionMap = Collections.emptyMap();
@@ -401,7 +396,8 @@ public final class MediaSessionConnector {
}
/**
- * Sets the player to be connected to the media session.
+ * Sets the player to be connected to the media session. Must be called on the same thread that is
+ * used to access the player.
*
*
The order in which any {@link CustomActionProvider}s are passed determines the order of the
* actions published with the playback state of the session.
@@ -425,14 +421,17 @@ public final class MediaSessionConnector {
this.playbackPreparer = playbackPreparer;
registerCommandReceiver(playbackPreparer);
- this.customActionProviders = (player != null && customActionProviders != null)
- ? customActionProviders : new CustomActionProvider[0];
+ this.customActionProviders =
+ (player != null && customActionProviders != null)
+ ? customActionProviders
+ : new CustomActionProvider[0];
if (player != null) {
+ Handler handler = new Handler(Util.getLooper());
mediaSession.setCallback(mediaSessionCallback, handler);
player.addListener(exoPlayerEventListener);
}
- updateMediaSessionPlaybackState();
- updateMediaSessionMetadata();
+ invalidateMediaSessionPlaybackState();
+ invalidateMediaSessionMetadata();
}
/**
@@ -444,7 +443,7 @@ public final class MediaSessionConnector {
@Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider) {
if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
}
}
@@ -490,23 +489,50 @@ public final class MediaSessionConnector {
}
}
- private void registerCommandReceiver(CommandReceiver commandReceiver) {
- if (commandReceiver != null && commandReceiver.getCommands() != null) {
- for (String command : commandReceiver.getCommands()) {
- commandMap.put(command, commandReceiver);
- }
+ /**
+ * Sets a custom error on the session.
+ *
+ *
This sets the error code via {@link PlaybackStateCompat.Builder#setErrorMessage(int,
+ * CharSequence)}. By default, the error code will be set to {@link
+ * PlaybackStateCompat#ERROR_CODE_APP_ERROR}.
+ *
+ * @param message The error string to report or {@code null} to clear the error.
+ */
+ public void setCustomErrorMessage(@Nullable CharSequence message) {
+ int code = (message == null) ? 0 : PlaybackStateCompat.ERROR_CODE_APP_ERROR;
+ setCustomErrorMessage(message, code);
+ }
+
+ /**
+ * Sets a custom error on the session.
+ *
+ * @param message The error string to report or {@code null} to clear the error.
+ * @param code The error code to report. Ignored when {@code message} is {@code null}.
+ */
+ public void setCustomErrorMessage(@Nullable CharSequence message, int code) {
+ customError = (message == null) ? null : new Pair<>(code, message);
+ invalidateMediaSessionPlaybackState();
+ }
+
+ /**
+ * Updates the metadata of the media session.
+ *
+ *
Apps normally only need to call this method when the backing data for a given media item has
+ * changed and the metadata should be updated immediately.
+ */
+ public final void invalidateMediaSessionMetadata() {
+ if (mediaMetadataProvider != null && player != null) {
+ mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
}
}
- private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
- if (commandReceiver != null && commandReceiver.getCommands() != null) {
- for (String command : commandReceiver.getCommands()) {
- commandMap.remove(command);
- }
- }
- }
-
- private void updateMediaSessionPlaybackState() {
+ /**
+ * Updates the playback state of the media session.
+ *
+ *
Apps normally only need to call this method when the custom actions provided by a {@link
+ * CustomActionProvider} changed and the playback state needs to be updated immediately.
+ */
+ public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
if (player == null) {
builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
@@ -527,36 +553,74 @@ public final class MediaSessionConnector {
int playbackState = player.getPlaybackState();
ExoPlaybackException playbackError =
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
+ boolean reportError = playbackError != null || customError != null;
int sessionPlaybackState =
- playbackError != null
+ reportError
? PlaybackStateCompat.STATE_ERROR
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
- if (playbackError != null && errorMessageProvider != null) {
+ if (customError != null) {
+ builder.setErrorMessage(customError.first, customError.second);
+ } else if (playbackError != null && errorMessageProvider != null) {
Pair message = errorMessageProvider.getErrorMessage(playbackError);
builder.setErrorMessage(message.first, message.second);
}
- long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
- : MediaSessionCompat.QueueItem.UNKNOWN_ID;
+ long activeQueueItemId =
+ queueNavigator != null
+ ? queueNavigator.getActiveQueueItemId(player)
+ : MediaSessionCompat.QueueItem.UNKNOWN_ID;
Bundle extras = new Bundle();
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
- builder.setActions(buildPlaybackActions())
+ builder
+ .setActions(buildPlaybackActions())
.setActiveQueueItemId(activeQueueItemId)
.setBufferedPosition(player.getBufferedPosition())
- .setState(sessionPlaybackState, player.getCurrentPosition(),
- player.getPlaybackParameters().speed, SystemClock.elapsedRealtime())
+ .setState(
+ sessionPlaybackState,
+ player.getCurrentPosition(),
+ player.getPlaybackParameters().speed,
+ SystemClock.elapsedRealtime())
.setExtras(extras);
mediaSession.setPlaybackState(builder.build());
}
+ /**
+ * Updates the queue of the media session by calling {@link
+ * QueueNavigator#onTimelineChanged(Player)}.
+ *
+ * Apps normally only need to call this method when the backing data for a given queue item has
+ * changed and the queue should be updated immediately.
+ */
+ public final void invalidateMediaSessionQueue() {
+ if (queueNavigator != null && player != null) {
+ queueNavigator.onTimelineChanged(player);
+ }
+ }
+
+ private void registerCommandReceiver(CommandReceiver commandReceiver) {
+ if (commandReceiver != null && commandReceiver.getCommands() != null) {
+ for (String command : commandReceiver.getCommands()) {
+ commandMap.put(command, commandReceiver);
+ }
+ }
+ }
+
+ private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
+ if (commandReceiver != null && commandReceiver.getCommands() != null) {
+ for (String command : commandReceiver.getCommands()) {
+ commandMap.remove(command);
+ }
+ }
+ }
+
private long buildPlaybackActions() {
- long actions = (PlaybackController.ACTIONS
- & playbackController.getSupportedPlaybackActions(player));
+ long actions =
+ (PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player));
if (playbackPreparer != null) {
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
}
if (queueNavigator != null) {
- actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(
- player));
+ actions |=
+ (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
}
if (ratingCallback != null) {
actions |= RatingCallback.ACTIONS;
@@ -564,17 +628,79 @@ public final class MediaSessionConnector {
return actions;
}
- private void updateMediaSessionMetadata() {
- if (doMaintainMetadata) {
+ private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
+ switch (exoPlayerPlaybackState) {
+ case Player.STATE_BUFFERING:
+ return PlaybackStateCompat.STATE_BUFFERING;
+ case Player.STATE_READY:
+ return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
+ case Player.STATE_ENDED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ default:
+ return PlaybackStateCompat.STATE_NONE;
+ }
+ }
+
+ private boolean canDispatchToPlaybackPreparer(long action) {
+ return playbackPreparer != null
+ && (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0;
+ }
+
+ private boolean canDispatchToRatingCallback(long action) {
+ return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
+ }
+
+ private boolean canDispatchToPlaybackController(long action) {
+ return (playbackController.getSupportedPlaybackActions(player)
+ & PlaybackController.ACTIONS
+ & action)
+ != 0;
+ }
+
+ private boolean canDispatchToQueueNavigator(long action) {
+ return queueNavigator != null
+ && (queueNavigator.getSupportedQueueNavigatorActions(player)
+ & QueueNavigator.ACTIONS
+ & action)
+ != 0;
+ }
+
+ /**
+ * Provides a default {@link MediaMetadataCompat} with properties and extras propagated from the
+ * active queue item to the session metadata.
+ */
+ public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {
+
+ private final MediaControllerCompat mediaController;
+ private final String metadataExtrasPrefix;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param mediaController The {@link MediaControllerCompat}.
+ * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the
+ * active queue item to the session metadata.
+ */
+ public DefaultMediaMetadataProvider(
+ MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix) {
+ this.mediaController = mediaController;
+ this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
+ }
+
+ @Override
+ public MediaMetadataCompat getMetadata(Player player) {
+ if (player.getCurrentTimeline().isEmpty()) {
+ return null;
+ }
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
- if (player != null && player.isPlayingAd()) {
+ if (player.isPlayingAd()) {
builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
}
- builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0
- : player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
-
- if (queueNavigator != null) {
- long activeQueueItemId = queueNavigator.getActiveQueueItemId(player);
+ builder.putLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION,
+ player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
+ long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();
+ if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {
List queue = mediaController.getQueue();
for (int i = 0; queue != null && i < queue.size(); i++) {
MediaSessionCompat.QueueItem queueItem = queue.get(i);
@@ -600,113 +726,92 @@ public final class MediaSessionConnector {
}
}
if (description.getTitle() != null) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
- String.valueOf(description.getTitle()));
+ String title = String.valueOf(description.getTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
}
if (description.getSubtitle() != null) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
String.valueOf(description.getSubtitle()));
}
if (description.getDescription() != null) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
String.valueOf(description.getDescription()));
}
if (description.getIconBitmap() != null) {
- builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON,
- description.getIconBitmap());
+ builder.putBitmap(
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
}
if (description.getIconUri() != null) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
String.valueOf(description.getIconUri()));
}
if (description.getMediaId() != null) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
String.valueOf(description.getMediaId()));
}
if (description.getMediaUri() != null) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
String.valueOf(description.getMediaUri()));
}
break;
}
}
}
- mediaSession.setMetadata(builder.build());
+ return builder.build();
}
}
- private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
- switch (exoPlayerPlaybackState) {
- case Player.STATE_BUFFERING:
- return PlaybackStateCompat.STATE_BUFFERING;
- case Player.STATE_READY:
- return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
- case Player.STATE_ENDED:
- return PlaybackStateCompat.STATE_PAUSED;
- default:
- return PlaybackStateCompat.STATE_NONE;
- }
- }
-
- private boolean canDispatchToPlaybackPreparer(long action) {
- return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions()
- & PlaybackPreparer.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToRatingCallback(long action) {
- return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToPlaybackController(long action) {
- return (playbackController.getSupportedPlaybackActions(player)
- & PlaybackController.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToQueueNavigator(long action) {
- return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player)
- & QueueNavigator.ACTIONS & action) != 0;
- }
-
- private class ExoPlayerEventListener extends Player.DefaultEventListener {
+ private class ExoPlayerEventListener implements Player.EventListener {
private int currentWindowIndex;
private int currentWindowCount;
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest,
- @Player.TimelineChangeReason int reason) {
+ public void onTimelineChanged(
+ Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
queueNavigator.onTimelineChanged(player);
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
// active queue item and queue navigation actions may need to be updated
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
}
currentWindowCount = windowCount;
currentWindowIndex = windowIndex;
- updateMediaSessionMetadata();
+ invalidateMediaSessionMetadata();
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
- mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE
- ? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL
- ? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE);
- updateMediaSessionPlaybackState();
+ mediaSession.setRepeatMode(
+ repeatMode == Player.REPEAT_MODE_ONE
+ ? PlaybackStateCompat.REPEAT_MODE_ONE
+ : repeatMode == Player.REPEAT_MODE_ALL
+ ? PlaybackStateCompat.REPEAT_MODE_ALL
+ : PlaybackStateCompat.REPEAT_MODE_NONE);
+ invalidateMediaSessionPlaybackState();
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
- mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL
- : PlaybackStateCompat.SHUFFLE_MODE_NONE);
- updateMediaSessionPlaybackState();
+ mediaSession.setShuffleMode(
+ shuffleModeEnabled
+ ? PlaybackStateCompat.SHUFFLE_MODE_ALL
+ : PlaybackStateCompat.SHUFFLE_MODE_NONE);
+ invalidateMediaSessionPlaybackState();
}
@Override
@@ -716,16 +821,19 @@ public final class MediaSessionConnector {
queueNavigator.onCurrentWindowIndexChanged(player);
}
currentWindowIndex = player.getCurrentWindowIndex();
- updateMediaSessionMetadata();
+ // Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called
+ // and before updating metadata.
+ invalidateMediaSessionPlaybackState();
+ invalidateMediaSessionMetadata();
+ return;
}
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
}
-
}
private class MediaSessionCallback extends MediaSessionCompat.Callback {
@@ -812,7 +920,7 @@ public final class MediaSessionConnector {
Map actionMap = customActionMap;
if (actionMap.containsKey(action)) {
actionMap.get(action).onCustomAction(action, extras);
- updateMediaSessionPlaybackState();
+ invalidateMediaSessionPlaybackState();
}
}
@@ -921,7 +1029,5 @@ public final class MediaSessionConnector {
queueEditor.onRemoveQueueItemAt(player, index);
}
}
-
}
-
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
index 1db5889e00..057f59f62c 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017 The Android Open Source Project
+ * 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.
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
index 853750077d..eadb320941 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017 The Android Open Source Project
+ * 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.
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
index 26a7b6150a..6671add7e5 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -175,7 +175,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
private void publishFloatingQueueWindow(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
- mediaSession.setQueue(Collections.emptyList());
+ mediaSession.setQueue(Collections.emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index 2b653c3f0e..4e6b11c495 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
@@ -28,7 +33,8 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- api 'com.squareup.okhttp3:okhttp:3.10.0'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ api 'com.squareup.okhttp3:okhttp:3.11.0'
}
ext {
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 f2898005c1..1d0dfddb3f 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
@@ -15,24 +15,25 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
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.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
-import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Predicate;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.HttpUrl;
@@ -40,30 +41,28 @@ import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
+import okhttp3.ResponseBody;
-/**
- * An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
- */
-public class OkHttpDataSource implements HttpDataSource {
+/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */
+public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp");
}
- private static final AtomicReference skipBufferReference = new AtomicReference<>();
+ private static final byte[] SKIP_BUFFER = new byte[4096];
- @NonNull private final Call.Factory callFactory;
- @NonNull private final RequestProperties requestProperties;
+ private final Call.Factory callFactory;
+ 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 final @Nullable String userAgent;
+ private final @Nullable Predicate contentTypePredicate;
+ private final @Nullable CacheControl cacheControl;
+ private final @Nullable RequestProperties defaultRequestProperties;
- private DataSpec dataSpec;
- private Response response;
- private InputStream responseByteStream;
+ private @Nullable DataSpec dataSpec;
+ private @Nullable Response response;
+ private @Nullable InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
@@ -77,11 +76,19 @@ public class OkHttpDataSource implements HttpDataSource {
* by the source.
* @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)}.
+ * predicate then a {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
*/
- public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ public OkHttpDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
@Nullable Predicate contentTypePredicate) {
- this(callFactory, userAgent, contentTypePredicate, null);
+ this(
+ callFactory,
+ userAgent,
+ contentTypePredicate,
+ /* cacheControl= */ null,
+ /* defaultRequestProperties= */ null);
}
/**
@@ -89,49 +96,35 @@ public class OkHttpDataSource implements HttpDataSource {
* by the source.
* @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(@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 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.
+ * predicate then a {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
- * the server as HTTP headers on every request.
+ * the server as HTTP headers on every request.
*/
- public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
+ public OkHttpDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
@Nullable Predicate contentTypePredicate,
- @Nullable TransferListener super OkHttpDataSource> listener,
- @Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
+ @Nullable CacheControl cacheControl,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = userAgent;
this.contentTypePredicate = contentTypePredicate;
- this.listener = listener;
this.cacheControl = cacheControl;
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
}
@Override
- public Uri getUri() {
+ public @Nullable Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}
@Override
public Map> getResponseHeaders() {
- return response == null ? null : response.headers().toMultimap();
+ return response == null ? Collections.emptyMap() : response.headers().toMultimap();
}
@Override
@@ -157,10 +150,16 @@ public class OkHttpDataSource implements HttpDataSource {
this.dataSpec = dataSpec;
this.bytesRead = 0;
this.bytesSkipped = 0;
+ transferInitializing(dataSpec);
+
Request request = makeRequest(dataSpec);
+ Response response;
+ ResponseBody responseBody;
try {
- response = callFactory.newCall(request).execute();
- responseByteStream = response.body().byteStream();
+ this.response = callFactory.newCall(request).execute();
+ response = this.response;
+ responseBody = Assertions.checkNotNull(response.body());
+ responseByteStream = responseBody.byteStream();
} catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
dataSpec, HttpDataSourceException.TYPE_OPEN);
@@ -181,8 +180,8 @@ public class OkHttpDataSource implements HttpDataSource {
}
// Check for a valid content type.
- MediaType mediaType = response.body().contentType();
- String contentType = mediaType != null ? mediaType.toString() : null;
+ MediaType mediaType = responseBody.contentType();
+ String contentType = mediaType != null ? mediaType.toString() : "";
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
closeConnectionQuietly();
throw new InvalidContentTypeException(contentType, dataSpec);
@@ -197,14 +196,12 @@ public class OkHttpDataSource implements HttpDataSource {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
- long contentLength = response.body().contentLength();
+ long contentLength = responseBody.contentLength();
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
opened = true;
- if (listener != null) {
- listener.onTransferStart(this, dataSpec);
- }
+ transferStarted(dataSpec);
return bytesToRead;
}
@@ -215,7 +212,8 @@ public class OkHttpDataSource implements HttpDataSource {
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
- throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
+ throw new HttpDataSourceException(
+ e, Assertions.checkNotNull(dataSpec), HttpDataSourceException.TYPE_READ);
}
}
@@ -223,9 +221,7 @@ public class OkHttpDataSource implements HttpDataSource {
public void close() throws HttpDataSourceException {
if (opened) {
opened = false;
- if (listener != null) {
- listener.onTransferEnd(this);
- }
+ transferEnded();
closeConnectionQuietly();
}
}
@@ -262,15 +258,18 @@ public class OkHttpDataSource implements HttpDataSource {
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
}
- /**
- * Establishes a connection.
- */
- private Request makeRequest(DataSpec dataSpec) {
+ /** Establishes a connection. */
+ private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
+ if (url == null) {
+ throw new HttpDataSourceException(
+ "Malformed URL", dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
Request.Builder builder = new Request.Builder().url(url);
if (cacheControl != null) {
builder.cacheControl(cacheControl);
@@ -297,9 +296,14 @@ public class OkHttpDataSource implements HttpDataSource {
if (!allowGzip) {
builder.addHeader("Accept-Encoding", "identity");
}
- if (dataSpec.postBody != null) {
- builder.post(RequestBody.create(null, dataSpec.postBody));
+ RequestBody requestBody = null;
+ if (dataSpec.httpBody != null) {
+ requestBody = RequestBody.create(null, dataSpec.httpBody);
+ } else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ // OkHttp requires a non-null body for POST requests.
+ requestBody = RequestBody.create(null, new byte[0]);
}
+ builder.method(dataSpec.getHttpMethodString(), requestBody);
return builder.build();
}
@@ -316,15 +320,9 @@ public class OkHttpDataSource implements HttpDataSource {
return;
}
- // Acquire the shared skip buffer.
- byte[] skipBuffer = skipBufferReference.getAndSet(null);
- if (skipBuffer == null) {
- skipBuffer = new byte[4096];
- }
-
while (bytesSkipped != bytesToSkip) {
- int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
- int read = responseByteStream.read(skipBuffer, 0, readLength);
+ int readLength = (int) Math.min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
+ int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
@@ -332,13 +330,8 @@ public class OkHttpDataSource implements HttpDataSource {
throw new EOFException();
}
bytesSkipped += read;
- if (listener != null) {
- listener.onBytesTransferred(this, read);
- }
+ bytesTransferred(read);
}
-
- // Release the shared skip buffer.
- skipBufferReference.set(skipBuffer);
}
/**
@@ -367,7 +360,7 @@ public class OkHttpDataSource implements HttpDataSource {
readLength = (int) Math.min(readLength, bytesRemaining);
}
- int read = responseByteStream.read(buffer, offset, readLength);
+ int read = castNonNull(responseByteStream).read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
@@ -377,9 +370,7 @@ public class OkHttpDataSource implements HttpDataSource {
}
bytesRead += read;
- if (listener != null) {
- listener.onBytesTransferred(this, read);
- }
+ bytesTransferred(read);
return read;
}
@@ -387,8 +378,10 @@ public class OkHttpDataSource implements HttpDataSource {
* Closes the current connection quietly, if there is one.
*/
private void closeConnectionQuietly() {
- response.body().close();
- response = null;
+ if (response != null) {
+ Assertions.checkNotNull(response.body()).close();
+ response = null;
+ }
responseByteStream = null;
}
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 32fc5a58cb..09f4e0b61a 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,9 +15,7 @@
*/
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;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
@@ -30,10 +28,30 @@ import okhttp3.Call;
*/
public final class OkHttpDataSourceFactory extends BaseFactory {
- @NonNull private final Call.Factory callFactory;
- @Nullable private final String userAgent;
- @Nullable private final TransferListener super DataSource> listener;
- @Nullable private final CacheControl cacheControl;
+ private final Call.Factory callFactory;
+ private final @Nullable String userAgent;
+ private final @Nullable TransferListener listener;
+ private final @Nullable CacheControl cacheControl;
+
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the sources created by the factory.
+ * @param userAgent An optional User-Agent string.
+ */
+ public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent) {
+ this(callFactory, userAgent, /* listener= */ null, /* cacheControl= */ null);
+ }
+
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the sources created by the factory.
+ * @param userAgent An optional User-Agent string.
+ * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
+ */
+ public OkHttpDataSourceFactory(
+ Call.Factory callFactory, @Nullable String userAgent, @Nullable CacheControl cacheControl) {
+ this(callFactory, userAgent, /* listener= */ null, cacheControl);
+ }
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
@@ -41,9 +59,9 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
* @param userAgent An optional User-Agent string.
* @param listener An optional listener.
*/
- public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
- @Nullable TransferListener super DataSource> listener) {
- this(callFactory, userAgent, listener, null);
+ public OkHttpDataSourceFactory(
+ Call.Factory callFactory, @Nullable String userAgent, @Nullable TransferListener listener) {
+ this(callFactory, userAgent, listener, /* cacheControl= */ null);
}
/**
@@ -53,8 +71,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
- public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
- @Nullable TransferListener super DataSource> listener,
+ public OkHttpDataSourceFactory(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
+ @Nullable TransferListener listener,
@Nullable CacheControl cacheControl) {
this.callFactory = callFactory;
this.userAgent = userAgent;
@@ -65,8 +85,16 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
@Override
protected OkHttpDataSource createDataSourceInternal(
HttpDataSource.RequestProperties defaultRequestProperties) {
- return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
- defaultRequestProperties);
+ OkHttpDataSource dataSource =
+ new OkHttpDataSource(
+ callFactory,
+ userAgent,
+ /* contentTypePredicate= */ null,
+ cacheControl,
+ defaultRequestProperties);
+ if (listener != null) {
+ dataSource.addTransferListener(listener);
+ }
+ return dataSource;
}
-
}
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index 2d20c65697..dc530d05aa 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
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 c547cff434..8e3a213af1 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
@@ -64,8 +64,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
}
}
- private static class TestPlaybackRunnable extends Player.DefaultEventListener
- implements Runnable {
+ private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
index b94f3e9332..57937b4282 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
@@ -30,8 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
*/
public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
+ /** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
- private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
+ /** The default input buffer size. */
+ private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
private OpusDecoder decoder;
@@ -88,8 +90,15 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
- decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
- format.initializationData, mediaCrypto);
+ int initialInputBufferSize =
+ format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
+ decoder =
+ new OpusDecoder(
+ NUM_BUFFERS,
+ NUM_BUFFERS,
+ initialInputBufferSize,
+ format.initializationData,
+ mediaCrypto);
return decoder;
}
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index c34e0b9999..2f2c65980a 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion 15
targetSdkVersion project.ext.targetSdkVersion
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
index 0601af4a2f..08c328ce81 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
@@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
@@ -26,40 +27,40 @@ import java.io.IOException;
import net.butterflytv.rtmp_client.RtmpClient;
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
-/**
- * A Real-Time Messaging Protocol (RTMP) {@link DataSource}.
- */
-public final class RtmpDataSource implements DataSource {
+/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */
+public final class RtmpDataSource extends BaseDataSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
}
- @Nullable private final TransferListener super RtmpDataSource> listener;
-
private RtmpClient rtmpClient;
private Uri uri;
public RtmpDataSource() {
- this(null);
+ super(/* isNetwork= */ true);
}
/**
* @param listener An optional listener.
+ * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
*/
- public RtmpDataSource(@Nullable TransferListener super RtmpDataSource> listener) {
- this.listener = listener;
+ @Deprecated
+ public RtmpDataSource(@Nullable TransferListener listener) {
+ this();
+ if (listener != null) {
+ addTransferListener(listener);
+ }
}
@Override
public long open(DataSpec dataSpec) throws RtmpIOException {
+ transferInitializing(dataSpec);
rtmpClient = new RtmpClient();
rtmpClient.open(dataSpec.uri.toString(), false);
this.uri = dataSpec.uri;
- if (listener != null) {
- listener.onTransferStart(this, dataSpec);
- }
+ transferStarted(dataSpec);
return C.LENGTH_UNSET;
}
@@ -69,9 +70,7 @@ public final class RtmpDataSource implements DataSource {
if (bytesRead == -1) {
return C.RESULT_END_OF_INPUT;
}
- if (listener != null) {
- listener.onBytesTransferred(this, bytesRead);
- }
+ bytesTransferred(bytesRead);
return bytesRead;
}
@@ -79,9 +78,7 @@ public final class RtmpDataSource implements DataSource {
public void close() {
if (uri != null) {
uri = null;
- if (listener != null) {
- listener.onTransferEnd(this);
- }
+ transferEnded();
}
if (rtmpClient != null) {
rtmpClient.close();
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
index 0510e9c7da..3cf9b8de37 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -25,17 +25,14 @@ import com.google.android.exoplayer2.upstream.TransferListener;
*/
public final class RtmpDataSourceFactory implements DataSource.Factory {
- @Nullable
- private final TransferListener super RtmpDataSource> listener;
+ private final @Nullable TransferListener listener;
public RtmpDataSourceFactory() {
this(null);
}
- /**
- * @param listener An optional listener.
- */
- public RtmpDataSourceFactory(@Nullable TransferListener super RtmpDataSource> listener) {
+ /** @param listener An optional listener. */
+ public RtmpDataSourceFactory(@Nullable TransferListener listener) {
this.listener = listener;
}
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 7dc95b388f..3fb627fd77 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
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 09701f9542..bab7cb6fd7 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
@@ -95,8 +95,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
}
}
- private static class TestPlaybackRunnable extends Player.DefaultEventListener
- implements Runnable {
+ private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
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 7fde7678b8..08c413aba7 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
@@ -99,11 +99,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
private static final int NUM_OUTPUT_BUFFERS = 8;
- /**
- * The initial input buffer size. Input buffers are reallocated dynamically if this value is
- * insufficient.
- */
- private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
+ /** The default input buffer size. */
+ private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
private final boolean scaleToFit;
private final boolean disableLoopFilter;
@@ -114,6 +111,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager drmSessionManager;
+ private final boolean useSurfaceYuvOutput;
private Format format;
private VpxDecoder decoder;
@@ -177,7 +175,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
- /* disableLoopFilter= */ false);
+ /* disableLoopFilter= */ false,
+ /* useSurfaceYuvOutput= */ false);
}
/**
@@ -197,11 +196,18 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
+ * @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
*/
- public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
- Handler eventHandler, VideoRendererEventListener eventListener,
- int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) {
+ public LibvpxVideoRenderer(
+ boolean scaleToFit,
+ long allowedJoiningTimeMs,
+ Handler eventHandler,
+ VideoRendererEventListener eventListener,
+ int maxDroppedFramesToNotify,
+ DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ boolean disableLoopFilter,
+ boolean useSurfaceYuvOutput) {
super(C.TRACK_TYPE_VIDEO);
this.scaleToFit = scaleToFit;
this.disableLoopFilter = disableLoopFilter;
@@ -209,6 +215,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ this.useSurfaceYuvOutput = useSurfaceYuvOutput;
joiningDeadlineMs = C.TIME_UNSET;
clearReportedVideoSize();
formatHolder = new FormatHolder();
@@ -549,21 +556,25 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*
* @param outputBuffer The buffer to render.
*/
- protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) {
+ protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
int bufferMode = outputBuffer.mode;
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
+ boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
- if (!renderRgb && !renderYuv) {
+ if (!renderRgb && !renderYuv && !renderSurface) {
dropOutputBuffer(outputBuffer);
} else {
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
if (renderRgb) {
renderRgbFrame(outputBuffer, scaleToFit);
outputBuffer.release();
- } else /* renderYuv */ {
+ } else if (renderYuv) {
outputBufferRenderer.setOutputBuffer(outputBuffer);
// The renderer will release the buffer.
+ } else { // renderSurface
+ decoder.renderToSurface(outputBuffer, surface);
+ outputBuffer.release();
}
consecutiveDroppedFrameCount = 0;
decoderCounters.renderedOutputBufferCount++;
@@ -633,8 +644,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
// The output has changed.
this.surface = surface;
this.outputBufferRenderer = outputBufferRenderer;
- outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV
- : surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE;
+ if (surface != null) {
+ outputMode =
+ useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
+ } else {
+ outputMode =
+ outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
+ }
if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
@@ -684,13 +700,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
try {
long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createVpxDecoder");
+ int initialInputBufferSize =
+ format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder =
new VpxDecoder(
NUM_INPUT_BUFFERS,
NUM_OUTPUT_BUFFERS,
- INITIAL_INPUT_BUFFER_SIZE,
+ initialInputBufferSize,
mediaCrypto,
- disableLoopFilter);
+ disableLoopFilter,
+ useSurfaceYuvOutput);
decoder.setOutputMode(outputMode);
TraceUtil.endSection();
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
@@ -817,7 +836,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* @throws ExoPlaybackException If an error occurs processing the output buffer.
*/
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
- throws ExoPlaybackException {
+ throws ExoPlaybackException, VpxDecoderException {
if (initialPositionUs == C.TIME_UNSET) {
initialPositionUs = positionUs;
}
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 6f8c0a1918..51ef8e9bcf 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
@@ -31,6 +32,7 @@ import java.nio.ByteBuffer;
public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0;
public static final int OUTPUT_MODE_RGB = 1;
+ public static final int OUTPUT_MODE_SURFACE_YUV = 2;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
@@ -50,10 +52,17 @@ import java.nio.ByteBuffer;
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
+ * @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
- public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
- ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException {
+ public VpxDecoder(
+ int numInputBuffers,
+ int numOutputBuffers,
+ int initialInputBufferSize,
+ ExoMediaCrypto exoMediaCrypto,
+ boolean disableLoopFilter,
+ boolean enableSurfaceYuvOutputMode)
+ throws VpxDecoderException {
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
@@ -62,7 +71,7 @@ import java.nio.ByteBuffer;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
- vpxDecContext = vpxInit(disableLoopFilter);
+ vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
}
@@ -96,6 +105,11 @@ import java.nio.ByteBuffer;
@Override
protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
+ // Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
+ // require a call to vpxReleaseFrame.
+ if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
+ vpxReleaseFrame(vpxDecContext, buffer);
+ }
super.releaseOutputBuffer(buffer);
}
@@ -145,13 +159,36 @@ import java.nio.ByteBuffer;
vpxClose(vpxDecContext);
}
- private native long vpxInit(boolean disableLoopFilter);
+ /** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */
+ public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface)
+ throws VpxDecoderException {
+ int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer);
+ if (getFrameResult == -1) {
+ throw new VpxDecoderException("Buffer render failed.");
+ }
+ }
+
+ private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode);
+
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
+
+ /**
+ * Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called
+ * if {@link #vpxInit} was called with {@code enableBufferManager = true}.
+ */
+ private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer);
+
+ /**
+ * Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link
+ * #vpxInit} was called with {@code enableBufferManager = true}.
+ */
+ private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer);
+
private native int vpxGetErrorCode(long context);
private native String vpxGetErrorMessage(long context);
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 2618bf7c62..fa0df1cfa9 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
@@ -30,6 +30,8 @@ import java.nio.ByteBuffer;
public static final int COLORSPACE_BT2020 = 3;
private final VpxDecoder owner;
+ /** Decoder private data. */
+ public int decoderPrivate;
public int mode;
/**
diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk
index 92fed0a064..868b869d56 100644
--- a/extensions/vp9/src/main/jni/Android.mk
+++ b/extensions/vp9/src/main/jni/Android.mk
@@ -35,7 +35,7 @@ LOCAL_MODULE := libvpxJNI
LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := vpx_jni.cc
-LOCAL_LDLIBS := -llog -lz -lm
+LOCAL_LDLIBS := -llog -lz -lm -landroid
LOCAL_SHARED_LIBRARIES := libvpx
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
include $(BUILD_SHARED_LIBRARY)
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index 12bc30112d..f36c433b22 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -21,7 +21,9 @@
#include
#include
-
+#include
+#include
+#include
#include
#include
#include
@@ -63,6 +65,11 @@ static jmethodID initForRgbFrame;
static jmethodID initForYuvFrame;
static jfieldID dataField;
static jfieldID outputModeField;
+static jfieldID decoderPrivateField;
+
+// android.graphics.ImageFormat.YV12.
+static const int kHalPixelFormatYV12 = 0x32315659;
+static const int kDecoderPrivateBase = 0x100;
static int errorCode;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
@@ -282,13 +289,166 @@ static void convert_16_to_8_standard(const vpx_image_t* const img,
}
}
-DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
- vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
+struct JniFrameBuffer {
+ friend class JniBufferManager;
+
+ int stride[4];
+ uint8_t* planes[4];
+ int d_w;
+ int d_h;
+
+ private:
+ int id;
+ int ref_count;
+ vpx_codec_frame_buffer_t vpx_fb;
+};
+
+class JniBufferManager {
+ static const int MAX_FRAMES = 32;
+
+ JniFrameBuffer* all_buffers[MAX_FRAMES];
+ int all_buffer_count = 0;
+
+ JniFrameBuffer* free_buffers[MAX_FRAMES];
+ int free_buffer_count = 0;
+
+ pthread_mutex_t mutex;
+
+ public:
+ JniBufferManager() { pthread_mutex_init(&mutex, NULL); }
+
+ ~JniBufferManager() {
+ while (all_buffer_count--) {
+ free(all_buffers[all_buffer_count]->vpx_fb.data);
+ }
+ }
+
+ int get_buffer(size_t min_size, vpx_codec_frame_buffer_t* fb) {
+ pthread_mutex_lock(&mutex);
+ JniFrameBuffer* out_buffer;
+ if (free_buffer_count) {
+ out_buffer = free_buffers[--free_buffer_count];
+ if (out_buffer->vpx_fb.size < min_size) {
+ free(out_buffer->vpx_fb.data);
+ out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size);
+ out_buffer->vpx_fb.size = min_size;
+ }
+ } else {
+ out_buffer = new JniFrameBuffer();
+ out_buffer->id = all_buffer_count;
+ all_buffers[all_buffer_count++] = out_buffer;
+ out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size);
+ out_buffer->vpx_fb.size = min_size;
+ out_buffer->vpx_fb.priv = &out_buffer->id;
+ }
+ *fb = out_buffer->vpx_fb;
+ int retVal = 0;
+ if (!out_buffer->vpx_fb.data || all_buffer_count >= MAX_FRAMES) {
+ LOGE("ERROR: JniBufferManager get_buffer OOM.");
+ retVal = -1;
+ } else {
+ memset(fb->data, 0, fb->size);
+ }
+ out_buffer->ref_count = 1;
+ pthread_mutex_unlock(&mutex);
+ return retVal;
+ }
+
+ JniFrameBuffer* get_buffer(int id) const {
+ if (id < 0 || id >= all_buffer_count) {
+ LOGE("ERROR: JniBufferManager get_buffer invalid id %d.", id);
+ return NULL;
+ }
+ return all_buffers[id];
+ }
+
+ void add_ref(int id) {
+ if (id < 0 || id >= all_buffer_count) {
+ LOGE("ERROR: JniBufferManager add_ref invalid id %d.", id);
+ return;
+ }
+ pthread_mutex_lock(&mutex);
+ all_buffers[id]->ref_count++;
+ pthread_mutex_unlock(&mutex);
+ }
+
+ int release(int id) {
+ if (id < 0 || id >= all_buffer_count) {
+ LOGE("ERROR: JniBufferManager release invalid id %d.", id);
+ return -1;
+ }
+ pthread_mutex_lock(&mutex);
+ JniFrameBuffer* buffer = all_buffers[id];
+ if (!buffer->ref_count) {
+ LOGE("ERROR: JniBufferManager release, buffer already released.");
+ pthread_mutex_unlock(&mutex);
+ return -1;
+ }
+ if (!--buffer->ref_count) {
+ free_buffers[free_buffer_count++] = buffer;
+ }
+ pthread_mutex_unlock(&mutex);
+ return 0;
+ }
+};
+
+struct JniCtx {
+ JniCtx(bool enableBufferManager) {
+ if (enableBufferManager) {
+ buffer_manager = new JniBufferManager();
+ }
+ }
+
+ ~JniCtx() {
+ if (native_window) {
+ ANativeWindow_release(native_window);
+ }
+ if (buffer_manager) {
+ delete buffer_manager;
+ }
+ }
+
+ void acquire_native_window(JNIEnv* env, jobject new_surface) {
+ if (surface != new_surface) {
+ if (native_window) {
+ ANativeWindow_release(native_window);
+ }
+ native_window = ANativeWindow_fromSurface(env, new_surface);
+ surface = new_surface;
+ width = 0;
+ }
+ }
+
+ JniBufferManager* buffer_manager = NULL;
+ vpx_codec_ctx_t* decoder = NULL;
+ ANativeWindow* native_window = NULL;
+ jobject surface = NULL;
+ int width = 0;
+ int height = 0;
+};
+
+int vpx_get_frame_buffer(void* priv, size_t min_size,
+ vpx_codec_frame_buffer_t* fb) {
+ JniBufferManager* const buffer_manager =
+ reinterpret_cast(priv);
+ return buffer_manager->get_buffer(min_size, fb);
+}
+
+int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {
+ JniBufferManager* const buffer_manager =
+ reinterpret_cast(priv);
+ return buffer_manager->release(*(int*)fb->priv);
+}
+
+DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
+ jboolean enableBufferManager) {
+ JniCtx* context = new JniCtx(enableBufferManager);
+ context->decoder = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount();
errorCode = 0;
- vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
- &cfg, 0);
+ vpx_codec_err_t err =
+ vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0);
if (err) {
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
errorCode = err;
@@ -296,11 +456,20 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
}
if (disableLoopFilter) {
// TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_().
- err = vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true);
+ err = vpx_codec_control_(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);
if (err) {
LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err);
}
}
+ if (enableBufferManager) {
+ err = vpx_codec_set_frame_buffer_functions(
+ context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
+ context->buffer_manager);
+ if (err) {
+ LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
+ err);
+ }
+ }
// Populate JNI References.
const jclass outputBufferClass = env->FindClass(
@@ -312,16 +481,17 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
-
+ decoderPrivateField =
+ env->GetFieldID(outputBufferClass, "decoderPrivate", "I");
return reinterpret_cast(context);
}
DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
- vpx_codec_ctx_t* const context = reinterpret_cast(jContext);
+ JniCtx* const context = reinterpret_cast(jContext);
const uint8_t* const buffer =
reinterpret_cast(env->GetDirectBufferAddress(encoded));
const vpx_codec_err_t status =
- vpx_codec_decode(context, buffer, len, NULL, 0);
+ vpx_codec_decode(context->decoder, buffer, len, NULL, 0);
errorCode = 0;
if (status != VPX_CODEC_OK) {
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
@@ -343,16 +513,16 @@ DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len,
}
DECODER_FUNC(jlong, vpxClose, jlong jContext) {
- vpx_codec_ctx_t* const context = reinterpret_cast(jContext);
- vpx_codec_destroy(context);
+ JniCtx* const context = reinterpret_cast(jContext);
+ vpx_codec_destroy(context->decoder);
delete context;
return 0;
}
DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
- vpx_codec_ctx_t* const context = reinterpret_cast(jContext);
+ JniCtx* const context = reinterpret_cast(jContext);
vpx_codec_iter_t iter = NULL;
- const vpx_image_t* const img = vpx_codec_get_frame(context, &iter);
+ const vpx_image_t* const img = vpx_codec_get_frame(context->decoder, &iter);
if (img == NULL) {
return 1;
@@ -360,6 +530,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
const int kOutputModeYuv = 0;
const int kOutputModeRgb = 1;
+ const int kOutputModeSurfaceYuv = 2;
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
if (outputMode == kOutputModeRgb) {
@@ -435,13 +606,93 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
}
+ } else if (outputMode == kOutputModeSurfaceYuv &&
+ img->fmt != VPX_IMG_FMT_I42016) {
+ if (!context->buffer_manager) {
+ return -1; // enableBufferManager was not set in vpxInit.
+ }
+ int id = *(int*)img->fb_priv;
+ context->buffer_manager->add_ref(id);
+ JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
+ for (int i = 2; i >= 0; i--) {
+ jfb->stride[i] = img->stride[i];
+ jfb->planes[i] = (uint8_t*)img->planes[i];
+ }
+ jfb->d_w = img->d_w;
+ jfb->d_h = img->d_h;
+ env->SetIntField(jOutputBuffer, decoderPrivateField,
+ id + kDecoderPrivateBase);
}
return 0;
}
+DECODER_FUNC(jint, vpxRenderFrame, jlong jContext, jobject jSurface,
+ jobject jOutputBuffer) {
+ JniCtx* const context = reinterpret_cast(jContext);
+ const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) -
+ kDecoderPrivateBase;
+ JniFrameBuffer* srcBuffer = context->buffer_manager->get_buffer(id);
+ context->acquire_native_window(env, jSurface);
+ if (context->native_window == NULL || !srcBuffer) {
+ return 1;
+ }
+ if (context->width != srcBuffer->d_w || context->height != srcBuffer->d_h) {
+ ANativeWindow_setBuffersGeometry(context->native_window, srcBuffer->d_w,
+ srcBuffer->d_h, kHalPixelFormatYV12);
+ context->width = srcBuffer->d_w;
+ context->height = srcBuffer->d_h;
+ }
+ ANativeWindow_Buffer buffer;
+ int result = ANativeWindow_lock(context->native_window, &buffer, NULL);
+ if (buffer.bits == NULL || result) {
+ return -1;
+ }
+ // Y
+ const size_t src_y_stride = srcBuffer->stride[VPX_PLANE_Y];
+ int stride = srcBuffer->d_w;
+ const uint8_t* src_base =
+ reinterpret_cast(srcBuffer->planes[VPX_PLANE_Y]);
+ uint8_t* dest_base = (uint8_t*)buffer.bits;
+ for (int y = 0; y < srcBuffer->d_h; y++) {
+ memcpy(dest_base, src_base, stride);
+ src_base += src_y_stride;
+ dest_base += buffer.stride;
+ }
+ // UV
+ const int src_uv_stride = srcBuffer->stride[VPX_PLANE_U];
+ const int dest_uv_stride = (buffer.stride / 2 + 15) & (~15);
+ const int32_t buffer_uv_height = (buffer.height + 1) / 2;
+ const int32_t height =
+ std::min((int32_t)(srcBuffer->d_h + 1) / 2, buffer_uv_height);
+ stride = (srcBuffer->d_w + 1) / 2;
+ src_base = reinterpret_cast(srcBuffer->planes[VPX_PLANE_U]);
+ const uint8_t* src_v_base =
+ reinterpret_cast(srcBuffer->planes[VPX_PLANE_V]);
+ uint8_t* dest_v_base =
+ ((uint8_t*)buffer.bits) + buffer.stride * buffer.height;
+ dest_base = dest_v_base + buffer_uv_height * dest_uv_stride;
+ for (int y = 0; y < height; y++) {
+ memcpy(dest_base, src_base, stride);
+ memcpy(dest_v_base, src_v_base, stride);
+ src_base += src_uv_stride;
+ src_v_base += src_uv_stride;
+ dest_base += dest_uv_stride;
+ dest_v_base += dest_uv_stride;
+ }
+ return ANativeWindow_unlockAndPost(context->native_window);
+}
+
+DECODER_FUNC(void, vpxReleaseFrame, jlong jContext, jobject jOutputBuffer) {
+ JniCtx* const context = reinterpret_cast(jContext);
+ const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) -
+ kDecoderPrivateBase;
+ env->SetIntField(jOutputBuffer, decoderPrivateField, -1);
+ context->buffer_manager->release(id);
+}
+
DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
- vpx_codec_ctx_t* const context = reinterpret_cast(jContext);
- return env->NewStringUTF(vpx_codec_error(context));
+ JniCtx* const context = reinterpret_cast(jContext);
+ return env->NewStringUTF(vpx_codec_error(context->decoder));
}
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; }
diff --git a/library/core/build.gradle b/library/core/build.gradle
index d2fa5e25f8..947972392f 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -18,10 +18,22 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ // The following argument makes the Android Test Orchestrator run its
+ // "pm clear" command after each test invocation. This command ensures
+ // that the app's state is completely cleared between tests.
+ testInstrumentationRunnerArguments clearPackageData: 'true'
}
// Workaround to prevent circular dependency on project :testutils.
@@ -47,10 +59,14 @@ android {
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
+ androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
+ androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
diff --git a/library/core/src/androidTest/assets/bitmap/image_256_256.png b/library/core/src/androidTest/assets/bitmap/image_256_256.png
new file mode 100644
index 0000000000..cf1403eebd
Binary files /dev/null and b/library/core/src/androidTest/assets/bitmap/image_256_256.png differ
diff --git a/library/core/src/androidTest/assets/bitmap/image_80_60.bmp b/library/core/src/androidTest/assets/bitmap/image_80_60.bmp
new file mode 100644
index 0000000000..440c80f1b5
Binary files /dev/null and b/library/core/src/androidTest/assets/bitmap/image_80_60.bmp differ
diff --git a/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 b/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4
new file mode 100644
index 0000000000..bbd2729c4d
Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 differ
diff --git a/library/core/src/androidTest/assets/mp4/video000.png b/library/core/src/androidTest/assets/mp4/video000.png
new file mode 100644
index 0000000000..5f2758fb29
Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video000.png differ
diff --git a/library/core/src/androidTest/assets/mp4/video014.png b/library/core/src/androidTest/assets/mp4/video014.png
new file mode 100644
index 0000000000..35bf00dcfa
Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video014.png differ
diff --git a/library/core/src/androidTest/assets/mp4/video015.png b/library/core/src/androidTest/assets/mp4/video015.png
new file mode 100644
index 0000000000..a6dfa8ce2b
Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video015.png differ
diff --git a/library/core/src/androidTest/assets/mp4/video016.png b/library/core/src/androidTest/assets/mp4/video016.png
new file mode 100644
index 0000000000..5877573d71
Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video016.png differ
diff --git a/library/core/src/androidTest/assets/mp4/video029.png b/library/core/src/androidTest/assets/mp4/video029.png
new file mode 100644
index 0000000000..9ab47773d4
Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video029.png differ
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
index 3465393853..49329c38c0 100644
--- 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
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.upstream;
import static com.google.common.truth.Truth.assertThat;
+import static junit.framework.Assert.fail;
-import android.app.Instrumentation;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -28,48 +28,58 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.test.InstrumentationTestCase;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
-/**
- * Unit tests for {@link ContentDataSource}.
- */
-public final class ContentDataSourceTest extends InstrumentationTestCase {
+/** Unit tests for {@link ContentDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public final class ContentDataSourceTest {
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
+ @Test
public void testRead() throws Exception {
- assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false);
+ assertData(0, C.LENGTH_UNSET, false);
}
+ @Test
public void testReadPipeMode() throws Exception {
- assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true);
+ assertData(0, C.LENGTH_UNSET, true);
}
+ @Test
public void testReadFixedLength() throws Exception {
- assertData(getInstrumentation(), 0, 100, false);
+ assertData(0, 100, false);
}
+ @Test
public void testReadFromOffsetToEndOfInput() throws Exception {
- assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false);
+ assertData(1, C.LENGTH_UNSET, false);
}
+ @Test
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
- assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true);
+ assertData(1, C.LENGTH_UNSET, true);
}
+ @Test
public void testReadFromOffsetFixedLength() throws Exception {
- assertData(getInstrumentation(), 1, 100, false);
+ assertData(1, 100, false);
}
+ @Test
public void testReadInvalidUri() throws Exception {
- ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
+ ContentDataSource dataSource =
+ new ContentDataSource(InstrumentationRegistry.getTargetContext());
Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
DataSpec dataSpec = new DataSpec(contentUri);
try {
@@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
}
}
- private static void assertData(Instrumentation instrumentation, int offset, int length,
- boolean pipeMode) throws IOException {
+ private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
- ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
+ ContentDataSource dataSource =
+ new ContentDataSource(InstrumentationRegistry.getTargetContext());
try {
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
- byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH);
+ byte[] completeData =
+ TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH);
byte[] expectedData = Arrays.copyOfRange(completeData, offset,
length == C.LENGTH_UNSET ? completeData.length : offset + length);
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
index 58531346ab..964f7266b5 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
@@ -19,8 +19,9 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.net.Uri;
-import android.test.InstrumentationTestCase;
import android.util.SparseArray;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
@@ -29,9 +30,14 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
/** Tests {@link CachedContentIndex}. */
-public class CachedContentIndexTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public class CachedContentIndexTest {
private final byte[] testIndexV1File = {
0, 0, 0, 1, // version
@@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
private CachedContentIndex index;
private File cacheDir;
- @Override
+ @Before
public void setUp() throws Exception {
- super.setUp();
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cacheDir =
+ Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
- @Override
- protected void tearDown() throws Exception {
+ @After
+ public void tearDown() {
Util.recursiveDelete(cacheDir);
- super.tearDown();
}
+ @Test
public void testAddGetRemove() throws Exception {
final String key1 = "key1";
final String key2 = "key2";
@@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(cacheSpanFile.exists()).isTrue();
}
+ @Test
public void testStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
}
+ @Test
public void testLoadV1() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV1File);
@@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
}
+ @Test
public void testLoadV2() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV2File);
@@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
}
- public void testAssignIdForKeyAndGetKeyForId() throws Exception {
+ @Test
+ public void testAssignIdForKeyAndGetKeyForId() {
final String key1 = "key1";
final String key2 = "key2";
int id1 = index.assignIdForKey(key1);
@@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
}
- public void testGetNewId() throws Exception {
+ @Test
+ public void testGetNewId() {
SparseArray idToKey = new SparseArray<>();
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
idToKey.put(10, "");
@@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
}
+ @Test
public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
@@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
}
- public void testRemoveEmptyNotLockedCachedContent() throws Exception {
+ @Test
+ public void testRemoveEmptyNotLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1");
index.maybeRemove(cachedContent.key);
@@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNull();
}
+ @Test
public void testCantRemoveNotEmptyCachedContent() throws Exception {
CachedContent cachedContent = index.getOrAdd("key1");
File cacheSpanFile =
@@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNotNull();
}
- public void testCantRemoveLockedCachedContent() throws Exception {
+ @Test
+ public void testCantRemoveLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
index 637a19cdd2..c422bf33fa 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.test.InstrumentationTestCase;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileOutputStream;
@@ -26,11 +27,14 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
-/**
- * Unit tests for {@link SimpleCacheSpan}.
- */
-public class SimpleCacheSpanTest extends InstrumentationTestCase {
+/** Unit tests for {@link SimpleCacheSpan}. */
+@RunWith(AndroidJUnit4.class)
+public class SimpleCacheSpanTest {
private CachedContentIndex index;
private File cacheDir;
@@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
}
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ @Before
+ public void setUp() throws Exception {
+ cacheDir =
+ Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
- @Override
- protected void tearDown() throws Exception {
+ @After
+ public void tearDown() {
Util.recursiveDelete(cacheDir);
- super.tearDown();
}
+ @Test
public void testCacheFile() throws Exception {
assertCacheSpan("key1", 0, 0);
assertCacheSpan("key2", 1, 2);
@@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
+ "A paragraph-separator character \u2029", 1, 2);
}
+ @Test
public void testUpgradeFileName() throws Exception {
String key = "asd\u00aa";
int id = index.assignIdForKey(key);
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 de210f5eff..87499a9cb1 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
@@ -24,6 +24,7 @@ import android.media.MediaFormat;
import android.support.annotation.IntDef;
import android.view.Surface;
import com.google.android.exoplayer2.PlayerMessage.Target;
+import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -77,6 +78,12 @@ public final class C {
*/
public static final long NANOS_PER_SECOND = 1000000000L;
+ /** The number of bits per byte. */
+ public static final int BITS_PER_BYTE = 8;
+
+ /** The number of bytes per float. */
+ public static final int BYTES_PER_FLOAT = 4;
+
/**
* The name of the ASCII charset.
*/
@@ -136,6 +143,8 @@ public final class C {
ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT,
+ ENCODING_PCM_MU_LAW,
+ ENCODING_PCM_A_LAW,
ENCODING_AC3,
ENCODING_E_AC3,
ENCODING_DTS,
@@ -144,12 +153,19 @@ public final class C {
})
public @interface Encoding {}
- /**
- * Represents a PCM audio encoding, or an invalid or unset value.
- */
+ /** Represents a PCM audio encoding, or an invalid or unset value. */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
- ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT})
+ @IntDef({
+ Format.NO_VALUE,
+ ENCODING_INVALID,
+ ENCODING_PCM_8BIT,
+ ENCODING_PCM_16BIT,
+ ENCODING_PCM_24BIT,
+ ENCODING_PCM_32BIT,
+ ENCODING_PCM_FLOAT,
+ ENCODING_PCM_MU_LAW,
+ ENCODING_PCM_A_LAW
+ })
public @interface PcmEncoding {}
/** @see AudioFormat#ENCODING_INVALID */
public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
@@ -163,6 +179,10 @@ public final class C {
public static final int ENCODING_PCM_32BIT = 0x40000000;
/** @see AudioFormat#ENCODING_PCM_FLOAT */
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
+ /** Audio encoding for mu-law. */
+ public static final int ENCODING_PCM_MU_LAW = 0x10000000;
+ /** Audio encoding for A-law. */
+ public static final int ENCODING_PCM_A_LAW = 0x20000000;
/** @see AudioFormat#ENCODING_AC3 */
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */
@@ -174,13 +194,6 @@ public final class C {
/** @see AudioFormat#ENCODING_DOLBY_TRUEHD */
public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
- /**
- * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
- */
- @SuppressWarnings("deprecation")
- public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
- ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
-
/**
* Stream types for an {@link android.media.AudioTrack}.
*/
@@ -271,24 +284,32 @@ public final class C {
public static final int FLAG_AUDIBILITY_ENFORCED =
android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
- /**
- * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
- */
+ /** 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})
+ @IntDef({
+ USAGE_ALARM,
+ USAGE_ASSISTANCE_ACCESSIBILITY,
+ USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
+ USAGE_ASSISTANCE_SONIFICATION,
+ USAGE_ASSISTANT,
+ 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
- */
+ /** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */
public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
/**
@@ -301,6 +322,8 @@ public final class C {
*/
public static final int USAGE_ASSISTANCE_SONIFICATION =
android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
+ /** @see android.media.AudioAttributes#USAGE_ASSISTANT */
+ public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT;
/**
* @see android.media.AudioAttributes#USAGE_GAME
*/
@@ -353,6 +376,29 @@ public final class C {
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+ /** Audio focus types. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ AUDIOFOCUS_NONE,
+ AUDIOFOCUS_GAIN,
+ AUDIOFOCUS_GAIN_TRANSIENT,
+ AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+ AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
+ })
+ public @interface AudioFocusGain {}
+ /** @see AudioManager#AUDIOFOCUS_NONE */
+ public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE;
+ /** @see AudioManager#AUDIOFOCUS_GAIN */
+ public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN;
+ /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT */
+ public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
+ /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK */
+ public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK =
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
+ /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE */
+ public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE =
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
+
/**
* Flags which can apply to a buffer containing a media sample.
*/
@@ -368,14 +414,10 @@ public final class C {
* Flag for empty buffers that signal that the end of the stream was reached.
*/
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
- /**
- * Indicates that a buffer is (at least partially) encrypted.
- */
- public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
- /**
- * Indicates that a buffer should be decoded but not rendered.
- */
- public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
+ /** Indicates that a buffer is (at least partially) encrypted. */
+ public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
+ /** Indicates that a buffer should be decoded but not rendered. */
+ public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000
/**
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
@@ -409,15 +451,13 @@ public final class C {
* Indicates that the track should be selected if user preferences do not state otherwise.
*/
public static final int SELECTION_FLAG_DEFAULT = 1;
- /**
- * Indicates that the track must be displayed. Only applies to text tracks.
- */
- public static final int SELECTION_FLAG_FORCED = 2;
+ /** Indicates that the track must be displayed. Only applies to text tracks. */
+ public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2
/**
* Indicates that the player may choose to play the track in absence of an explicit user
* preference.
*/
- public static final int SELECTION_FLAG_AUTOSELECT = 4;
+ public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
/**
* Represents an undetermined language as an ISO 639 alpha-3 language code.
@@ -469,32 +509,24 @@ public final class C {
*/
public static final int RESULT_FORMAT_READ = -5;
- /**
- * A data type constant for data of unknown or unspecified type.
- */
+ /** A data type constant for data of unknown or unspecified type. */
public static final int DATA_TYPE_UNKNOWN = 0;
- /**
- * A data type constant for media, typically containing media samples.
- */
+ /** A data type constant for media, typically containing media samples. */
public static final int DATA_TYPE_MEDIA = 1;
- /**
- * A data type constant for media, typically containing only initialization data.
- */
+ /** A data type constant for media, typically containing only initialization data. */
public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;
- /**
- * A data type constant for drm or encryption data.
- */
+ /** A data type constant for drm or encryption data. */
public static final int DATA_TYPE_DRM = 3;
- /**
- * A data type constant for a manifest file.
- */
+ /** A data type constant for a manifest file. */
public static final int DATA_TYPE_MANIFEST = 4;
- /**
- * A data type constant for time synchronization data.
- */
+ /** A data type constant for time synchronization data. */
public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
/** A data type constant for ads loader data. */
public static final int DATA_TYPE_AD = 6;
+ /**
+ * A data type constant for live progressive media streams, typically containing media samples.
+ */
+ public static final int DATA_TYPE_MEDIA_PROGRESSIVE_LIVE = 7;
/**
* Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
* equal to this value.
@@ -694,6 +726,13 @@ public final class C {
*/
public static final int MSG_SET_SCALING_MODE = 4;
+ /**
+ * A type of a message that can be passed to an audio {@link Renderer} via {@link
+ * ExoPlayer#createMessage(Target)}. The message payload should be an {@link AuxEffectInfo}
+ * instance representing an auxiliary audio effect for the underlying audio track.
+ */
+ public static final int MSG_SET_AUX_EFFECT_INFO = 5;
+
/**
* Applications or extensions may define custom {@code MSG_*} constants that can be passed to
* {@link Renderer}s. These custom constants must be greater than or equal to this value.
@@ -797,6 +836,45 @@ public final class C {
*/
public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000;
+ /** Network connection type. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ NETWORK_TYPE_UNKNOWN,
+ NETWORK_TYPE_OFFLINE,
+ NETWORK_TYPE_WIFI,
+ NETWORK_TYPE_2G,
+ NETWORK_TYPE_3G,
+ NETWORK_TYPE_4G,
+ NETWORK_TYPE_CELLULAR_UNKNOWN,
+ NETWORK_TYPE_ETHERNET,
+ NETWORK_TYPE_OTHER
+ })
+ public @interface NetworkType {}
+ /** Unknown network type. */
+ public static final int NETWORK_TYPE_UNKNOWN = 0;
+ /** No network connection. */
+ public static final int NETWORK_TYPE_OFFLINE = 1;
+ /** Network type for a Wifi connection. */
+ public static final int NETWORK_TYPE_WIFI = 2;
+ /** Network type for a 2G cellular connection. */
+ public static final int NETWORK_TYPE_2G = 3;
+ /** Network type for a 3G cellular connection. */
+ public static final int NETWORK_TYPE_3G = 4;
+ /** Network type for a 4G cellular connection. */
+ public static final int NETWORK_TYPE_4G = 5;
+ /**
+ * Network type for cellular connections which cannot be mapped to one of {@link
+ * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.
+ */
+ public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;
+ /** Network type for an Ethernet connection. */
+ public static final int NETWORK_TYPE_ETHERNET = 7;
+ /**
+ * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,
+ * Bluetooth).
+ */
+ public static final int NETWORK_TYPE_OTHER = 8;
+
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
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 39a6243933..5780f7b418 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
@@ -89,12 +89,13 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* model">
*
*
- * It is strongly recommended that ExoPlayer instances are created and accessed from a single
- * application thread. The application's main thread is ideal. Accessing an instance from
- * multiple threads is discouraged as it may cause synchronization problems.
- * 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.
+ * ExoPlayer instances must be accessed from the thread associated with {@link
+ * #getApplicationLooper()}. This Looper can be specified when creating the player, or this is
+ * the Looper of the thread the player is created on, or the Looper of the application's main
+ * thread if the player is created on a thread without Looper.
+ * Registered listeners are called on the thread associated with {@link
+ * #getApplicationLooper()}. Note that this means registered listeners are called on the same
+ * thread which must be used to access the player.
* 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.
@@ -178,13 +179,15 @@ public interface ExoPlayer extends Player {
@Deprecated
@RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
- /**
- * Gets the {@link Looper} associated with the playback thread.
- *
- * @return The {@link Looper} associated with the playback thread.
- */
+ /** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
+ /**
+ * Returns the {@link Looper} associated with the application thread that's used to access the
+ * player and on which player events are received.
+ */
+ Looper getApplicationLooper();
+
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}.
@@ -239,4 +242,7 @@ public interface ExoPlayer extends Player {
* @param seekParameters The seek parameters, or {@code null} to use the defaults.
*/
void setSeekParameters(@Nullable SeekParameters seekParameters);
+
+ /** Returns the currently active {@link SeekParameters} of the player. */
+ SeekParameters getSeekParameters();
}
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 8095ed9c64..b00a485843 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,18 +16,25 @@
package com.google.android.exoplayer2;
import android.content.Context;
+import android.os.Looper;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
+import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.Util;
/**
* A factory for {@link ExoPlayer} instances.
*/
public final class ExoPlayerFactory {
+ private static @Nullable BandwidthMeter singletonBandwidthMeter;
+
private ExoPlayerFactory() {}
/**
@@ -36,13 +43,14 @@ public final class ExoPlayerFactory {
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
- * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
+ * LoadControl)}.
*/
@Deprecated
- public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
- LoadControl loadControl) {
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context, TrackSelector trackSelector, LoadControl loadControl) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
}
/**
@@ -53,14 +61,18 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
- * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
+ * LoadControl)}.
*/
@Deprecated
- public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
}
/**
@@ -74,14 +86,19 @@ public final class ExoPlayerFactory {
* @param extensionRendererMode The extension renderer mode, which determines if and how available
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
- * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
+ * LoadControl)}.
*/
@Deprecated
- public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
- LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager,
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
}
/**
@@ -97,16 +114,21 @@ public final class ExoPlayerFactory {
* build for them to be considered available.
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback.
- * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
+ * LoadControl)}.
*/
@Deprecated
- public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
- LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager,
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
RenderersFactory renderersFactory =
new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
- return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
}
/**
@@ -116,7 +138,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {
- return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector);
+ return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector);
}
/**
@@ -124,44 +146,74 @@ public final class ExoPlayerFactory {
*
* @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.
+ * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. The use
+ * of {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio
+ * focus will be unavailable for the {@link SimpleExoPlayer} returned by this method.
*/
- public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
- TrackSelector trackSelector) {
- return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl());
+ @Deprecated
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ public static SimpleExoPlayer newSimpleInstance(
+ RenderersFactory renderersFactory, TrackSelector trackSelector) {
+ return newSimpleInstance(
+ /* context= */ null, renderersFactory, trackSelector, new DefaultLoadControl());
}
/**
* Creates a {@link SimpleExoPlayer} instance.
*
+ * @param context A {@link Context}.
+ * @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.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) {
+ return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl());
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
* @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.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
*/
public static SimpleExoPlayer newSimpleInstance(
+ Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
@Nullable DrmSessionManager drmSessionManager) {
return newSimpleInstance(
- renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
+ context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
}
/**
* Creates a {@link SimpleExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
*/
- public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
- TrackSelector trackSelector, LoadControl loadControl) {
- return new SimpleExoPlayer(
- renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null);
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl) {
+ return newSimpleInstance(
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ /* drmSessionManager= */ null,
+ Util.getLooper());
}
/**
* Creates a {@link SimpleExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
@@ -169,16 +221,48 @@ public final class ExoPlayerFactory {
* will not be used for DRM protected playbacks.
*/
public static SimpleExoPlayer newSimpleInstance(
+ Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager) {
- return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper());
}
/**
* Creates a {@link SimpleExoPlayer} instance.
*
+ * @param context A {@link Context}.
+ * @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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ BandwidthMeter bandwidthMeter) {
+ return newSimpleInstance(
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ bandwidthMeter,
+ new AnalyticsCollector.Factory(),
+ Util.getLooper());
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
@@ -188,13 +272,116 @@ public final class ExoPlayerFactory {
* will collect and forward all player events.
*/
public static SimpleExoPlayer newSimpleInstance(
+ Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
AnalyticsCollector.Factory analyticsCollectorFactory) {
+ return newSimpleInstance(
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ analyticsCollectorFactory,
+ Util.getLooper());
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
+ * @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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ Looper looper) {
+ return newSimpleInstance(
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ new AnalyticsCollector.Factory(),
+ looper);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
+ * @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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
+ * will collect and forward all player events.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ AnalyticsCollector.Factory analyticsCollectorFactory,
+ Looper looper) {
+ return newSimpleInstance(
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ getDefaultBandwidthMeter(),
+ analyticsCollectorFactory,
+ looper);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
+ * @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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
+ * will collect and forward all player events.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager,
+ BandwidthMeter bandwidthMeter,
+ AnalyticsCollector.Factory analyticsCollectorFactory,
+ Looper looper) {
return new SimpleExoPlayer(
- renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory);
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ bandwidthMeter,
+ analyticsCollectorFactory,
+ looper);
}
/**
@@ -216,7 +403,47 @@ public final class ExoPlayerFactory {
*/
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
- return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT);
+ return newInstance(renderers, trackSelector, loadControl, Util.getLooper());
}
+ /**
+ * 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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static ExoPlayer newInstance(
+ Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Looper looper) {
+ return newInstance(renderers, trackSelector, loadControl, getDefaultBandwidthMeter(), 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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ public static ExoPlayer newInstance(
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ BandwidthMeter bandwidthMeter,
+ Looper looper) {
+ return new ExoPlayerImpl(
+ renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);
+ }
+
+ private static synchronized BandwidthMeter getDefaultBandwidthMeter() {
+ if (singletonBandwidthMeter == null) {
+ singletonBandwidthMeter = new DefaultBandwidthMeter.Builder().build();
+ }
+ return singletonBandwidthMeter;
+ }
}
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 4125a203a6..648168816f 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
@@ -30,11 +30,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
@@ -44,23 +47,33 @@ import java.util.concurrent.CopyOnWriteArraySet;
private static final String TAG = "ExoPlayerImpl";
+ /**
+ * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult}
+ * when the player does not have any track selection made (such as when player is reset, or when
+ * player seeks to an unprepared period). It will not be used as result of any {@link
+ * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} operation.
+ */
+ /* package */ final TrackSelectorResult emptyTrackSelectorResult;
+
private final Renderer[] renderers;
private final TrackSelector trackSelector;
- private final TrackSelectorResult emptyTrackSelectorResult;
private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer;
private final Handler internalPlayerHandler;
private final CopyOnWriteArraySet listeners;
private final Timeline.Window window;
private final Timeline.Period period;
+ private final ArrayDeque pendingPlaybackInfoUpdates;
private boolean playWhenReady;
+ private boolean internalPlayWhenReady;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
private int pendingOperationAcks;
private boolean hasPendingPrepare;
private boolean hasPendingSeek;
private PlaybackParameters playbackParameters;
+ private SeekParameters seekParameters;
private @Nullable ExoPlaybackException playbackError;
// Playback information when there is no pending seek/set source operation.
@@ -77,11 +90,19 @@ import java.util.concurrent.CopyOnWriteArraySet;
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param clock The {@link Clock} that will be used by the instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
*/
@SuppressLint("HandlerLeak")
public ExoPlayerImpl(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ BandwidthMeter bandwidthMeter,
+ Clock clock,
+ Looper looper) {
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
Assertions.checkState(renderers.length > 0);
@@ -99,25 +120,23 @@ import java.util.concurrent.CopyOnWriteArraySet;
window = new Timeline.Window();
period = new Timeline.Period();
playbackParameters = PlaybackParameters.DEFAULT;
- Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
- eventHandler = new Handler(eventLooper) {
- @Override
- public void handleMessage(Message msg) {
- ExoPlayerImpl.this.handleEvent(msg);
- }
- };
- playbackInfo =
- new PlaybackInfo(
- Timeline.EMPTY,
- /* startPositionUs= */ 0,
- TrackGroupArray.EMPTY,
- emptyTrackSelectorResult);
+ seekParameters = SeekParameters.DEFAULT;
+ eventHandler =
+ new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ ExoPlayerImpl.this.handleEvent(msg);
+ }
+ };
+ playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
+ pendingPlaybackInfoUpdates = new ArrayDeque<>();
internalPlayer =
new ExoPlayerImplInternal(
renderers,
trackSelector,
emptyTrackSelectorResult,
loadControl,
+ bandwidthMeter,
playWhenReady,
repeatMode,
shuffleModeEnabled,
@@ -127,6 +146,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
}
+ @Override
+ public AudioComponent getAudioComponent() {
+ return null;
+ }
+
@Override
public VideoComponent getVideoComponent() {
return null;
@@ -142,6 +166,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
return internalPlayer.getPlaybackLooper();
}
+ @Override
+ public Looper getApplicationLooper() {
+ return eventHandler.getLooper();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
listeners.add(listener);
@@ -185,18 +214,30 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false);
+ /* seekProcessed= */ false,
+ /* playWhenReadyChanged= */ false);
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
+ setPlayWhenReady(playWhenReady, /* suppressPlayback= */ false);
+ }
+
+ public void setPlayWhenReady(boolean playWhenReady, boolean suppressPlayback) {
+ boolean internalPlayWhenReady = playWhenReady && !suppressPlayback;
+ if (this.internalPlayWhenReady != internalPlayWhenReady) {
+ this.internalPlayWhenReady = internalPlayWhenReady;
+ internalPlayer.setPlayWhenReady(internalPlayWhenReady);
+ }
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
- internalPlayer.setPlayWhenReady(playWhenReady);
- PlaybackInfo playbackInfo = this.playbackInfo;
- for (Player.EventListener listener : listeners) {
- listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
- }
+ updatePlaybackInfo(
+ playbackInfo,
+ /* positionDiscontinuity= */ false,
+ /* ignored */ DISCONTINUITY_REASON_INTERNAL,
+ /* ignored */ TIMELINE_CHANGE_REASON_RESET,
+ /* seekProcessed= */ false,
+ /* playWhenReadyChanged= */ true);
}
}
@@ -286,10 +327,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
} else {
long windowPositionUs = positionMs == C.TIME_UNSET
? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs);
- Pair periodIndexAndPositon =
+ Pair periodIndexAndPosition =
timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
maskingWindowPositionMs = C.usToMs(windowPositionUs);
- maskingPeriodIndex = periodIndexAndPositon.first;
+ maskingPeriodIndex = periodIndexAndPosition.first;
}
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
for (Player.EventListener listener : listeners) {
@@ -315,7 +356,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (seekParameters == null) {
seekParameters = SeekParameters.DEFAULT;
}
- internalPlayer.setSeekParameters(seekParameters);
+ if (!this.seekParameters.equals(seekParameters)) {
+ this.seekParameters = seekParameters;
+ internalPlayer.setSeekParameters(seekParameters);
+ }
+ }
+
+ @Override
+ public SeekParameters getSeekParameters() {
+ return seekParameters;
}
@Override
@@ -352,7 +401,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false);
+ /* seekProcessed= */ false,
+ /* playWhenReadyChanged= */ false);
}
@Override
@@ -461,29 +511,37 @@ import java.util.concurrent.CopyOnWriteArraySet;
public long getCurrentPosition() {
if (shouldMaskPosition()) {
return maskingWindowPositionMs;
+ } else if (playbackInfo.periodId.isAd()) {
+ return C.usToMs(playbackInfo.positionUs);
} else {
- return playbackInfoPositionUsToWindowPositionMs(playbackInfo.positionUs);
+ return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs);
}
}
@Override
public long getBufferedPosition() {
- // TODO - Implement this properly.
- if (shouldMaskPosition()) {
- return maskingWindowPositionMs;
- } else {
- return playbackInfoPositionUsToWindowPositionMs(playbackInfo.bufferedPositionUs);
+ if (isPlayingAd()) {
+ return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)
+ ? C.usToMs(playbackInfo.bufferedPositionUs)
+ : getDuration();
}
+ return getContentBufferedPosition();
}
@Override
public int getBufferedPercentage() {
long position = getBufferedPosition();
long duration = getDuration();
- return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
+ return position == C.TIME_UNSET || duration == C.TIME_UNSET
+ ? 0
: (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100));
}
+ @Override
+ public long getTotalBufferedDuration() {
+ return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs));
+ }
+
@Override
public boolean isCurrentWindowDynamic() {
Timeline timeline = playbackInfo.timeline;
@@ -521,6 +579,29 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
+ @Override
+ public long getContentBufferedPosition() {
+ if (shouldMaskPosition()) {
+ return maskingWindowPositionMs;
+ }
+ if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber
+ != playbackInfo.periodId.windowSequenceNumber) {
+ return playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
+ long contentBufferedPositionUs = playbackInfo.bufferedPositionUs;
+ if (playbackInfo.loadingMediaPeriodId.isAd()) {
+ Timeline.Period loadingPeriod =
+ playbackInfo.timeline.getPeriod(playbackInfo.loadingMediaPeriodId.periodIndex, period);
+ contentBufferedPositionUs =
+ loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex);
+ if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) {
+ contentBufferedPositionUs = loadingPeriod.durationUs;
+ }
+ }
+ return periodPositionUsToWindowPositionMs(
+ playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs);
+ }
+
@Override
public int getRendererCount() {
return renderers.length;
@@ -615,7 +696,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
- seekProcessed);
+ seekProcessed,
+ /* playWhenReadyChanged= */ false);
}
}
@@ -639,68 +721,133 @@ import java.util.concurrent.CopyOnWriteArraySet;
playbackState,
/* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
- resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
+ resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
+ playbackInfo.periodId,
+ playbackInfo.startPositionUs,
+ /* totalBufferedDurationUs= */ 0,
+ playbackInfo.startPositionUs);
}
private void updatePlaybackInfo(
- PlaybackInfo newPlaybackInfo,
+ PlaybackInfo playbackInfo,
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
- boolean seekProcessed) {
- boolean timelineOrManifestChanged =
- playbackInfo.timeline != newPlaybackInfo.timeline
- || playbackInfo.manifest != newPlaybackInfo.manifest;
- boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
- boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
- boolean trackSelectorResultChanged =
- playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
- playbackInfo = newPlaybackInfo;
- if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
- for (Player.EventListener listener : listeners) {
- listener.onTimelineChanged(
- playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
- }
+ boolean seekProcessed,
+ boolean playWhenReadyChanged) {
+ boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty();
+ pendingPlaybackInfoUpdates.addLast(
+ new PlaybackInfoUpdate(
+ playbackInfo,
+ /* previousPlaybackInfo= */ this.playbackInfo,
+ listeners,
+ trackSelector,
+ positionDiscontinuity,
+ positionDiscontinuityReason,
+ timelineChangeReason,
+ seekProcessed,
+ playWhenReady,
+ playWhenReadyChanged));
+ // Assign playback info immediately such that all getters return the right values.
+ this.playbackInfo = playbackInfo;
+ if (isRunningRecursiveListenerNotification) {
+ return;
}
- if (positionDiscontinuity) {
- for (Player.EventListener listener : listeners) {
- listener.onPositionDiscontinuity(positionDiscontinuityReason);
- }
- }
- if (trackSelectorResultChanged) {
- trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
- for (Player.EventListener listener : listeners) {
- listener.onTracksChanged(
- playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
- }
- }
- if (isLoadingChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onLoadingChanged(playbackInfo.isLoading);
- }
- }
- if (playbackStateChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
- }
- }
- if (seekProcessed) {
- for (Player.EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ while (!pendingPlaybackInfoUpdates.isEmpty()) {
+ pendingPlaybackInfoUpdates.peekFirst().notifyListeners();
+ pendingPlaybackInfoUpdates.removeFirst();
}
}
- private long playbackInfoPositionUsToWindowPositionMs(long positionUs) {
+ private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) {
long positionMs = C.usToMs(positionUs);
- if (!playbackInfo.periodId.isAd()) {
- playbackInfo.timeline.getPeriod(playbackInfo.periodId.periodIndex, period);
- positionMs += period.getPositionInWindowMs();
- }
+ playbackInfo.timeline.getPeriod(periodId.periodIndex, period);
+ positionMs += period.getPositionInWindowMs();
return positionMs;
}
private boolean shouldMaskPosition() {
return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
}
+
+ private static final class PlaybackInfoUpdate {
+
+ private final PlaybackInfo playbackInfo;
+ private final Set listeners;
+ private final TrackSelector trackSelector;
+ private final boolean positionDiscontinuity;
+ private final @Player.DiscontinuityReason int positionDiscontinuityReason;
+ private final @Player.TimelineChangeReason int timelineChangeReason;
+ private final boolean seekProcessed;
+ private final boolean playWhenReady;
+ private final boolean playbackStateOrPlayWhenReadyChanged;
+ private final boolean timelineOrManifestChanged;
+ private final boolean isLoadingChanged;
+ private final boolean trackSelectorResultChanged;
+
+ public PlaybackInfoUpdate(
+ PlaybackInfo playbackInfo,
+ PlaybackInfo previousPlaybackInfo,
+ Set listeners,
+ TrackSelector trackSelector,
+ boolean positionDiscontinuity,
+ @Player.DiscontinuityReason int positionDiscontinuityReason,
+ @Player.TimelineChangeReason int timelineChangeReason,
+ boolean seekProcessed,
+ boolean playWhenReady,
+ boolean playWhenReadyChanged) {
+ this.playbackInfo = playbackInfo;
+ this.listeners = listeners;
+ this.trackSelector = trackSelector;
+ this.positionDiscontinuity = positionDiscontinuity;
+ this.positionDiscontinuityReason = positionDiscontinuityReason;
+ this.timelineChangeReason = timelineChangeReason;
+ this.seekProcessed = seekProcessed;
+ this.playWhenReady = playWhenReady;
+ playbackStateOrPlayWhenReadyChanged =
+ playWhenReadyChanged || previousPlaybackInfo.playbackState != playbackInfo.playbackState;
+ timelineOrManifestChanged =
+ previousPlaybackInfo.timeline != playbackInfo.timeline
+ || previousPlaybackInfo.manifest != playbackInfo.manifest;
+ isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
+ trackSelectorResultChanged =
+ previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
+ }
+
+ public void notifyListeners() {
+ if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
+ for (Player.EventListener listener : listeners) {
+ listener.onTimelineChanged(
+ playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
+ }
+ }
+ if (positionDiscontinuity) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPositionDiscontinuity(positionDiscontinuityReason);
+ }
+ }
+ if (trackSelectorResultChanged) {
+ trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
+ for (Player.EventListener listener : listeners) {
+ listener.onTracksChanged(
+ playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
+ }
+ }
+ if (isLoadingChanged) {
+ for (Player.EventListener listener : listeners) {
+ listener.onLoadingChanged(playbackInfo.isLoading);
+ }
+ }
+ if (playbackStateOrPlayWhenReadyChanged) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
+ }
+ }
+ if (seekProcessed) {
+ for (Player.EventListener listener : listeners) {
+ listener.onSeekProcessed();
+ }
+ }
+ }
+ }
}
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 fc946804f4..7e54726daf 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
@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
@@ -77,6 +78,7 @@ import java.util.Collections;
private static final int MSG_SET_SHUFFLE_ENABLED = 13;
private static final int MSG_SEND_MESSAGE = 14;
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
+ private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -87,6 +89,7 @@ import java.util.Collections;
private final TrackSelector trackSelector;
private final TrackSelectorResult emptyTrackSelectorResult;
private final LoadControl loadControl;
+ private final BandwidthMeter bandwidthMeter;
private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
@@ -123,6 +126,7 @@ import java.util.Collections;
TrackSelector trackSelector,
TrackSelectorResult emptyTrackSelectorResult,
LoadControl loadControl,
+ BandwidthMeter bandwidthMeter,
boolean playWhenReady,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
@@ -133,6 +137,7 @@ import java.util.Collections;
this.trackSelector = trackSelector;
this.emptyTrackSelectorResult = emptyTrackSelectorResult;
this.loadControl = loadControl;
+ this.bandwidthMeter = bandwidthMeter;
this.playWhenReady = playWhenReady;
this.repeatMode = repeatMode;
this.shuffleModeEnabled = shuffleModeEnabled;
@@ -146,11 +151,7 @@ import java.util.Collections;
seekParameters = SeekParameters.DEFAULT;
playbackInfo =
- new PlaybackInfo(
- Timeline.EMPTY,
- /* startPositionUs= */ C.TIME_UNSET,
- TrackGroupArray.EMPTY,
- emptyTrackSelectorResult);
+ PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
playbackInfoUpdate = new PlaybackInfoUpdate();
rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
@@ -162,7 +163,7 @@ import java.util.Collections;
enabledRenderers = new Renderer[0];
window = new Timeline.Window();
period = new Timeline.Period();
- trackSelector.init(this);
+ trackSelector.init(/* listener= */ this, bandwidthMeter);
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
@@ -271,8 +272,9 @@ import java.util.Collections;
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
- updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
+ handler
+ .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters)
+ .sendToTarget();
}
// Handler.Callback implementation.
@@ -324,6 +326,9 @@ import java.util.Collections;
case MSG_TRACK_SELECTION_INVALIDATED:
reselectTracksInternal();
break;
+ case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:
+ handlePlaybackParameters((PlaybackParameters) msg.obj);
+ break;
case MSG_SEND_MESSAGE:
sendMessageInternal((PlayerMessage) msg.obj);
break;
@@ -393,7 +398,11 @@ import java.util.Collections;
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
- mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener= */ this);
+ mediaSource.prepareSource(
+ player,
+ /* isTopLevelSource= */ true,
+ /* listener= */ this,
+ bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -419,6 +428,7 @@ import java.util.Collections;
if (!queue.updateRepeatMode(repeatMode)) {
seekToCurrentPosition(/* sendDiscontinuity= */ true);
}
+ updateLoadingMediaPeriodId();
}
private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
@@ -427,6 +437,7 @@ import java.util.Collections;
if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) {
seekToCurrentPosition(/* sendDiscontinuity= */ true);
}
+ updateLoadingMediaPeriodId();
}
private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException {
@@ -483,11 +494,12 @@ import java.util.Collections;
playbackInfo.positionUs = periodPositionUs;
}
- // Update the buffered position.
+ // Update the buffered position and total buffered duration.
+ MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
playbackInfo.bufferedPositionUs =
- enabledRenderers.length == 0
- ? playingPeriodHolder.info.durationUs
- : playingPeriodHolder.getBufferedPositionUs(/* convertEosToDuration= */ true);
+ loadingPeriod.getBufferedPositionUs(/* convertEosToDuration= */ true);
+ playbackInfo.totalBufferedDurationUs =
+ playbackInfo.bufferedPositionUs - loadingPeriod.toPeriodTime(rendererPositionUs);
}
private void doSomeWork() throws ExoPlaybackException, IOException {
@@ -587,7 +599,7 @@ import java.util.Collections;
if (resolvedSeekPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed or is not ready and a suitable seek position could not be resolved.
- periodId = new MediaPeriodId(getFirstPeriodIndex());
+ periodId = getFirstMediaPeriodId();
periodPositionUs = C.TIME_UNSET;
contentPositionUs = C.TIME_UNSET;
seekPositionAdjusted = true;
@@ -660,7 +672,7 @@ import java.util.Collections;
MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;
while (newPlayingPeriodHolder != null) {
- if (shouldKeepPeriodHolder(periodId, periodPositionUs, newPlayingPeriodHolder)) {
+ if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) {
queue.removeAfter(newPlayingPeriodHolder);
break;
}
@@ -688,26 +700,17 @@ import java.util.Collections;
maybeContinueLoading();
} else {
queue.clear(/* keepFrontPeriodUid= */ true);
+ // New period has not been prepared.
+ playbackInfo =
+ playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult);
resetRendererPosition(periodPositionUs);
}
+ updateLoadingMediaPeriodId();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
return periodPositionUs;
}
- private boolean shouldKeepPeriodHolder(
- MediaPeriodId seekPeriodId, long positionUs, MediaPeriodHolder holder) {
- if (seekPeriodId.equals(holder.info.id) && holder.prepared) {
- playbackInfo.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 =
!queue.hasPlayingPeriod()
@@ -749,12 +752,15 @@ import java.util.Collections;
}
}
- private int getFirstPeriodIndex() {
+ private MediaPeriodId getFirstMediaPeriodId() {
Timeline timeline = playbackInfo.timeline;
- return timeline.isEmpty()
- ? 0
- : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
+ if (timeline.isEmpty()) {
+ return PlaybackInfo.DUMMY_MEDIA_PERIOD_ID;
+ }
+ int firstPeriodIndex =
+ timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
.firstPeriodIndex;
+ return new MediaPeriodId(firstPeriodIndex);
}
private void resetInternal(
@@ -785,18 +791,25 @@ import java.util.Collections;
pendingMessages.clear();
nextPendingMessageIndex = 0;
}
+ // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
+ MediaPeriodId mediaPeriodId = resetPosition ? getFirstMediaPeriodId() : playbackInfo.periodId;
+ long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
+ long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
playbackInfo =
new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
resetState ? null : playbackInfo.manifest,
- resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId,
- // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
- resetPosition ? C.TIME_UNSET : playbackInfo.positionUs,
- resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs,
+ mediaPeriodId,
+ startPositionUs,
+ contentPositionUs,
playbackInfo.playbackState,
/* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
- resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
+ resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
+ mediaPeriodId,
+ startPositionUs,
+ /* totalBufferedDurationUs= */ 0,
+ startPositionUs);
if (releaseMediaSource) {
if (mediaSource != null) {
mediaSource.releaseSource(/* listener= */ this);
@@ -892,7 +905,7 @@ import java.util.Collections;
pendingMessageInfo.setResolvedPosition(
periodPosition.first,
periodPosition.second,
- playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid);
+ playbackInfo.timeline.getUidOfPeriod(periodPosition.first));
} else {
// Position has been resolved for a previous timeline. Try to find the updated period index.
int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid);
@@ -1051,6 +1064,7 @@ import java.util.Collections;
updateLoadControlTrackSelection(periodHolder.trackGroups, periodHolder.trackSelectorResult);
}
}
+ updateLoadingMediaPeriodId();
if (playbackInfo.playbackState != Player.STATE_ENDED) {
maybeContinueLoading();
updatePlaybackPositions();
@@ -1142,8 +1156,15 @@ import java.util.Collections;
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
pendingPrepareCount = 0;
if (pendingInitialSeekPosition != null) {
- Pair periodPosition =
- resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
+ Pair periodPosition;
+ try {
+ periodPosition =
+ resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
+ } catch (IllegalSeekPositionException e) {
+ playbackInfo =
+ playbackInfo.fromNewPosition(getFirstMediaPeriodId(), C.TIME_UNSET, C.TIME_UNSET);
+ throw e;
+ }
pendingInitialSeekPosition = null;
if (periodPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
@@ -1176,22 +1197,28 @@ import java.util.Collections;
return;
}
- int playingPeriodIndex = playbackInfo.periodId.periodIndex;
- long contentPositionUs = playbackInfo.contentPositionUs;
if (oldTimeline.isEmpty()) {
// If the old timeline is empty, the period queue is also empty.
if (!timeline.isEmpty()) {
- MediaPeriodId periodId =
- queue.resolveMediaPeriodIdForAds(playingPeriodIndex, contentPositionUs);
+ Pair defaultPosition =
+ getPeriodPosition(
+ timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
+ int periodIndex = defaultPosition.first;
+ long startPositionUs = defaultPosition.second;
+ MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs);
playbackInfo =
playbackInfo.fromNewPosition(
- periodId, periodId.isAd() ? 0 : contentPositionUs, contentPositionUs);
+ periodId,
+ /* startPositionUs= */ periodId.isAd() ? 0 : startPositionUs,
+ /* contentPositionUs= */ startPositionUs);
}
return;
}
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
- Object playingPeriodUid = periodHolder == null
- ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid;
+ int playingPeriodIndex = playbackInfo.periodId.periodIndex;
+ long contentPositionUs = playbackInfo.contentPositionUs;
+ Object playingPeriodUid =
+ periodHolder == null ? oldTimeline.getUidOfPeriod(playingPeriodIndex) : periodHolder.uid;
int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid);
if (periodIndex == C.INDEX_UNSET) {
// We didn't find the current period in the new timeline. Attempt to resolve a subsequent
@@ -1208,11 +1235,10 @@ import java.util.Collections;
newPeriodIndex = defaultPosition.first;
contentPositionUs = defaultPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodIndex, contentPositionUs);
- timeline.getPeriod(newPeriodIndex, period, true);
if (periodHolder != null) {
// 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;
+ Object newPeriodUid = timeline.getUidOfPeriod(newPeriodIndex);
periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET);
while (periodHolder.next != null) {
periodHolder = periodHolder.next;
@@ -1249,6 +1275,7 @@ import java.util.Collections;
if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) {
seekToCurrentPosition(/* sendDiscontinuity= */ false);
}
+ updateLoadingMediaPeriodId();
}
private void handleSourceInfoRefreshEndedPlayback() {
@@ -1279,8 +1306,7 @@ import java.util.Collections;
// We've reached the end of the old timeline.
break;
}
- newPeriodIndex = newTimeline.getIndexOfPeriod(
- oldTimeline.getPeriod(oldPeriodIndex, period, true).uid);
+ newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex));
}
return newPeriodIndex;
}
@@ -1324,8 +1350,7 @@ import java.util.Collections;
return periodPosition;
}
// Attempt to find the mapped period in the internal timeline.
- int periodIndex = timeline.getIndexOfPeriod(
- seekTimeline.getPeriod(periodPosition.first, period, true).uid);
+ int periodIndex = timeline.getIndexOfPeriod(seekTimeline.getUidOfPeriod(periodPosition.first));
if (periodIndex != C.INDEX_UNSET) {
// We successfully located the period in the internal timeline.
return Pair.create(periodIndex, periodPosition.second);
@@ -1381,8 +1406,9 @@ import java.util.Collections;
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
boolean advancedPlayingPeriod = false;
- while (playWhenReady && playingPeriodHolder != readingPeriodHolder
- && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) {
+ while (playWhenReady
+ && playingPeriodHolder != readingPeriodHolder
+ && rendererPositionUs >= playingPeriodHolder.next.getStartPositionRendererTime()) {
// All enabled renderers' streams have been read to the end, and the playback position reached
// the end of the playing period, so advance playback to the next period.
if (advancedPlayingPeriod) {
@@ -1483,7 +1509,7 @@ import java.util.Collections;
if (info == null) {
mediaSource.maybeThrowSourceInfoRefreshError();
} else {
- Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid;
+ Object uid = playbackInfo.timeline.getUidOfPeriod(info.id.periodIndex);
MediaPeriod mediaPeriod =
queue.enqueueNextMediaPeriod(
rendererCapabilities,
@@ -1494,6 +1520,7 @@ import java.util.Collections;
info);
mediaPeriod.prepare(this, info.startPositionUs);
setIsLoading(true);
+ updateLoadingMediaPeriodId();
}
}
}
@@ -1525,6 +1552,17 @@ import java.util.Collections;
maybeContinueLoading();
}
+ private void handlePlaybackParameters(PlaybackParameters playbackParameters)
+ throws ExoPlaybackException {
+ eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+ updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
+ for (Renderer renderer : renderers) {
+ if (renderer != null) {
+ renderer.setOperatingRate(playbackParameters.speed);
+ }
+ }
+ }
+
private void maybeContinueLoading() {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
@@ -1543,6 +1581,7 @@ import java.util.Collections;
}
}
+ @SuppressWarnings("ParameterNotNullable")
private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
throws ExoPlaybackException {
MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();
@@ -1619,6 +1658,13 @@ import java.util.Collections;
&& renderer.hasReadStreamToEnd();
}
+ private void updateLoadingMediaPeriodId() {
+ MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod();
+ MediaPeriodId loadingMediaPeriodId =
+ loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id;
+ playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);
+ }
+
@NonNull
private static Format[] getFormats(TrackSelection newSelection) {
// Build an array of formats contained by the selection.
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 98d5fe91b7..b8bf0e8813 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
@@ -29,11 +29,11 @@ public final class 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.
- public static final String VERSION = "2.8.0";
+ public static final String VERSION = "2.8.4";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.4";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2008000;
+ public static final int VERSION_INT = 2008004;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
@@ -51,6 +51,9 @@ public final class ExoPlayerLibraryInfo {
*/
public static final boolean ASSERTIONS_ENABLED = true;
+ /** Whether an exception should be thrown in case of an OpenGl error. */
+ public static final boolean GL_ASSERTIONS_ENABLED = false;
+
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}
* trace enabled.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
index 61d416da09..4c6e1838a8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -46,6 +46,9 @@ public final class Format implements Parcelable {
/** An identifier for the format, or null if unknown or not applicable. */
public final @Nullable String id;
+ /** The human readable label, or null if unknown or not applicable. */
+ public final @Nullable String label;
+
/**
* The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.
*/
@@ -80,6 +83,13 @@ public final class Format implements Parcelable {
/** DRM initialization data if the stream is protected, or null otherwise. */
public final @Nullable DrmInitData drmInitData;
+ /**
+ * For samples that contain subsamples, this is an offset that should be added to subsample
+ * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
+ * relative to the timestamps of their parent samples.
+ */
+ public final long subsampleOffsetUs;
+
// Video specific.
/**
@@ -125,12 +135,12 @@ public final class Format implements Parcelable {
public final int sampleRate;
/**
* The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}
- * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT},
- * {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for
- * other media types.
+ * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link
+ * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link
+ * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other
+ * media types.
*/
- @C.PcmEncoding
- public final int pcmEncoding;
+ public final @C.PcmEncoding int pcmEncoding;
/**
* The number of frames to trim from the start of the decoded audio stream, or 0 if not
* applicable.
@@ -141,15 +151,6 @@ public final class Format implements Parcelable {
*/
public final int encoderPadding;
- // Text specific.
-
- /**
- * For samples that contain subsamples, this is an offset that should be added to subsample
- * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
- * relative to the timestamps of their parent samples.
- */
- public final long subsampleOffsetUs;
-
// Audio and text specific.
/**
@@ -171,6 +172,7 @@ public final class Format implements Parcelable {
// Video.
+ @Deprecated
public static Format createVideoContainerFormat(
@Nullable String id,
@Nullable String containerMimeType,
@@ -180,12 +182,62 @@ public final class Format implements Parcelable {
int width,
int height,
float frameRate,
- List initializationData,
+ @Nullable List initializationData,
@C.SelectionFlags int selectionFlags) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
- height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
- initializationData, null, null);
+ return createVideoContainerFormat(
+ id,
+ /* label= */ null,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ width,
+ height,
+ frameRate,
+ initializationData,
+ selectionFlags);
+ }
+
+ public static Format createVideoContainerFormat(
+ @Nullable String id,
+ @Nullable String label,
+ @Nullable String containerMimeType,
+ String sampleMimeType,
+ String codecs,
+ int bitrate,
+ int width,
+ int height,
+ float frameRate,
+ @Nullable List initializationData,
+ @C.SelectionFlags int selectionFlags) {
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ /* maxInputSize= */ NO_VALUE,
+ width,
+ height,
+ frameRate,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ selectionFlags,
+ /* language= */ null,
+ /* accessibilityChannel= */ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ initializationData,
+ /* drmInitData= */ null,
+ /* metadata= */ null);
}
public static Format createVideoSampleFormat(
@@ -197,10 +249,21 @@ public final class Format implements Parcelable {
int width,
int height,
float frameRate,
- List initializationData,
+ @Nullable List initializationData,
@Nullable DrmInitData drmInitData) {
- return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData);
+ return createVideoSampleFormat(
+ id,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ initializationData,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ drmInitData);
}
public static Format createVideoSampleFormat(
@@ -212,13 +275,26 @@ public final class Format implements Parcelable {
int width,
int height,
float frameRate,
- List initializationData,
+ @Nullable List initializationData,
int rotationDegrees,
float pixelWidthHeightRatio,
@Nullable DrmInitData drmInitData) {
- return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null,
- NO_VALUE, null, drmInitData);
+ return createVideoSampleFormat(
+ id,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ initializationData,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ drmInitData);
}
public static Format createVideoSampleFormat(
@@ -230,21 +306,46 @@ public final class Format implements Parcelable {
int width,
int height,
float frameRate,
- List initializationData,
+ @Nullable List initializationData,
int rotationDegrees,
float pixelWidthHeightRatio,
byte[] projectionData,
@C.StereoMode int stereoMode,
@Nullable ColorInfo colorInfo,
@Nullable DrmInitData drmInitData) {
- return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
- frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE,
- OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null);
+ return new Format(
+ id,
+ /* label= */ null,
+ /* containerMimeType= */ null,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ /* accessibilityChannel= */ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ initializationData,
+ drmInitData,
+ /* metadata= */ null);
}
// Audio.
+ @Deprecated
public static Format createAudioContainerFormat(
@Nullable String id,
@Nullable String containerMimeType,
@@ -253,13 +354,63 @@ public final class Format implements Parcelable {
int bitrate,
int channelCount,
int sampleRate,
- List initializationData,
+ @Nullable List initializationData,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate,
- NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
- initializationData, null, null);
+ return createAudioContainerFormat(
+ id,
+ /* label= */ null,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ channelCount,
+ sampleRate,
+ initializationData,
+ selectionFlags,
+ language);
+ }
+
+ public static Format createAudioContainerFormat(
+ @Nullable String id,
+ @Nullable String label,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int channelCount,
+ int sampleRate,
+ @Nullable List initializationData,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ channelCount,
+ sampleRate,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ selectionFlags,
+ language,
+ /* accessibilityChannel= */ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ initializationData,
+ /* drmInitData= */ null,
+ /* metadata= */ null);
}
public static Format createAudioSampleFormat(
@@ -270,12 +421,23 @@ public final class Format implements Parcelable {
int maxInputSize,
int channelCount,
int sampleRate,
- List initializationData,
+ @Nullable List initializationData,
@Nullable DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
- return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
- sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language);
+ return createAudioSampleFormat(
+ id,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ channelCount,
+ sampleRate,
+ /* pcmEncoding= */ NO_VALUE,
+ initializationData,
+ drmInitData,
+ selectionFlags,
+ language);
}
public static Format createAudioSampleFormat(
@@ -287,13 +449,26 @@ public final class Format implements Parcelable {
int channelCount,
int sampleRate,
@C.PcmEncoding int pcmEncoding,
- List initializationData,
+ @Nullable List initializationData,
@Nullable DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
- return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
- sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
- selectionFlags, language, null);
+ return createAudioSampleFormat(
+ id,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ initializationData,
+ drmInitData,
+ selectionFlags,
+ language,
+ /* metadata= */ null);
}
public static Format createAudioSampleFormat(
@@ -307,19 +482,44 @@ public final class Format implements Parcelable {
@C.PcmEncoding int pcmEncoding,
int encoderDelay,
int encoderPadding,
- List initializationData,
+ @Nullable List initializationData,
@Nullable DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags,
@Nullable String language,
@Nullable Metadata metadata) {
- return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding,
- encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
- initializationData, drmInitData, metadata);
+ return new Format(
+ id,
+ /* label= */ null,
+ /* containerMimeType= */ null,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ /* accessibilityChannel= */ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ initializationData,
+ drmInitData,
+ metadata);
}
// Text.
+ @Deprecated
public static Format createTextContainerFormat(
@Nullable String id,
@Nullable String containerMimeType,
@@ -328,12 +528,41 @@ public final class Format implements Parcelable {
int bitrate,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
- return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
- selectionFlags, language, NO_VALUE);
+ return createTextContainerFormat(
+ id,
+ /* label= */ null,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ selectionFlags,
+ language);
}
public static Format createTextContainerFormat(
@Nullable String id,
+ @Nullable String label,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
+ return createTextContainerFormat(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ selectionFlags,
+ language,
+ /* accessibilityChannel= */ NO_VALUE);
+ }
+
+ public static Format createTextContainerFormat(
+ @Nullable String id,
+ @Nullable String label,
@Nullable String containerMimeType,
@Nullable String sampleMimeType,
@Nullable String codecs,
@@ -341,10 +570,34 @@ public final class Format implements Parcelable {
@C.SelectionFlags int selectionFlags,
@Nullable String language,
int accessibilityChannel) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel,
- OFFSET_SAMPLE_RELATIVE, null, null, null);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ OFFSET_SAMPLE_RELATIVE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* metadata= */ null);
}
public static Format createTextSampleFormat(
@@ -361,8 +614,17 @@ public final class Format implements Parcelable {
@C.SelectionFlags int selectionFlags,
@Nullable String language,
@Nullable DrmInitData drmInitData) {
- return createTextSampleFormat(id, sampleMimeType, null, NO_VALUE, selectionFlags, language,
- NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList());
+ return createTextSampleFormat(
+ id,
+ sampleMimeType,
+ /* codecs= */ null,
+ /* bitrate= */ NO_VALUE,
+ selectionFlags,
+ language,
+ NO_VALUE,
+ drmInitData,
+ OFFSET_SAMPLE_RELATIVE,
+ Collections.emptyList());
}
public static Format createTextSampleFormat(
@@ -374,8 +636,17 @@ public final class Format implements Parcelable {
@Nullable String language,
int accessibilityChannel,
@Nullable DrmInitData drmInitData) {
- return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
- accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList());
+ return createTextSampleFormat(
+ id,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ drmInitData,
+ OFFSET_SAMPLE_RELATIVE,
+ Collections.emptyList());
}
public static Format createTextSampleFormat(
@@ -387,8 +658,17 @@ public final class Format implements Parcelable {
@Nullable String language,
@Nullable DrmInitData drmInitData,
long subsampleOffsetUs) {
- return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
- NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList());
+ return createTextSampleFormat(
+ id,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ selectionFlags,
+ language,
+ /* accessibilityChannel= */ NO_VALUE,
+ drmInitData,
+ subsampleOffsetUs,
+ Collections.emptyList());
}
public static Format createTextSampleFormat(
@@ -402,10 +682,34 @@ public final class Format implements Parcelable {
@Nullable DrmInitData drmInitData,
long subsampleOffsetUs,
List initializationData) {
- return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
- initializationData, drmInitData, null);
+ return new Format(
+ id,
+ /* label= */ null,
+ /* containerMimeType= */ null,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ /* metadata= */ null);
}
// Image.
@@ -416,40 +720,42 @@ public final class Format implements Parcelable {
@Nullable String codecs,
int bitrate,
@C.SelectionFlags int selectionFlags,
- List initializationData,
+ @Nullable List initializationData,
@Nullable String language,
@Nullable DrmInitData drmInitData) {
return new Format(
id,
- null,
+ /* label= */ null,
+ /* containerMimeType= */ null,
sampleMimeType,
codecs,
bitrate,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
- null,
- NO_VALUE,
- null,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
- NO_VALUE,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
selectionFlags,
language,
- NO_VALUE,
+ /* accessibilityChannel= */ NO_VALUE,
OFFSET_SAMPLE_RELATIVE,
initializationData,
drmInitData,
- null);
+ /* metadata=*/ null);
}
// Generic.
+ @Deprecated
public static Format createContainerFormat(
@Nullable String id,
@Nullable String containerMimeType,
@@ -458,17 +764,86 @@ public final class Format implements Parcelable {
int bitrate,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
- null);
+ return createContainerFormat(
+ id,
+ /* label= */ null,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ selectionFlags,
+ language);
+ }
+
+ public static Format createContainerFormat(
+ @Nullable String id,
+ @Nullable String label,
+ @Nullable String containerMimeType,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ selectionFlags,
+ language,
+ /* accessibilityChannel= */ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* metadata= */ null);
}
public static Format createSampleFormat(
@Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) {
- return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
+ return new Format(
+ id,
+ /* label= */ null,
+ /* containerMimeType= */ null,
+ sampleMimeType,
+ /* codecs= */ null,
+ /* bitrate= */ NO_VALUE,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ /* accessibilityChannel= */ NO_VALUE,
+ subsampleOffsetUs,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* metadata= */ null);
}
public static Format createSampleFormat(
@@ -477,13 +852,39 @@ public final class Format implements Parcelable {
@Nullable String codecs,
int bitrate,
@Nullable DrmInitData drmInitData) {
- return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
+ return new Format(
+ id,
+ /* label= */ null,
+ /* containerMimeType= */ null,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ /* maxInputSize= */ NO_VALUE,
+ /* width= */ NO_VALUE,
+ /* height= */ NO_VALUE,
+ /* frameRate= */ NO_VALUE,
+ /* rotationDegrees= */ NO_VALUE,
+ /* pixelWidthHeightRatio= */ NO_VALUE,
+ /* projectionData= */ null,
+ /* stereoMode= */ NO_VALUE,
+ /* colorInfo= */ null,
+ /* channelCount= */ NO_VALUE,
+ /* sampleRate= */ NO_VALUE,
+ /* pcmEncoding= */ NO_VALUE,
+ /* encoderDelay= */ NO_VALUE,
+ /* encoderPadding= */ NO_VALUE,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ /* accessibilityChannel= */ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ /* initializationData= */ null,
+ drmInitData,
+ /* metadata= */ null);
}
/* package */ Format(
@Nullable String id,
+ @Nullable String label,
@Nullable String containerMimeType,
@Nullable String sampleMimeType,
@Nullable String codecs,
@@ -510,6 +911,7 @@ public final class Format implements Parcelable {
@Nullable DrmInitData drmInitData,
@Nullable Metadata metadata) {
this.id = id;
+ this.label = label;
this.containerMimeType = containerMimeType;
this.sampleMimeType = sampleMimeType;
this.codecs = codecs;
@@ -533,8 +935,8 @@ public final class Format implements Parcelable {
this.language = language;
this.accessibilityChannel = accessibilityChannel;
this.subsampleOffsetUs = subsampleOffsetUs;
- this.initializationData = initializationData == null ? Collections.emptyList()
- : initializationData;
+ this.initializationData =
+ initializationData == null ? Collections.emptyList() : initializationData;
this.drmInitData = drmInitData;
this.metadata = metadata;
}
@@ -542,6 +944,7 @@ public final class Format implements Parcelable {
@SuppressWarnings("ResourceType")
/* package */ Format(Parcel in) {
id = in.readString();
+ label = in.readString();
containerMimeType = in.readString();
sampleMimeType = in.readString();
codecs = in.readString();
@@ -575,23 +978,70 @@ public final class Format implements Parcelable {
}
public Format copyWithMaxInputSize(int maxInputSize) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
public Format copyWithContainerInfo(
@Nullable String id,
+ @Nullable String label,
@Nullable String sampleMimeType,
@Nullable String codecs,
int bitrate,
@@ -599,11 +1049,34 @@ public final class Format implements Parcelable {
int height,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
@SuppressWarnings("ReferenceEquality")
@@ -612,51 +1085,193 @@ public final class Format implements Parcelable {
// No need to copy from ourselves.
return this;
}
+
+ int trackType = MimeTypes.getTrackType(sampleMimeType);
+
+ // Use manifest value only.
String id = manifestFormat.id;
- String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
+
+ // Prefer manifest values, but fill in from sample format if missing.
+ String label = manifestFormat.label != null ? manifestFormat.label : this.label;
+ String language = this.language;
+ if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO)
+ && manifestFormat.language != null) {
+ language = manifestFormat.language;
+ }
+
+ // Prefer sample format values, but fill in from manifest if missing.
int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
- float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
- @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
- String language = this.language == null ? manifestFormat.language : this.language;
+ String codecs = this.codecs;
+ if (codecs == null) {
+ // The manifest format may be muxed, so filter only codecs of this format's type. If we still
+ // have more than one codec then we're unable to uniquely identify which codec to fill in.
+ String codecsOfType = Util.getCodecsOfType(manifestFormat.codecs, trackType);
+ if (Util.splitCodecs(codecsOfType).length == 1) {
+ codecs = codecsOfType;
+ }
+ }
+ float frameRate = this.frameRate;
+ if (frameRate == NO_VALUE && trackType == C.TRACK_TYPE_VIDEO) {
+ frameRate = manifestFormat.frameRate;
+ }
+
+ // Merge manifest and sample format values.
+ @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
DrmInitData drmInitData =
DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData);
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
public Format copyWithMetadata(@Nullable Metadata metadata) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
public Format copyWithRotationDegrees(int rotationDegrees) {
- return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
- height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
- colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
- selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
- drmInitData, metadata);
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
}
/**
@@ -669,9 +1284,32 @@ public final class Format implements Parcelable {
@Override
public String toString() {
- return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
- + language + ", [" + width + ", " + height + ", " + frameRate + "]"
- + ", [" + channelCount + ", " + sampleRate + "])";
+ return "Format("
+ + id
+ + ", "
+ + label
+ + ", "
+ + containerMimeType
+ + ", "
+ + sampleMimeType
+ + ", "
+ + codecs
+ + ", "
+ + bitrate
+ + ", "
+ + language
+ + ", ["
+ + width
+ + ", "
+ + height
+ + ", "
+ + frameRate
+ + "]"
+ + ", ["
+ + channelCount
+ + ", "
+ + sampleRate
+ + "])";
}
@Override
@@ -691,6 +1329,18 @@ public final class Format implements Parcelable {
result = 31 * result + accessibilityChannel;
result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
+ result = 31 * result + (label != null ? label.hashCode() : 0);
+ result = 31 * result + maxInputSize;
+ result = 31 * result + (int) subsampleOffsetUs;
+ result = 31 * result + Float.floatToIntBits(frameRate);
+ result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio);
+ result = 31 * result + rotationDegrees;
+ result = 31 * result + stereoMode;
+ result = 31 * result + pcmEncoding;
+ result = 31 * result + encoderDelay;
+ result = 31 * result + encoderPadding;
+ result = 31 * result + selectionFlags;
+ // Not all of the fields are included to keep the calculation quick enough.
hashCode = result;
}
return hashCode;
@@ -705,13 +1355,16 @@ public final class Format implements Parcelable {
return false;
}
Format other = (Format) obj;
+ if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) {
+ return false;
+ }
return bitrate == other.bitrate
&& maxInputSize == other.maxInputSize
&& width == other.width
&& height == other.height
- && frameRate == other.frameRate
+ && Float.compare(frameRate, other.frameRate) == 0
&& rotationDegrees == other.rotationDegrees
- && pixelWidthHeightRatio == other.pixelWidthHeightRatio
+ && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0
&& stereoMode == other.stereoMode
&& channelCount == other.channelCount
&& sampleRate == other.sampleRate
@@ -721,6 +1374,7 @@ public final class Format implements Parcelable {
&& subsampleOffsetUs == other.subsampleOffsetUs
&& selectionFlags == other.selectionFlags
&& Util.areEqual(id, other.id)
+ && Util.areEqual(label, other.label)
&& Util.areEqual(language, other.language)
&& accessibilityChannel == other.accessibilityChannel
&& Util.areEqual(containerMimeType, other.containerMimeType)
@@ -767,6 +1421,9 @@ public final class Format implements Parcelable {
if (format.bitrate != Format.NO_VALUE) {
builder.append(", bitrate=").append(format.bitrate);
}
+ if (format.codecs != null) {
+ builder.append(", codecs=").append(format.codecs);
+ }
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
}
@@ -782,6 +1439,9 @@ public final class Format implements Parcelable {
if (format.language != null) {
builder.append(", language=").append(format.language);
}
+ if (format.label != null) {
+ builder.append(", label=").append(format.label);
+ }
return builder.toString();
}
@@ -795,6 +1455,7 @@ public final class Format implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
+ dest.writeString(label);
dest.writeString(containerMimeType);
dest.writeString(sampleMimeType);
dest.writeString(codecs);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java
index b26787517e..8c7ba1eb91 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java
@@ -15,14 +15,13 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
+
/**
* Holds a {@link Format}.
*/
public final class FormatHolder {
- /**
- * The held {@link Format}.
- */
- public Format format;
-
+ /** The held {@link Format}. */
+ public @Nullable Format format;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
index 2f71d0d547..a74a2ac1ca 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
@@ -39,7 +39,6 @@ import com.google.android.exoplayer2.util.Assertions;
public final SampleStream[] sampleStreams;
public final boolean[] mayRetainStreamFlags;
- public long rendererPositionOffsetUs;
public boolean prepared;
public boolean hasEnabledTracks;
public MediaPeriodInfo info;
@@ -51,6 +50,7 @@ import com.google.android.exoplayer2.util.Assertions;
private final TrackSelector trackSelector;
private final MediaSource mediaSource;
+ private long rendererPositionOffsetUs;
private TrackSelectorResult periodTrackSelectorResult;
/**
@@ -82,13 +82,13 @@ import com.google.android.exoplayer2.util.Assertions;
sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator);
- if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaPeriod =
new ClippingMediaPeriod(
mediaPeriod,
/* enableInitialDiscontinuity= */ true,
/* startUs= */ 0,
- info.endPositionUs);
+ info.id.endPositionUs);
}
this.mediaPeriod = mediaPeriod;
}
@@ -105,6 +105,10 @@ import com.google.android.exoplayer2.util.Assertions;
return rendererPositionOffsetUs;
}
+ public long getStartPositionRendererTime() {
+ return info.startPositionUs + rendererPositionOffsetUs;
+ }
+
public boolean isFullyBuffered() {
return prepared
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
@@ -127,7 +131,8 @@ import com.google.android.exoplayer2.util.Assertions;
if (!prepared) {
return info.startPositionUs;
}
- long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
+ long bufferedPositionUs =
+ hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE;
return bufferedPositionUs == C.TIME_END_OF_SOURCE && convertEosToDuration
? info.durationUs
: bufferedPositionUs;
@@ -218,7 +223,7 @@ import com.google.android.exoplayer2.util.Assertions;
public void release() {
updatePeriodTrackSelectorResult(null);
try {
- if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} else {
mediaSource.releasePeriod(mediaPeriod);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
index fce1780b71..b887e8222e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
@@ -25,18 +25,13 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
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;
/**
* If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
* otherwise.
*/
public final long contentPositionUs;
/**
- * The duration of the media period, like {@link #endPositionUs} but with {@link
+ * The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link
* C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if
* the end position is not known.
*/
@@ -55,14 +50,12 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
MediaPeriodInfo(
MediaPeriodId id,
long startPositionUs,
- long endPositionUs,
long contentPositionUs,
long durationUs,
boolean isLastInTimelinePeriod,
boolean isFinal) {
this.id = id;
this.startPositionUs = startPositionUs;
- this.endPositionUs = endPositionUs;
this.contentPositionUs = contentPositionUs;
this.durationUs = durationUs;
this.isLastInTimelinePeriod = isLastInTimelinePeriod;
@@ -77,7 +70,6 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
return new MediaPeriodInfo(
id.copyWithPeriodIndex(periodIndex),
startPositionUs,
- endPositionUs,
contentPositionUs,
durationUs,
isLastInTimelinePeriod,
@@ -89,7 +81,6 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
return new MediaPeriodInfo(
id,
startPositionUs,
- endPositionUs,
contentPositionUs,
durationUs,
isLastInTimelinePeriod,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
index 717f873622..e9be2d985e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
@@ -47,17 +47,18 @@ import com.google.android.exoplayer2.util.Assertions;
private Timeline timeline;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
- private MediaPeriodHolder playing;
- private MediaPeriodHolder reading;
- private MediaPeriodHolder loading;
+ private @Nullable MediaPeriodHolder playing;
+ private @Nullable MediaPeriodHolder reading;
+ private @Nullable MediaPeriodHolder loading;
private int length;
- private Object oldFrontPeriodUid;
+ private @Nullable Object oldFrontPeriodUid;
private long oldFrontPeriodWindowSequenceNumber;
/** Creates a new media period queue. */
public MediaPeriodQueue() {
period = new Timeline.Period();
window = new Timeline.Window();
+ timeline = Timeline.EMPTY;
}
/**
@@ -228,11 +229,13 @@ import com.google.android.exoplayer2.util.Assertions;
reading = playing.next;
}
playing.release();
- playing = playing.next;
length--;
if (length == 0) {
loading = null;
+ oldFrontPeriodUid = playing.uid;
+ oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
}
+ playing = playing.next;
} else {
playing = loading;
reading = loading;
@@ -312,7 +315,7 @@ import com.google.android.exoplayer2.util.Assertions;
} else {
// Check this period holder still follows the previous one, based on the new timeline.
if (periodIndex == C.INDEX_UNSET
- || !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
+ || !periodHolder.uid.equals(timeline.getUidOfPeriod(periodIndex))) {
// The holder uid is inconsistent with the new timeline.
return !removeAfter(previousPeriodHolder);
}
@@ -389,7 +392,12 @@ import com.google.android.exoplayer2.util.Assertions;
timeline.getPeriod(periodIndex, period);
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex == C.INDEX_UNSET) {
- return new MediaPeriodId(periodIndex, windowSequenceNumber);
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
+ long endPositionUs =
+ nextAdGroupIndex == C.INDEX_UNSET
+ ? C.TIME_END_OF_SOURCE
+ : period.getAdGroupTimeUs(nextAdGroupIndex);
+ return new MediaPeriodId(periodIndex, windowSequenceNumber, endPositionUs);
} else {
int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
@@ -448,7 +456,6 @@ import com.google.android.exoplayer2.util.Assertions;
private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) {
MediaPeriodInfo periodHolderInfo = periodHolder.info;
return periodHolderInfo.startPositionUs == info.startPositionUs
- && periodHolderInfo.endPositionUs == info.endPositionUs
&& periodHolderInfo.id.equals(info.id);
}
@@ -591,14 +598,14 @@ import com.google.android.exoplayer2.util.Assertions;
mediaPeriodInfo.contentPositionUs,
currentPeriodId.windowSequenceNumber);
}
- } else if (mediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) {
+ } else if (mediaPeriodInfo.id.endPositionUs != C.TIME_END_OF_SOURCE) {
// Play the next ad group if it's available.
- int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
+ int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs);
if (nextAdGroupIndex == C.INDEX_UNSET) {
// The next ad group can't be played. Play content from the ad group position instead.
return getMediaPeriodInfoForContent(
currentPeriodId.periodIndex,
- mediaPeriodInfo.endPositionUs,
+ mediaPeriodInfo.id.endPositionUs,
currentPeriodId.windowSequenceNumber);
}
int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex);
@@ -608,7 +615,7 @@ import com.google.android.exoplayer2.util.Assertions;
currentPeriodId.periodIndex,
nextAdGroupIndex,
adIndexInAdGroup,
- mediaPeriodInfo.endPositionUs,
+ mediaPeriodInfo.id.endPositionUs,
currentPeriodId.windowSequenceNumber);
} else {
// Check if the postroll ad should be played.
@@ -637,18 +644,18 @@ import com.google.android.exoplayer2.util.Assertions;
private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) {
long startPositionUs = info.startPositionUs;
- long endPositionUs = info.endPositionUs;
- boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs);
+ boolean isLastInPeriod = isLastInPeriod(newId);
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);
+ : (newId.endPositionUs == C.TIME_END_OF_SOURCE
+ ? period.getDurationUs()
+ : newId.endPositionUs);
return new MediaPeriodInfo(
newId,
startPositionUs,
- endPositionUs,
info.contentPositionUs,
durationUs,
isLastInPeriod,
@@ -681,7 +688,7 @@ import com.google.android.exoplayer2.util.Assertions;
long windowSequenceNumber) {
MediaPeriodId id =
new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
- boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE);
+ boolean isLastInPeriod = isLastInPeriod(id);
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
long durationUs =
timeline
@@ -694,7 +701,6 @@ import com.google.android.exoplayer2.util.Assertions;
return new MediaPeriodInfo(
id,
startPositionUs,
- C.TIME_END_OF_SOURCE,
contentPositionUs,
durationUs,
isLastInPeriod,
@@ -703,21 +709,22 @@ import com.google.android.exoplayer2.util.Assertions;
private MediaPeriodInfo getMediaPeriodInfoForContent(
int periodIndex, long startPositionUs, long windowSequenceNumber) {
- MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber);
- timeline.getPeriod(id.periodIndex, period);
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
- long endUs =
+ long endPositionUs =
nextAdGroupIndex == C.INDEX_UNSET
? C.TIME_END_OF_SOURCE
: period.getAdGroupTimeUs(nextAdGroupIndex);
- boolean isLastInPeriod = isLastInPeriod(id, endUs);
+ MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber, endPositionUs);
+ timeline.getPeriod(id.periodIndex, period);
+ boolean isLastInPeriod = isLastInPeriod(id);
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
- long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs;
+ long durationUs =
+ endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs;
return new MediaPeriodInfo(
- id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline);
+ id, startPositionUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline);
}
- private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) {
+ private boolean isLastInPeriod(MediaPeriodId id) {
int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount();
if (adGroupCount == 0) {
return true;
@@ -727,7 +734,7 @@ import com.google.android.exoplayer2.util.Assertions;
boolean isAd = id.isAd();
if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) {
// There's no postroll ad.
- return !isAd && endPositionUs == C.TIME_END_OF_SOURCE;
+ return !isAd && id.endPositionUs == C.TIME_END_OF_SOURCE;
}
int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
index 80de073e2d..b338de15b4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
@@ -25,34 +25,81 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
*/
/* package */ final class PlaybackInfo {
+ /**
+ * Dummy media period id used while the timeline is empty and no period id is specified. This id
+ * is used when playback infos are created with {@link #createDummy(long, TrackSelectorResult)}.
+ */
+ public static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID = new MediaPeriodId(/* periodIndex= */ 0);
+
+ /** The current {@link Timeline}. */
public final Timeline timeline;
+ /** The current manifest. */
public final @Nullable Object manifest;
+ /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */
public final MediaPeriodId periodId;
+ /**
+ * The start position at which playback started in {@link #periodId} relative to the start of the
+ * associated period in the {@link #timeline}, in microseconds.
+ */
public final long startPositionUs;
+ /**
+ * If {@link #periodId} refers to an ad, the position of the suspended content relative to the
+ * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET}
+ * if {@link #periodId} does not refer to an ad.
+ */
public final long contentPositionUs;
+ /** The current playback state. One of the {@link Player}.STATE_ constants. */
public final int playbackState;
+ /** Whether the player is currently loading. */
public final boolean isLoading;
+ /** The currently available track groups. */
public final TrackGroupArray trackGroups;
+ /** The result of the current track selection. */
public final TrackSelectorResult trackSelectorResult;
+ /** The {@link MediaPeriodId} of the currently loading media period in the {@link #timeline}. */
+ public final MediaPeriodId loadingMediaPeriodId;
- public volatile long positionUs;
+ /**
+ * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start
+ * of the associated period in the {@link #timeline}, in microseconds.
+ */
public volatile long bufferedPositionUs;
+ /**
+ * Total duration of buffered media from {@link #positionUs} to {@link #bufferedPositionUs}
+ * including all ads.
+ */
+ public volatile long totalBufferedDurationUs;
+ /**
+ * Current playback position in {@link #periodId} relative to the start of the associated period
+ * in the {@link #timeline}, in microseconds.
+ */
+ public volatile long positionUs;
- public PlaybackInfo(
- Timeline timeline,
- long startPositionUs,
- TrackGroupArray trackGroups,
- TrackSelectorResult trackSelectorResult) {
- this(
- timeline,
+ /**
+ * Creates empty dummy playback info which can be used for masking as long as no real playback
+ * info is available.
+ *
+ * @param startPositionUs The start position at which playback should start, in microseconds.
+ * @param emptyTrackSelectorResult An empty track selector result with null entries for each
+ * renderer.
+ * @return A dummy playback info.
+ */
+ public static PlaybackInfo createDummy(
+ long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) {
+ return new PlaybackInfo(
+ Timeline.EMPTY,
/* manifest= */ null,
- new MediaPeriodId(/* periodIndex= */ 0),
+ DUMMY_MEDIA_PERIOD_ID,
startPositionUs,
- /* contentPositionUs =*/ C.TIME_UNSET,
+ /* contentPositionUs= */ C.TIME_UNSET,
Player.STATE_IDLE,
/* isLoading= */ false,
- trackGroups,
- trackSelectorResult);
+ TrackGroupArray.EMPTY,
+ emptyTrackSelectorResult,
+ DUMMY_MEDIA_PERIOD_ID,
+ startPositionUs,
+ /* totalBufferedDurationUs= */ 0,
+ startPositionUs);
}
public PlaybackInfo(
@@ -64,18 +111,24 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
int playbackState,
boolean isLoading,
TrackGroupArray trackGroups,
- TrackSelectorResult trackSelectorResult) {
+ TrackSelectorResult trackSelectorResult,
+ MediaPeriodId loadingMediaPeriodId,
+ long bufferedPositionUs,
+ long totalBufferedDurationUs,
+ long positionUs) {
this.timeline = timeline;
this.manifest = manifest;
this.periodId = periodId;
this.startPositionUs = startPositionUs;
this.contentPositionUs = contentPositionUs;
- this.positionUs = startPositionUs;
- this.bufferedPositionUs = startPositionUs;
this.playbackState = playbackState;
this.isLoading = isLoading;
this.trackGroups = trackGroups;
this.trackSelectorResult = trackSelectorResult;
+ this.loadingMediaPeriodId = loadingMediaPeriodId;
+ this.bufferedPositionUs = bufferedPositionUs;
+ this.totalBufferedDurationUs = totalBufferedDurationUs;
+ this.positionUs = positionUs;
}
public PlaybackInfo fromNewPosition(
@@ -89,93 +142,113 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
playbackState,
isLoading,
trackGroups,
- trackSelectorResult);
+ trackSelectorResult,
+ periodId,
+ startPositionUs,
+ /* totalBufferedDurationUs= */ 0,
+ startPositionUs);
}
public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
- PlaybackInfo playbackInfo =
- new PlaybackInfo(
- timeline,
- manifest,
- periodId.copyWithPeriodIndex(periodIndex),
- startPositionUs,
- contentPositionUs,
- playbackState,
- isLoading,
- trackGroups,
- trackSelectorResult);
- copyMutablePositions(this, playbackInfo);
- return playbackInfo;
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId.copyWithPeriodIndex(periodIndex),
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackGroups,
+ trackSelectorResult,
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) {
- PlaybackInfo playbackInfo =
- new PlaybackInfo(
- timeline,
- manifest,
- periodId,
- startPositionUs,
- contentPositionUs,
- playbackState,
- isLoading,
- trackGroups,
- trackSelectorResult);
- copyMutablePositions(this, playbackInfo);
- return playbackInfo;
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackGroups,
+ trackSelectorResult,
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
public PlaybackInfo copyWithPlaybackState(int playbackState) {
- PlaybackInfo playbackInfo =
- new PlaybackInfo(
- timeline,
- manifest,
- periodId,
- startPositionUs,
- contentPositionUs,
- playbackState,
- isLoading,
- trackGroups,
- trackSelectorResult);
- copyMutablePositions(this, playbackInfo);
- return playbackInfo;
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackGroups,
+ trackSelectorResult,
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
public PlaybackInfo copyWithIsLoading(boolean isLoading) {
- PlaybackInfo playbackInfo =
- new PlaybackInfo(
- timeline,
- manifest,
- periodId,
- startPositionUs,
- contentPositionUs,
- playbackState,
- isLoading,
- trackGroups,
- trackSelectorResult);
- copyMutablePositions(this, playbackInfo);
- return playbackInfo;
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackGroups,
+ trackSelectorResult,
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
public PlaybackInfo copyWithTrackInfo(
TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
- PlaybackInfo playbackInfo =
- new PlaybackInfo(
- timeline,
- manifest,
- periodId,
- startPositionUs,
- contentPositionUs,
- playbackState,
- isLoading,
- trackGroups,
- trackSelectorResult);
- copyMutablePositions(this, playbackInfo);
- return playbackInfo;
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackGroups,
+ trackSelectorResult,
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
- private static void copyMutablePositions(PlaybackInfo from, PlaybackInfo to) {
- to.positionUs = from.positionUs;
- to.bufferedPositionUs = from.bufferedPositionUs;
+ public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) {
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackGroups,
+ trackSelectorResult,
+ loadingMediaPeriodId,
+ bufferedPositionUs,
+ totalBufferedDurationUs,
+ positionUs);
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
index 328816d709..87aec0c253 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -22,9 +22,13 @@ 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.AudioListener;
+import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -50,6 +54,89 @@ import java.lang.annotation.RetentionPolicy;
*/
public interface Player {
+ /** The audio component of a {@link Player}. */
+ interface AudioComponent {
+
+ /**
+ * Adds a listener to receive audio events.
+ *
+ * @param listener The listener to register.
+ */
+ void addAudioListener(AudioListener listener);
+
+ /**
+ * Removes a listener of audio events.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeAudioListener(AudioListener listener);
+
+ /**
+ * 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.
+ * @deprecated Use {@link AudioComponent#setAudioAttributes(AudioAttributes, boolean)}.
+ */
+ @Deprecated
+ void setAudioAttributes(AudioAttributes audioAttributes);
+
+ /**
+ * 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)}.
+ *
+ *
If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link
+ * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link
+ * IllegalArgumentException}.
+ *
+ * @param audioAttributes The attributes to use for audio playback.
+ * @param handleAudioFocus True if the player should handle audio focus, false otherwise.
+ */
+ void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus);
+
+ /** Returns the attributes for audio playback. */
+ AudioAttributes getAudioAttributes();
+
+ /** Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */
+ int getAudioSessionId();
+
+ /** Sets information on an auxiliary audio effect to attach to the underlying audio track. */
+ void setAuxEffectInfo(AuxEffectInfo auxEffectInfo);
+
+ /** Detaches any previously attached auxiliary audio effect from the underlying audio track. */
+ void clearAuxEffectInfo();
+
+ /**
+ * Sets the audio volume, with 0 being silence and 1 being unity gain.
+ *
+ * @param audioVolume The audio volume.
+ */
+ void setVolume(float audioVolume);
+
+ /** Returns the audio volume, with 0 being silence and 1 being unity gain. */
+ float getVolume();
+ }
+
/** The video component of a {@link Player}. */
interface VideoComponent {
@@ -97,7 +184,7 @@ public interface Player {
*
* @param surface The {@link Surface}.
*/
- void setVideoSurface(Surface surface);
+ void setVideoSurface(@Nullable Surface surface);
/**
* Clears the {@link Surface} onto which video is being rendered if it matches the one passed.
@@ -175,23 +262,25 @@ public interface Player {
}
/**
- * Listener of changes in player state.
+ * Listener of changes in player state. All methods have no-op default implementations to allow
+ * selective overrides.
*/
interface EventListener {
/**
* 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. This will not be reported via a separate call to
+ *
+ *
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. This will not be reported via a separate call to
* {@link #onPositionDiscontinuity(int)}.
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/
- void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason);
+ default void onTimelineChanged(
+ Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {}
/**
* Called when the available or selected tracks change.
@@ -200,46 +289,47 @@ public interface Player {
* @param trackSelections The track selections for each renderer. Never null and always of
* length {@link #getRendererCount()}, but may contain null elements.
*/
- void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+ default void onTracksChanged(
+ TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
/**
* Called when the player starts or stops loading the source.
*
* @param isLoading Whether the source is currently being loaded.
*/
- void onLoadingChanged(boolean isLoading);
+ default void onLoadingChanged(boolean isLoading) {}
/**
- * Called when the value returned from either {@link #getPlayWhenReady()} or
- * {@link #getPlaybackState()} changes.
+ * Called when the value returned from either {@link #getPlayWhenReady()} or {@link
+ * #getPlaybackState()} changes.
*
* @param playWhenReady Whether playback will proceed when ready.
* @param playbackState One of the {@code STATE} constants.
*/
- void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+ default 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);
+ default void onRepeatModeChanged(@RepeatMode int repeatMode) {}
/**
* Called when the value of {@link #getShuffleModeEnabled()} changes.
*
* @param shuffleModeEnabled Whether shuffling of windows is enabled.
*/
- void onShuffleModeEnabledChanged(boolean shuffleModeEnabled);
+ default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {}
/**
* 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
- * {@link #release()} must still be called on the player should it no longer be required.
+ * immediately after this method is called. The player instance can still be used, and {@link
+ * #release()} must still be called on the player should it no longer be required.
*
* @param error The error.
*/
- void onPlayerError(ExoPlaybackException error);
+ default void onPlayerError(ExoPlaybackException error) {}
/**
* Called when a position discontinuity occurs without a change to the timeline. A position
@@ -247,14 +337,14 @@ public interface Player {
* transitioning from one period in the timeline to the next), or when the playback position
* jumps within the period currently being played (as a result of a seek being performed, or
* when the source introduces a discontinuity internally).
- *
- * When a position discontinuity occurs as a result of a change to the timeline this method is
- * not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this
+ *
+ *
When a position discontinuity occurs as a result of a change to the timeline this method
+ * is not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this
* case.
*
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
- void onPositionDiscontinuity(@DiscontinuityReason int reason);
+ default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
/**
* Called when the current playback parameters change. The playback parameters may change due to
@@ -264,83 +354,35 @@ public interface Player {
*
* @param playbackParameters The playback parameters.
*/
- void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
+ default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}
/**
* Called when all pending seek requests have been processed by the player. This is guaranteed
- * to happen after any necessary changes to the player state were reported to
- * {@link #onPlayerStateChanged(boolean, int)}.
+ * to happen after any necessary changes to the player state were reported to {@link
+ * #onPlayerStateChanged(boolean, int)}.
*/
- void onSeekProcessed();
-
+ default void onSeekProcessed() {}
}
/**
- * {@link EventListener} allowing selective overrides. All methods are implemented as no-ops.
+ * @deprecated Use {@link EventListener} interface directly for selective overrides as all methods
+ * are implemented as no-op default methods.
*/
+ @Deprecated
abstract class DefaultEventListener implements EventListener {
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest,
- @TimelineChangeReason int reason) {
+ public void onTimelineChanged(
+ Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
// Call deprecated version. Otherwise, do nothing.
onTimelineChanged(timeline, manifest);
}
- @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) {
- // Do nothing.
- }
-
- @Override
- public void onRepeatModeChanged(@RepeatMode int repeatMode) {
- // Do nothing.
- }
-
- @Override
- public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
- // Do nothing.
- }
-
- @Override
- public void onPlayerError(ExoPlaybackException error) {
- // Do nothing.
- }
-
- @Override
- public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
- // Do nothing.
- }
-
- @Override
- public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- // Do nothing.
- }
-
- @Override
- public void onSeekProcessed() {
- // Do nothing.
- }
-
- /**
- * @deprecated Use {@link DefaultEventListener#onTimelineChanged(Timeline, Object, int)}
- * instead.
- */
+ /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, Object, int)} instead. */
@Deprecated
- public void onTimelineChanged(Timeline timeline, Object manifest) {
+ public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {
// Do nothing.
}
-
}
/**
@@ -428,6 +470,10 @@ public interface Player {
*/
int TIMELINE_CHANGE_REASON_DYNAMIC = 2;
+ /** Returns the component of this player for audio output, or null if audio is not supported. */
+ @Nullable
+ AudioComponent getAudioComponent();
+
/** Returns the component of this player for video output, or null if video is not supported. */
@Nullable
VideoComponent getVideoComponent();
@@ -678,23 +724,27 @@ public interface Player {
*/
long getDuration();
- /**
- * Returns the playback position in the current window, in milliseconds.
- */
+ /** Returns the playback position in the current content window or ad, in milliseconds. */
long getCurrentPosition();
/**
- * Returns an estimate of the position in the current window up to which data is buffered, in
- * milliseconds.
+ * Returns an estimate of the position in the current content window or ad up to which data is
+ * buffered, in milliseconds.
*/
long getBufferedPosition();
/**
- * Returns an estimate of the percentage in the current window up to which data is buffered, or 0
- * if no estimate is available.
+ * Returns an estimate of the percentage in the current content window or ad up to which data is
+ * buffered, or 0 if no estimate is available.
*/
int getBufferedPercentage();
+ /**
+ * Returns an estimate of the total buffered duration from the current position, in milliseconds.
+ * This includes pre-buffered data for subsequent ads and windows.
+ */
+ long getTotalBufferedDuration();
+
/**
* Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
* empty.
@@ -735,4 +785,10 @@ public interface Player {
*/
long getContentPosition();
+ /**
+ * If {@link #isPlayingAd()} returns {@code true}, returns an estimate of the content position in
+ * the current content window up to which data is buffered, in milliseconds. If there is no ad
+ * playing, the returned position is the same as that returned by {@link #getBufferedPosition()}.
+ */
+ long getContentBufferedPosition();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
index e53db4568d..c29017856f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
@@ -192,6 +192,18 @@ public interface Renderer extends PlayerMessage.Target {
*/
void resetPosition(long positionUs) throws ExoPlaybackException;
+ /**
+ * Sets the operating rate of this renderer, where 1 is the default rate, 2 is twice the default
+ * rate, 0.5 is half the default rate and so on. The operating rate is a hint to the renderer of
+ * the speed at which playback will proceed, and may be used for resource planning.
+ *
+ *
The default implementation is a no-op.
+ *
+ * @param operatingRate The operating rate.
+ * @throws ExoPlaybackException If an error occurs handling the operating rate.
+ */
+ default void setOperatingRate(float operatingRate) throws ExoPlaybackException {};
+
/**
* Incrementally renders the {@link SampleStream}.
*
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 482e2c970a..055cf1de17 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
@@ -16,6 +16,8 @@
package com.google.android.exoplayer2;
import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.PlaybackParams;
@@ -30,7 +32,10 @@ import android.view.TextureView;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.AudioAttributes;
+import com.google.android.exoplayer2.audio.AudioFocusManager;
+import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
@@ -43,6 +48,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
@@ -56,7 +62,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
* be obtained from {@link ExoPlayerFactory}.
*/
@TargetApi(16)
-public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent {
+public class SimpleExoPlayer
+ implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent {
/** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */
@Deprecated
@@ -66,103 +73,170 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
protected final Renderer[] renderers;
- private final ExoPlayer player;
+ private final ExoPlayerImpl player;
private final Handler eventHandler;
private final ComponentListener componentListener;
private final CopyOnWriteArraySet
videoListeners;
+ private final CopyOnWriteArraySet audioListeners;
private final CopyOnWriteArraySet textOutputs;
private final CopyOnWriteArraySet metadataOutputs;
private final CopyOnWriteArraySet videoDebugListeners;
private final CopyOnWriteArraySet audioDebugListeners;
+ private final BandwidthMeter bandwidthMeter;
private final AnalyticsCollector analyticsCollector;
+ private final AudioFocusManager audioFocusManager;
+
private Format videoFormat;
private Format audioFormat;
private Surface surface;
private boolean ownsSurface;
- @C.VideoScalingMode
- private int videoScalingMode;
+ private @C.VideoScalingMode int videoScalingMode;
private SurfaceHolder surfaceHolder;
private TextureView textureView;
+ private int surfaceWidth;
+ private int surfaceHeight;
private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters;
private int audioSessionId;
private AudioAttributes audioAttributes;
private float audioVolume;
private MediaSource mediaSource;
+ private List currentCues;
/**
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl,
+ * BandwidthMeter, DrmSessionManager, Looper)}. The use of {@link
+ * SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio focus will be
+ * unavailable for a player created with this constructor.
*/
+ @Deprecated
protected SimpleExoPlayer(
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
- @Nullable DrmSessionManager drmSessionManager) {
+ BandwidthMeter bandwidthMeter,
+ @Nullable DrmSessionManager drmSessionManager,
+ Looper looper) {
this(
+ /* context= */ null,
renderersFactory,
trackSelector,
loadControl,
drmSessionManager,
- new AnalyticsCollector.Factory());
+ bandwidthMeter,
+ new AnalyticsCollector.Factory(),
+ looper);
}
/**
+ * @param context A {@link Context}.
+ * @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.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ protected SimpleExoPlayer(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ BandwidthMeter bandwidthMeter,
+ @Nullable DrmSessionManager drmSessionManager,
+ Looper looper) {
+ this(
+ context,
+ renderersFactory,
+ trackSelector,
+ loadControl,
+ drmSessionManager,
+ bandwidthMeter,
+ new AnalyticsCollector.Factory(),
+ looper);
+ }
+
+ /**
+ * @param context A {@link Context}.
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
* will collect and forward all player events.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
*/
protected SimpleExoPlayer(
+ Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
- AnalyticsCollector.Factory analyticsCollectorFactory) {
+ BandwidthMeter bandwidthMeter,
+ AnalyticsCollector.Factory analyticsCollectorFactory,
+ Looper looper) {
this(
+ context,
renderersFactory,
trackSelector,
loadControl,
drmSessionManager,
+ bandwidthMeter,
analyticsCollectorFactory,
- Clock.DEFAULT);
+ Clock.DEFAULT,
+ looper);
}
/**
+ * @param context A {@link Context}.
* @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.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
* will collect and forward all player events.
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
* Clock#DEFAULT}, unless the player is being used from a test.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
*/
protected SimpleExoPlayer(
+ Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
+ BandwidthMeter bandwidthMeter,
AnalyticsCollector.Factory analyticsCollectorFactory,
- Clock clock) {
+ Clock clock,
+ Looper looper) {
+ this.bandwidthMeter = bandwidthMeter;
componentListener = new ComponentListener();
videoListeners = new CopyOnWriteArraySet<>();
+ audioListeners = new CopyOnWriteArraySet<>();
textOutputs = new CopyOnWriteArraySet<>();
metadataOutputs = new CopyOnWriteArraySet<>();
videoDebugListeners = new CopyOnWriteArraySet<>();
audioDebugListeners = new CopyOnWriteArraySet<>();
- Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
- eventHandler = new Handler(eventLooper);
+ eventHandler = new Handler(looper);
renderers =
renderersFactory.createRenderers(
eventHandler,
@@ -177,17 +251,28 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioAttributes = AudioAttributes.DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+ currentCues = Collections.emptyList();
// Build the player and associated objects.
- player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
+ player =
+ new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock);
addListener(analyticsCollector);
videoDebugListeners.add(analyticsCollector);
+ videoListeners.add(analyticsCollector);
audioDebugListeners.add(analyticsCollector);
+ audioListeners.add(analyticsCollector);
addMetadataOutput(analyticsCollector);
+ bandwidthMeter.addEventListener(eventHandler, analyticsCollector);
if (drmSessionManager instanceof DefaultDrmSessionManager) {
((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector);
}
+ audioFocusManager = new AudioFocusManager(context, componentListener);
+ }
+
+ @Override
+ public AudioComponent getAudioComponent() {
+ return this;
}
@Override
@@ -236,6 +321,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
public void setVideoSurface(Surface surface) {
removeSurfaceCallbacks();
setVideoSurfaceInternal(surface, false);
+ int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET;
+ maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize);
}
@Override
@@ -251,10 +338,18 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
this.surfaceHolder = surfaceHolder;
if (surfaceHolder == null) {
setVideoSurfaceInternal(null, false);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
} else {
surfaceHolder.addCallback(componentListener);
Surface surface = surfaceHolder.getSurface();
- setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false);
+ if (surface != null && surface.isValid()) {
+ setVideoSurfaceInternal(surface, /* ownsSurface= */ false);
+ Rect surfaceSize = surfaceHolder.getSurfaceFrame();
+ maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height());
+ } else {
+ setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
+ }
}
}
@@ -281,6 +376,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
this.textureView = textureView;
if (textureView == null) {
setVideoSurfaceInternal(null, true);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
} else {
if (textureView.getSurfaceTextureListener() != null) {
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
@@ -288,7 +384,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
textureView.setSurfaceTextureListener(componentListener);
SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture()
: null;
- setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
+ if (surfaceTexture == null) {
+ setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
+ } else {
+ setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true);
+ maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight());
+ }
}
}
@@ -299,6 +401,92 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
}
}
+ @Override
+ public void addAudioListener(AudioListener listener) {
+ audioListeners.add(listener);
+ }
+
+ @Override
+ public void removeAudioListener(AudioListener listener) {
+ audioListeners.remove(listener);
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributes audioAttributes) {
+ setAudioAttributes(audioAttributes, /* handleAudioFocus= */ false);
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) {
+ if (!Util.areEqual(this.audioAttributes, audioAttributes)) {
+ this.audioAttributes = audioAttributes;
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+ player
+ .createMessage(renderer)
+ .setType(C.MSG_SET_AUDIO_ATTRIBUTES)
+ .setPayload(audioAttributes)
+ .send();
+ }
+ }
+ for (AudioListener audioListener : audioListeners) {
+ audioListener.onAudioAttributesChanged(audioAttributes);
+ }
+ }
+
+ @AudioFocusManager.PlayerCommand
+ int playerCommand =
+ audioFocusManager.setAudioAttributes(
+ handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState());
+ updatePlayWhenReady(getPlayWhenReady(), playerCommand);
+ }
+
+ @Override
+ public AudioAttributes getAudioAttributes() {
+ return audioAttributes;
+ }
+
+ @Override
+ public int getAudioSessionId() {
+ return audioSessionId;
+ }
+
+ @Override
+ public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+ player
+ .createMessage(renderer)
+ .setType(C.MSG_SET_AUX_EFFECT_INFO)
+ .setPayload(auxEffectInfo)
+ .send();
+ }
+ }
+ }
+
+ @Override
+ public void clearAuxEffectInfo() {
+ setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f));
+ }
+
+ @Override
+ public void setVolume(float audioVolume) {
+ audioVolume = Util.constrainValue(audioVolume, /* min= */ 0, /* max= */ 1);
+ if (this.audioVolume == audioVolume) {
+ return;
+ }
+ this.audioVolume = audioVolume;
+ sendVolumeToRenderers();
+ for (AudioListener audioListener : audioListeners) {
+ audioListener.onVolumeChanged(audioVolume);
+ }
+ }
+
+ @Override
+ public float getVolume() {
+ return audioVolume;
+ }
+
/**
* Sets the stream type for audio playback, used by the underlying audio track.
*
@@ -353,63 +541,6 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
analyticsCollector.removeListener(listener);
}
- /**
- * 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;
- for (Renderer renderer : renderers) {
- if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- player
- .createMessage(renderer)
- .setType(C.MSG_SET_AUDIO_ATTRIBUTES)
- .setPayload(audioAttributes)
- .send();
- }
- }
- }
-
- /**
- * Returns the attributes for audio playback.
- */
- public AudioAttributes getAudioAttributes() {
- return audioAttributes;
- }
-
- /**
- * Sets the audio volume, with 0 being silence and 1 being unity gain.
- *
- * @param audioVolume The audio volume.
- */
- public void setVolume(float audioVolume) {
- this.audioVolume = audioVolume;
- for (Renderer renderer : renderers) {
- if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send();
- }
- }
- }
-
- /**
- * Returns the audio volume, with 0 being silence and 1 being unity gain.
- */
- public float getVolume() {
- return audioVolume;
- }
-
/**
* Sets the {@link PlaybackParams} governing audio playback.
*
@@ -443,13 +574,6 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return audioFormat;
}
- /**
- * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set.
- */
- public int getAudioSessionId() {
- return audioSessionId;
- }
-
/**
* Returns {@link DecoderCounters} for video, or null if no video is being played.
*/
@@ -502,6 +626,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void addTextOutput(TextOutput listener) {
+ if (!currentCues.isEmpty()) {
+ listener.onCues(currentCues);
+ }
textOutputs.add(listener);
}
@@ -645,6 +772,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return player.getPlaybackLooper();
}
+ @Override
+ public Looper getApplicationLooper() {
+ return player.getApplicationLooper();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
player.addListener(listener);
@@ -680,12 +812,17 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
mediaSource.addEventListener(eventHandler, analyticsCollector);
this.mediaSource = mediaSource;
}
+ @AudioFocusManager.PlayerCommand
+ int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
+ updatePlayWhenReady(getPlayWhenReady(), playerCommand);
player.prepare(mediaSource, resetPosition, resetState);
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
- player.setPlayWhenReady(playWhenReady);
+ @AudioFocusManager.PlayerCommand
+ int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState());
+ updatePlayWhenReady(playWhenReady, playerCommand);
}
@Override
@@ -757,6 +894,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
player.setSeekParameters(seekParameters);
}
+ @Override
+ public SeekParameters getSeekParameters() {
+ return player.getSeekParameters();
+ }
+
@Override
public @Nullable Object getCurrentTag() {
return player.getCurrentTag();
@@ -775,10 +917,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
mediaSource = null;
analyticsCollector.resetForNewMediaSource();
}
+ audioFocusManager.handleStop();
+ currentCues = Collections.emptyList();
}
@Override
public void release() {
+ audioFocusManager.handleStop();
player.release();
removeSurfaceCallbacks();
if (surface != null) {
@@ -790,6 +935,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector);
}
+ bandwidthMeter.removeEventListener(analyticsCollector);
+ currentCues = Collections.emptyList();
}
@Override
@@ -877,6 +1024,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return player.getBufferedPercentage();
}
+ @Override
+ public long getTotalBufferedDuration() {
+ return player.getTotalBufferedDuration();
+ }
+
@Override
public boolean isCurrentWindowDynamic() {
return player.isCurrentWindowDynamic();
@@ -907,22 +1059,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
return player.getContentPosition();
}
- // Internal methods.
-
- /**
- * Creates the {@link ExoPlayer} implementation used by this 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.
- * @param loadControl The {@link LoadControl} that will be used by the instance.
- * @param clock The {@link Clock} that will be used by this instance.
- * @return A new {@link ExoPlayer} instance.
- */
- protected ExoPlayer createExoPlayerImpl(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
- return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock);
+ @Override
+ public long getContentBufferedPosition() {
+ return player.getContentBufferedPosition();
}
+ // Internal methods.
+
private void removeSurfaceCallbacks() {
if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) {
@@ -966,9 +1109,41 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
this.ownsSurface = ownsSurface;
}
- private final class ComponentListener implements VideoRendererEventListener,
- AudioRendererEventListener, TextOutput, MetadataOutput, SurfaceHolder.Callback,
- TextureView.SurfaceTextureListener {
+ private void maybeNotifySurfaceSizeChanged(int width, int height) {
+ if (width != surfaceWidth || height != surfaceHeight) {
+ surfaceWidth = width;
+ surfaceHeight = height;
+ for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
+ videoListener.onSurfaceSizeChanged(width, height);
+ }
+ }
+ }
+
+ private void sendVolumeToRenderers() {
+ float scaledVolume = audioVolume * audioFocusManager.getVolumeMultiplier();
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+ player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(scaledVolume).send();
+ }
+ }
+ }
+
+ private void updatePlayWhenReady(
+ boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {
+
+ player.setPlayWhenReady(
+ playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY,
+ playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY);
+ }
+
+ private final class ComponentListener
+ implements VideoRendererEventListener,
+ AudioRendererEventListener,
+ TextOutput,
+ MetadataOutput,
+ SurfaceHolder.Callback,
+ TextureView.SurfaceTextureListener,
+ AudioFocusManager.PlayerControl {
// VideoRendererEventListener implementation
@@ -1008,8 +1183,12 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
- videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
- pixelWidthHeightRatio);
+ // Prevent duplicate notification if a listener is both a VideoRendererEventListener and
+ // a VideoListener, as they have the same method signature.
+ if (!videoDebugListeners.contains(videoListener)) {
+ videoListener.onVideoSizeChanged(
+ width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
+ }
}
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
@@ -1050,7 +1229,17 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void onAudioSessionId(int sessionId) {
+ if (audioSessionId == sessionId) {
+ return;
+ }
audioSessionId = sessionId;
+ for (AudioListener audioListener : audioListeners) {
+ // Prevent duplicate notification if a listener is both a AudioRendererEventListener and
+ // a AudioListener, as they have the same method signature.
+ if (!audioDebugListeners.contains(audioListener)) {
+ audioListener.onAudioSessionId(sessionId);
+ }
+ }
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioSessionId(sessionId);
}
@@ -1095,6 +1284,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void onCues(List cues) {
+ currentCues = cues;
for (TextOutput textOutput : textOutputs) {
textOutput.onCues(cues);
}
@@ -1118,12 +1308,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- // Do nothing.
+ maybeNotifySurfaceSizeChanged(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
setVideoSurfaceInternal(null, false);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
}
// TextureView.SurfaceTextureListener implementation
@@ -1131,16 +1322,18 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
setVideoSurfaceInternal(new Surface(surfaceTexture), true);
+ maybeNotifySurfaceSizeChanged(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
- // Do nothing.
+ maybeNotifySurfaceSizeChanged(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
setVideoSurfaceInternal(null, true);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
return true;
}
@@ -1149,6 +1342,16 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
// Do nothing.
}
- }
+ // AudioFocusManager.PlayerControl implementation
+ @Override
+ public void setVolumeMultiplier(float volumeMultiplier) {
+ sendVolumeToRenderers();
+ }
+
+ @Override
+ public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) {
+ updatePlayWhenReady(getPlayWhenReady(), playerCommand);
+ }
+ }
}
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 600fbc3014..a1a0e9b152 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
@@ -520,6 +520,11 @@ public abstract class Timeline {
public int getIndexOfPeriod(Object uid) {
return C.INDEX_UNSET;
}
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ throw new IndexOutOfBoundsException();
+ }
};
/**
@@ -737,6 +742,17 @@ public abstract class Timeline {
return Pair.create(periodIndex, periodPositionUs);
}
+ /**
+ * Populates a {@link Period} with data for the period with the specified unique identifier.
+ *
+ * @param periodUid The unique identifier of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public Period getPeriodByUid(Object periodUid, Period period) {
+ return getPeriod(getIndexOfPeriod(periodUid), period, /* setIds= */ true);
+ }
+
/**
* Populates a {@link Period} with data for the period at the specified index. Does not populate
* {@link Period#id} and {@link Period#uid}.
@@ -770,4 +786,11 @@ public abstract class Timeline {
*/
public abstract int getIndexOfPeriod(Object uid);
+ /**
+ * Returns the unique id of the period identified by its index in the timeline.
+ *
+ * @param periodIndex The index of the period.
+ * @return The unique id of the period.
+ */
+ public abstract Object getUidOfPeriod(int periodIndex);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
index 43ef308f27..262187586b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.analytics;
-import android.net.NetworkInfo;
import android.support.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.C;
@@ -27,6 +26,8 @@ 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.analytics.AnalyticsListener.EventTime;
+import com.google.android.exoplayer2.audio.AudioAttributes;
+import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;
@@ -39,6 +40,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
import java.util.ArrayList;
@@ -46,6 +48,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Data collector which is able to forward analytics events to {@link AnalyticsListener}s by
@@ -58,7 +61,9 @@ public class AnalyticsCollector
VideoRendererEventListener,
MediaSourceEventListener,
BandwidthMeter.EventListener,
- DefaultDrmSessionEventListener {
+ DefaultDrmSessionEventListener,
+ VideoListener,
+ AudioListener {
/** Factory for an analytics collector. */
public static class Factory {
@@ -66,29 +71,34 @@ public class AnalyticsCollector
/**
* Creates an analytics collector for the specified player.
*
- * @param player The {@link Player} for which data will be collected.
+ * @param player The {@link Player} for which data will be collected. Can be null, if the player
+ * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics
+ * collector.
* @param clock A {@link Clock} used to generate timestamps.
* @return An analytics collector.
*/
- public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) {
+ public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) {
return new AnalyticsCollector(player, clock);
}
}
private final CopyOnWriteArraySet listeners;
- private final Player player;
private final Clock clock;
private final Window window;
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
+ private @MonotonicNonNull Player player;
+
/**
* Creates an analytics collector for the specified player.
*
- * @param player The {@link Player} for which data will be collected.
+ * @param player The {@link Player} for which data will be collected. Can be null, if the player
+ * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics
+ * collector.
* @param clock A {@link Clock} used to generate timestamps.
*/
- protected AnalyticsCollector(Player player, Clock clock) {
- this.player = Assertions.checkNotNull(player);
+ protected AnalyticsCollector(@Nullable Player player, Clock clock) {
+ this.player = player;
this.clock = Assertions.checkNotNull(clock);
listeners = new CopyOnWriteArraySet<>();
mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
@@ -113,6 +123,17 @@ public class AnalyticsCollector
listeners.remove(listener);
}
+ /**
+ * Sets the player for which data will be collected. Must only be called if no player has been set
+ * yet.
+ *
+ * @param player The {@link Player} for which data will be collected.
+ */
+ public void setPlayer(Player player) {
+ Assertions.checkState(this.player == null);
+ this.player = Assertions.checkNotNull(player);
+ }
+
// External events.
/**
@@ -129,31 +150,6 @@ public class AnalyticsCollector
}
}
- /**
- * Notify analytics collector that the viewport size changed.
- *
- * @param width The new width of the viewport in device-independent pixels (dp).
- * @param height The new height of the viewport in device-independent pixels (dp).
- */
- public final void notifyViewportSizeChanged(int width, int height) {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onViewportSizeChange(eventTime, width, height);
- }
- }
-
- /**
- * Notify analytics collector that the network type or connectivity changed.
- *
- * @param networkInfo The new network info, or null if no network connection exists.
- */
- public final void notifyNetworkTypeChanged(@Nullable NetworkInfo networkInfo) {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onNetworkTypeChanged(eventTime, networkInfo);
- }
- }
-
/**
* Resets the analytics collector for a new media source. Should be called before the player is
* prepared with a new media source.
@@ -188,14 +184,6 @@ public class AnalyticsCollector
}
}
- @Override
- public final void onAudioSessionId(int audioSessionId) {
- EventTime eventTime = generateReadingMediaPeriodEventTime();
- for (AnalyticsListener listener : listeners) {
- listener.onAudioSessionId(eventTime, audioSessionId);
- }
- }
-
@Override
public final void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
@@ -233,6 +221,32 @@ public class AnalyticsCollector
}
}
+ // AudioListener implementation.
+
+ @Override
+ public final void onAudioSessionId(int audioSessionId) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioSessionId(eventTime, audioSessionId);
+ }
+ }
+
+ @Override
+ public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioAttributesChanged(eventTime, audioAttributes);
+ }
+ }
+
+ @Override
+ public void onVolumeChanged(float audioVolume) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onVolumeChanged(eventTime, audioVolume);
+ }
+ }
+
// VideoRendererEventListener implementation.
@Override
@@ -271,12 +285,12 @@ public class AnalyticsCollector
}
@Override
- public final void onVideoSizeChanged(
- int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
- EventTime eventTime = generateReadingMediaPeriodEventTime();
+ public final void onVideoDisabled(DecoderCounters counters) {
+ // The renderers are disabled after we changed the playing media period on the playback thread
+ // but before this change is reported to the app thread.
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
- listener.onVideoSizeChanged(
- eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
+ listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
}
}
@@ -288,16 +302,31 @@ public class AnalyticsCollector
}
}
+ // VideoListener implementation.
+
@Override
- public final void onVideoDisabled(DecoderCounters counters) {
- // The renderers are disabled after we changed the playing media period on the playback thread
- // but before this change is reported to the app thread.
- EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ public final void onVideoSizeChanged(
+ int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
- listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
+ listener.onVideoSizeChanged(
+ eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
}
}
+ @Override
+ public void onSurfaceSizeChanged(int width, int height) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onSurfaceSizeChanged(eventTime, width, height);
+ }
+ }
+
+ @Override
+ public final void onRenderedFirstFrame() {
+ // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame.
+ }
+
// MediaSourceEventListener implementation.
@Override
@@ -403,7 +432,7 @@ public class AnalyticsCollector
@Override
public final void onTimelineChanged(
- Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
+ Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
@@ -541,6 +570,7 @@ public class AnalyticsCollector
/** Returns a new {@link EventTime} for the specified window index and media period id. */
protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ Assertions.checkNotNull(player);
long realtimeMs = clock.elapsedRealtime();
Timeline timeline = player.getCurrentTimeline();
long eventPositionMs;
@@ -565,8 +595,6 @@ public class AnalyticsCollector
// This event is for content in a future window. Assume default start position.
eventPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs();
}
- // TODO(b/30792113): implement this properly (player.getTotalBufferedDuration()).
- long bufferedDurationMs = player.getBufferedPosition() - player.getContentPosition();
return new EventTime(
realtimeMs,
timeline,
@@ -574,12 +602,12 @@ public class AnalyticsCollector
mediaPeriodId,
eventPositionMs,
player.getCurrentPosition(),
- bufferedDurationMs);
+ player.getTotalBufferedDuration());
}
private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) {
if (mediaPeriod == null) {
- int windowIndex = player.getCurrentWindowIndex();
+ int windowIndex = Assertions.checkNotNull(player).getCurrentWindowIndex();
MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
return generateEventTime(windowIndex, mediaPeriodId);
}
@@ -756,8 +784,7 @@ public class AnalyticsCollector
if (newTimeline.isEmpty() || timeline.isEmpty()) {
return mediaPeriod;
}
- Object uid =
- timeline.getPeriod(mediaPeriod.mediaPeriodId.periodIndex, period, /* setIds= */ true).uid;
+ Object uid = timeline.getUidOfPeriod(mediaPeriod.mediaPeriodId.periodIndex);
int newPeriodIndex = newTimeline.getIndexOfPeriod(uid);
if (newPeriodIndex == C.INDEX_UNSET) {
return mediaPeriod;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
index 48057f2bff..adc4b3cdb9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.analytics;
-import android.net.NetworkInfo;
import android.support.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.C;
@@ -26,6 +25,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -41,6 +41,8 @@ import java.io.IOException;
*
* All events are recorded with an {@link EventTime} specifying the elapsed real time and media
* time at the time of the event.
+ *
+ *
All methods have no-op default implementations to allow selective overrides.
*/
public interface AnalyticsListener {
@@ -127,7 +129,8 @@ public interface AnalyticsListener {
* @param playWhenReady Whether the playback will proceed when ready.
* @param playbackState One of the {@link Player}.STATE constants.
*/
- void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState);
+ default void onPlayerStateChanged(
+ EventTime eventTime, boolean playWhenReady, int playbackState) {}
/**
* Called when the timeline changed.
@@ -135,7 +138,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param reason The reason for the timeline change.
*/
- void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason);
+ default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {}
/**
* Called when a position discontinuity occurred.
@@ -143,21 +146,21 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param reason The reason for the position discontinuity.
*/
- void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);
+ default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {}
/**
* Called when a seek operation started.
*
* @param eventTime The event time.
*/
- void onSeekStarted(EventTime eventTime);
+ default void onSeekStarted(EventTime eventTime) {}
/**
* Called when a seek operation was processed.
*
* @param eventTime The event time.
*/
- void onSeekProcessed(EventTime eventTime);
+ default void onSeekProcessed(EventTime eventTime) {}
/**
* Called when the playback parameters changed.
@@ -165,7 +168,8 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param playbackParameters The new playback parameters.
*/
- void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters);
+ default void onPlaybackParametersChanged(
+ EventTime eventTime, PlaybackParameters playbackParameters) {}
/**
* Called when the repeat mode changed.
@@ -173,7 +177,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param repeatMode The new repeat mode.
*/
- void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode);
+ default void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {}
/**
* Called when the shuffle mode changed.
@@ -181,7 +185,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param shuffleModeEnabled Whether the shuffle mode is enabled.
*/
- void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled);
+ default void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {}
/**
* Called when the player starts or stops loading data from a source.
@@ -189,7 +193,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param isLoading Whether the player is loading.
*/
- void onLoadingChanged(EventTime eventTime, boolean isLoading);
+ default void onLoadingChanged(EventTime eventTime, boolean isLoading) {}
/**
* Called when a fatal player error occurred.
@@ -197,7 +201,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param error The error.
*/
- void onPlayerError(EventTime eventTime, ExoPlaybackException error);
+ default void onPlayerError(EventTime eventTime, ExoPlaybackException error) {}
/**
* Called when the available or selected tracks for the renderers changed.
@@ -206,8 +210,8 @@ public interface AnalyticsListener {
* @param trackGroups The available tracks. May be empty.
* @param trackSelections The track selections for each renderer. May contain null elements.
*/
- void onTracksChanged(
- EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+ default void onTracksChanged(
+ EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
/**
* Called when a media source started loading data.
@@ -216,7 +220,8 @@ public interface AnalyticsListener {
* @param loadEventInfo The {@link LoadEventInfo} defining the load event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
- void onLoadStarted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData);
+ default void onLoadStarted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
/**
* Called when a media source completed loading data.
@@ -225,8 +230,8 @@ public interface AnalyticsListener {
* @param loadEventInfo The {@link LoadEventInfo} defining the load event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
- void onLoadCompleted(
- EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData);
+ default void onLoadCompleted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
/**
* Called when a media source canceled loading data.
@@ -235,8 +240,8 @@ public interface AnalyticsListener {
* @param loadEventInfo The {@link LoadEventInfo} defining the load event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
- void onLoadCanceled(
- EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData);
+ default void onLoadCanceled(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
/**
* Called when a media source loading error occurred. These errors are just for informational
@@ -248,12 +253,12 @@ public interface AnalyticsListener {
* @param error The load error.
* @param wasCanceled Whether the load was canceled as a result of the error.
*/
- void onLoadError(
+ default void onLoadError(
EventTime eventTime,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
- boolean wasCanceled);
+ boolean wasCanceled) {}
/**
* Called when the downstream format sent to the renderers changed.
@@ -261,7 +266,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data.
*/
- void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData);
+ default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}
/**
* Called when data is removed from the back of a media buffer, typically so that it can be
@@ -270,28 +275,28 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.
*/
- void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData);
+ default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}
/**
* Called when a media source created a media period.
*
* @param eventTime The event time.
*/
- void onMediaPeriodCreated(EventTime eventTime);
+ default void onMediaPeriodCreated(EventTime eventTime) {}
/**
* Called when a media source released a media period.
*
* @param eventTime The event time.
*/
- void onMediaPeriodReleased(EventTime eventTime);
+ default void onMediaPeriodReleased(EventTime eventTime) {}
/**
* Called when the player started reading a media period.
*
* @param eventTime The event time.
*/
- void onReadingStarted(EventTime eventTime);
+ default void onReadingStarted(EventTime eventTime) {}
/**
* Called when the bandwidth estimate for the current data source has been updated.
@@ -301,25 +306,19 @@ public interface AnalyticsListener {
* @param totalBytesLoaded The total bytes loaded this update is based on.
* @param bitrateEstimate The bandwidth estimate, in bits per second.
*/
- void onBandwidthEstimate(
- EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate);
+ default void onBandwidthEstimate(
+ EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}
/**
- * Called when the viewport size of the output surface changed.
+ * Called when the output surface size changed.
*
* @param eventTime The event time.
- * @param width The width of the viewport in device-independent pixels (dp).
- * @param height The height of the viewport in device-independent pixels (dp).
+ * @param width The surface width in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if the
+ * video is not rendered onto a surface.
+ * @param height The surface height in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if
+ * the video is not rendered onto a surface.
*/
- void onViewportSizeChange(EventTime eventTime, int width, int height);
-
- /**
- * Called when the type of the network connection changed.
- *
- * @param eventTime The event time.
- * @param networkInfo The network info for the current connection, or null if disconnected.
- */
- void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo);
+ default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}
/**
* Called when there is {@link Metadata} associated with the current playback time.
@@ -327,7 +326,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param metadata The metadata.
*/
- void onMetadata(EventTime eventTime, Metadata metadata);
+ default void onMetadata(EventTime eventTime, Metadata metadata) {}
/**
* Called when an audio or video decoder has been enabled.
@@ -337,7 +336,8 @@ public interface AnalyticsListener {
* {@link C#TRACK_TYPE_VIDEO}.
* @param decoderCounters The accumulated event counters associated with this decoder.
*/
- void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters);
+ default void onDecoderEnabled(
+ EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
/**
* Called when an audio or video decoder has been initialized.
@@ -348,8 +348,8 @@ public interface AnalyticsListener {
* @param decoderName The decoder that was created.
* @param initializationDurationMs Time taken to initialize the decoder, in milliseconds.
*/
- void onDecoderInitialized(
- EventTime eventTime, int trackType, String decoderName, long initializationDurationMs);
+ default void onDecoderInitialized(
+ EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {}
/**
* Called when an audio or video decoder input format changed.
@@ -359,7 +359,7 @@ public interface AnalyticsListener {
* C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}.
* @param format The new input format for the decoder.
*/
- void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format);
+ default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {}
/**
* Called when an audio or video decoder has been disabled.
@@ -369,7 +369,8 @@ public interface AnalyticsListener {
* {@link C#TRACK_TYPE_VIDEO}.
* @param decoderCounters The accumulated event counters associated with this decoder.
*/
- void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters);
+ default void onDecoderDisabled(
+ EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
/**
* Called when the audio session id is set.
@@ -377,7 +378,23 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param audioSessionId The audio session id.
*/
- void onAudioSessionId(EventTime eventTime, int audioSessionId);
+ default void onAudioSessionId(EventTime eventTime, int audioSessionId) {}
+
+ /**
+ * Called when the audio attributes change.
+ *
+ * @param eventTime The event time.
+ * @param audioAttributes The audio attributes.
+ */
+ default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {}
+
+ /**
+ * Called when the volume changes.
+ *
+ * @param eventTime The event time.
+ * @param volume The new volume, with 0 being silence and 1 being unity gain.
+ */
+ default void onVolumeChanged(EventTime eventTime, float volume) {}
/**
* Called when an audio underrun occurred.
@@ -389,8 +406,8 @@ public interface AnalyticsListener {
* as the buffered media can have a variable bitrate so the duration may be unknown.
* @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data.
*/
- void onAudioUnderrun(
- EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
+ default void onAudioUnderrun(
+ EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
/**
* Called after video frames have been dropped.
@@ -401,7 +418,7 @@ public interface AnalyticsListener {
* is timed from when the renderer was started or from when dropped frames were last reported
* (whichever was more recent), and not from when the first of the reported drops occurred.
*/
- void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs);
+ default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}
/**
* Called before a frame is rendered for the first time since setting the surface, and each time
@@ -416,12 +433,12 @@ public interface AnalyticsListener {
* since the renderer will apply all necessary rotations internally.
* @param pixelWidthHeightRatio The width to height ratio of each pixel.
*/
- void onVideoSizeChanged(
+ default void onVideoSizeChanged(
EventTime eventTime,
int width,
int height,
int unappliedRotationDegrees,
- float pixelWidthHeightRatio);
+ float pixelWidthHeightRatio) {}
/**
* Called when a frame is rendered for the first time since setting the surface, and when a frame
@@ -431,14 +448,14 @@ public interface AnalyticsListener {
* @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
* the renderer renders to something that isn't a {@link Surface}.
*/
- void onRenderedFirstFrame(EventTime eventTime, Surface surface);
+ default void onRenderedFirstFrame(EventTime eventTime, Surface surface) {}
/**
* Called each time drm keys are loaded.
*
* @param eventTime The event time.
*/
- void onDrmKeysLoaded(EventTime eventTime);
+ default void onDrmKeysLoaded(EventTime eventTime) {}
/**
* Called when a drm error occurs. These errors are just for informational purposes and the player
@@ -447,19 +464,19 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param error The error.
*/
- void onDrmSessionManagerError(EventTime eventTime, Exception error);
+ default void onDrmSessionManagerError(EventTime eventTime, Exception error) {}
/**
* Called each time offline drm keys are restored.
*
* @param eventTime The event time.
*/
- void onDrmKeysRestored(EventTime eventTime);
+ default void onDrmKeysRestored(EventTime eventTime) {}
/**
* Called each time offline drm keys are removed.
*
* @param eventTime The event time.
*/
- void onDrmKeysRemoved(EventTime eventTime);
+ default void onDrmKeysRemoved(EventTime eventTime) {}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
index 4a49de56b0..d487a8aa99 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
@@ -15,152 +15,9 @@
*/
package com.google.android.exoplayer2.analytics;
-import android.net.NetworkInfo;
-import android.view.Surface;
-import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.PlaybackParameters;
-import com.google.android.exoplayer2.decoder.DecoderCounters;
-import com.google.android.exoplayer2.metadata.Metadata;
-import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
-import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import java.io.IOException;
-
/**
- * {@link AnalyticsListener} allowing selective overrides. All methods are implemented as no-ops.
+ * @deprecated Use {@link AnalyticsListener} directly for selective overrides as all methods are
+ * implemented as no-op default methods.
*/
-public abstract class DefaultAnalyticsListener implements AnalyticsListener {
-
- @Override
- public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {}
-
- @Override
- public void onTimelineChanged(EventTime eventTime, int reason) {}
-
- @Override
- public void onPositionDiscontinuity(EventTime eventTime, int reason) {}
-
- @Override
- public void onSeekStarted(EventTime eventTime) {}
-
- @Override
- public void onSeekProcessed(EventTime eventTime) {}
-
- @Override
- public void onPlaybackParametersChanged(
- EventTime eventTime, PlaybackParameters playbackParameters) {}
-
- @Override
- public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {}
-
- @Override
- public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {}
-
- @Override
- public void onLoadingChanged(EventTime eventTime, boolean isLoading) {}
-
- @Override
- public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {}
-
- @Override
- public void onTracksChanged(
- EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
-
- @Override
- public void onLoadStarted(
- EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
-
- @Override
- public void onLoadCompleted(
- EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
-
- @Override
- public void onLoadCanceled(
- EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
-
- @Override
- public void onLoadError(
- EventTime eventTime,
- LoadEventInfo loadEventInfo,
- MediaLoadData mediaLoadData,
- IOException error,
- boolean wasCanceled) {}
-
- @Override
- public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}
-
- @Override
- public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}
-
- @Override
- public void onMediaPeriodCreated(EventTime eventTime) {}
-
- @Override
- public void onMediaPeriodReleased(EventTime eventTime) {}
-
- @Override
- public void onReadingStarted(EventTime eventTime) {}
-
- @Override
- public void onBandwidthEstimate(
- EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}
-
- @Override
- public void onViewportSizeChange(EventTime eventTime, int width, int height) {}
-
- @Override
- public void onNetworkTypeChanged(EventTime eventTime, NetworkInfo networkInfo) {}
-
- @Override
- public void onMetadata(EventTime eventTime, Metadata metadata) {}
-
- @Override
- public void onDecoderEnabled(
- EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
-
- @Override
- public void onDecoderInitialized(
- EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {}
-
- @Override
- public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {}
-
- @Override
- public void onDecoderDisabled(
- EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
-
- @Override
- public void onAudioSessionId(EventTime eventTime, int audioSessionId) {}
-
- @Override
- public void onAudioUnderrun(
- EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
-
- @Override
- public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}
-
- @Override
- public void onVideoSizeChanged(
- EventTime eventTime,
- int width,
- int height,
- int unappliedRotationDegrees,
- float pixelWidthHeightRatio) {}
-
- @Override
- public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {}
-
- @Override
- public void onDrmKeysLoaded(EventTime eventTime) {}
-
- @Override
- public void onDrmSessionManagerError(EventTime eventTime, Exception error) {}
-
- @Override
- public void onDrmKeysRestored(EventTime eventTime) {}
-
- @Override
- public void onDrmKeysRemoved(EventTime eventTime) {}
-}
+@Deprecated
+public abstract class DefaultAnalyticsListener implements AnalyticsListener {}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
index c61b8ff24c..94fe759a9b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -137,17 +137,17 @@ public final class Ac3Util {
121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};
/**
- * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to
- * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified.
+ * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS
+ * 102 366 Annex F. The reading position of {@code data} will be modified.
*
* @param data The AC3SpecificBox to parse.
- * @param trackId The track identifier to set on the format, or null.
+ * @param trackId The track identifier to set on the format.
* @param language The language to set on the format.
* @param drmInitData {@link DrmInitData} to be included in the format.
* @return The AC-3 format parsed from data in the header.
*/
- public static Format parseAc3AnnexFFormat(ParsableByteArray data, String trackId,
- String language, DrmInitData drmInitData) {
+ public static Format parseAc3AnnexFFormat(
+ ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) {
int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
int nextByte = data.readUnsignedByte();
@@ -155,22 +155,32 @@ public final class Ac3Util {
if ((nextByte & 0x04) != 0) { // lfeon
channelCount++;
}
- return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE,
- Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+ return Format.createAudioSampleFormat(
+ trackId,
+ MimeTypes.AUDIO_AC3,
+ /* codecs= */ null,
+ Format.NO_VALUE,
+ Format.NO_VALUE,
+ channelCount,
+ sampleRate,
+ /* initializationData= */ null,
+ drmInitData,
+ /* selectionFlags= */ 0,
+ language);
}
/**
- * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to
- * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified.
+ * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS
+ * 102 366 Annex F. The reading position of {@code data} will be modified.
*
* @param data The EC3SpecificBox to parse.
- * @param trackId The track identifier to set on the format, or null.
+ * @param trackId The track identifier to set on the format.
* @param language The language to set on the format.
* @param drmInitData {@link DrmInitData} to be included in the format.
* @return The E-AC-3 format parsed from data in the header.
*/
- public static Format parseEAc3AnnexFFormat(ParsableByteArray data, String trackId,
- String language, DrmInitData drmInitData) {
+ public static Format parseEAc3AnnexFFormat(
+ ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) {
data.skipBytes(2); // data_rate, num_ind_sub
// Read the first independent substream.
@@ -200,8 +210,18 @@ public final class Ac3Util {
mimeType = MimeTypes.AUDIO_E_AC3_JOC;
}
}
- return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE,
- Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+ return Format.createAudioSampleFormat(
+ trackId,
+ mimeType,
+ /* codecs= */ null,
+ Format.NO_VALUE,
+ Format.NO_VALUE,
+ channelCount,
+ sampleRate,
+ /* initializationData= */ null,
+ drmInitData,
+ /* selectionFlags= */ 0,
+ language);
}
/**
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
index 5e963a2540..848b3ee10c 100644
--- 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
@@ -39,12 +39,9 @@ public final class AudioAttributes {
*/
public static final class Builder {
- @C.AudioContentType
- private int contentType;
- @C.AudioFlags
- private int flags;
- @C.AudioUsage
- private int usage;
+ private @C.AudioContentType int contentType;
+ private @C.AudioFlags int flags;
+ private @C.AudioUsage int usage;
/**
* Creates a new builder for {@link AudioAttributes}.
@@ -91,14 +88,11 @@ public final class AudioAttributes {
}
- @C.AudioContentType
- public final int contentType;
- @C.AudioFlags
- public final int flags;
- @C.AudioUsage
- public final int usage;
+ public final @C.AudioContentType int contentType;
+ public final @C.AudioFlags int flags;
+ public final @C.AudioUsage int usage;
- private android.media.AudioAttributes audioAttributesV21;
+ private @Nullable android.media.AudioAttributes audioAttributesV21;
private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags,
@C.AudioUsage int usage) {
@@ -108,7 +102,7 @@ public final class AudioAttributes {
}
@TargetApi(21)
- /* package */ android.media.AudioAttributes getAudioAttributesV21() {
+ public android.media.AudioAttributes getAudioAttributesV21() {
if (audioAttributesV21 == null) {
audioAttributesV21 = new android.media.AudioAttributes.Builder()
.setContentType(contentType)
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
index 4b03a5047b..92d39dec65 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
@@ -25,9 +25,7 @@ import android.media.AudioManager;
import android.support.annotation.Nullable;
import java.util.Arrays;
-/**
- * Represents the set of audio formats that a device is capable of playing.
- */
+/** Represents the set of audio formats that a device is capable of playing. */
@TargetApi(21)
public final class AudioCapabilities {
@@ -50,7 +48,7 @@ public final class AudioCapabilities {
}
@SuppressLint("InlinedApi")
- /* package */ static AudioCapabilities getCapabilities(Intent intent) {
+ /* package */ static AudioCapabilities getCapabilities(@Nullable Intent intent) {
if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {
return DEFAULT_AUDIO_CAPABILITIES;
}
@@ -65,11 +63,15 @@ public final class AudioCapabilities {
* Constructs new audio capabilities based on a set of supported encodings and a maximum channel
* count.
*
+ *
Applications should generally call {@link #getCapabilities(Context)} to obtain an instance
+ * based on the capabilities advertised by the platform, rather than calling this constructor.
+ *
* @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
- * {@code ENCODING_*} constants.
+ * {@code ENCODING_*} constants. Passing {@code null} indicates that no encodings are
+ * supported.
* @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
*/
- /* package */ AudioCapabilities(int[] supportedEncodings, int maxChannelCount) {
+ public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) {
if (supportedEncodings != null) {
this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);
Arrays.sort(this.supportedEncodings);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java
index 49ec96e3d6..aa610db8b1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java
@@ -20,6 +20,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
+import android.os.Handler;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
@@ -44,17 +46,29 @@ public final class AudioCapabilitiesReceiver {
}
private final Context context;
+ private final @Nullable Handler handler;
private final Listener listener;
- private final BroadcastReceiver receiver;
+ private final @Nullable BroadcastReceiver receiver;
- /* package */ AudioCapabilities audioCapabilities;
+ /* package */ @Nullable AudioCapabilities audioCapabilities;
/**
* @param context A context for registering the receiver.
* @param listener The listener to notify when audio capabilities change.
*/
public AudioCapabilitiesReceiver(Context context, Listener listener) {
+ this(context, /* handler= */ null, listener);
+ }
+
+ /**
+ * @param context A context for registering the receiver.
+ * @param handler The handler to which {@link Listener} events will be posted. If null, listener
+ * methods are invoked on the main thread.
+ * @param listener The listener to notify when audio capabilities change.
+ */
+ public AudioCapabilitiesReceiver(Context context, @Nullable Handler handler, Listener listener) {
this.context = Assertions.checkNotNull(context);
+ this.handler = handler;
this.listener = Assertions.checkNotNull(listener);
this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null;
}
@@ -68,8 +82,17 @@ public final class AudioCapabilitiesReceiver {
*/
@SuppressWarnings("InlinedApi")
public AudioCapabilities register() {
- Intent stickyIntent = receiver == null ? null
- : context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
+ Intent stickyIntent = null;
+ if (receiver != null) {
+ IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG);
+ if (handler != null) {
+ stickyIntent =
+ context.registerReceiver(
+ receiver, intentFilter, /* broadcastPermission= */ null, handler);
+ } else {
+ stickyIntent = context.registerReceiver(receiver, intentFilter);
+ }
+ }
audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent);
return audioCapabilities;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java
new file mode 100644
index 0000000000..d078cddcc1
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.content.Context;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** Manages requesting and responding to changes in audio focus. */
+public final class AudioFocusManager {
+
+ /** Interface to allow AudioFocusManager to give commands to a player. */
+ public interface PlayerControl {
+ /**
+ * Called when the volume multiplier on the player should be changed.
+ *
+ * @param volumeMultiplier The new volume multiplier.
+ */
+ void setVolumeMultiplier(float volumeMultiplier);
+
+ /**
+ * Called when a command must be executed on the player.
+ *
+ * @param playerCommand The command that must be executed.
+ */
+ void executePlayerCommand(@PlayerCommand int playerCommand);
+ }
+
+ /** Player commands. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ PLAYER_COMMAND_DO_NOT_PLAY,
+ PLAYER_COMMAND_WAIT_FOR_CALLBACK,
+ PLAYER_COMMAND_PLAY_WHEN_READY,
+ })
+ public @interface PlayerCommand {}
+ /** Do not play. */
+ public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1;
+ /** Do not play now. Wait for callback to play. */
+ public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0;
+ /** Play freely. */
+ public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1;
+
+ /** Audio focus state. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ AUDIO_FOCUS_STATE_LOST_FOCUS,
+ AUDIO_FOCUS_STATE_NO_FOCUS,
+ AUDIO_FOCUS_STATE_HAVE_FOCUS,
+ AUDIO_FOCUS_STATE_LOSS_TRANSIENT,
+ AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK
+ })
+ private @interface AudioFocusState {}
+ /** No audio focus was held, but has been lost by another app taking it permanently. */
+ private static final int AUDIO_FOCUS_STATE_LOST_FOCUS = -1;
+ /** No audio focus is currently being held. */
+ private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0;
+ /** The requested audio focus is currently held. */
+ private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1;
+ /** Audio focus has been temporarily lost. */
+ private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2;
+ /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */
+ private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 3;
+
+ private static final String TAG = "AudioFocusManager";
+
+ private static final float VOLUME_MULTIPLIER_DUCK = 0.2f;
+ private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f;
+
+ private final @Nullable AudioManager audioManager;
+ private final AudioFocusListener focusListener;
+ private final PlayerControl playerControl;
+ private @Nullable AudioAttributes audioAttributes;
+
+ private @AudioFocusState int audioFocusState;
+ private int focusGain;
+ private float volumeMultiplier = 1.0f;
+
+ private @MonotonicNonNull AudioFocusRequest audioFocusRequest;
+ private boolean rebuildAudioFocusRequest;
+
+ /**
+ * Constructs an AudioFocusManager to automatically handle audio focus for a player.
+ *
+ * @param context The current context.
+ * @param playerControl A {@link PlayerControl} to handle commands from this instance.
+ */
+ public AudioFocusManager(@Nullable Context context, PlayerControl playerControl) {
+ this.audioManager =
+ context == null
+ ? null
+ : (AudioManager)
+ context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+ this.playerControl = playerControl;
+ this.focusListener = new AudioFocusListener();
+ this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;
+ }
+
+ /** Gets the current player volume multiplier. */
+ public float getVolumeMultiplier() {
+ return volumeMultiplier;
+ }
+
+ /**
+ * Sets audio attributes that should be used to manage audio focus.
+ *
+ * @param audioAttributes The audio attributes or {@code null} if audio focus should not be
+ * managed automatically.
+ * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}.
+ * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}.
+ * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link
+ * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}.
+ */
+ public @PlayerCommand int setAudioAttributes(
+ @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) {
+ if (audioAttributes == null) {
+ return PLAYER_COMMAND_PLAY_WHEN_READY;
+ }
+
+ Assertions.checkNotNull(
+ audioManager, "SimpleExoPlayer must be created with a context to handle audio focus.");
+ if (!Util.areEqual(this.audioAttributes, audioAttributes)) {
+ this.audioAttributes = audioAttributes;
+ focusGain = convertAudioAttributesToFocusGain(audioAttributes);
+
+ Assertions.checkArgument(
+ focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE,
+ "Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME.");
+ if (playWhenReady
+ && (playerState == Player.STATE_BUFFERING || playerState == Player.STATE_READY)) {
+ return requestAudioFocus();
+ }
+ }
+
+ if (playerState == Player.STATE_IDLE) {
+ return PLAYER_COMMAND_WAIT_FOR_CALLBACK;
+ } else {
+ return handlePrepare(playWhenReady);
+ }
+ }
+
+ /**
+ * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}.
+ *
+ * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}.
+ * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link
+ * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}.
+ */
+ public @PlayerCommand int handlePrepare(boolean playWhenReady) {
+ if (audioManager == null) {
+ return PLAYER_COMMAND_PLAY_WHEN_READY;
+ }
+
+ return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY;
+ }
+
+ /**
+ * Called by the player as part of {@link ExoPlayer#setPlayWhenReady(boolean)}.
+ *
+ * @param playWhenReady The desired value of playWhenReady.
+ * @param playerState The current state of the player.
+ * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link
+ * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}.
+ */
+ public @PlayerCommand int handleSetPlayWhenReady(boolean playWhenReady, int playerState) {
+ if (audioManager == null) {
+ return PLAYER_COMMAND_PLAY_WHEN_READY;
+ }
+
+ if (!playWhenReady) {
+ abandonAudioFocus();
+ return PLAYER_COMMAND_DO_NOT_PLAY;
+ } else if (playerState != Player.STATE_IDLE) {
+ return requestAudioFocus();
+ }
+ return focusGain != C.AUDIOFOCUS_NONE
+ ? PLAYER_COMMAND_WAIT_FOR_CALLBACK
+ : PLAYER_COMMAND_PLAY_WHEN_READY;
+ }
+
+ /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */
+ public void handleStop() {
+ if (audioManager == null) {
+ return;
+ }
+
+ abandonAudioFocus(/* forceAbandon= */ true);
+ }
+
+ // Internal methods.
+
+ private @PlayerCommand int requestAudioFocus() {
+ int focusRequestResult;
+
+ if (focusGain == C.AUDIOFOCUS_NONE) {
+ if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) {
+ abandonAudioFocus(/* forceAbandon= */ true);
+ }
+ return PLAYER_COMMAND_PLAY_WHEN_READY;
+ }
+
+ if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {
+ if (Util.SDK_INT >= 26) {
+ focusRequestResult = requestAudioFocusV26();
+ } else {
+ focusRequestResult = requestAudioFocusDefault();
+ }
+ audioFocusState =
+ focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
+ ? AUDIO_FOCUS_STATE_HAVE_FOCUS
+ : AUDIO_FOCUS_STATE_NO_FOCUS;
+ }
+
+ if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {
+ return PLAYER_COMMAND_DO_NOT_PLAY;
+ }
+
+ return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT
+ ? PLAYER_COMMAND_WAIT_FOR_CALLBACK
+ : PLAYER_COMMAND_PLAY_WHEN_READY;
+ }
+
+ private void abandonAudioFocus() {
+ abandonAudioFocus(/* forceAbandon= */ false);
+ }
+
+ private void abandonAudioFocus(boolean forceAbandon) {
+ if (focusGain == C.AUDIOFOCUS_NONE && audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {
+ return;
+ }
+
+ if (focusGain != C.AUDIOFOCUS_GAIN
+ || audioFocusState == AUDIO_FOCUS_STATE_LOST_FOCUS
+ || forceAbandon) {
+ if (Util.SDK_INT >= 26) {
+ abandonAudioFocusV26();
+ } else {
+ abandonAudioFocusDefault();
+ }
+ audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;
+ }
+ }
+
+ private int requestAudioFocusDefault() {
+ AudioManager audioManager = Assertions.checkNotNull(this.audioManager);
+ return audioManager.requestAudioFocus(
+ focusListener,
+ Util.getStreamTypeForAudioUsage(Assertions.checkNotNull(audioAttributes).usage),
+ focusGain);
+ }
+
+ @RequiresApi(26)
+ private int requestAudioFocusV26() {
+ if (audioFocusRequest == null || rebuildAudioFocusRequest) {
+ AudioFocusRequest.Builder builder =
+ audioFocusRequest == null
+ ? new AudioFocusRequest.Builder(focusGain)
+ : new AudioFocusRequest.Builder(audioFocusRequest);
+
+ boolean willPauseWhenDucked = willPauseWhenDucked();
+ audioFocusRequest =
+ builder
+ .setAudioAttributes(Assertions.checkNotNull(audioAttributes).getAudioAttributesV21())
+ .setWillPauseWhenDucked(willPauseWhenDucked)
+ .setOnAudioFocusChangeListener(focusListener)
+ .build();
+
+ rebuildAudioFocusRequest = false;
+ }
+ return Assertions.checkNotNull(audioManager).requestAudioFocus(audioFocusRequest);
+ }
+
+ private void abandonAudioFocusDefault() {
+ Assertions.checkNotNull(audioManager).abandonAudioFocus(focusListener);
+ }
+
+ @RequiresApi(26)
+ private void abandonAudioFocusV26() {
+ if (audioFocusRequest != null) {
+ Assertions.checkNotNull(audioManager).abandonAudioFocusRequest(audioFocusRequest);
+ }
+ }
+
+ private boolean willPauseWhenDucked() {
+ return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH;
+ }
+
+ /**
+ * Converts {@link AudioAttributes} to one of the audio focus request.
+ *
+ *
This follows the class Javadoc of {@link AudioFocusRequest}.
+ *
+ * @param audioAttributes The audio attributes associated with this focus request.
+ * @return The type of audio focus gain that should be requested.
+ */
+ private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) {
+
+ if (audioAttributes == null) {
+ // Don't handle audio focus. It may be either video only contents or developers
+ // want to have more finer grained control. (e.g. adding audio focus listener)
+ return C.AUDIOFOCUS_NONE;
+ }
+
+ switch (audioAttributes.usage) {
+ // USAGE_VOICE_COMMUNICATION_SIGNALLING is for DTMF that may happen multiple times
+ // during the phone call when AUDIOFOCUS_GAIN_TRANSIENT is requested for that.
+ // Don't request audio focus here.
+ case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+ return C.AUDIOFOCUS_NONE;
+
+ // Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music
+ // playback, for a game or a video player'
+ case C.USAGE_GAME:
+ case C.USAGE_MEDIA:
+ return C.AUDIOFOCUS_GAIN;
+
+ // Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent
+ // multiple media playback happen at the same time.
+ case C.USAGE_UNKNOWN:
+ Log.w(
+ TAG,
+ "Specify a proper usage in the audio attributes for audio focus"
+ + " handling. Using AUDIOFOCUS_GAIN by default.");
+ return C.AUDIOFOCUS_GAIN;
+
+ // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or
+ // during a VoIP call'
+ case C.USAGE_ALARM:
+ case C.USAGE_VOICE_COMMUNICATION:
+ return C.AUDIOFOCUS_GAIN_TRANSIENT;
+
+ // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing
+ // driving directions or notifications'
+ case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+ case C.USAGE_ASSISTANCE_SONIFICATION:
+ case C.USAGE_NOTIFICATION:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+ case C.USAGE_NOTIFICATION_EVENT:
+ case C.USAGE_NOTIFICATION_RINGTONE:
+ return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
+
+ // Javadoc says 'AUDIOFOCUS_GAIN_EXCLUSIVE: This is typically used if you are doing
+ // audio recording or speech recognition'.
+ // Assistant is considered as both recording and notifying developer
+ case C.USAGE_ASSISTANT:
+ if (Util.SDK_INT >= 19) {
+ return C.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
+ } else {
+ return C.AUDIOFOCUS_GAIN_TRANSIENT;
+ }
+
+ // Special usages:
+ case C.USAGE_ASSISTANCE_ACCESSIBILITY:
+ if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) {
+ // Voice shouldn't be interrupted by other playback.
+ return C.AUDIOFOCUS_GAIN_TRANSIENT;
+ }
+ return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
+ default:
+ Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage);
+ return C.AUDIOFOCUS_NONE;
+ }
+ }
+
+ // Internal audio focus listener.
+
+ private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ // Convert the platform focus change to internal state.
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS;
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT;
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ if (willPauseWhenDucked()) {
+ audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT;
+ } else {
+ audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK;
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS;
+ break;
+ default:
+ Log.w(TAG, "Unknown focus change type: " + focusChange);
+ // Early return.
+ return;
+ }
+
+ // Handle the internal state (change).
+ switch (audioFocusState) {
+ case AUDIO_FOCUS_STATE_NO_FOCUS:
+ // Focus was not requested; nothing to do.
+ break;
+ case AUDIO_FOCUS_STATE_LOST_FOCUS:
+ playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY);
+ abandonAudioFocus(/* forceAbandon= */ true);
+ break;
+ case AUDIO_FOCUS_STATE_LOSS_TRANSIENT:
+ playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK);
+ break;
+ case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK:
+ // Volume will be adjusted by the code below.
+ break;
+ case AUDIO_FOCUS_STATE_HAVE_FOCUS:
+ playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY);
+ break;
+ default:
+ throw new IllegalStateException("Unknown audio focus state: " + audioFocusState);
+ }
+
+ float volumeMultiplier =
+ (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK)
+ ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK
+ : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT;
+ if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) {
+ AudioFocusManager.this.volumeMultiplier = volumeMultiplier;
+ playerControl.setVolumeMultiplier(volumeMultiplier);
+ }
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java
new file mode 100644
index 0000000000..8ce365b283
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+/** A listener for changes in audio configuration. */
+public interface AudioListener {
+
+ /**
+ * Called when the audio session is set.
+ *
+ * @param audioSessionId The audio session id.
+ */
+ default void onAudioSessionId(int audioSessionId) {}
+
+ /**
+ * Called when the audio attributes change.
+ *
+ * @param audioAttributes The audio attributes.
+ */
+ default void onAudioAttributesChanged(AudioAttributes audioAttributes) {}
+
+ /**
+ * Called when the volume changes.
+ *
+ * @param volume The new volume, with 0 being silence and 1 being unity gain.
+ */
+ default void onVolumeChanged(float volume) {}
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java
index 07584d575e..0db52daa12 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java
@@ -283,11 +283,12 @@ public interface AudioSink {
*/
void setAudioAttributes(AudioAttributes audioAttributes);
- /**
- * Sets the audio session id.
- */
+ /** Sets the audio session id. */
void setAudioSessionId(int audioSessionId);
+ /** Sets the auxiliary effect. */
+ void setAuxEffectInfo(AuxEffectInfo auxEffectInfo);
+
/**
* Enables tunneling, if possible. The sink is reset if tunneling was previously disabled or if
* the audio session id has changed. Enabling tunneling is only possible if the sink is based on a
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
index 4714db8902..0095001299 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.audio;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.media.AudioTimestamp;
import android.media.AudioTrack;
import android.os.SystemClock;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
@@ -128,10 +131,10 @@ import java.lang.reflect.Method;
private final Listener listener;
private final long[] playheadOffsets;
- private AudioTrack audioTrack;
+ private @Nullable AudioTrack audioTrack;
private int outputPcmFrameSize;
private int bufferSize;
- private AudioTimestampPoller audioTimestampPoller;
+ private @Nullable AudioTimestampPoller audioTimestampPoller;
private int outputSampleRate;
private boolean needsPassthroughWorkarounds;
private long bufferSizeUs;
@@ -139,7 +142,7 @@ import java.lang.reflect.Method;
private long smoothedPlayheadOffsetUs;
private long lastPlayheadSampleTimeUs;
- private Method getLatencyMethod;
+ private @Nullable Method getLatencyMethod;
private long latencyUs;
private boolean hasData;
@@ -193,7 +196,7 @@ import java.lang.reflect.Method;
audioTimestampPoller = new AudioTimestampPoller(audioTrack);
outputSampleRate = audioTrack.getSampleRate();
needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding);
- isOutputPcm = Util.isEncodingPcm(outputEncoding);
+ isOutputPcm = Util.isEncodingLinearPcm(outputEncoding);
bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0;
@@ -205,13 +208,14 @@ import java.lang.reflect.Method;
}
public long getCurrentPositionUs(boolean sourceEnded) {
- if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
+ if (Assertions.checkNotNull(this.audioTrack).getPlayState() == PLAYSTATE_PLAYING) {
maybeSampleSyncParams();
}
// If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
// Otherwise, derive a smoothed position by sampling the track's frame position.
long systemTimeUs = System.nanoTime() / 1000;
+ AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller);
if (audioTimestampPoller.hasTimestamp()) {
// Calculate the speed-adjusted position using the timestamp (which may be in the future).
long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
@@ -241,12 +245,12 @@ import java.lang.reflect.Method;
/** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */
public void start() {
- audioTimestampPoller.reset();
+ Assertions.checkNotNull(audioTimestampPoller).reset();
}
/** Returns whether the audio track is in the playing state. */
public boolean isPlaying() {
- return audioTrack.getPlayState() == PLAYSTATE_PLAYING;
+ return Assertions.checkNotNull(audioTrack).getPlayState() == PLAYSTATE_PLAYING;
}
/**
@@ -257,7 +261,7 @@ import java.lang.reflect.Method;
* @return Whether the caller can write data to the track.
*/
public boolean mayHandleBuffer(long writtenFrames) {
- @PlayState int playState = audioTrack.getPlayState();
+ @PlayState int playState = Assertions.checkNotNull(audioTrack).getPlayState();
if (needsPassthroughWorkarounds) {
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
// buffer empties. See [Internal: b/18899620].
@@ -339,7 +343,7 @@ import java.lang.reflect.Method;
if (stopTimestampUs == C.TIME_UNSET) {
// The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't
// supply an advancing position.
- audioTimestampPoller.reset();
+ Assertions.checkNotNull(audioTimestampPoller).reset();
return true;
}
// We've handled the end of the stream already, so there's no need to pause the track.
@@ -388,6 +392,7 @@ import java.lang.reflect.Method;
}
private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) {
+ AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller);
if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) {
return;
}
@@ -423,7 +428,9 @@ import java.lang.reflect.Method;
// Compute the audio track latency, excluding the latency due to the buffer (leaving
// latency due to the mixer and audio hardware driver).
latencyUs =
- (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs;
+ castNonNull((Integer) getLatencyMethod.invoke(Assertions.checkNotNull(audioTrack)))
+ * 1000L
+ - bufferSizeUs;
// Sanity check that the latency is non-negative.
latencyUs = Math.max(latencyUs, 0);
// Sanity check that the latency isn't too large.
@@ -457,7 +464,7 @@ import java.lang.reflect.Method;
*/
private boolean forceHasPendingData() {
return needsPassthroughWorkarounds
- && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED
+ && Assertions.checkNotNull(audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED
&& getPlaybackHeadPosition() == 0;
}
@@ -483,6 +490,7 @@ import java.lang.reflect.Method;
* @return The playback head position, in frames.
*/
private long getPlaybackHeadPosition() {
+ AudioTrack audioTrack = Assertions.checkNotNull(this.audioTrack);
if (stopTimestampUs != C.TIME_UNSET) {
// Simulate the playback head position up to the total number of frames submitted.
long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java
new file mode 100644
index 0000000000..7462a9c4b0
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.media.AudioTrack;
+import android.media.audiofx.AudioEffect;
+import android.support.annotation.Nullable;
+
+/**
+ * Represents auxiliary effect information, which can be used to attach an auxiliary effect to an
+ * underlying {@link AudioTrack}.
+ *
+ *
Auxiliary effects can only be applied if the application has the {@code
+ * android.permission.MODIFY_AUDIO_SETTINGS} permission. Apps are responsible for retaining the
+ * associated audio effect instance and releasing it when it's no longer needed. See the
+ * documentation of {@link AudioEffect} for more information.
+ */
+public final class AuxEffectInfo {
+
+ /** Value for {@link #effectId} representing no auxiliary effect. */
+ public static final int NO_AUX_EFFECT_ID = 0;
+
+ /**
+ * The identifier of the effect, or {@link #NO_AUX_EFFECT_ID} if there is no effect.
+ *
+ * @see android.media.AudioTrack#attachAuxEffect(int)
+ */
+ public final int effectId;
+ /**
+ * The send level for the effect.
+ *
+ * @see android.media.AudioTrack#setAuxEffectSendLevel(float)
+ */
+ public final float sendLevel;
+
+ /**
+ * Creates an instance with the given effect identifier and send level.
+ *
+ * @param effectId The effect identifier. This is the value returned by {@link
+ * AudioEffect#getId()} on the effect, or {@value NO_AUX_EFFECT_ID} which represents no
+ * effect. This value is passed to {@link AudioTrack#attachAuxEffect(int)} on the underlying
+ * audio track.
+ * @param sendLevel The send level for the effect, where 0 represents no effect and a value of 1
+ * is full send. If {@code effectId} is not {@value #NO_AUX_EFFECT_ID}, this value is passed
+ * to {@link AudioTrack#setAuxEffectSendLevel(float)} on the underlying audio track.
+ */
+ public AuxEffectInfo(int effectId, float sendLevel) {
+ this.effectId = effectId;
+ this.sendLevel = sendLevel;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AuxEffectInfo auxEffectInfo = (AuxEffectInfo) o;
+ return effectId == auxEffectInfo.effectId
+ && Float.compare(auxEffectInfo.sendLevel, sendLevel) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + effectId;
+ result = 31 * result + Float.floatToIntBits(sendLevel);
+ return result;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
index 1025cb953b..aed4c63c75 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
@@ -26,6 +26,7 @@ import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
@@ -279,6 +280,7 @@ public final class DefaultAudioSink implements AudioSink {
private boolean playing;
private int audioSessionId;
+ private AuxEffectInfo auxEffectInfo;
private boolean tunneling;
private long lastFeedElapsedRealtimeMs;
@@ -355,6 +357,7 @@ public final class DefaultAudioSink implements AudioSink {
startMediaTimeState = START_NOT_SET;
audioAttributes = AudioAttributes.DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);
playbackParameters = PlaybackParameters.DEFAULT;
drainingAudioProcessorIndex = C.INDEX_UNSET;
activeAudioProcessors = new AudioProcessor[0];
@@ -371,7 +374,7 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public boolean isEncodingSupported(@C.Encoding int encoding) {
- if (Util.isEncodingPcm(encoding)) {
+ if (Util.isEncodingLinearPcm(encoding)) {
// AudioTrack supports 16-bit integer PCM output in all platform API versions, and float
// output from platform API version 21 only. Other integer PCM encodings are resampled by this
// sink to 16-bit PCM.
@@ -405,7 +408,7 @@ public final class DefaultAudioSink implements AudioSink {
this.inputSampleRate = inputSampleRate;
int channelCount = inputChannelCount;
int sampleRate = inputSampleRate;
- isInputPcm = Util.isEncodingPcm(inputEncoding);
+ isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
shouldConvertHighResIntPcmToFloat =
enableConvertHighResIntPcmToFloat
&& isEncodingSupported(C.ENCODING_PCM_32BIT)
@@ -416,6 +419,16 @@ public final class DefaultAudioSink implements AudioSink {
@C.Encoding int encoding = inputEncoding;
boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;
canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat;
+
+ if (Util.SDK_INT < 21 && channelCount == 8 && outputChannels == null) {
+ // AudioTrack doesn't support 8 channel output before Android L. Discard the last two (side)
+ // channels to give a 6 channel stream that is supported.
+ outputChannels = new int[6];
+ for (int i = 0; i < outputChannels.length; i++) {
+ outputChannels[i] = i;
+ }
+ }
+
if (processingEnabled) {
trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames);
channelMappingAudioProcessor.setChannelMap(outputChannels);
@@ -433,55 +446,9 @@ public final class DefaultAudioSink implements AudioSink {
}
}
- int channelConfig;
- switch (channelCount) {
- case 1:
- channelConfig = AudioFormat.CHANNEL_OUT_MONO;
- break;
- case 2:
- channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
- break;
- case 3:
- channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
- break;
- case 4:
- channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
- break;
- case 5:
- channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
- break;
- case 6:
- channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
- break;
- case 7:
- channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
- break;
- case 8:
- channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
- break;
- default:
- throw new ConfigurationException("Unsupported channel count: " + channelCount);
- }
-
- // Workaround for overly strict channel configuration checks on nVidia Shield.
- if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
- switch (channelCount) {
- case 7:
- channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
- break;
- case 3:
- case 5:
- channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
- break;
- default:
- break;
- }
- }
-
- // Workaround for Nexus Player not reporting support for mono passthrough.
- // (See [Internal: b/34268671].)
- if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) {
- channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+ int channelConfig = getChannelConfig(channelCount, isInputPcm);
+ if (channelConfig == AudioFormat.CHANNEL_INVALID) {
+ throw new ConfigurationException("Unsupported channel count: " + channelCount);
}
if (!flush
@@ -501,29 +468,22 @@ public final class DefaultAudioSink implements AudioSink {
outputEncoding = encoding;
outputPcmFrameSize =
isInputPcm ? Util.getPcmFrameSize(outputEncoding, channelCount) : C.LENGTH_UNSET;
- if (specifiedBufferSize != 0) {
- bufferSize = specifiedBufferSize;
- } else if (isInputPcm) {
- int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
+ bufferSize = specifiedBufferSize != 0 ? specifiedBufferSize : getDefaultBufferSize();
+ }
+
+ private int getDefaultBufferSize() {
+ if (isInputPcm) {
+ int minBufferSize =
+ AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding);
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
int maxAppBufferSize = (int) Math.max(minBufferSize,
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
- bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
+ return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
} else {
- // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
- // account. [Internal: b/25181305]
- if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
- // AC-3 allows bitrates up to 640 kbit/s.
- bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND);
- } else if (outputEncoding == C.ENCODING_DTS) {
- // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
- bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
- } else /* outputEncoding == C.ENCODING_DTS_HD || outputEncoding == C.ENCODING_DOLBY_TRUEHD*/ {
- // HD passthrough requires a larger buffer to avoid underrun.
- bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND);
- }
+ int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding);
+ return (int) (PASSTHROUGH_BUFFER_DURATION_US * rate / C.MICROS_PER_SECOND);
}
}
@@ -589,6 +549,11 @@ public final class DefaultAudioSink implements AudioSink {
audioTrackPositionTracker.setAudioTrack(
audioTrack, outputEncoding, outputPcmFrameSize, bufferSize);
setVolumeInternal();
+
+ if (auxEffectInfo.effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) {
+ audioTrack.attachAuxEffect(auxEffectInfo.effectId);
+ audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel);
+ }
}
@Override
@@ -909,6 +874,24 @@ public final class DefaultAudioSink implements AudioSink {
}
}
+ @Override
+ public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
+ if (this.auxEffectInfo.equals(auxEffectInfo)) {
+ return;
+ }
+ int effectId = auxEffectInfo.effectId;
+ float sendLevel = auxEffectInfo.sendLevel;
+ if (audioTrack != null) {
+ if (this.auxEffectInfo.effectId != effectId) {
+ audioTrack.attachAuxEffect(effectId);
+ }
+ if (effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) {
+ audioTrack.setAuxEffectSendLevel(sendLevel);
+ }
+ }
+ this.auxEffectInfo = auxEffectInfo;
+ }
+
@Override
public void enableTunnelingV21(int tunnelingAudioSessionId) {
Assertions.checkState(Util.SDK_INT >= 21);
@@ -1170,6 +1153,55 @@ public final class DefaultAudioSink implements AudioSink {
: toIntPcmAvailableAudioProcessors;
}
+ private static int getChannelConfig(int channelCount, boolean isInputPcm) {
+ if (Util.SDK_INT <= 28 && !isInputPcm) {
+ // In passthrough mode the channel count used to configure the audio track doesn't affect how
+ // the stream is handled, except that some devices do overly-strict channel configuration
+ // checks. Therefore we override the channel count so that a known-working channel
+ // configuration is chosen in all cases. See [Internal: b/29116190].
+ if (channelCount == 7) {
+ channelCount = 8;
+ } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) {
+ channelCount = 6;
+ }
+ }
+
+ // Workaround for Nexus Player not reporting support for mono passthrough.
+ // (See [Internal: b/34268671].)
+ if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) {
+ channelCount = 2;
+ }
+
+ return Util.getAudioTrackChannelConfig(channelCount);
+ }
+
+ private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
+ switch (encoding) {
+ case C.ENCODING_AC3:
+ return 640 * 1000 / 8;
+ case C.ENCODING_E_AC3:
+ return 6144 * 1000 / 8;
+ case C.ENCODING_DTS:
+ // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
+ return 1536 * 1000 / 8;
+ case C.ENCODING_DTS_HD:
+ return 18000 * 1000 / 8;
+ case C.ENCODING_DOLBY_TRUEHD:
+ return 24500 * 1000 / 8;
+ case C.ENCODING_INVALID:
+ case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_PCM_24BIT:
+ case C.ENCODING_PCM_32BIT:
+ case C.ENCODING_PCM_8BIT:
+ case C.ENCODING_PCM_A_LAW:
+ case C.ENCODING_PCM_FLOAT:
+ case C.ENCODING_PCM_MU_LAW:
+ case Format.NO_VALUE:
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {
return DtsUtil.parseDtsAudioSampleCount(buffer);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
index dc07b1a646..f65dc3fc4e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
@@ -74,13 +74,13 @@ public final class DtsUtil {
* subsections 5.3/5.4.
*
* @param frame The DTS frame to parse.
- * @param trackId The track identifier to set on the format, or null.
+ * @param trackId The track identifier to set on the format.
* @param language The language to set on the format.
* @param drmInitData {@link DrmInitData} to be included in the format.
* @return The DTS format parsed from data in the header.
*/
- public static Format parseDtsFormat(byte[] frame, String trackId, String language,
- DrmInitData drmInitData) {
+ public static Format parseDtsFormat(
+ byte[] frame, String trackId, String language, DrmInitData drmInitData) {
ParsableBitArray frameBits = getNormalizedFrameHeader(frame);
frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
int amode = frameBits.readBits(6);
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 9ab066ee7d..1197cb5a71 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
@@ -45,6 +45,8 @@ import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
/**
* Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}.
@@ -58,6 +60,9 @@ import java.nio.ByteBuffer;
*
Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The
* message payload should be an {@link com.google.android.exoplayer2.audio.AudioAttributes}
* instance that will configure the underlying audio track.
+ * Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The
+ * message payload should be an {@link AuxEffectInfo} instance that will configure the
+ * underlying audio track.
*
*/
@TargetApi(16)
@@ -71,8 +76,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat;
- @C.Encoding
- private int pcmEncoding;
+ private @C.Encoding int pcmEncoding;
private int channelCount;
private int encoderDelay;
private int encoderPadding;
@@ -229,7 +233,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink) {
- super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
+ super(
+ C.TRACK_TYPE_AUDIO,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ /* assumedMinimumCodecOperatingRate= */ 44100);
this.context = context.getApplicationContext();
this.audioSink = audioSink;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
@@ -262,35 +271,41 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;
}
}
- MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType,
- requiresSecureDecryption);
- if (decoderInfo == null) {
- return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null
- ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE;
+ List decoderInfos =
+ mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecryption);
+ if (decoderInfos.isEmpty()) {
+ return requiresSecureDecryption
+ && !mediaCodecSelector
+ .getDecoderInfos(format.sampleMimeType, /* requiresSecureDecoder= */ false)
+ .isEmpty()
+ ? FORMAT_UNSUPPORTED_DRM
+ : FORMAT_UNSUPPORTED_SUBTYPE;
}
if (!supportsFormatDrm) {
return FORMAT_UNSUPPORTED_DRM;
}
- // Note: We assume support for unknown sampleRate and channelCount.
- boolean decoderCapable = Util.SDK_INT < 21
- || ((format.sampleRate == Format.NO_VALUE
- || decoderInfo.isAudioSampleRateSupportedV21(format.sampleRate))
- && (format.channelCount == Format.NO_VALUE
- || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
- int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
- return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
+ // Check capabilities for the first decoder in the list, which takes priority.
+ MediaCodecInfo decoderInfo = decoderInfos.get(0);
+ boolean isFormatSupported = decoderInfo.isFormatSupported(format);
+ int adaptiveSupport =
+ isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format)
+ ? ADAPTIVE_SEAMLESS
+ : ADAPTIVE_NOT_SEAMLESS;
+ int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
+ return adaptiveSupport | tunnelingSupport | formatSupport;
}
@Override
- protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
- Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
+ protected List getDecoderInfos(
+ MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
if (allowPassthrough(format.sampleMimeType)) {
MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
if (passthroughDecoderInfo != null) {
- return passthroughDecoderInfo;
+ return Collections.singletonList(passthroughDecoderInfo);
}
}
- return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder);
+ return super.getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder);
}
/**
@@ -307,13 +322,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
@Override
- protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
- MediaCrypto crypto) {
+ protected void configureCodec(
+ MediaCodecInfo codecInfo,
+ MediaCodec codec,
+ Format format,
+ MediaCrypto crypto,
+ float codecOperatingRate) {
codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats());
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
passthroughEnabled = codecInfo.passthrough;
String codecMimeType = codecInfo.mimeType == null ? MimeTypes.AUDIO_RAW : codecInfo.mimeType;
- MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize);
+ MediaFormat mediaFormat =
+ getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate);
codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);
if (passthroughEnabled) {
// Store the input MIME type if we're using the passthrough codec.
@@ -327,13 +347,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected @KeepCodecResult int canKeepCodec(
MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {
- return KEEP_CODEC_RESULT_NO;
- // TODO: Determine when codecs can be safely kept. When doing so, also uncomment the commented
- // out code in getCodecMaxInputSize.
- // return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize
- // && areAdaptationCompatible(oldFormat, newFormat)
- // ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION
- // : KEEP_CODEC_RESULT_NO;
+ if (getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize
+ && codecInfo.isSeamlessAdaptationSupported(oldFormat, newFormat)
+ && oldFormat.encoderDelay == 0
+ && oldFormat.encoderPadding == 0
+ && newFormat.encoderDelay == 0
+ && newFormat.encoderPadding == 0) {
+ return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION;
+ } else {
+ return KEEP_CODEC_RESULT_NO;
+ }
}
@Override
@@ -341,6 +364,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return this;
}
+ @Override
+ protected float getCodecOperatingRate(
+ float operatingRate, Format format, Format[] streamFormats) {
+ // Use the highest known stream sample-rate up front, to avoid having to reconfigure the codec
+ // should an adaptive switch to that stream occur.
+ int maxSampleRate = -1;
+ for (Format streamFormat : streamFormats) {
+ int streamSampleRate = streamFormat.sampleRate;
+ if (streamSampleRate != Format.NO_VALUE) {
+ maxSampleRate = Math.max(maxSampleRate, streamSampleRate);
+ }
+ }
+ return maxSampleRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxSampleRate * operatingRate);
+ }
+
@Override
protected void onCodecInitialized(String name, long initializedTimestampMs,
long initializationDurationMs) {
@@ -556,6 +594,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
AudioAttributes audioAttributes = (AudioAttributes) message;
audioSink.setAudioAttributes(audioAttributes);
break;
+ case C.MSG_SET_AUX_EFFECT_INFO:
+ AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message;
+ audioSink.setAuxEffectInfo(auxEffectInfo);
+ break;
default:
super.handleMessage(messageType, message);
break;
@@ -574,16 +616,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
protected int getCodecMaxInputSize(
MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {
int maxInputSize = getCodecMaxInputSize(codecInfo, format);
- // if (streamFormats.length == 1) {
- // // The single entry in streamFormats must correspond to the format for which the codec is
- // // being configured.
- // return maxInputSize;
- // }
- // for (Format streamFormat : streamFormats) {
- // if (areAdaptationCompatible(format, streamFormat)) {
- // maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat));
- // }
- // }
+ if (streamFormats.length == 1) {
+ // The single entry in streamFormats must correspond to the format for which the codec is
+ // being configured.
+ return maxInputSize;
+ }
+ for (Format streamFormat : streamFormats) {
+ if (codecInfo.isSeamlessAdaptationSupported(format, streamFormat)) {
+ maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat));
+ }
+ }
return maxInputSize;
}
@@ -624,10 +666,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param format The format of the media.
* @param codecMimeType The MIME type handled by the codec.
* @param codecMaxInputSize The maximum input size supported by the codec.
+ * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
+ * no codec operating rate should be set.
* @return The framework media format.
*/
@SuppressLint("InlinedApi")
- protected MediaFormat getMediaFormat(Format format, String codecMimeType, int codecMaxInputSize) {
+ protected MediaFormat getMediaFormat(
+ Format format, String codecMimeType, int codecMaxInputSize, float codecOperatingRate) {
MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set.
mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType);
@@ -639,6 +684,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
// Set codec configuration values.
if (Util.SDK_INT >= 23) {
mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */);
+ if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) {
+ mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);
+ }
}
return mediaFormat;
}
@@ -654,25 +702,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
- /**
- * Returns whether a codec with suitable maximum input size will support adaptation between two
- * {@link Format}s.
- *
- * @param first The first format.
- * @param second The second format.
- * @return Whether the codec will support adaptation between the two {@link Format}s.
- */
- private static boolean areAdaptationCompatible(Format first, Format second) {
- return first.sampleMimeType.equals(second.sampleMimeType)
- && first.channelCount == second.channelCount
- && first.sampleRate == second.sampleRate
- && first.encoderDelay == 0
- && first.encoderPadding == 0
- && second.encoderDelay == 0
- && second.encoderPadding == 0
- && first.initializationDataEquals(second);
- }
-
/**
* Returns whether the decoder is known to output six audio channels when provided with input with
* fewer than six channels.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
index a289ced128..96400cd70b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
@@ -33,12 +33,12 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
* The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
* that part of audio as silent, in microseconds.
*/
- private static final long MINIMUM_SILENCE_DURATION_US = 100_000;
+ private static final long MINIMUM_SILENCE_DURATION_US = 150_000;
/**
* The duration of silence by which to extend non-silent sections, in microseconds. The value must
* not exceed {@link #MINIMUM_SILENCE_DURATION_US}.
*/
- private static final long PADDING_SILENCE_US = 10_000;
+ private static final long PADDING_SILENCE_US = 20_000;
/**
* The absolute level below which an individual PCM sample is classified as silent. Note: the
* specified value will be rounded so that the threshold check only depends on the more
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 c404912882..83b14c071d 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
@@ -20,6 +20,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -57,6 +58,9 @@ import java.lang.annotation.RetentionPolicy;
* Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The
* message payload should be an {@link com.google.android.exoplayer2.audio.AudioAttributes}
* instance that will configure the underlying audio track.
+ * Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The
+ * message payload should be an {@link AuxEffectInfo} instance that will configure the
+ * underlying audio track.
*
*/
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
@@ -121,7 +125,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
this(
eventHandler,
@@ -139,8 +145,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
*/
- public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- AudioCapabilities audioCapabilities) {
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities) {
this(
eventHandler,
eventListener,
@@ -164,9 +172,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys, AudioProcessor... audioProcessors) {
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, drmSessionManager,
playClearSamplesWithoutKeys, new DefaultAudioSink(audioCapabilities, audioProcessors));
}
@@ -184,8 +196,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioSink The sink to which audio will be output.
*/
- public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
AudioSink audioSink) {
super(C.TRACK_TYPE_AUDIO);
this.drmSessionManager = drmSessionManager;
@@ -206,6 +221,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public final int supportsFormat(Format format) {
+ if (!MimeTypes.isAudio(format.sampleMimeType)) {
+ return FORMAT_UNSUPPORTED_TYPE;
+ }
int formatSupport = supportsFormatInternal(drmSessionManager, format);
if (formatSupport <= FORMAT_UNSUPPORTED_DRM) {
return formatSupport;
@@ -215,15 +233,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
/**
- * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
- * {@link #supportsFormat(Format)}.
+ * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link
+ * #supportsFormat(Format)}.
*
* @param drmSessionManager The renderer's {@link DrmSessionManager}.
- * @param format The format.
+ * @param format The format, which has an audio {@link Format#sampleMimeType}.
* @return The extent to which the renderer supports the format itself.
*/
- protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format);
+ protected abstract int supportsFormatInternal(
+ DrmSessionManager drmSessionManager, Format format);
/**
* Returns whether the audio sink can accept audio in the specified encoding.
@@ -575,6 +593,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
AudioAttributes audioAttributes = (AudioAttributes) message;
audioSink.setAudioAttributes(audioAttributes);
break;
+ case C.MSG_SET_AUX_EFFECT_INFO:
+ AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message;
+ audioSink.setAuxEffectInfo(auxEffectInfo);
+ break;
default:
super.handleMessage(messageType, message);
break;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java
new file mode 100644
index 0000000000..654d4edc56
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Audio processor that outputs its input unmodified and also outputs its input to a given sink.
+ * This is intended to be used for diagnostics and debugging.
+ *
+ * This audio processor can be inserted into the audio processor chain to access audio data
+ * before/after particular processing steps have been applied. For example, to get audio output
+ * after playback speed adjustment and silence skipping have been applied it is necessary to pass a
+ * custom {@link com.google.android.exoplayer2.audio.DefaultAudioSink.AudioProcessorChain} when
+ * creating the audio sink, and include this audio processor after all other audio processors.
+ */
+public final class TeeAudioProcessor implements AudioProcessor {
+
+ /** A sink for audio buffers handled by the audio processor. */
+ public interface AudioBufferSink {
+
+ /** Called when the audio processor is flushed with a format of subsequent input. */
+ void flush(int sampleRateHz, int channelCount, @C.Encoding int encoding);
+
+ /**
+ * Called when data is written to the audio processor.
+ *
+ * @param buffer A read-only buffer containing input which the audio processor will handle.
+ */
+ void handleBuffer(ByteBuffer buffer);
+ }
+
+ private final AudioBufferSink audioBufferSink;
+
+ private int sampleRateHz;
+ private int channelCount;
+ private @C.Encoding int encoding;
+ private boolean isActive;
+
+ private ByteBuffer buffer;
+ private ByteBuffer outputBuffer;
+ private boolean inputEnded;
+
+ /**
+ * Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}.
+ *
+ * @param audioBufferSink The audio buffer sink that will receive input queued to this audio
+ * processor.
+ */
+ public TeeAudioProcessor(AudioBufferSink audioBufferSink) {
+ this.audioBufferSink = Assertions.checkNotNull(audioBufferSink);
+
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ }
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
+ throws UnhandledFormatException {
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ this.encoding = encoding;
+ boolean wasActive = isActive;
+ isActive = true;
+ return !wasActive;
+ }
+
+ @Override
+ public boolean isActive() {
+ return isActive;
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return channelCount;
+ }
+
+ @Override
+ public int getOutputEncoding() {
+ return encoding;
+ }
+
+ @Override
+ public int getOutputSampleRateHz() {
+ return sampleRateHz;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer buffer) {
+ int remaining = buffer.remaining();
+ if (remaining == 0) {
+ return;
+ }
+
+ audioBufferSink.handleBuffer(buffer.asReadOnlyBuffer());
+
+ if (this.buffer.capacity() < remaining) {
+ this.buffer = ByteBuffer.allocateDirect(remaining).order(ByteOrder.nativeOrder());
+ } else {
+ this.buffer.clear();
+ }
+
+ this.buffer.put(buffer);
+
+ this.buffer.flip();
+ outputBuffer = this.buffer;
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public boolean isEnded() {
+ return inputEnded && buffer == EMPTY_BUFFER;
+ }
+
+ @Override
+ public void flush() {
+ outputBuffer = EMPTY_BUFFER;
+ inputEnded = false;
+
+ audioBufferSink.flush(sampleRateHz, channelCount, encoding);
+ }
+
+ @Override
+ public void reset() {
+ flush();
+ buffer = EMPTY_BUFFER;
+ sampleRateHz = Format.NO_VALUE;
+ channelCount = Format.NO_VALUE;
+ encoding = Format.NO_VALUE;
+ }
+
+ /**
+ * A sink for audio buffers that writes output audio as .wav files with a given path prefix. When
+ * new audio data is handled after flushing the audio processor, a counter is incremented and its
+ * value is appended to the output file name.
+ *
+ *
Note: if writing to external storage it's necessary to grant the {@code
+ * WRITE_EXTERNAL_STORAGE} permission.
+ */
+ public static final class WavFileAudioBufferSink implements AudioBufferSink {
+
+ private static final String TAG = "WaveFileAudioBufferSink";
+
+ private static final int FILE_SIZE_MINUS_8_OFFSET = 4;
+ private static final int FILE_SIZE_MINUS_44_OFFSET = 40;
+ private static final int HEADER_LENGTH = 44;
+
+ private final String outputFileNamePrefix;
+ private final byte[] scratchBuffer;
+ private final ByteBuffer scratchByteBuffer;
+
+ private int sampleRateHz;
+ private int channelCount;
+ private @C.Encoding int encoding;
+ private @Nullable RandomAccessFile randomAccessFile;
+ private int counter;
+ private int bytesWritten;
+
+ /**
+ * Creates a new audio buffer sink that writes to .wav files with the given prefix.
+ *
+ * @param outputFileNamePrefix The prefix for output files.
+ */
+ public WavFileAudioBufferSink(String outputFileNamePrefix) {
+ this.outputFileNamePrefix = outputFileNamePrefix;
+ scratchBuffer = new byte[1024];
+ scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ @Override
+ public void flush(int sampleRateHz, int channelCount, int encoding) {
+ try {
+ reset();
+ } catch (IOException e) {
+ Log.e(TAG, "Error resetting", e);
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ this.encoding = encoding;
+ }
+
+ @Override
+ public void handleBuffer(ByteBuffer buffer) {
+ try {
+ maybePrepareFile();
+ writeBuffer(buffer);
+ } catch (IOException e) {
+ Log.e(TAG, "Error writing data", e);
+ }
+ }
+
+ private void maybePrepareFile() throws IOException {
+ if (randomAccessFile != null) {
+ return;
+ }
+ RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), "rw");
+ writeFileHeader(randomAccessFile);
+ this.randomAccessFile = randomAccessFile;
+ bytesWritten = HEADER_LENGTH;
+ }
+
+ private void writeFileHeader(RandomAccessFile randomAccessFile) throws IOException {
+ // Write the start of the header as big endian data.
+ randomAccessFile.writeInt(WavUtil.RIFF_FOURCC);
+ randomAccessFile.writeInt(-1);
+ randomAccessFile.writeInt(WavUtil.WAVE_FOURCC);
+ randomAccessFile.writeInt(WavUtil.FMT_FOURCC);
+
+ // Write the rest of the header as little endian data.
+ scratchByteBuffer.clear();
+ scratchByteBuffer.putInt(16);
+ scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding));
+ scratchByteBuffer.putShort((short) channelCount);
+ scratchByteBuffer.putInt(sampleRateHz);
+ int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
+ scratchByteBuffer.putInt(bytesPerSample * sampleRateHz);
+ scratchByteBuffer.putShort((short) bytesPerSample);
+ scratchByteBuffer.putShort((short) (8 * bytesPerSample / channelCount));
+ randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position());
+
+ // Write the start of the data chunk as big endian data.
+ randomAccessFile.writeInt(WavUtil.DATA_FOURCC);
+ randomAccessFile.writeInt(-1);
+ }
+
+ private void writeBuffer(ByteBuffer buffer) throws IOException {
+ RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile);
+ while (buffer.hasRemaining()) {
+ int bytesToWrite = Math.min(buffer.remaining(), scratchBuffer.length);
+ buffer.get(scratchBuffer, 0, bytesToWrite);
+ randomAccessFile.write(scratchBuffer, 0, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ }
+
+ private void reset() throws IOException {
+ RandomAccessFile randomAccessFile = this.randomAccessFile;
+ if (randomAccessFile == null) {
+ return;
+ }
+
+ try {
+ scratchByteBuffer.clear();
+ scratchByteBuffer.putInt(bytesWritten - 8);
+ randomAccessFile.seek(FILE_SIZE_MINUS_8_OFFSET);
+ randomAccessFile.write(scratchBuffer, 0, 4);
+
+ scratchByteBuffer.clear();
+ scratchByteBuffer.putInt(bytesWritten - 44);
+ randomAccessFile.seek(FILE_SIZE_MINUS_44_OFFSET);
+ randomAccessFile.write(scratchBuffer, 0, 4);
+ } catch (IOException e) {
+ // The file may still be playable, so just log a warning.
+ Log.w(TAG, "Error updating file size", e);
+ }
+
+ try {
+ randomAccessFile.close();
+ } finally {
+ this.randomAccessFile = null;
+ }
+ }
+
+ private String getNextOutputFileName() {
+ return Util.formatInvariant("%s-%04d.wav", outputFileNamePrefix, counter++);
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java
new file mode 100644
index 0000000000..473a91fedf
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Util;
+
+/** Utilities for handling WAVE files. */
+public final class WavUtil {
+
+ /** Four character code for "RIFF". */
+ public static final int RIFF_FOURCC = Util.getIntegerCodeForString("RIFF");
+ /** Four character code for "WAVE". */
+ public static final int WAVE_FOURCC = Util.getIntegerCodeForString("WAVE");
+ /** Four character code for "fmt ". */
+ public static final int FMT_FOURCC = Util.getIntegerCodeForString("fmt ");
+ /** Four character code for "data". */
+ public static final int DATA_FOURCC = Util.getIntegerCodeForString("data");
+
+ /** WAVE type value for integer PCM audio data. */
+ private static final int TYPE_PCM = 0x0001;
+ /** WAVE type value for float PCM audio data. */
+ private static final int TYPE_FLOAT = 0x0003;
+ /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */
+ private static final int TYPE_A_LAW = 0x0006;
+ /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */
+ private static final int TYPE_MU_LAW = 0x0007;
+ /** WAVE type value for extended WAVE format. */
+ private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+
+ /** Returns the WAVE type value for the given {@code encoding}. */
+ public static int getTypeForEncoding(@C.PcmEncoding int encoding) {
+ switch (encoding) {
+ case C.ENCODING_PCM_8BIT:
+ case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_PCM_24BIT:
+ case C.ENCODING_PCM_32BIT:
+ return TYPE_PCM;
+ case C.ENCODING_PCM_A_LAW:
+ return TYPE_A_LAW;
+ case C.ENCODING_PCM_MU_LAW:
+ return TYPE_MU_LAW;
+ case C.ENCODING_PCM_FLOAT:
+ return TYPE_FLOAT;
+ case C.ENCODING_INVALID:
+ case Format.NO_VALUE:
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /** Returns the PCM encoding for the given WAVE {@code type} value. */
+ public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) {
+ switch (type) {
+ case TYPE_PCM:
+ case TYPE_WAVE_FORMAT_EXTENSIBLE:
+ return Util.getPcmEncoding(bitsPerSample);
+ case TYPE_FLOAT:
+ return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
+ case TYPE_A_LAW:
+ return C.ENCODING_PCM_A_LAW;
+ case TYPE_MU_LAW:
+ return C.ENCODING_PCM_MU_LAW;
+ default:
+ return C.ENCODING_INVALID;
+ }
+ }
+
+ private WavUtil() {
+ // Prevent instantiation.
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
index 68089d7b41..98b1c7ca0f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
@@ -15,9 +15,10 @@
*/
package com.google.android.exoplayer2.decoder;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
-import java.util.LinkedList;
+import java.util.ArrayDeque;
/**
* Base class for {@link Decoder}s that use their own decode thread.
@@ -28,8 +29,8 @@ public abstract class SimpleDecoder queuedInputBuffers;
- private final LinkedList queuedOutputBuffers;
+ private final ArrayDeque queuedInputBuffers;
+ private final ArrayDeque queuedOutputBuffers;
private final I[] availableInputBuffers;
private final O[] availableOutputBuffers;
@@ -48,8 +49,8 @@ public abstract class SimpleDecoder();
- queuedOutputBuffers = new LinkedList<>();
+ queuedInputBuffers = new ArrayDeque<>();
+ queuedOutputBuffers = new ArrayDeque<>();
availableInputBuffers = inputBuffers;
availableInputBufferCount = inputBuffers.length;
for (int i = 0; i < availableInputBufferCount; i++) {
@@ -142,7 +143,7 @@ public abstract class SimpleDecoder mediaDrm;
private final ProvisioningManager provisioningManager;
- private final byte[] initData;
- private final String mimeType;
+ private final SchemeData schemeData;
private final @DefaultDrmSessionManager.Mode int mode;
private final HashMap optionalKeyRequestParameters;
- private final EventDispatcher eventDispatcher;
+ private final EventDispatcher eventDispatcher;
private final int initialDrmRequestRetryCount;
/* package */ final MediaDrmCallback callback;
@@ -97,15 +97,20 @@ import java.util.UUID;
private byte[] sessionId;
private byte[] offlineLicenseKeySetId;
+ private Object currentKeyRequest;
+ private Object currentProvisionRequest;
+
/**
* Instantiates a new DRM session.
*
* @param uuid The UUID of the drm scheme.
* @param mediaDrm The media DRM.
* @param provisioningManager The manager for provisioning.
- * @param initData The DRM init data.
+ * @param schemeData The DRM data for this session, or null if a {@code offlineLicenseKeySetId} is
+ * provided.
* @param mode The DRM mode.
- * @param offlineLicenseKeySetId The offlineLicense KeySetId.
+ * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using
+ * offline keys.
* @param optionalKeyRequestParameters The optional key request parameters.
* @param callback The media DRM callback.
* @param playbackLooper The playback looper.
@@ -117,20 +122,20 @@ import java.util.UUID;
UUID uuid,
ExoMediaDrm mediaDrm,
ProvisioningManager provisioningManager,
- byte[] initData,
- String mimeType,
+ @Nullable SchemeData schemeData,
@DefaultDrmSessionManager.Mode int mode,
- byte[] offlineLicenseKeySetId,
+ @Nullable byte[] offlineLicenseKeySetId,
HashMap optionalKeyRequestParameters,
MediaDrmCallback callback,
Looper playbackLooper,
- EventDispatcher eventDispatcher,
+ EventDispatcher eventDispatcher,
int initialDrmRequestRetryCount) {
this.uuid = uuid;
this.provisioningManager = provisioningManager;
this.mediaDrm = mediaDrm;
this.mode = mode;
this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+ this.schemeData = offlineLicenseKeySetId == null ? schemeData : null;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.callback = callback;
this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;
@@ -141,14 +146,6 @@ import java.util.UUID;
requestHandlerThread = new HandlerThread("DrmRequestHandler");
requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
-
- if (offlineLicenseKeySetId == null) {
- this.initData = initData;
- this.mimeType = mimeType;
- } else {
- this.initData = null;
- this.mimeType = null;
- }
}
// Life cycle.
@@ -177,6 +174,8 @@ import java.util.UUID;
requestHandlerThread = null;
mediaCrypto = null;
lastException = null;
+ currentKeyRequest = null;
+ currentProvisionRequest = null;
if (sessionId != null) {
mediaDrm.closeSession(sessionId);
sessionId = null;
@@ -187,18 +186,42 @@ import java.util.UUID;
}
public boolean hasInitData(byte[] initData) {
- return Arrays.equals(this.initData, initData);
+ return Arrays.equals(schemeData != null ? schemeData.data : null, initData);
}
public boolean hasSessionId(byte[] sessionId) {
return Arrays.equals(this.sessionId, sessionId);
}
+ @SuppressWarnings("deprecation")
+ public void onMediaDrmEvent(int what) {
+ if (!isOpen()) {
+ return;
+ }
+ switch (what) {
+ case ExoMediaDrm.EVENT_KEY_REQUIRED:
+ doLicense(false);
+ break;
+ case ExoMediaDrm.EVENT_KEY_EXPIRED:
+ // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
+ // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
+ // waiting for key response.
+ onKeysExpired();
+ break;
+ case ExoMediaDrm.EVENT_PROVISION_REQUIRED:
+ state = STATE_OPENED;
+ provisioningManager.provisionRequired(this);
+ break;
+ default:
+ break;
+ }
+ }
+
// Provisioning implementation.
public void provision() {
- ProvisionRequest request = mediaDrm.getProvisionRequest();
- postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget();
+ currentProvisionRequest = mediaDrm.getProvisionRequest();
+ postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true);
}
public void onProvisionCompleted() {
@@ -271,11 +294,12 @@ import java.util.UUID;
return false;
}
- private void onProvisionResponse(Object response) {
- if (state != STATE_OPENING && !isOpen()) {
+ private void onProvisionResponse(Object request, Object response) {
+ if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) {
// This event is stale.
return;
}
+ currentProvisionRequest = null;
if (response instanceof Exception) {
provisioningManager.onProvisionError((Exception) response);
@@ -309,7 +333,7 @@ import java.util.UUID;
onError(new KeysExpiredException());
} else {
state = STATE_OPENED_WITH_KEYS;
- eventDispatcher.drmKeysRestored();
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored);
}
}
break;
@@ -356,24 +380,30 @@ import java.util.UUID;
private void postKeyRequest(int type, boolean allowRetry) {
byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId;
+ byte[] initData = null;
+ String mimeType = null;
+ String licenseServerUrl = null;
+ if (schemeData != null) {
+ initData = schemeData.data;
+ mimeType = schemeData.mimeType;
+ licenseServerUrl = schemeData.licenseServerUrl;
+ }
try {
- KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type,
- optionalKeyRequestParameters);
- if (C.CLEARKEY_UUID.equals(uuid)) {
- request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()),
- request.getDefaultUrl());
- }
- postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget();
+ KeyRequest mediaDrmKeyRequest =
+ mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters);
+ currentKeyRequest = Pair.create(mediaDrmKeyRequest, licenseServerUrl);
+ postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry);
} catch (Exception e) {
onKeysError(e);
}
}
- private void onKeyResponse(Object response) {
- if (!isOpen()) {
+ private void onKeyResponse(Object request, Object response) {
+ if (request != currentKeyRequest || !isOpen()) {
// This event is stale.
return;
}
+ currentKeyRequest = null;
if (response instanceof Exception) {
onKeysError((Exception) response);
@@ -382,12 +412,9 @@ import java.util.UUID;
try {
byte[] responseData = (byte[]) response;
- if (C.CLEARKEY_UUID.equals(uuid)) {
- responseData = ClearKeyUtil.adjustResponseData(responseData);
- }
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData);
- eventDispatcher.drmKeysRemoved();
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored);
} else {
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
@@ -396,7 +423,7 @@ import java.util.UUID;
offlineLicenseKeySetId = keySetId;
}
state = STATE_OPENED_WITH_KEYS;
- eventDispatcher.drmKeysLoaded();
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysLoaded);
}
} catch (Exception e) {
onKeysError(e);
@@ -420,7 +447,7 @@ import java.util.UUID;
private void onError(final Exception e) {
lastException = new DrmSessionException(e);
- eventDispatcher.drmSessionManagerError(e);
+ eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(e));
if (state != STATE_OPENED_WITH_KEYS) {
state = STATE_ERROR;
}
@@ -430,30 +457,7 @@ import java.util.UUID;
return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;
}
- @SuppressWarnings("deprecation")
- public void onMediaDrmEvent(int what) {
- if (!isOpen()) {
- return;
- }
- switch (what) {
- case ExoMediaDrm.EVENT_KEY_REQUIRED:
- doLicense(false);
- break;
- case ExoMediaDrm.EVENT_KEY_EXPIRED:
- // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
- // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
- // waiting for key response.
- onKeysExpired();
- break;
- case ExoMediaDrm.EVENT_PROVISION_REQUIRED:
- state = STATE_OPENED;
- provisioningManager.provisionRequired(this);
- break;
- default:
- break;
- }
-
- }
+ // Internal classes.
@SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
@@ -464,12 +468,15 @@ import java.util.UUID;
@Override
public void handleMessage(Message msg) {
+ Pair, ?> requestAndResponse = (Pair, ?>) msg.obj;
+ Object request = requestAndResponse.first;
+ Object response = requestAndResponse.second;
switch (msg.what) {
case MSG_PROVISION:
- onProvisionResponse(msg.obj);
+ onProvisionResponse(request, response);
break;
case MSG_KEYS:
- onKeyResponse(msg.obj);
+ onKeyResponse(request, response);
break;
default:
break;
@@ -486,21 +493,27 @@ import java.util.UUID;
super(backgroundLooper);
}
- Message obtainMessage(int what, Object object, boolean allowRetry) {
- return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */,
- object);
+ void post(int what, Object request, boolean allowRetry) {
+ int allowRetryInt = allowRetry ? 1 : 0;
+ int errorCount = 0;
+ obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget();
}
@Override
+ @SuppressWarnings("unchecked")
public void handleMessage(Message msg) {
+ Object request = msg.obj;
Object response;
try {
switch (msg.what) {
case MSG_PROVISION:
- response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
+ response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request);
break;
case MSG_KEYS:
- response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
+ Pair keyRequest = (Pair) request;
+ KeyRequest mediaDrmKeyRequest = keyRequest.first;
+ String licenseServerUrl = keyRequest.second;
+ response = callback.executeKeyRequest(uuid, mediaDrmKeyRequest, licenseServerUrl);
break;
default:
throw new RuntimeException();
@@ -511,7 +524,7 @@ import java.util.UUID;
}
response = e;
}
- postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
+ postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget();
}
private boolean maybeRetryRequest(Message originalMsg) {
@@ -534,5 +547,4 @@ import java.util.UUID;
}
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
index 7cdee7c537..afec4b6114 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
@@ -15,10 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
-import android.os.Handler;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.util.Assertions;
-import java.util.concurrent.CopyOnWriteArrayList;
/** Listener of {@link DefaultDrmSessionManager} events. */
public interface DefaultDrmSessionEventListener {
@@ -45,97 +42,4 @@ public interface DefaultDrmSessionEventListener {
/** Called each time offline keys are removed. */
void onDrmKeysRemoved();
-
- /** Dispatches drm events to all registered listeners. */
- final class EventDispatcher {
-
- private final CopyOnWriteArrayList listeners;
-
- /** Creates event dispatcher. */
- public EventDispatcher() {
- listeners = new CopyOnWriteArrayList<>();
- }
-
- /** Adds listener to event dispatcher. */
- public void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
- Assertions.checkArgument(handler != null && eventListener != null);
- listeners.add(new HandlerAndListener(handler, eventListener));
- }
-
- /** Removes listener from event dispatcher. */
- public void removeListener(DefaultDrmSessionEventListener eventListener) {
- for (HandlerAndListener handlerAndListener : listeners) {
- if (handlerAndListener.listener == eventListener) {
- listeners.remove(handlerAndListener);
- }
- }
- }
-
- /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysLoaded()}. */
- public void drmKeysLoaded() {
- for (HandlerAndListener handlerAndListener : listeners) {
- final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
- handlerAndListener.handler.post(
- new Runnable() {
- @Override
- public void run() {
- listener.onDrmKeysLoaded();
- }
- });
- }
- }
-
- /** Dispatches {@link DefaultDrmSessionEventListener#onDrmSessionManagerError(Exception)}. */
- public void drmSessionManagerError(final Exception e) {
- for (HandlerAndListener handlerAndListener : listeners) {
- final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
- handlerAndListener.handler.post(
- new Runnable() {
- @Override
- public void run() {
- listener.onDrmSessionManagerError(e);
- }
- });
- }
- }
-
- /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRestored()}. */
- public void drmKeysRestored() {
- for (HandlerAndListener handlerAndListener : listeners) {
- final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
- handlerAndListener.handler.post(
- new Runnable() {
- @Override
- public void run() {
- listener.onDrmKeysRestored();
- }
- });
- }
- }
-
- /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRemoved()}. */
- public void drmKeysRemoved() {
- for (HandlerAndListener handlerAndListener : listeners) {
- final DefaultDrmSessionEventListener listener = handlerAndListener.listener;
- handlerAndListener.handler.post(
- new Runnable() {
- @Override
- public void run() {
- listener.onDrmKeysRemoved();
- }
- });
- }
- }
-
- private static final class HandlerAndListener {
-
- public final Handler handler;
- public final DefaultDrmSessionEventListener listener;
-
- public HandlerAndListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
- this.handler = handler;
- this.listener = eventListener;
- }
- }
- }
}
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 66c9e5cde7..895c27ad93 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
@@ -26,13 +26,12 @@ import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager;
-import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.EventDispatcher;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -89,13 +88,12 @@ public class DefaultDrmSessionManager implements DrmSe
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
private static final String TAG = "DefaultDrmSessionMgr";
- private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid;
private final ExoMediaDrm mediaDrm;
private final MediaDrmCallback callback;
private final HashMap optionalKeyRequestParameters;
- private final EventDispatcher eventDispatcher;
+ private final EventDispatcher eventDispatcher;
private final boolean multiSession;
private final int initialDrmRequestRetryCount;
@@ -356,7 +354,7 @@ public class DefaultDrmSessionManager implements DrmSe
this.mediaDrm = mediaDrm;
this.callback = callback;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
- this.eventDispatcher = new EventDispatcher();
+ this.eventDispatcher = new EventDispatcher<>();
this.multiSession = multiSession;
this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;
mode = MODE_PLAYBACK;
@@ -509,17 +507,14 @@ public class DefaultDrmSessionManager implements DrmSe
}
}
- byte[] initData = null;
- String mimeType = null;
+ SchemeData schemeData = null;
if (offlineLicenseKeySetId == null) {
- SchemeData data = getSchemeData(drmInitData, uuid, false);
- if (data == null) {
+ schemeData = getSchemeData(drmInitData, uuid, false);
+ if (schemeData == null) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
- eventDispatcher.drmSessionManagerError(error);
+ eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error));
return new ErrorStateDrmSession<>(new DrmSessionException(error));
}
- initData = getSchemeInitData(data, uuid);
- mimeType = getSchemeMimeType(data, uuid);
}
DefaultDrmSession session;
@@ -528,6 +523,7 @@ public class DefaultDrmSessionManager implements DrmSe
} else {
// Only use an existing session if it has matching init data.
session = null;
+ byte[] initData = schemeData != null ? schemeData.data : null;
for (DefaultDrmSession existingSession : sessions) {
if (existingSession.hasInitData(initData)) {
session = existingSession;
@@ -543,8 +539,7 @@ public class DefaultDrmSessionManager implements DrmSe
uuid,
mediaDrm,
this,
- initData,
- mimeType,
+ schemeData,
mode,
offlineLicenseKeySetId,
optionalKeyRequestParameters,
@@ -650,31 +645,6 @@ public class DefaultDrmSessionManager implements DrmSe
return matchingSchemeDatas.get(0);
}
- private static byte[] getSchemeInitData(SchemeData data, UUID uuid) {
- byte[] schemeInitData = data.data;
- if (Util.SDK_INT < 21) {
- // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
- byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid);
- if (psshData == null) {
- // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
- } else {
- schemeInitData = psshData;
- }
- }
- return schemeInitData;
- }
-
- private static String getSchemeMimeType(SchemeData data, UUID uuid) {
- String schemeMimeType = data.mimeType;
- if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
- && (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
- || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
- // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
- schemeMimeType = CENC_SCHEME_MIME_TYPE;
- }
- return schemeMimeType;
- }
-
@SuppressLint("HandlerLeak")
private class MediaDrmHandler extends Handler {
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 c2de662010..b9415c74af 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
@@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator, Parcelable {
* applies to all schemes).
*/
private final UUID uuid;
- /**
- * The mimeType of {@link #data}.
- */
+ /** The URL of the server to which license requests should be made. May be null if unknown. */
+ public final @Nullable String licenseServerUrl;
+ /** The mimeType of {@link #data}. */
public final String mimeType;
/**
* The initialization data. May be null for scheme support checks only.
@@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator, Parcelable {
* @param requiresSecureDecryption See {@link #requiresSecureDecryption}.
*/
public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
+ this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption);
+ }
+
+ /**
+ * @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 licenseServerUrl See {@link #licenseServerUrl}.
+ * @param mimeType See {@link #mimeType}.
+ * @param data See {@link #data}.
+ * @param requiresSecureDecryption See {@link #requiresSecureDecryption}.
+ */
+ public SchemeData(
+ UUID uuid,
+ @Nullable String licenseServerUrl,
+ String mimeType,
+ byte[] data,
+ boolean requiresSecureDecryption) {
this.uuid = Assertions.checkNotNull(uuid);
+ this.licenseServerUrl = licenseServerUrl;
this.mimeType = Assertions.checkNotNull(mimeType);
this.data = data;
this.requiresSecureDecryption = requiresSecureDecryption;
@@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator, Parcelable {
/* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong());
+ licenseServerUrl = in.readString();
mimeType = in.readString();
data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0;
@@ -337,6 +356,16 @@ public final class DrmInitData implements Comparator, Parcelable {
return data != null;
}
+ /**
+ * Returns a copy of this instance with the specified data.
+ *
+ * @param data The data to include in the copy.
+ * @return The new instance.
+ */
+ public SchemeData copyWithData(@Nullable byte[] data) {
+ return new SchemeData(uuid, licenseServerUrl, mimeType, data, requiresSecureDecryption);
+ }
+
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SchemeData)) {
@@ -346,7 +375,9 @@ public final class DrmInitData implements Comparator, Parcelable {
return true;
}
SchemeData other = (SchemeData) obj;
- return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid)
+ return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
+ && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data);
}
@@ -354,6 +385,7 @@ public final class DrmInitData implements Comparator, Parcelable {
public int hashCode() {
if (hashCode == 0) {
int result = uuid.hashCode();
+ result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());
result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data);
hashCode = result;
@@ -372,6 +404,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(licenseServerUrl);
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/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
index 2699559c5f..78994a9b80 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -105,101 +105,63 @@ public interface ExoMediaDrm {
boolean hasNewUsableKey);
}
- /**
- * @see android.media.MediaDrm.KeyStatus
- */
- interface KeyStatus {
- /** Returns the status code for the key. */
- int getStatusCode();
- /** Returns the id for the key. */
- byte[] getKeyId();
- }
-
- /**
- * Default implementation of {@link KeyStatus}.
- */
- final class DefaultKeyStatus implements KeyStatus {
+ /** @see android.media.MediaDrm.KeyStatus */
+ final class KeyStatus {
private final int statusCode;
private final byte[] keyId;
- DefaultKeyStatus(int statusCode, byte[] keyId) {
+ public KeyStatus(int statusCode, byte[] keyId) {
this.statusCode = statusCode;
this.keyId = keyId;
}
- @Override
public int getStatusCode() {
return statusCode;
}
- @Override
public byte[] getKeyId() {
return keyId;
}
}
- /**
- * @see android.media.MediaDrm.KeyRequest
- */
- interface KeyRequest {
- byte[] getData();
- String getDefaultUrl();
- }
-
- /**
- * Default implementation of {@link KeyRequest}.
- */
- final class DefaultKeyRequest implements KeyRequest {
+ /** @see android.media.MediaDrm.KeyRequest */
+ final class KeyRequest {
private final byte[] data;
private final String defaultUrl;
- public DefaultKeyRequest(byte[] data, String defaultUrl) {
+ public KeyRequest(byte[] data, String defaultUrl) {
this.data = data;
this.defaultUrl = defaultUrl;
}
- @Override
public byte[] getData() {
return data;
}
- @Override
public String getDefaultUrl() {
return defaultUrl;
}
}
- /**
- * @see android.media.MediaDrm.ProvisionRequest
- */
- interface ProvisionRequest {
- byte[] getData();
- String getDefaultUrl();
- }
-
- /**
- * Default implementation of {@link ProvisionRequest}.
- */
- final class DefaultProvisionRequest implements ProvisionRequest {
+ /** @see android.media.MediaDrm.ProvisionRequest */
+ final class ProvisionRequest {
private final byte[] data;
private final String defaultUrl;
- public DefaultProvisionRequest(byte[] data, String defaultUrl) {
+ public ProvisionRequest(byte[] data, String defaultUrl) {
this.data = data;
this.defaultUrl = defaultUrl;
}
- @Override
public byte[] getData() {
return data;
}
- @Override
public String getDefaultUrl() {
return defaultUrl;
}
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 f960cd637f..8937768ff4 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.DeniedByServerException;
import android.media.MediaCrypto;
@@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.HashMap;
@@ -40,6 +43,8 @@ import java.util.UUID;
@TargetApi(23)
public final class FrameworkMediaDrm implements ExoMediaDrm {
+ private static final String CENC_SCHEME_MIME_TYPE = "cenc";
+
private final UUID uuid;
private final MediaDrm mediaDrm;
@@ -67,6 +72,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm keyInfo, boolean hasNewUsableKey) {
- List exoKeyInfo = new ArrayList<>();
- for (MediaDrm.KeyStatus keyStatus : keyInfo) {
- exoKeyInfo.add(new DefaultKeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId()));
- }
- listener.onKeyStatusChange(FrameworkMediaDrm.this, sessionId, exoKeyInfo,
- hasNewUsableKey);
- }
- }, null);
+
+ mediaDrm.setOnKeyStatusChangeListener(
+ listener == null
+ ? null
+ : new MediaDrm.OnKeyStatusChangeListener() {
+ @Override
+ public void onKeyStatusChange(
+ @NonNull MediaDrm md,
+ @NonNull byte[] sessionId,
+ @NonNull List keyInfo,
+ boolean hasNewUsableKey) {
+ List exoKeyInfo = new ArrayList<>();
+ for (MediaDrm.KeyStatus keyStatus : keyInfo) {
+ exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId()));
+ }
+ listener.onKeyStatusChange(
+ FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey);
+ }
+ },
+ null);
}
@Override
@@ -114,23 +128,63 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException {
+ public KeyRequest getKeyRequest(
+ byte[] scope,
+ byte[] init,
+ String mimeType,
+ int keyType,
+ HashMap optionalParameters)
+ throws NotProvisionedException {
+
+ // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon
+ // devices also required data to be extracted from the PSSH atom for PlayReady.
+ if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid))
+ || (C.PLAYREADY_UUID.equals(uuid)
+ && "Amazon".equals(Util.MANUFACTURER)
+ && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1
+ || "AFTS".equals(Util.MODEL) // Fire TV Gen 2
+ || "AFTM".equals(Util.MODEL)))) { // Fire TV Stick Gen 1
+ byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid);
+ if (psshData == null) {
+ // Extraction failed. schemeData isn't a PSSH atom, so leave it unchanged.
+ } else {
+ init = psshData;
+ }
+ }
+
+ // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
+ if (Util.SDK_INT < 26
+ && C.CLEARKEY_UUID.equals(uuid)
+ && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) {
+ mimeType = CENC_SCHEME_MIME_TYPE;
+ }
+
final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType,
optionalParameters);
- return new DefaultKeyRequest(request.getData(), request.getDefaultUrl());
+
+ byte[] requestData = request.getData();
+ if (C.CLEARKEY_UUID.equals(uuid)) {
+ requestData = ClearKeyUtil.adjustRequestData(requestData);
+ }
+
+ return new KeyRequest(requestData, request.getDefaultUrl());
}
@Override
public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException {
+
+ if (C.CLEARKEY_UUID.equals(uuid)) {
+ response = ClearKeyUtil.adjustResponseData(response);
+ }
+
return mediaDrm.provideKeyResponse(scope, response);
}
@Override
public ProvisionRequest getProvisionRequest() {
final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest();
- return new DefaultProvisionRequest(request.getData(), request.getDefaultUrl());
+ return new ProvisionRequest(request.getData(), request.getDefaultUrl());
}
@Override
@@ -183,4 +237,17 @@ public final class FrameworkMediaDrm implements ExoMediaDrmSee GitHub issue #4413 .
+ */
+ private static boolean needsForceWidevineL3Workaround() {
+ return "ASUS_Z00AD".equals(Util.MODEL);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
index 4a93ac8333..fc1e62a89c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.net.Uri;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
@@ -108,13 +109,19 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
@Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
- String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+ String url =
+ request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData());
return executePost(dataSourceFactory, url, new byte[0], null);
}
@Override
- public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
+ public byte[] executeKeyRequest(
+ UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
+ throws Exception {
String url = request.getDefaultUrl();
+ if (TextUtils.isEmpty(url)) {
+ url = mediaProvidedLicenseServerUrl;
+ }
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java
index 7b9aeca30a..7ed4a61a60 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.util.Assertions;
@@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback {
}
@Override
- public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
+ public byte[] executeKeyRequest(
+ UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
+ throws Exception {
return keyResponse;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java
index 617e168f9a..4405d6e538 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import java.util.UUID;
@@ -38,10 +39,13 @@ public interface MediaDrmCallback {
* Executes a key request.
*
* @param uuid The UUID of the content protection scheme.
- * @param request The request.
+ * @param request The request generated by the content decryption module.
+ * @param mediaProvidedLicenseServerUrl A license server URL provided by the media, or null if the
+ * media does not include any license server URL.
* @return The response data.
* @throws Exception If an error occurred executing the request.
*/
- byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception;
-
+ byte[] executeKeyRequest(
+ UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
+ throws Exception;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java
new file mode 100644
index 0000000000..435fb13648
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+
+/**
+ * A seeker that supports seeking within a stream by searching for the target frame using binary
+ * search.
+ *
+ * This seeker operates on a stream that contains multiple frames (or samples). Each frame is
+ * associated with some kind of timestamps, such as stream time, or frame indices. Given a target
+ * seek time, the seeker will find the corresponding target timestamp, and perform a search
+ * operation within the stream to identify the target frame and return the byte position in the
+ * stream of the target frame.
+ */
+public abstract class BinarySearchSeeker {
+
+ /** A seeker that looks for a given timestamp from an input. */
+ protected interface TimestampSeeker {
+
+ /**
+ * Searches for a given timestamp from the input.
+ *
+ *
Given a target timestamp and an input stream, this seeker will try to read up to a range
+ * of {@code searchRangeBytes} bytes from that input, look for all available timestamps from all
+ * frames in that range, compare those with the target timestamp, and return one of the {@link
+ * TimestampSearchResult}.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param targetTimestamp The target timestamp that we are looking for.
+ * @param outputFrameHolder If {@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is
+ * returned, this holder may be updated to hold the extracted frame that contains the target
+ * frame/sample associated with the target timestamp.
+ * @return A {@link TimestampSearchResult}, that includes a {@link TimestampSearchResult#result}
+ * value, and other necessary info:
+ *
+ * {@link TimestampSearchResult#RESULT_NO_TIMESTAMP} is returned if there is no
+ * timestamp in the reading range.
+ * {@link TimestampSearchResult#RESULT_POSITION_UNDERESTIMATED} is returned if all
+ * timestamps in the range are smaller than the target timestamp.
+ * {@link TimestampSearchResult#RESULT_POSITION_OVERESTIMATED} is returned if all
+ * timestamps in the range are larger than the target timestamp.
+ * {@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is returned if this
+ * seeker can find a timestamp that it deems close enough to the given target.
+ *
+ *
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ TimestampSearchResult searchForTimestamp(
+ ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
+ throws IOException, InterruptedException;
+ }
+
+ /**
+ * Holds a frame extracted from a stream, together with the time stamp of the frame in
+ * microseconds.
+ */
+ public static final class OutputFrameHolder {
+
+ public long timeUs;
+ public ByteBuffer byteBuffer;
+
+ /** Constructs an instance, wrapping the given byte buffer. */
+ public OutputFrameHolder(ByteBuffer outputByteBuffer) {
+ this.timeUs = 0;
+ this.byteBuffer = outputByteBuffer;
+ }
+ }
+
+ /**
+ * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
+ * timestamp for a seek time position.
+ */
+ public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {
+
+ @Override
+ public long timeUsToTargetTime(long timeUs) {
+ return timeUs;
+ }
+ }
+
+ /**
+ * A converter that converts seek time in stream time into target timestamp for the {@link
+ * BinarySearchSeeker}.
+ */
+ protected interface SeekTimestampConverter {
+ /**
+ * Converts a seek time in microseconds into target timestamp for the {@link
+ * BinarySearchSeeker}.
+ */
+ long timeUsToTargetTime(long timeUs);
+ }
+
+ /**
+ * When seeking within the source, if the offset is smaller than or equal to this value, the seek
+ * operation will be performed using a skip operation. Otherwise, the source will be reloaded at
+ * the new seek position.
+ */
+ private static final long MAX_SKIP_BYTES = 256 * 1024;
+
+ protected final BinarySearchSeekMap seekMap;
+ protected final TimestampSeeker timestampSeeker;
+ protected @Nullable SeekOperationParams seekOperationParams;
+
+ private final int minimumSearchRange;
+
+ /**
+ * Constructs an instance.
+ *
+ * @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in
+ * stream time into target timestamp.
+ * @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps
+ * within the stream.
+ * @param durationUs The duration of the stream in microseconds.
+ * @param floorTimePosition The minimum timestamp value (inclusive) in the stream.
+ * @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream.
+ * @param floorBytePosition The starting position of the frame with minimum timestamp value
+ * (inclusive) in the stream.
+ * @param ceilingBytePosition The position after the frame with maximum timestamp value in the
+ * stream.
+ * @param approxBytesPerFrame Approximated bytes per frame.
+ * @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If
+ * the remaining search range is smaller than this value, the search will stop, and the seeker
+ * will return the position at the floor of the range as the result.
+ */
+ @SuppressWarnings("initialization")
+ protected BinarySearchSeeker(
+ SeekTimestampConverter seekTimestampConverter,
+ TimestampSeeker timestampSeeker,
+ long durationUs,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame,
+ int minimumSearchRange) {
+ this.timestampSeeker = timestampSeeker;
+ this.minimumSearchRange = minimumSearchRange;
+ this.seekMap =
+ new BinarySearchSeekMap(
+ seekTimestampConverter,
+ durationUs,
+ floorTimePosition,
+ ceilingTimePosition,
+ floorBytePosition,
+ ceilingBytePosition,
+ approxBytesPerFrame);
+ }
+
+ /** Returns the seek map for the stream. */
+ public final SeekMap getSeekMap() {
+ return seekMap;
+ }
+
+ /**
+ * Sets the target time in microseconds within the stream to seek to.
+ *
+ * @param timeUs The target time in microseconds within the stream.
+ */
+ public final void setSeekTargetUs(long timeUs) {
+ if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) {
+ return;
+ }
+ seekOperationParams = createSeekParamsForTargetTimeUs(timeUs);
+ }
+
+ /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
+ public final boolean isSeeking() {
+ return seekOperationParams != null;
+ }
+
+ /**
+ * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
+ * {@link Extractor}.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @param outputFrameHolder If {@link Extractor#RESULT_CONTINUE} is returned, this holder may be
+ * updated to hold the extracted frame that contains the target sample. The caller needs to
+ * check the byte buffer limit to see if an extracted frame is available.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public int handlePendingSeek(
+ ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder)
+ throws InterruptedException, IOException {
+ TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker);
+ while (true) {
+ SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams);
+ long floorPosition = seekOperationParams.getFloorBytePosition();
+ long ceilingPosition = seekOperationParams.getCeilingBytePosition();
+ long searchPosition = seekOperationParams.getNextSearchBytePosition();
+
+ if (ceilingPosition - floorPosition <= minimumSearchRange) {
+ // The seeking range is too small, so we can just continue from the floor position.
+ markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition);
+ return seekToPosition(input, floorPosition, seekPositionHolder);
+ }
+ if (!skipInputUntilPosition(input, searchPosition)) {
+ return seekToPosition(input, searchPosition, seekPositionHolder);
+ }
+
+ input.resetPeekPosition();
+ TimestampSearchResult timestampSearchResult =
+ timestampSeeker.searchForTimestamp(
+ input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);
+
+ switch (timestampSearchResult.result) {
+ case TimestampSearchResult.RESULT_POSITION_OVERESTIMATED:
+ seekOperationParams.updateSeekCeiling(
+ timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
+ break;
+ case TimestampSearchResult.RESULT_POSITION_UNDERESTIMATED:
+ seekOperationParams.updateSeekFloor(
+ timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
+ break;
+ case TimestampSearchResult.RESULT_TARGET_TIMESTAMP_FOUND:
+ markSeekOperationFinished(
+ /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
+ skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
+ return seekToPosition(
+ input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);
+ case TimestampSearchResult.RESULT_NO_TIMESTAMP:
+ // We can't find any timestamp in the search range from the search position.
+ // Give up, and just continue reading from the last search position in this case.
+ markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);
+ return seekToPosition(input, searchPosition, seekPositionHolder);
+ default:
+ throw new IllegalStateException("Invalid case");
+ }
+ }
+ }
+
+ protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) {
+ return new SeekOperationParams(
+ timeUs,
+ seekMap.timeUsToTargetTime(timeUs),
+ seekMap.floorTimePosition,
+ seekMap.ceilingTimePosition,
+ seekMap.floorBytePosition,
+ seekMap.ceilingBytePosition,
+ seekMap.approxBytesPerFrame);
+ }
+
+ protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
+ seekOperationParams = null;
+ onSeekOperationFinished(foundTargetFrame, resultPosition);
+ }
+
+ protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
+ // Do nothing.
+ }
+
+ protected final boolean skipInputUntilPosition(ExtractorInput input, long position)
+ throws IOException, InterruptedException {
+ long bytesToSkip = position - input.getPosition();
+ if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
+ input.skipFully((int) bytesToSkip);
+ return true;
+ }
+ return false;
+ }
+
+ protected final int seekToPosition(
+ ExtractorInput input, long position, PositionHolder seekPositionHolder) {
+ if (position == input.getPosition()) {
+ return Extractor.RESULT_CONTINUE;
+ } else {
+ seekPositionHolder.position = position;
+ return Extractor.RESULT_SEEK;
+ }
+ }
+
+ /**
+ * Contains parameters for a pending seek operation by {@link BinarySearchSeeker}.
+ *
+ * This class holds parameters for a binary-search for the {@code targetTimePosition} in the
+ * range [floorPosition, ceilingPosition).
+ */
+ protected static class SeekOperationParams {
+ private final long seekTimeUs;
+ private final long targetTimePosition;
+ private final long approxBytesPerFrame;
+
+ private long floorTimePosition;
+ private long ceilingTimePosition;
+ private long floorBytePosition;
+ private long ceilingBytePosition;
+ private long nextSearchBytePosition;
+
+ /**
+ * Returns the next position in the stream to search for target frame, given [floorBytePosition,
+ * ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition).
+ */
+ protected static long calculateNextSearchBytePosition(
+ long targetTimePosition,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame) {
+ if (floorBytePosition + 1 >= ceilingBytePosition
+ || floorTimePosition + 1 >= ceilingTimePosition) {
+ return floorBytePosition;
+ }
+ long seekTimeDuration = targetTimePosition - floorTimePosition;
+ float estimatedBytesPerTimeUnit =
+ (float) (ceilingBytePosition - floorBytePosition)
+ / (ceilingTimePosition - floorTimePosition);
+ // It's better to under-estimate rather than over-estimate, because the extractor
+ // input can skip forward easily, but cannot rewind easily (it may require a new connection
+ // to be made).
+ // Therefore, we should reduce the estimated position by some amount, so it will converge to
+ // the correct frame earlier.
+ long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit);
+ long confidenceInterval = bytesToSkip / 20;
+ long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame;
+ long estimatedPosition = estimatedFramePosition - confidenceInterval;
+ return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1);
+ }
+
+ protected SeekOperationParams(
+ long seekTimeUs,
+ long targetTimePosition,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame) {
+ this.seekTimeUs = seekTimeUs;
+ this.targetTimePosition = targetTimePosition;
+ this.floorTimePosition = floorTimePosition;
+ this.ceilingTimePosition = ceilingTimePosition;
+ this.floorBytePosition = floorBytePosition;
+ this.ceilingBytePosition = ceilingBytePosition;
+ this.approxBytesPerFrame = approxBytesPerFrame;
+ this.nextSearchBytePosition =
+ calculateNextSearchBytePosition(
+ targetTimePosition,
+ floorTimePosition,
+ ceilingTimePosition,
+ floorBytePosition,
+ ceilingBytePosition,
+ approxBytesPerFrame);
+ }
+
+ /**
+ * Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek
+ * operation.
+ */
+ private long getFloorBytePosition() {
+ return floorBytePosition;
+ }
+
+ /**
+ * Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek
+ * operation.
+ */
+ private long getCeilingBytePosition() {
+ return ceilingBytePosition;
+ }
+
+ /** Returns the target timestamp as translated from the seek time. */
+ private long getTargetTimePosition() {
+ return targetTimePosition;
+ }
+
+ /** Returns the target seek time in microseconds. */
+ private long getSeekTimeUs() {
+ return seekTimeUs;
+ }
+
+ /** Updates the floor constraints (inclusive) of the seek operation. */
+ private void updateSeekFloor(long floorTimePosition, long floorBytePosition) {
+ this.floorTimePosition = floorTimePosition;
+ this.floorBytePosition = floorBytePosition;
+ updateNextSearchBytePosition();
+ }
+
+ /** Updates the ceiling constraints (exclusive) of the seek operation. */
+ private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) {
+ this.ceilingTimePosition = ceilingTimePosition;
+ this.ceilingBytePosition = ceilingBytePosition;
+ updateNextSearchBytePosition();
+ }
+
+ /** Returns the next position in the stream to search. */
+ private long getNextSearchBytePosition() {
+ return nextSearchBytePosition;
+ }
+
+ private void updateNextSearchBytePosition() {
+ this.nextSearchBytePosition =
+ calculateNextSearchBytePosition(
+ targetTimePosition,
+ floorTimePosition,
+ ceilingTimePosition,
+ floorBytePosition,
+ ceilingBytePosition,
+ approxBytesPerFrame);
+ }
+ }
+
+ /**
+ * Represents possible search results for {@link
+ * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}.
+ */
+ public static final class TimestampSearchResult {
+
+ public static final int RESULT_TARGET_TIMESTAMP_FOUND = 0;
+ public static final int RESULT_POSITION_OVERESTIMATED = -1;
+ public static final int RESULT_POSITION_UNDERESTIMATED = -2;
+ public static final int RESULT_NO_TIMESTAMP = -3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RESULT_TARGET_TIMESTAMP_FOUND,
+ RESULT_POSITION_OVERESTIMATED,
+ RESULT_POSITION_UNDERESTIMATED,
+ RESULT_NO_TIMESTAMP
+ })
+ @interface SearchResult {}
+
+ public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT =
+ new TimestampSearchResult(RESULT_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET);
+
+ /** @see TimestampSeeker */
+ private final @SearchResult int result;
+
+ /**
+ * When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link
+ * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@code
+ * result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link
+ * SeekOperationParams#floorTimePosition} should be updated with this value.
+ */
+ private final long timestampToUpdate;
+ /**
+ * When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link
+ * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@code
+ * result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link
+ * SeekOperationParams#floorBytePosition} should be updated with this value.
+ */
+ private final long bytePositionToUpdate;
+
+ private TimestampSearchResult(
+ @SearchResult int result, long timestampToUpdate, long bytePositionToUpdate) {
+ this.result = result;
+ this.timestampToUpdate = timestampToUpdate;
+ this.bytePositionToUpdate = bytePositionToUpdate;
+ }
+
+ /**
+ * Returns a result to signal that the current position in the input stream overestimates the
+ * true position of the target frame, and the {@link BinarySearchSeeker} should modify its
+ * {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values.
+ */
+ public static TimestampSearchResult overestimatedResult(
+ long newCeilingTimestamp, long newCeilingBytePosition) {
+ return new TimestampSearchResult(
+ RESULT_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);
+ }
+
+ /**
+ * Returns a result to signal that the current position in the input stream underestimates the
+ * true position of the target frame, and the {@link BinarySearchSeeker} should modify its
+ * {@link SeekOperationParams}'s floor timestamp and byte position using the given values.
+ */
+ public static TimestampSearchResult underestimatedResult(
+ long newFloorTimestamp, long newCeilingBytePosition) {
+ return new TimestampSearchResult(
+ RESULT_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition);
+ }
+
+ /**
+ * Returns a result to signal that the target timestamp has been found at the {@code
+ * resultBytePosition}, and the seek operation can stop.
+ *
+ *
Note that when this value is returned from {@link
+ * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}, the {@link
+ * OutputFrameHolder} may be updated to hold the target frame as an optimization.
+ */
+ public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
+ return new TimestampSearchResult(
+ RESULT_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition);
+ }
+ }
+
+ /**
+ * A {@link SeekMap} implementation that returns the estimated byte location from {@link
+ * SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for
+ * each {@link #getSeekPoints(long)} query.
+ */
+ public static class BinarySearchSeekMap implements SeekMap {
+ private final SeekTimestampConverter seekTimestampConverter;
+ private final long durationUs;
+ private final long floorTimePosition;
+ private final long ceilingTimePosition;
+ private final long floorBytePosition;
+ private final long ceilingBytePosition;
+ private final long approxBytesPerFrame;
+
+ /** Constructs a new instance of this seek map. */
+ public BinarySearchSeekMap(
+ SeekTimestampConverter seekTimestampConverter,
+ long durationUs,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame) {
+ this.seekTimestampConverter = seekTimestampConverter;
+ this.durationUs = durationUs;
+ this.floorTimePosition = floorTimePosition;
+ this.ceilingTimePosition = ceilingTimePosition;
+ this.floorBytePosition = floorBytePosition;
+ this.ceilingBytePosition = ceilingBytePosition;
+ this.approxBytesPerFrame = approxBytesPerFrame;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ long nextSearchPosition =
+ SeekOperationParams.calculateNextSearchBytePosition(
+ /* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs),
+ /* floorTimePosition= */ floorTimePosition,
+ /* ceilingTimePosition= */ ceilingTimePosition,
+ /* floorBytePosition= */ floorBytePosition,
+ /* ceilingBytePosition= */ ceilingBytePosition,
+ /* approxBytesPerFrame= */ approxBytesPerFrame);
+ return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /** @see SeekTimestampConverter#timeUsToTargetTime(long) */
+ public long timeUsToTargetTime(long timeUs) {
+ return seekTimestampConverter.timeUsToTargetTime(timeUs);
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java
new file mode 100644
index 0000000000..abce01b5ef
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of
+ * multiple independent frames of the same size. Seek points are calculated to be at frame
+ * boundaries.
+ */
+public class ConstantBitrateSeekMap implements SeekMap {
+
+ private final long inputLength;
+ private final long firstFrameBytePosition;
+ private final int frameSize;
+ private final long dataSize;
+ private final int bitrate;
+ private final long durationUs;
+
+ /**
+ * Constructs a new instance from a stream.
+ *
+ * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
+ * @param firstFrameBytePosition The byte-position of the first frame in the stream.
+ * @param bitrate The bitrate (which is assumed to be constant in the stream).
+ * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}
+ * if unknown.
+ */
+ public ConstantBitrateSeekMap(
+ long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) {
+ this.inputLength = inputLength;
+ this.firstFrameBytePosition = firstFrameBytePosition;
+ this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize;
+ this.bitrate = bitrate;
+
+ if (inputLength == C.LENGTH_UNSET) {
+ dataSize = C.LENGTH_UNSET;
+ durationUs = C.TIME_UNSET;
+ } else {
+ dataSize = inputLength - firstFrameBytePosition;
+ durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate);
+ }
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return dataSize != C.LENGTH_UNSET;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ if (dataSize == C.LENGTH_UNSET) {
+ return new SeekPoints(new SeekPoint(0, firstFrameBytePosition));
+ }
+ long seekFramePosition = getFramePositionForTimeUs(timeUs);
+ long seekTimeUs = getTimeUsAtPosition(seekFramePosition);
+ SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition);
+ if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) {
+ return new SeekPoints(seekPoint);
+ } else {
+ long secondSeekPosition = seekFramePosition + frameSize;
+ long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition);
+ SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the stream time in microseconds for a given position.
+ *
+ * @param position The stream byte-position.
+ * @return The stream time in microseconds for the given position.
+ */
+ public long getTimeUsAtPosition(long position) {
+ return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate);
+ }
+
+ // Internal methods
+
+ /**
+ * Returns the stream time in microseconds for a given stream position.
+ *
+ * @param position The stream byte-position.
+ * @param firstFrameBytePosition The position of the first frame in the stream.
+ * @param bitrate The bitrate (which is assumed to be constant in the stream).
+ * @return The stream time in microseconds for the given stream position.
+ */
+ private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) {
+ return Math.max(0, position - firstFrameBytePosition)
+ * C.BITS_PER_BYTE
+ * C.MICROS_PER_SECOND
+ / bitrate;
+ }
+
+ private long getFramePositionForTimeUs(long timeUs) {
+ long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE);
+ // Constrain to nearest preceding frame offset.
+ positionOffset = (positionOffset / frameSize) * frameSize;
+ positionOffset =
+ Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize);
+ return firstFrameBytePosition + positionOffset;
+ }
+}
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 425f2b77cd..f2c3d982d2 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
@@ -72,6 +72,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor;
}
+ private boolean constantBitrateSeekingEnabled;
+ private @AdtsExtractor.Flags int adtsFlags;
+ private @AmrExtractor.Flags int amrFlags;
private @MatroskaExtractor.Flags int matroskaFlags;
private @Mp4Extractor.Flags int mp4Flags;
private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
@@ -83,6 +86,45 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
tsMode = TsExtractor.MODE_SINGLE_PMT;
}
+ /**
+ * Convenience method to set whether approximate seeking using constant bitrate assumptions should
+ * be enabled for all extractors that support it. If set to true, the flags required to enable
+ * this functionality will be OR'd with those passed to the setters when creating extractor
+ * instances. If set to false then the flags passed to the setters will be used without
+ * modification.
+ *
+ * @param constantBitrateSeekingEnabled Whether approximate seeking using a constant bitrate
+ * assumption should be enabled for all extractors that support it.
+ */
+ public void setConstantBitrateSeekingEnabled(boolean constantBitrateSeekingEnabled) {
+ this.constantBitrateSeekingEnabled = constantBitrateSeekingEnabled;
+ }
+
+ /**
+ * Sets flags for {@link AdtsExtractor} instances created by the factory.
+ *
+ * @see AdtsExtractor#AdtsExtractor(long, int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setAdtsExtractorFlags(
+ @AdtsExtractor.Flags int flags) {
+ this.adtsFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link AmrExtractor} instances created by the factory.
+ *
+ * @see AmrExtractor#AmrExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setAmrExtractorFlags(@AmrExtractor.Flags int flags) {
+ this.amrFlags = flags;
+ return this;
+ }
+
/**
* Sets flags for {@link MatroskaExtractor} instances created by the factory.
*
@@ -165,15 +207,31 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors[0] = new MatroskaExtractor(matroskaFlags);
extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
extractors[2] = new Mp4Extractor(mp4Flags);
- extractors[3] = new Mp3Extractor(mp3Flags);
- extractors[4] = new AdtsExtractor();
+ extractors[3] =
+ new Mp3Extractor(
+ mp3Flags
+ | (constantBitrateSeekingEnabled
+ ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0));
+ extractors[4] =
+ new AdtsExtractor(
+ /* firstStreamSampleTimestampUs= */ 0,
+ adtsFlags
+ | (constantBitrateSeekingEnabled
+ ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0));
extractors[5] = new Ac3Extractor();
extractors[6] = new TsExtractor(tsMode, tsFlags);
extractors[7] = new FlvExtractor();
extractors[8] = new OggExtractor();
extractors[9] = new PsExtractor();
extractors[10] = new WavExtractor();
- extractors[11] = new AmrExtractor();
+ extractors[11] =
+ new AmrExtractor(
+ amrFlags
+ | (constantBitrateSeekingEnabled
+ ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0));
if (FLAC_EXTRACTOR_CONSTRUCTOR != null) {
try {
extractors[12] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();
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 c023b0de95..9eaf0f7ef7 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -50,9 +51,12 @@ public final class DummyTrackOutput implements TrackOutput {
}
@Override
- public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
- CryptoData cryptoData) {
+ public void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData cryptoData) {
// Do nothing.
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java
index 7a2bc15da9..c63aad541b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.extractor;
+import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Extracts media data from a container format.
@@ -41,6 +44,11 @@ public interface Extractor {
*/
int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
+ /** Result values that can be returned by {@link #read(ExtractorInput, PositionHolder)}. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {RESULT_CONTINUE, RESULT_SEEK, RESULT_END_OF_INPUT})
+ @interface ReadResult {}
+
/**
* Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must
* provide data from the start of the stream.
@@ -63,14 +71,14 @@ public interface Extractor {
void init(ExtractorOutput output);
/**
- * Extracts data read from a provided {@link ExtractorInput}. Must not be called before
- * {@link #init(ExtractorOutput)}.
- *
- * A single call to this method will block until some progress has been made, but will not block
- * for longer than this. Hence each call will consume only a small amount of input data.
- *
- * In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the
- * {@link ExtractorInput} passed to the next read is required to provide data continuing from the
+ * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link
+ * #init(ExtractorOutput)}.
+ *
+ *
A single call to this method will block until some progress has been made, but will not
+ * block for longer than this. Hence each call will consume only a small amount of input data.
+ *
+ *
In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the {@link
+ * ExtractorInput} passed to the next read is required to provide data continuing from the
* position in the stream reached by the returning call. If the extractor requires data to be
* provided from a different position, then that position is set in {@code seekPosition} and
* {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the
@@ -83,6 +91,7 @@ public interface Extractor {
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
+ @ReadResult
int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
index 75d8b4cf2d..54d48350fc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
+import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -39,7 +40,8 @@ public final class GaplessInfoHolder {
}
};
- private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
+ private static final String GAPLESS_DOMAIN = "com.apple.iTunes";
+ private static final String GAPLESS_DESCRIPTION = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
@@ -91,7 +93,15 @@ public final class GaplessInfoHolder {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
- if (setFromComment(commentFrame.description, commentFrame.text)) {
+ if (GAPLESS_DESCRIPTION.equals(commentFrame.description)
+ && setFromComment(commentFrame.text)) {
+ return true;
+ }
+ } else if (entry instanceof InternalFrame) {
+ InternalFrame internalFrame = (InternalFrame) entry;
+ if (GAPLESS_DOMAIN.equals(internalFrame.domain)
+ && GAPLESS_DESCRIPTION.equals(internalFrame.description)
+ && setFromComment(internalFrame.text)) {
return true;
}
}
@@ -103,14 +113,10 @@ public final class GaplessInfoHolder {
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
* or MPEG 4 user data), if valid and non-zero.
*
- * @param name The comment's identifier.
* @param data The comment's payload data.
* @return Whether the holder was populated.
*/
- private boolean setFromComment(String name, String data) {
- if (!GAPLESS_COMMENT_ID.equals(name)) {
- return false;
- }
+ private boolean setFromComment(String data) {
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) {
try {
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 6a8cef6b64..7b832eb400 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
@@ -125,21 +125,23 @@ public interface TrackOutput {
/**
* Called when metadata associated with a sample has been extracted from the stream.
- *
- * The corresponding sample data will have already been passed to the output via calls to
- * {@link #sampleData(ExtractorInput, int, boolean)} or
- * {@link #sampleData(ParsableByteArray, int)}.
+ *
+ *
The corresponding sample data will have already been passed to the output via calls to
+ * {@link #sampleData(ExtractorInput, int, boolean)} or {@link #sampleData(ParsableByteArray,
+ * int)}.
*
* @param timeUs The media timestamp associated with the sample, in microseconds.
* @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}.
* @param size The size of the sample data, in bytes.
- * @param offset The number of bytes that have been passed to
- * {@link #sampleData(ExtractorInput, int, boolean)} or
- * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
- * whose metadata is being passed.
+ * @param offset The number of bytes that have been passed to {@link #sampleData(ExtractorInput,
+ * int, boolean)} or {@link #sampleData(ParsableByteArray, int)} since the last byte belonging
+ * to the sample whose metadata is being passed.
* @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,
- CryptoData encryptionData);
-
+ void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData encryptionData);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
index b58e979c26..b94ea7cb58 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
@@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.extractor.amr;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -29,6 +32,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
/**
@@ -40,14 +45,19 @@ import java.util.Arrays;
public final class AmrExtractor implements Extractor {
/** Factory for {@link AmrExtractor} instances. */
- public static final ExtractorsFactory FACTORY =
- new ExtractorsFactory() {
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()};
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new AmrExtractor()};
- }
- };
+ /** Flags controlling the behavior of the extractor. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})
+ public @interface Flags {}
+ /**
+ * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
+ * otherwise not be possible.
+ */
+ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
@@ -100,23 +110,43 @@ public final class AmrExtractor implements Extractor {
/** Theoretical maximum frame size for a AMR frame. */
private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8];
+ /**
+ * The required number of samples in the stream with same sample size to classify the stream as a
+ * constant-bitrate-stream.
+ */
+ private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20;
private static final int SAMPLE_RATE_WB = 16_000;
private static final int SAMPLE_RATE_NB = 8_000;
private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;
private final byte[] scratch;
+ private final @Flags int flags;
private boolean isWideBand;
private long currentSampleTimeUs;
- private int currentSampleTotalBytes;
+ private int currentSampleSize;
private int currentSampleBytesRemaining;
+ private boolean hasOutputSeekMap;
+ private long firstSamplePosition;
+ private int firstSampleSize;
+ private int numSamplesWithSameSize;
+ private long timeOffsetUs;
+ private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
+ private @Nullable SeekMap seekMap;
private boolean hasOutputFormat;
public AmrExtractor() {
+ this(/* flags= */ 0);
+ }
+
+ /** @param flags Flags that control the extractor's behavior. */
+ public AmrExtractor(@Flags int flags) {
+ this.flags = flags;
scratch = new byte[1];
+ firstSampleSize = C.LENGTH_UNSET;
}
// Extractor implementation.
@@ -127,10 +157,10 @@ public final class AmrExtractor implements Extractor {
}
@Override
- public void init(ExtractorOutput output) {
- output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
- trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
- output.endTracks();
+ public void init(ExtractorOutput extractorOutput) {
+ this.extractorOutput = extractorOutput;
+ trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
+ extractorOutput.endTracks();
}
@Override
@@ -142,14 +172,21 @@ public final class AmrExtractor implements Extractor {
}
}
maybeOutputFormat();
- return readSample(input);
+ int sampleReadResult = readSample(input);
+ maybeOutputSeekMap(input.getLength(), sampleReadResult);
+ return sampleReadResult;
}
@Override
public void seek(long position, long timeUs) {
currentSampleTimeUs = 0;
- currentSampleTotalBytes = 0;
+ currentSampleSize = 0;
currentSampleBytesRemaining = 0;
+ if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) {
+ timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position);
+ } else {
+ timeOffsetUs = 0;
+ }
}
@Override
@@ -228,11 +265,18 @@ public final class AmrExtractor implements Extractor {
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (currentSampleBytesRemaining == 0) {
try {
- currentSampleTotalBytes = readNextSampleSize(extractorInput);
+ currentSampleSize = peekNextSampleSize(extractorInput);
} catch (EOFException e) {
return RESULT_END_OF_INPUT;
}
- currentSampleBytesRemaining = currentSampleTotalBytes;
+ currentSampleBytesRemaining = currentSampleSize;
+ if (firstSampleSize == C.LENGTH_UNSET) {
+ firstSamplePosition = extractorInput.getPosition();
+ firstSampleSize = currentSampleSize;
+ }
+ if (firstSampleSize == currentSampleSize) {
+ numSamplesWithSameSize++;
+ }
}
int bytesAppended =
@@ -247,16 +291,16 @@ public final class AmrExtractor implements Extractor {
}
trackOutput.sampleMetadata(
- currentSampleTimeUs,
+ timeOffsetUs + currentSampleTimeUs,
C.BUFFER_FLAG_KEY_FRAME,
- currentSampleTotalBytes,
+ currentSampleSize,
/* offset= */ 0,
/* encryptionData= */ null);
currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
return RESULT_CONTINUE;
}
- private int readNextSampleSize(ExtractorInput extractorInput)
+ private int peekNextSampleSize(ExtractorInput extractorInput)
throws IOException, InterruptedException {
extractorInput.resetPeekPosition();
extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);
@@ -296,4 +340,39 @@ public final class AmrExtractor implements Extractor {
// For narrow band, type 12-14 are for future use.
return !isWideBand && (frameType < 12 || frameType > 14);
}
+
+ private void maybeOutputSeekMap(long inputLength, int sampleReadResult) {
+ if (hasOutputSeekMap) {
+ return;
+ }
+
+ if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0
+ || inputLength == C.LENGTH_UNSET
+ || (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) {
+ seekMap = new SeekMap.Unseekable(C.TIME_UNSET);
+ extractorOutput.seekMap(seekMap);
+ hasOutputSeekMap = true;
+ } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD
+ || sampleReadResult == RESULT_END_OF_INPUT) {
+ seekMap = getConstantBitrateSeekMap(inputLength);
+ extractorOutput.seekMap(seekMap);
+ hasOutputSeekMap = true;
+ }
+ }
+
+ private SeekMap getConstantBitrateSeekMap(long inputLength) {
+ int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US);
+ return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize);
+ }
+
+ /**
+ * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.
+ *
+ * @param frameSize The size of each frame in the stream.
+ * @param durationUsPerFrame The duration of the given frame in microseconds.
+ * @return The stream bitrate.
+ */
+ private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {
+ return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
index d908f28945..604a520526 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
@@ -34,17 +34,8 @@ import java.lang.annotation.RetentionPolicy;
*/
public final class FlvExtractor implements Extractor {
- /**
- * Factory for {@link FlvExtractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new FlvExtractor()};
- }
-
- };
+ /** Factory for {@link FlvExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()};
/**
* Extractor states.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
index 21cb3775e5..c0494e1ee0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
@@ -24,7 +24,7 @@ import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Stack;
+import java.util.ArrayDeque;
/**
* Default implementation of {@link EbmlReader}.
@@ -46,15 +46,21 @@ import java.util.Stack;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
- private final byte[] scratch = new byte[8];
- private final Stack masterElementsStack = new Stack<>();
- private final VarintReader varintReader = new VarintReader();
+ private final byte[] scratch;
+ private final ArrayDeque masterElementsStack;
+ private final VarintReader varintReader;
private EbmlReaderOutput output;
private @ElementState int elementState;
private int elementId;
private long elementContentSize;
+ public DefaultEbmlReader() {
+ scratch = new byte[8];
+ masterElementsStack = new ArrayDeque<>();
+ varintReader = new VarintReader();
+ }
+
@Override
public void init(EbmlReaderOutput eventHandler) {
this.output = eventHandler;
@@ -100,7 +106,7 @@ import java.util.Stack;
case EbmlReaderOutput.TYPE_MASTER:
long elementContentPosition = input.getPosition();
long elementEndPosition = elementContentPosition + elementContentSize;
- masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
+ masterElementsStack.push(new MasterElement(elementId, elementEndPosition));
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
return true;
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 1049554f7a..355e299325 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
@@ -61,17 +61,8 @@ import java.util.UUID;
*/
public final class MatroskaExtractor implements Extractor {
- /**
- * Factory for {@link MatroskaExtractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new MatroskaExtractor()};
- }
-
- };
+ /** Factory for {@link MatroskaExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()};
/**
* Flags controlling the behavior of the extractor.
@@ -616,10 +607,10 @@ public final class MatroskaExtractor implements Extractor {
currentTrack.number = (int) value;
break;
case ID_FLAG_DEFAULT:
- currentTrack.flagForced = value == 1;
+ currentTrack.flagDefault = value == 1;
break;
case ID_FLAG_FORCED:
- currentTrack.flagDefault = value == 1;
+ currentTrack.flagForced = value == 1;
break;
case ID_TRACK_TYPE:
currentTrack.type = (int) value;
@@ -1560,7 +1551,7 @@ public final class MatroskaExtractor implements Extractor {
if (!foundSyncframe) {
input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH);
input.resetPeekPosition();
- if ((Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == C.INDEX_UNSET)) {
+ if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) {
return;
}
foundSyncframe = true;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
index a3fde6d455..62c9404916 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
@@ -78,8 +78,9 @@ import java.io.IOException;
return false;
}
if (size != 0) {
- input.advancePeekPosition((int) size);
- peekLength += size;
+ int sizeInt = (int) size;
+ input.advancePeekPosition(sizeInt);
+ peekLength += sizeInt;
}
}
return peekLength == headerStart + headerSize;
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 d358c0cae1..bffc43a540 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,78 +16,27 @@
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
-import com.google.android.exoplayer2.extractor.SeekPoint;
-import com.google.android.exoplayer2.util.Util;
/**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
*/
-/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker {
-
- private static final int BITS_PER_BYTE = 8;
-
- private final long firstFramePosition;
- private final int frameSize;
- private final long dataSize;
- private final int bitrate;
- private final long durationUs;
+/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap
+ implements Mp3Extractor.Seeker {
/**
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFramePosition The position of the first frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
*/
- public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
- MpegAudioHeader mpegAudioHeader) {
- this.firstFramePosition = firstFramePosition;
- this.frameSize = mpegAudioHeader.frameSize;
- this.bitrate = mpegAudioHeader.bitrate;
- if (inputLength == C.LENGTH_UNSET) {
- dataSize = C.LENGTH_UNSET;
- durationUs = C.TIME_UNSET;
- } else {
- dataSize = inputLength - firstFramePosition;
- durationUs = getTimeUs(inputLength);
- }
- }
-
- @Override
- public boolean isSeekable() {
- return dataSize != C.LENGTH_UNSET;
- }
-
- @Override
- public SeekPoints getSeekPoints(long timeUs) {
- if (dataSize == C.LENGTH_UNSET) {
- return new SeekPoints(new SeekPoint(0, firstFramePosition));
- }
- long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
- // Constrain to nearest preceding frame offset.
- positionOffset = (positionOffset / frameSize) * frameSize;
- positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize);
- long seekPosition = firstFramePosition + positionOffset;
- long seekTimeUs = getTimeUs(seekPosition);
- SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
- if (seekTimeUs >= timeUs || positionOffset == dataSize - frameSize) {
- return new SeekPoints(seekPoint);
- } else {
- long secondSeekPosition = seekPosition + frameSize;
- long secondSeekTimeUs = getTimeUs(secondSeekPosition);
- SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
- return new SeekPoints(seekPoint, secondSeekPoint);
- }
+ public ConstantBitrateSeeker(
+ long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) {
+ super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize);
}
@Override
public long getTimeUs(long position) {
- return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE)
- / bitrate;
+ return getTimeUsAtPosition(position);
}
-
- @Override
- public long getDurationUs() {
- return durationUs;
- }
-
}
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 bd786191a0..73dd0ec218 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
@@ -43,17 +43,8 @@ import java.lang.annotation.RetentionPolicy;
*/
public final class Mp3Extractor implements Extractor {
- /**
- * Factory for {@link Mp3Extractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new Mp3Extractor()};
- }
-
- };
+ /** Factory for {@link Mp3Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()};
/**
* Flags controlling the behavior of the extractor.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
index 21d861af30..f59214fc37 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
@@ -214,14 +215,14 @@ import java.util.List;
/**
* Returns the child leaf of the given type.
- *
- * If no child exists with the given type then null is returned. If multiple children exist with
- * the given type then the first one to have been added is returned.
+ *
+ *
If no child exists with the given type then null is returned. If multiple children exist
+ * with the given type then the first one to have been added is returned.
*
* @param type The leaf type.
* @return The child leaf of the given type, or null if no such child exists.
*/
- public LeafAtom getLeafAtomOfType(int type) {
+ public @Nullable LeafAtom getLeafAtomOfType(int type) {
int childrenSize = leafChildren.size();
for (int i = 0; i < childrenSize; i++) {
LeafAtom atom = leafChildren.get(i);
@@ -234,14 +235,14 @@ import java.util.List;
/**
* Returns the child container of the given type.
- *
- * If no child exists with the given type then null is returned. If multiple children exist with
- * the given type then the first one to have been added is returned.
+ *
+ *
If no child exists with the given type then null is returned. If multiple children exist
+ * with the given type then the first one to have been added is returned.
*
* @param type The container type.
* @return The child container of the given type, or null if no such child exists.
*/
- public ContainerAtom getContainerAtomOfType(int type) {
+ public @Nullable ContainerAtom getContainerAtomOfType(int type) {
int childrenSize = containerChildren.size();
for (int i = 0; i < childrenSize; i++) {
ContainerAtom atom = containerChildren.get(i);
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 a2b787d6b0..fe79185697 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
@@ -43,6 +43,9 @@ import java.util.List;
*/
/* package */ final class AtomParsers {
+ /** Thrown if an edit list couldn't be applied. */
+ public static final class UnhandledEditListException extends ParserException {}
+
private static final String TAG = "AtomParsers";
private static final int TYPE_vide = Util.getIntegerCodeForString("vide");
@@ -117,10 +120,12 @@ import java.util.List;
* @param stblAtom stbl (sample table) atom to decode.
* @param gaplessInfoHolder Holder to populate with gapless playback information.
* @return Sample table described by the stbl atom.
- * @throws ParserException If the resulting sample sequence does not contain a sync sample.
+ * @throws UnhandledEditListException Thrown if the edit list can't be applied.
+ * @throws ParserException Thrown if the stbl atom can't be parsed.
*/
- public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom,
- GaplessInfoHolder gaplessInfoHolder) throws ParserException {
+ public static TrackSampleTable parseStbl(
+ Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)
+ throws ParserException {
SampleSizeBox sampleSizeBox;
Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
if (stszAtom != null) {
@@ -136,7 +141,13 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) {
return new TrackSampleTable(
- new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET);
+ track,
+ /* offsets= */ new long[0],
+ /* sizes= */ new int[0],
+ /* maximumSize= */ 0,
+ /* timestampsUs= */ new long[0],
+ /* flags= */ new int[0],
+ /* durationUs= */ C.TIME_UNSET);
}
// Entries are byte offsets of chunks.
@@ -315,7 +326,8 @@ import java.util.List;
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
// This implementation does not support applying both gapless metadata and an edit list.
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
@@ -342,7 +354,8 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
}
}
@@ -359,7 +372,8 @@ import java.util.List;
}
durationUs =
Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
// Omit any sample at the end point of an edit for audio tracks.
@@ -409,6 +423,11 @@ import java.util.List;
System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
}
+ if (startIndex < endIndex && (editedFlags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+ // Applying the edit list would require prerolling from a sync sample.
+ Log.w(TAG, "Ignoring edit list: edit does not start with a sync sample.");
+ throw new UnhandledEditListException();
+ }
for (int j = startIndex; j < endIndex; j++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs =
@@ -424,20 +443,8 @@ import java.util.List;
pts += editDuration;
}
long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale);
-
- boolean hasSyncSample = false;
- for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
- hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
- }
- if (!hasSyncSample) {
- // We don't support edit lists where the edited sample sequence doesn't contain a sync sample.
- // Such edit lists are often (although not always) broken, so we ignore it and continue.
- Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
- Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
- }
-
return new TrackSampleTable(
+ track,
editedOffsets,
editedSizes,
editedMaximumSize,
@@ -700,8 +707,18 @@ import java.util.List;
throw new IllegalStateException();
}
- out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
- Format.NO_VALUE, 0, language, Format.NO_VALUE, null, subsampleOffsetUs, initializationData);
+ out.format =
+ Format.createTextSampleFormat(
+ Integer.toString(trackId),
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* selectionFlags= */ 0,
+ language,
+ /* accessibilityChannel= */ Format.NO_VALUE,
+ /* drmInitData= */ null,
+ subsampleOffsetUs,
+ initializationData);
}
private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
index 8336a280a2..536f70048c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
@@ -108,4 +108,7 @@ import com.google.android.exoplayer2.util.Util;
return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
}
+ private FixedSampleSizeRechunker() {
+ // Prevent instantiation.
+ }
}
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 d1134dc3f6..12da11fd6b 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
@@ -50,7 +50,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Stack;
import java.util.UUID;
/**
@@ -58,17 +57,9 @@ import java.util.UUID;
*/
public final class FragmentedMp4Extractor implements Extractor {
- /**
- * Factory for {@link FragmentedMp4Extractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new FragmentedMp4Extractor()};
- }
-
- };
+ /** Factory for {@link FragmentedMp4Extractor} instances. */
+ public static final ExtractorsFactory FACTORY =
+ () -> new Extractor[] {new FragmentedMp4Extractor()};
/**
* Flags controlling the behavior of the extractor.
@@ -86,24 +77,20 @@ public final class FragmentedMp4Extractor implements Extractor {
* This flag does nothing if the stream is not a video stream.
*/
public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
- /**
- * Flag to ignore any tfdt boxes in the stream.
- */
- public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2;
+ /** Flag to ignore any tfdt boxes in the stream. */
+ public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2
/**
* Flag to indicate that the extractor should output an event message metadata track. Any event
* messages in the stream will be delivered as samples to this track.
*/
- public static final int FLAG_ENABLE_EMSG_TRACK = 4;
+ public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4
/**
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
* container.
*/
- private static final int FLAG_SIDELOADED = 8;
- /**
- * Flag to ignore any edit lists in the stream.
- */
- public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16;
+ private static final int FLAG_SIDELOADED = 1 << 3; // 8
+ /** Flag to ignore any edit lists in the stream. */
+ public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16
private static final String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
@@ -141,7 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// Parser state.
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
- private final Stack containerAtoms;
+ private final ArrayDeque containerAtoms;
private final ArrayDeque pendingMetadataSampleInfos;
private final @Nullable TrackOutput additionalEmsgTrackOutput;
@@ -202,8 +189,7 @@ public final class FragmentedMp4Extractor implements Extractor {
@Nullable TimestampAdjuster timestampAdjuster,
@Nullable Track sideloadedTrack,
@Nullable DrmInitData sideloadedDrmInitData) {
- this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,
- Collections.emptyList());
+ this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList());
}
/**
@@ -257,7 +243,7 @@ public final class FragmentedMp4Extractor implements Extractor {
nalPrefix = new ParsableByteArray(5);
nalBuffer = new ParsableByteArray();
extendedTypeScratch = new byte[16];
- containerAtoms = new Stack<>();
+ containerAtoms = new ArrayDeque<>();
pendingMetadataSampleInfos = new ArrayDeque<>();
trackBundles = new SparseArray<>();
durationUs = C.TIME_UNSET;
@@ -390,7 +376,7 @@ public final class FragmentedMp4Extractor implements Extractor {
if (shouldParseContainerAtom(atomType)) {
long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
- containerAtoms.add(new ContainerAtom(atomType, endPosition));
+ containerAtoms.push(new ContainerAtom(atomType, endPosition));
if (atomSize == atomHeaderBytesRead) {
processAtomEnded(endPosition);
} else {
@@ -500,7 +486,7 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i);
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
- trackBundle.init(track, defaultSampleValuesArray.get(track.id));
+ trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
trackBundles.put(track.id, trackBundle);
durationUs = Math.max(durationUs, track.durationUs);
}
@@ -510,11 +496,23 @@ public final class FragmentedMp4Extractor implements Extractor {
Assertions.checkState(trackBundles.size() == trackCount);
for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i);
- trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
+ trackBundles
+ .get(track.id)
+ .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
}
}
}
+ private DefaultSampleValues getDefaultSampleValues(
+ SparseArray defaultSampleValuesArray, int trackId) {
+ if (defaultSampleValuesArray.size() == 1) {
+ // Ignore track id if there is only one track to cope with non-matching track indices.
+ // See https://github.com/google/ExoPlayer/issues/4477.
+ return defaultSampleValuesArray.valueAt(/* index= */ 0);
+ }
+ return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId));
+ }
+
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
parseMoof(moof, trackBundles, flags, extendedTypeScratch);
// If drm init data is sideloaded, we ignore pssh boxes.
@@ -587,10 +585,13 @@ public final class FragmentedMp4Extractor implements Extractor {
// Output the sample metadata.
if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
+ long sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs;
+ if (timestampAdjuster != null) {
+ sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
+ }
for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
emsgTrackOutput.sampleMetadata(
- segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
- C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
+ sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null);
}
} else {
// We need the first sample timestamp in the segment before we can output the metadata.
@@ -643,7 +644,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray,
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
- TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags);
+ TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
if (trackBundle == null) {
return;
}
@@ -729,7 +730,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
TrackFragment out) throws ParserException {
- int vectorSize = encryptionBox.initializationVectorSize;
+ int vectorSize = encryptionBox.perSampleIvSize;
saiz.setPosition(Atom.HEADER_SIZE);
int fullAtom = saiz.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
@@ -794,13 +795,13 @@ public final class FragmentedMp4Extractor implements Extractor {
* @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd
* does not refer to any {@link TrackBundle}.
*/
- private static TrackBundle parseTfhd(ParsableByteArray tfhd,
- SparseArray trackBundles, int flags) {
+ private static TrackBundle parseTfhd(
+ ParsableByteArray tfhd, SparseArray trackBundles) {
tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt();
int atomFlags = Atom.parseFullAtomFlags(fullAtom);
int trackId = tfhd.readInt();
- TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0);
+ TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);
if (trackBundle == null) {
return null;
}
@@ -825,6 +826,17 @@ public final class FragmentedMp4Extractor implements Extractor {
return trackBundle;
}
+ private static @Nullable TrackBundle getTrackBundle(
+ SparseArray trackBundles, int trackId) {
+ if (trackBundles.size() == 1) {
+ // Ignore track id if there is only one track. This is either because we have a side-loaded
+ // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see
+ // https://github.com/google/ExoPlayer/issues/4083).
+ return trackBundles.valueAt(/* index= */ 0);
+ }
+ return trackBundles.get(trackId);
+ }
+
/**
* Parses a tfdt atom (defined in 14496-12).
*
@@ -1186,6 +1198,10 @@ public final class FragmentedMp4Extractor implements Extractor {
Track track = currentTrackBundle.track;
TrackOutput output = currentTrackBundle.output;
int sampleIndex = currentTrackBundle.currentSampleIndex;
+ long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
+ if (timestampAdjuster != null) {
+ sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
+ }
if (track.nalUnitLengthFieldLength != 0) {
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
// they're only 1 or 2 bytes long.
@@ -1226,8 +1242,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte.
nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
nalBuffer.setLimit(unescapedLength);
- CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer,
- cea608TrackOutputs);
+ CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs);
} else {
// Write the payload of the NAL unit.
writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
@@ -1243,21 +1258,14 @@ public final class FragmentedMp4Extractor implements Extractor {
}
}
- long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
- if (timestampAdjuster != null) {
- sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
- }
-
@C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex]
? C.BUFFER_FLAG_KEY_FRAME : 0;
// Encryption data.
TrackOutput.CryptoData cryptoData = null;
- if (fragment.definesEncryptionData) {
+ TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted();
+ if (encryptionBox != null) {
sampleFlags |= C.BUFFER_FLAG_ENCRYPTED;
- TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null
- ? fragment.trackEncryptionBox
- : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
cryptoData = encryptionBox.cryptoData;
}
@@ -1276,10 +1284,17 @@ public final class FragmentedMp4Extractor implements Extractor {
while (!pendingMetadataSampleInfos.isEmpty()) {
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
pendingMetadataSampleBytes -= sampleInfo.size;
+ long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs;
+ if (timestampAdjuster != null) {
+ metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs);
+ }
for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
emsgTrackOutput.sampleMetadata(
- sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
- C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
+ metadataTimeUs,
+ C.BUFFER_FLAG_KEY_FRAME,
+ sampleInfo.size,
+ pendingMetadataSampleBytes,
+ null);
}
}
}
@@ -1454,16 +1469,16 @@ public final class FragmentedMp4Extractor implements Extractor {
* @return The number of written bytes.
*/
public int outputSampleEncryptionData() {
- if (!fragment.definesEncryptionData) {
+ TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
+ if (encryptionBox == null) {
return 0;
}
- TrackEncryptionBox encryptionBox = getEncryptionBox();
ParsableByteArray initializationVectorData;
int vectorSize;
- if (encryptionBox.initializationVectorSize != 0) {
+ if (encryptionBox.perSampleIvSize != 0) {
initializationVectorData = fragment.sampleEncryptionData;
- vectorSize = encryptionBox.initializationVectorSize;
+ vectorSize = encryptionBox.perSampleIvSize;
} else {
// The default initialization vector should be used.
byte[] initVectorData = encryptionBox.defaultInitializationVector;
@@ -1472,7 +1487,7 @@ public final class FragmentedMp4Extractor implements Extractor {
vectorSize = initVectorData.length;
}
- boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable[currentSampleIndex];
+ boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex);
// Write the signal byte, containing the vector size and the subsample encryption flag.
encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));
@@ -1495,25 +1510,27 @@ public final class FragmentedMp4Extractor implements Extractor {
/** Skips the encryption data for the current sample. */
private void skipSampleEncryptionData() {
- if (!fragment.definesEncryptionData) {
+ TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
+ if (encryptionBox == null) {
return;
}
ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData;
- TrackEncryptionBox encryptionBox = getEncryptionBox();
- if (encryptionBox.initializationVectorSize != 0) {
- sampleEncryptionData.skipBytes(encryptionBox.initializationVectorSize);
+ if (encryptionBox.perSampleIvSize != 0) {
+ sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize);
}
- if (fragment.sampleHasSubsampleEncryptionTable[currentSampleIndex]) {
+ if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) {
sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort());
}
}
- private TrackEncryptionBox getEncryptionBox() {
+ private TrackEncryptionBox getEncryptionBoxIfEncrypted() {
int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
- return fragment.trackEncryptionBox != null
- ? fragment.trackEncryptionBox
- : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);
+ TrackEncryptionBox encryptionBox =
+ fragment.trackEncryptionBox != null
+ ? fragment.trackEncryptionBox
+ : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);
+ return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
index fed1694925..ed7c539118 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -15,11 +15,13 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
+import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@@ -68,6 +70,8 @@ import com.google.android.exoplayer2.util.Util;
// Type for items that are intended for internal use by the player.
private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----");
+ private static final int PICTURE_TYPE_FRONT_COVER = 3;
+
// Standard genres.
private static final String[] STANDARD_GENRES = new String[] {
// These are the official ID3v1 genres.
@@ -103,13 +107,13 @@ import com.google.android.exoplayer2.util.Util;
/**
* Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
- * from the current position of the {@link ParsableByteArray}, and the position is advanced by
- * the size of the element. The position is advanced even if the element's type is unrecognized.
+ * from the current position of the {@link ParsableByteArray}, and the position is advanced by the
+ * size of the element. The position is advanced even if the element's type is unrecognized.
*
* @param ilst Holds the data to be parsed.
* @return The parsed element, or null if the element's type was not recognized.
*/
- public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
+ public static @Nullable Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
int position = ilst.getPosition();
int endPosition = position + ilst.readInt();
int type = ilst.readInt();
@@ -181,20 +185,20 @@ import com.google.android.exoplayer2.util.Util;
}
}
- private static TextInformationFrame parseTextAttribute(int type, String id,
- ParsableByteArray data) {
+ private static @Nullable TextInformationFrame parseTextAttribute(
+ int type, String id, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16);
- return new TextInformationFrame(id, null, value);
+ return new TextInformationFrame(id, /* description= */ null, value);
}
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
return null;
}
- private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
+ private static @Nullable CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
@@ -206,22 +210,27 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data,
- boolean isTextInformationFrame, boolean isBoolean) {
+ private static @Nullable Id3Frame parseUint8Attribute(
+ int type,
+ String id,
+ ParsableByteArray data,
+ boolean isTextInformationFrame,
+ boolean isBoolean) {
int value = parseUint8AttributeValue(data);
if (isBoolean) {
value = Math.min(1, value);
}
if (value >= 0) {
- return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value))
+ return isTextInformationFrame
+ ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value))
: new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
}
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
return null;
}
- private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName,
- ParsableByteArray data) {
+ private static @Nullable TextInformationFrame parseIndexAndCountAttribute(
+ int type, String attributeName, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data && atomSize >= 22) {
@@ -233,25 +242,26 @@ import com.google.android.exoplayer2.util.Util;
if (count > 0) {
value += "/" + count;
}
- return new TextInformationFrame(attributeName, null, value);
+ return new TextInformationFrame(attributeName, /* description= */ null, value);
}
}
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
return null;
}
- private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
+ private static @Nullable TextInformationFrame parseStandardGenreAttribute(
+ ParsableByteArray data) {
int genreCode = parseUint8AttributeValue(data);
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null;
if (genreString != null) {
- return new TextInformationFrame("TCON", null, genreString);
+ return new TextInformationFrame("TCON", /* description= */ null, genreString);
}
Log.w(TAG, "Failed to parse standard genre code");
return null;
}
- private static ApicFrame parseCoverArt(ParsableByteArray data) {
+ private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
@@ -265,13 +275,18 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(4); // empty (4)
byte[] pictureData = new byte[atomSize - 16];
data.readBytes(pictureData, 0, pictureData.length);
- return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData);
+ return new ApicFrame(
+ mimeType,
+ /* description= */ null,
+ /* pictureType= */ PICTURE_TYPE_FRONT_COVER,
+ pictureData);
}
Log.w(TAG, "Failed to parse cover art attribute");
return null;
}
- private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
+ private static @Nullable Id3Frame parseInternalAttribute(
+ ParsableByteArray data, int endPosition) {
String domain = null;
String name = null;
int dataAtomPosition = -1;
@@ -293,14 +308,13 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(atomSize - 12);
}
}
- if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) {
- // We're only interested in iTunSMPB.
+ if (domain == null || name == null || dataAtomPosition == -1) {
return null;
}
data.setPosition(dataAtomPosition);
data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(dataAtomSize - 16);
- return new CommentFrame(LANGUAGE_UNDEFINED, name, value);
+ return new InternalFrame(domain, name, value);
}
private static int parseUint8AttributeValue(ParsableByteArray data) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
index 75bd2c16ee..5bb5e214c9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -37,26 +37,17 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
-import java.util.Stack;
/**
* Extracts data from the MP4 container format.
*/
public final class Mp4Extractor implements Extractor, SeekMap {
- /**
- * Factory for {@link Mp4Extractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new Mp4Extractor()};
- }
-
- };
+ /** Factory for {@link Mp4Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()};
/**
* Flags controlling the behavior of the extractor.
@@ -101,7 +92,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private final ParsableByteArray nalLength;
private final ParsableByteArray atomHeader;
- private final Stack containerAtoms;
+ private final ArrayDeque containerAtoms;
@State private int parserState;
private int atomType;
@@ -137,7 +128,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public Mp4Extractor(@Flags int flags) {
this.flags = flags;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
- containerAtoms = new Stack<>();
+ containerAtoms = new ArrayDeque<>();
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
sampleTrackIndex = C.INDEX_UNSET;
@@ -303,7 +294,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (shouldParseContainerAtom(atomType)) {
long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
- containerAtoms.add(new ContainerAtom(atomType, endPosition));
+ containerAtoms.push(new ContainerAtom(atomType, endPosition));
if (atomSize == atomHeaderBytesRead) {
processAtomEnded(endPosition);
} else {
@@ -391,25 +382,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
- for (int i = 0; i < moov.containerChildren.size(); i++) {
- Atom.ContainerAtom atom = moov.containerChildren.get(i);
- if (atom.type != Atom.TYPE_trak) {
- continue;
- }
-
- Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd),
- C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime);
- if (track == null) {
- continue;
- }
-
- Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
- .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
- TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
- if (trackSampleTable.sampleCount == 0) {
- continue;
- }
+ boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
+ ArrayList trackSampleTables;
+ try {
+ trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);
+ } catch (AtomParsers.UnhandledEditListException e) {
+ // Discard gapless info as we aren't able to handle corresponding edits.
+ gaplessInfoHolder = new GaplessInfoHolder();
+ trackSampleTables =
+ getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true);
+ }
+ int trackCount = trackSampleTables.size();
+ for (int i = 0; i < trackCount; i++) {
+ TrackSampleTable trackSampleTable = trackSampleTables.get(i);
+ Track track = trackSampleTable.track;
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,
extractorOutput.track(i, track.type));
// Each sample has up to three bytes of overhead for the start code that replaces its length.
@@ -445,6 +432,39 @@ public final class Mp4Extractor implements Extractor, SeekMap {
extractorOutput.seekMap(this);
}
+ private ArrayList getTrackSampleTables(
+ ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists)
+ throws ParserException {
+ ArrayList trackSampleTables = new ArrayList<>();
+ for (int i = 0; i < moov.containerChildren.size(); i++) {
+ Atom.ContainerAtom atom = moov.containerChildren.get(i);
+ if (atom.type != Atom.TYPE_trak) {
+ continue;
+ }
+ Track track =
+ AtomParsers.parseTrak(
+ atom,
+ moov.getLeafAtomOfType(Atom.TYPE_mvhd),
+ /* duration= */ C.TIME_UNSET,
+ /* drmInitData= */ null,
+ ignoreEditLists,
+ isQuickTime);
+ if (track == null) {
+ continue;
+ }
+ Atom.ContainerAtom stblAtom =
+ atom.getContainerAtomOfType(Atom.TYPE_mdia)
+ .getContainerAtomOfType(Atom.TYPE_minf)
+ .getContainerAtomOfType(Atom.TYPE_stbl);
+ TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
+ if (trackSampleTable.sampleCount == 0) {
+ continue;
+ }
+ trackSampleTables.add(trackSampleTable);
+ }
+ return trackSampleTables;
+ }
+
/**
* Attempts to extract the next sample in the current mdat atom for the specified track.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
index 84513ef4d3..983c23dc3d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -49,28 +49,28 @@ public final class PsshAtomUtil {
* @param data The scheme specific data.
* @return The PSSH atom.
*/
+ @SuppressWarnings("ParameterNotNullable")
public static byte[] buildPsshAtom(
UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {
- boolean buildV1Atom = keyIds != null;
int dataLength = data != null ? data.length : 0;
int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength;
- if (buildV1Atom) {
+ if (keyIds != null) {
psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */;
}
ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength);
psshBox.putInt(psshBoxLength);
psshBox.putInt(Atom.TYPE_pssh);
- psshBox.putInt(buildV1Atom ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */);
+ psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */);
psshBox.putLong(systemId.getMostSignificantBits());
psshBox.putLong(systemId.getLeastSignificantBits());
- if (buildV1Atom) {
+ if (keyIds != null) {
psshBox.putInt(keyIds.length);
for (UUID keyId : keyIds) {
psshBox.putLong(keyId.getMostSignificantBits());
psshBox.putLong(keyId.getLeastSignificantBits());
}
}
- if (dataLength != 0) {
+ if (data != null && data.length != 0) {
psshBox.putInt(data.length);
psshBox.put(data);
} // Else the last 4 bytes are a 0 DataSize.
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 d39aae0c5f..54207f351f 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
@@ -45,33 +45,36 @@ public final class 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 initialization vector size in bytes for the samples in the corresponding sample group. */
+ public final int perSampleIvSize;
/**
- * If {@link #initializationVectorSize} is 0, holds the default initialization vector as defined
- * in the track encryption box or sample group description box. Null otherwise.
+ * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the
+ * track encryption box or sample group description box. Null otherwise.
*/
public final byte[] defaultInitializationVector;
/**
* @param isEncrypted See {@link #isEncrypted}.
* @param schemeType See {@link #schemeType}.
- * @param initializationVectorSize See {@link #initializationVectorSize}.
+ * @param perSampleIvSize See {@link #perSampleIvSize}.
* @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, @Nullable String schemeType,
- int initializationVectorSize, byte[] keyId, int defaultEncryptedBlocks,
- int defaultClearBlocks, @Nullable byte[] defaultInitializationVector) {
- Assertions.checkArgument(initializationVectorSize == 0 ^ defaultInitializationVector == null);
+ public TrackEncryptionBox(
+ boolean isEncrypted,
+ @Nullable String schemeType,
+ int perSampleIvSize,
+ byte[] keyId,
+ int defaultEncryptedBlocks,
+ int defaultClearBlocks,
+ @Nullable byte[] defaultInitializationVector) {
+ Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null);
this.isEncrypted = isEncrypted;
this.schemeType = schemeType;
- this.initializationVectorSize = initializationVectorSize;
+ this.perSampleIvSize = perSampleIvSize;
this.defaultInitializationVector = defaultInitializationVector;
cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId,
defaultEncryptedBlocks, defaultClearBlocks);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
index 5ac673d037..51ec2bf282 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
@@ -190,4 +190,8 @@ import java.io.IOException;
return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
}
+ /** Returns whether the sample at the given index has a subsample encryption table. */
+ public boolean sampleHasSubsampleEncryptionTable(int index) {
+ return definesEncryptionData && sampleHasSubsampleEncryptionTable[index];
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
index 9f77c49664..56851fc1e0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class TrackSampleTable {
- /**
- * Number of samples.
- */
+ /** The track corresponding to this sample table. */
+ public final Track track;
+ /** Number of samples. */
public final int sampleCount;
- /**
- * Sample offsets in bytes.
- */
+ /** Sample offsets in bytes. */
public final long[] offsets;
- /**
- * Sample sizes in bytes.
- */
+ /** Sample sizes in bytes. */
public final int[] sizes;
- /**
- * Maximum sample size in {@link #sizes}.
- */
+ /** Maximum sample size in {@link #sizes}. */
public final int maximumSize;
- /**
- * Sample timestamps in microseconds.
- */
+ /** Sample timestamps in microseconds. */
public final long[] timestampsUs;
- /**
- * Sample flags.
- */
+ /** Sample flags. */
public final int[] flags;
/**
* The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
@@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util;
public final long durationUs;
public TrackSampleTable(
+ Track track,
long[] offsets,
int[] sizes,
int maximumSize,
@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length);
+ this.track = track;
this.offsets = offsets;
this.sizes = sizes;
this.maximumSize = maximumSize;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
index a4d8f97d5b..5e74eab8d4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -31,17 +31,8 @@ import java.io.IOException;
*/
public class OggExtractor implements Extractor {
- /**
- * Factory for {@link OggExtractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new OggExtractor()};
- }
-
- };
+ /** Factory for {@link OggExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()};
private static final int MAX_VERIFICATION_BYTES = 8;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
index 8ed8a4a01d..ce3b9ea6ba 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
@@ -130,6 +130,6 @@ import java.util.List;
} else {
length = 10000 << length;
}
- return frames * length;
+ return (long) frames * length;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java
index 79767a00d8..0235fba272 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java
@@ -357,12 +357,12 @@ import java.util.Arrays;
for (int i = 0; i < lengthMap.length; i++) {
if (isSparse) {
if (bitArray.readBit()) {
- lengthMap[i] = bitArray.readBits(5) + 1;
+ lengthMap[i] = (long) (bitArray.readBits(5) + 1);
} else { // entry unused
lengthMap[i] = 0;
}
} else { // not sparse
- lengthMap[i] = bitArray.readBits(5) + 1;
+ lengthMap[i] = (long) (bitArray.readBits(5) + 1);
}
}
} else {
@@ -392,7 +392,7 @@ import java.util.Arrays;
lookupValuesCount = 0;
}
} else {
- lookupValuesCount = entries * dimensions;
+ lookupValuesCount = (long) entries * dimensions;
}
// discard (no decoding required yet)
bitArray.skipBits((int) (lookupValuesCount * valueBits));
@@ -407,6 +407,10 @@ import java.util.Arrays;
return (long) Math.floor(Math.pow(entries, 1.d / dimension));
}
+ private VorbisUtil() {
+ // Prevent instantiation.
+ }
+
public static final class CodeBook {
public final int dimensions;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
index bc37277c57..cd806cfe05 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
@@ -33,17 +33,8 @@ import java.io.IOException;
*/
public final class Ac3Extractor implements Extractor {
- /**
- * Factory for {@link Ac3Extractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new Ac3Extractor()};
- }
-
- };
+ /** Factory for {@link Ac3Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac3Extractor()};
/**
* The maximum number of bytes to search when sniffing, excluding ID3 information, before giving
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
index a0a748660e..ef7b763306 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -15,7 +15,11 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -23,50 +27,90 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Extracts data from AAC bit streams with ADTS framing.
*/
public final class AdtsExtractor implements Extractor {
+ /** Factory for {@link AdtsExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()};
+
+ /** Flags controlling the behavior of the extractor. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})
+ public @interface Flags {}
/**
- * Factory for {@link AdtsExtractor} instances.
+ * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
+ * otherwise not be possible.
+ *
+ *
Note that this approach may result in approximated stream duration and seek position that
+ * are not precise, especially when the stream bitrate varies a lot.
*/
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new AdtsExtractor()};
- }
-
- };
-
- private static final int MAX_PACKET_SIZE = 200;
+ private static final int MAX_PACKET_SIZE = 2 * 1024;
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
/**
* The maximum number of bytes to search when sniffing, excluding the header, before giving up.
* Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes.
*/
private static final int MAX_SNIFF_BYTES = 8 * 1024;
+ /**
+ * The maximum number of frames to use when calculating the average frame size for constant
+ * bitrate seeking.
+ */
+ private static final int NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE = 1000;
+
+ private final @Flags int flags;
- private final long firstSampleTimestampUs;
private final AdtsReader reader;
private final ParsableByteArray packetBuffer;
+ private final ParsableByteArray scratch;
+ private final ParsableBitArray scratchBits;
+ private final long firstStreamSampleTimestampUs;
+ private @Nullable ExtractorOutput extractorOutput;
+
+ private long firstSampleTimestampUs;
+ private long firstFramePosition;
+ private int averageFrameSize;
+ private boolean hasCalculatedAverageFrameSize;
private boolean startedPacket;
+ private boolean hasOutputSeekMap;
public AdtsExtractor() {
this(0);
}
- public AdtsExtractor(long firstSampleTimestampUs) {
- this.firstSampleTimestampUs = firstSampleTimestampUs;
+ public AdtsExtractor(long firstStreamSampleTimestampUs) {
+ this(/* firstStreamSampleTimestampUs= */ firstStreamSampleTimestampUs, /* flags= */ 0);
+ }
+
+ /**
+ * @param firstStreamSampleTimestampUs The timestamp to be used for the first sample of the stream
+ * output from this extractor.
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public AdtsExtractor(long firstStreamSampleTimestampUs, @Flags int flags) {
+ this.firstStreamSampleTimestampUs = firstStreamSampleTimestampUs;
+ this.firstSampleTimestampUs = firstStreamSampleTimestampUs;
+ this.flags = flags;
reader = new AdtsReader(true);
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
+ averageFrameSize = C.LENGTH_UNSET;
+ firstFramePosition = C.POSITION_UNSET;
+ scratch = new ParsableByteArray(10);
+ scratchBits = new ParsableBitArray(scratch.data);
}
// Extractor implementation.
@@ -74,41 +118,26 @@ public final class AdtsExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
// Skip any ID3 headers.
- ParsableByteArray scratch = new ParsableByteArray(10);
- ParsableBitArray scratchBits = new ParsableBitArray(scratch.data);
- int startPosition = 0;
- while (true) {
- input.peekFully(scratch.data, 0, 10);
- scratch.setPosition(0);
- if (scratch.readUnsignedInt24() != ID3_TAG) {
- break;
- }
- scratch.skipBytes(3);
- int length = scratch.readSynchSafeInt();
- startPosition += 10 + length;
- input.advancePeekPosition(length);
- }
- input.resetPeekPosition();
- input.advancePeekPosition(startPosition);
+ int startPosition = peekId3Header(input);
// Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size.
int headerPosition = startPosition;
- int validFramesSize = 0;
+ int totalValidFramesSize = 0;
int validFramesCount = 0;
while (true) {
input.peekFully(scratch.data, 0, 2);
scratch.setPosition(0);
int syncBytes = scratch.readUnsignedShort();
- if ((syncBytes & 0xFFF6) != 0xFFF0) {
+ if (!AdtsReader.isAdtsSyncWord(syncBytes)) {
validFramesCount = 0;
- validFramesSize = 0;
+ totalValidFramesSize = 0;
input.resetPeekPosition();
if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
return false;
}
input.advancePeekPosition(headerPosition);
} else {
- if (++validFramesCount >= 4 && validFramesSize > 188) {
+ if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) {
return true;
}
@@ -121,22 +150,23 @@ public final class AdtsExtractor implements Extractor {
return false;
}
input.advancePeekPosition(frameSize - 6);
- validFramesSize += frameSize;
+ totalValidFramesSize += frameSize;
}
}
}
@Override
public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
reader.createTracks(output, new TrackIdGenerator(0, 1));
output.endTracks();
- output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
}
@Override
public void seek(long position, long timeUs) {
startedPacket = false;
reader.seek();
+ firstSampleTimestampUs = firstStreamSampleTimestampUs + timeUs;
}
@Override
@@ -147,8 +177,17 @@ public final class AdtsExtractor implements Extractor {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ boolean canUseConstantBitrateSeeking =
+ (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET;
+ if (canUseConstantBitrateSeeking) {
+ calculateAverageFrameSize(input);
+ }
+
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
- if (bytesRead == C.RESULT_END_OF_INPUT) {
+ boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT;
+ maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream);
+ if (readEndOfStream) {
return RESULT_END_OF_INPUT;
}
@@ -167,4 +206,117 @@ public final class AdtsExtractor implements Extractor {
return RESULT_CONTINUE;
}
+ private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException {
+ int firstFramePosition = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 10);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != ID3_TAG) {
+ break;
+ }
+ scratch.skipBytes(3);
+ int length = scratch.readSynchSafeInt();
+ firstFramePosition += 10 + length;
+ input.advancePeekPosition(length);
+ }
+ input.resetPeekPosition();
+ input.advancePeekPosition(firstFramePosition);
+ if (this.firstFramePosition == C.POSITION_UNSET) {
+ this.firstFramePosition = firstFramePosition;
+ }
+ return firstFramePosition;
+ }
+
+ private void maybeOutputSeekMap(
+ long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) {
+ if (hasOutputSeekMap) {
+ return;
+ }
+ boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0;
+ if (useConstantBitrateSeeking
+ && reader.getSampleDurationUs() == C.TIME_UNSET
+ && !readEndOfStream) {
+ // Wait for the sampleDurationUs to be available, or for the end of the stream to be reached,
+ // before creating seek map.
+ return;
+ }
+
+ ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput);
+ if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) {
+ extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength));
+ } else {
+ extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+ hasOutputSeekMap = true;
+ }
+
+ private void calculateAverageFrameSize(ExtractorInput input)
+ throws IOException, InterruptedException {
+ if (hasCalculatedAverageFrameSize) {
+ return;
+ }
+ averageFrameSize = C.LENGTH_UNSET;
+ input.resetPeekPosition();
+ if (input.getPosition() == 0) {
+ // Skip any ID3 headers.
+ peekId3Header(input);
+ }
+
+ int numValidFrames = 0;
+ long totalValidFramesSize = 0;
+ while (input.peekFully(
+ scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) {
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (!AdtsReader.isAdtsSyncWord(syncBytes)) {
+ // Invalid sync byte pattern.
+ // Constant bit-rate seeking will probably fail for this stream.
+ numValidFrames = 0;
+ break;
+ } else {
+ // Read the frame size.
+ if (!input.peekFully(
+ scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) {
+ break;
+ }
+ scratchBits.setPosition(14);
+ int currentFrameSize = scratchBits.readBits(13);
+ // Either the stream is malformed OR we're not parsing an ADTS stream.
+ if (currentFrameSize <= 6) {
+ hasCalculatedAverageFrameSize = true;
+ throw new ParserException("Malformed ADTS stream");
+ }
+ totalValidFramesSize += currentFrameSize;
+ if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) {
+ break;
+ }
+ if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) {
+ break;
+ }
+ }
+ }
+ input.resetPeekPosition();
+ if (numValidFrames > 0) {
+ averageFrameSize = (int) (totalValidFramesSize / numValidFrames);
+ } else {
+ averageFrameSize = C.LENGTH_UNSET;
+ }
+ hasCalculatedAverageFrameSize = true;
+ }
+
+ private SeekMap getConstantBitrateSeekMap(long inputLength) {
+ int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs());
+ return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize);
+ }
+
+ /**
+ * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.
+ *
+ * @param frameSize The size of each frame in the stream.
+ * @param durationUsPerFrame The duration of the given frame in microseconds.
+ * @return The stream bitrate.
+ */
+ private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {
+ return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
index 96b964a4c4..7f6a22b58b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -39,9 +39,10 @@ public final class AdtsReader implements ElementaryStreamReader {
private static final String TAG = "AdtsReader";
private static final int STATE_FINDING_SAMPLE = 0;
- private static final int STATE_READING_ID3_HEADER = 1;
- private static final int STATE_READING_ADTS_HEADER = 2;
- private static final int STATE_READING_SAMPLE = 3;
+ private static final int STATE_CHECKING_ADTS_HEADER = 1;
+ private static final int STATE_READING_ID3_HEADER = 2;
+ private static final int STATE_READING_ADTS_HEADER = 3;
+ private static final int STATE_READING_SAMPLE = 4;
private static final int HEADER_SIZE = 5;
private static final int CRC_SIZE = 2;
@@ -56,6 +57,7 @@ public final class AdtsReader implements ElementaryStreamReader {
private static final int ID3_HEADER_SIZE = 10;
private static final int ID3_SIZE_OFFSET = 6;
private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
+ private static final int VERSION_UNSET = -1;
private final boolean exposeId3;
private final ParsableBitArray adtsScratch;
@@ -72,6 +74,14 @@ public final class AdtsReader implements ElementaryStreamReader {
private int matchState;
private boolean hasCrc;
+ private boolean foundFirstFrame;
+
+ // Used to verifies sync words
+ private int firstFrameVersion;
+ private int firstFrameSampleRateIndex;
+
+ private int currentFrameVersion;
+ private int currentFrameSampleRateIndex;
// Used when parsing the header.
private boolean hasOutputFormat;
@@ -99,13 +109,21 @@ public final class AdtsReader implements ElementaryStreamReader {
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
setFindingSampleState();
+ firstFrameVersion = VERSION_UNSET;
+ firstFrameSampleRateIndex = C.INDEX_UNSET;
+ sampleDurationUs = C.TIME_UNSET;
this.exposeId3 = exposeId3;
this.language = language;
}
+ /** Returns whether an integer matches an ADTS SYNC word. */
+ public static boolean isAdtsSyncWord(int candidateSyncWord) {
+ return (candidateSyncWord & 0xFFF6) == 0xFFF0;
+ }
+
@Override
public void seek() {
- setFindingSampleState();
+ resetSync();
}
@Override
@@ -140,6 +158,9 @@ public final class AdtsReader implements ElementaryStreamReader {
parseId3Header();
}
break;
+ case STATE_CHECKING_ADTS_HEADER:
+ checkAdtsHeader(data);
+ break;
case STATE_READING_ADTS_HEADER:
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.data, targetLength)) {
@@ -158,6 +179,19 @@ public final class AdtsReader implements ElementaryStreamReader {
// Do nothing.
}
+ /**
+ * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration
+ * is not available.
+ */
+ public long getSampleDurationUs() {
+ return sampleDurationUs;
+ }
+
+ private void resetSync() {
+ foundFirstFrame = false;
+ setFindingSampleState();
+ }
+
/**
* Continues a read from the provided {@code source} into a given {@code target}. It's assumed
* that the data should be written into {@code target} starting from an offset of zero.
@@ -219,6 +253,12 @@ public final class AdtsReader implements ElementaryStreamReader {
bytesRead = 0;
}
+ /** Sets the state to STATE_CHECKING_ADTS_HEADER. */
+ private void setCheckingAdtsHeaderState() {
+ state = STATE_CHECKING_ADTS_HEADER;
+ bytesRead = 0;
+ }
+
/**
* Locates the next sample start, advancing the position to the byte that immediately follows
* identifier. If a sample was not located, the position is advanced to the limit.
@@ -231,12 +271,21 @@ public final class AdtsReader implements ElementaryStreamReader {
int endOffset = pesBuffer.limit();
while (position < endOffset) {
int data = adtsData[position++] & 0xFF;
- if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) {
- hasCrc = (data & 0x1) == 0;
- setReadingAdtsHeaderState();
- pesBuffer.setPosition(position);
- return;
+ if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) {
+ if (foundFirstFrame
+ || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) {
+ currentFrameVersion = (data & 0x8) >> 3;
+ hasCrc = (data & 0x1) == 0;
+ if (!foundFirstFrame) {
+ setCheckingAdtsHeaderState();
+ } else {
+ setReadingAdtsHeaderState();
+ }
+ pesBuffer.setPosition(position);
+ return;
+ }
}
+
switch (matchState | data) {
case MATCH_STATE_START | 0xFF:
matchState = MATCH_STATE_FF;
@@ -264,6 +313,117 @@ public final class AdtsReader implements ElementaryStreamReader {
pesBuffer.setPosition(position);
}
+ /**
+ * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid,
+ * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link
+ * #STATE_FINDING_SAMPLE}.
+ */
+ private void checkAdtsHeader(ParsableByteArray buffer) {
+ if (buffer.bytesLeft() == 0) {
+ // Not enough data to check yet, defer this check.
+ return;
+ }
+ // Peek the next byte of buffer into scratch array.
+ adtsScratch.data[0] = buffer.data[buffer.getPosition()];
+
+ adtsScratch.setPosition(2);
+ currentFrameSampleRateIndex = adtsScratch.readBits(4);
+ if (firstFrameSampleRateIndex != C.INDEX_UNSET
+ && currentFrameSampleRateIndex != firstFrameSampleRateIndex) {
+ // Invalid header.
+ resetSync();
+ return;
+ }
+
+ if (!foundFirstFrame) {
+ foundFirstFrame = true;
+ firstFrameVersion = currentFrameVersion;
+ firstFrameSampleRateIndex = currentFrameSampleRateIndex;
+ }
+ setReadingAdtsHeaderState();
+ }
+
+ /**
+ * Returns whether the given syncPositionCandidate is a real SYNC word.
+ *
+ *
SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is
+ * really a SYNC word. This includes:
+ *
+ *
+ * Checking if MPEG version of this frame matches the first detected version.
+ * Checking if the sample rate index of this frame matches the first detected sample rate
+ * index.
+ * Checking if the bytes immediately after the current package also match a SYNC-word.
+ *
+ *
+ * If the buffer runs out of data for any check, optimistically skip that check, because
+ * AdtsReader consumes each buffer as a whole. We will still run a header validity check later.
+ */
+ private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) {
+ // The SYNC word contains 2 bytes, and the first byte may be in the previously consumed buffer.
+ // Hence the second byte of the SYNC word may be byte 0 of this buffer, and
+ // syncPositionCandidate (which indicates position of the first byte of the SYNC word) may be
+ // -1.
+ // Since the first byte of the SYNC word is always FF, which does not contain any informational
+ // bits, we set the byte position to be the second byte in the SYNC word to ensure it's always
+ // within this buffer.
+ pesBuffer.setPosition(syncPositionCandidate + 1);
+ if (!tryRead(pesBuffer, adtsScratch.data, 1)) {
+ return false;
+ }
+
+ adtsScratch.setPosition(4);
+ int currentFrameVersion = adtsScratch.readBits(1);
+ if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) {
+ return false;
+ }
+
+ if (firstFrameSampleRateIndex != C.INDEX_UNSET) {
+ if (!tryRead(pesBuffer, adtsScratch.data, 1)) {
+ return true;
+ }
+ adtsScratch.setPosition(2);
+ int currentFrameSampleRateIndex = adtsScratch.readBits(4);
+ if (currentFrameSampleRateIndex != firstFrameSampleRateIndex) {
+ return false;
+ }
+ pesBuffer.setPosition(syncPositionCandidate + 2);
+ }
+
+ // Optionally check the byte after this frame matches SYNC word.
+
+ if (!tryRead(pesBuffer, adtsScratch.data, 4)) {
+ return true;
+ }
+ adtsScratch.setPosition(14);
+ int frameSize = adtsScratch.readBits(13);
+ if (frameSize <= 6) {
+ // Not a frame.
+ return false;
+ }
+ int nextSyncPosition = syncPositionCandidate + frameSize;
+ if (nextSyncPosition + 1 >= pesBuffer.limit()) {
+ return true;
+ }
+ return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1])
+ && (firstFrameVersion == VERSION_UNSET
+ || ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion));
+ }
+
+ private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) {
+ int syncWord = (firstByte & 0xFF) << 8 | (secondByte & 0xFF);
+ return isAdtsSyncWord(syncWord);
+ }
+
+ /** Reads {@code targetLength} bytes into target, and returns whether the read succeeded. */
+ private boolean tryRead(ParsableByteArray source, byte[] target, int targetLength) {
+ if (source.bytesLeft() < targetLength) {
+ return false;
+ }
+ source.readBytes(target, /* offset= */ 0, targetLength);
+ return true;
+ }
+
/**
* Parses the Id3 header.
*/
@@ -296,12 +456,12 @@ public final class AdtsReader implements ElementaryStreamReader {
audioObjectType = 2;
}
- int sampleRateIndex = adtsScratch.readBits(4);
- adtsScratch.skipBits(1);
+ adtsScratch.skipBits(5);
int channelConfig = adtsScratch.readBits(3);
- byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig(
- audioObjectType, sampleRateIndex, channelConfig);
+ byte[] audioSpecificConfig =
+ CodecSpecificDataUtil.buildAacAudioSpecificConfig(
+ audioObjectType, firstFrameSampleRateIndex, channelConfig);
Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
audioSpecificConfig);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
index 2d16b46895..085e3443c1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -19,6 +19,7 @@ import android.support.annotation.IntDef;
import android.util.SparseArray;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import com.google.android.exoplayer2.text.cea.Cea708InitializationData;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.lang.annotation.Retention;
@@ -61,7 +62,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
* readers.
*/
public DefaultTsPayloadReaderFactory(@Flags int flags) {
- this(flags, Collections.emptyList());
+ this(
+ flags,
+ Collections.singletonList(
+ Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));
}
/**
@@ -76,10 +80,6 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
*/
public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) {
this.flags = flags;
- if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) {
- closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null,
- MimeTypes.APPLICATION_CEA608, 0, null));
- }
this.closedCaptionFormats = closedCaptionFormats;
}
@@ -107,7 +107,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
return new PesReader(new DtsReader(esInfo.language));
case TsExtractor.TS_STREAM_TYPE_H262:
- return new PesReader(new H262Reader());
+ return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
case TsExtractor.TS_STREAM_TYPE_H264:
return isSet(FLAG_IGNORE_H264_STREAM) ? null
: new PesReader(new H264Reader(buildSeiReader(esInfo),
@@ -137,8 +137,34 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
* @return A {@link SeiReader} for closed caption tracks.
*/
private SeiReader buildSeiReader(EsInfo esInfo) {
+ return new SeiReader(getClosedCaptionFormats(esInfo));
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for
+ * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
+ * {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the
+ * descriptor is not present.
+ *
+ * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
+ * @return A {@link UserDataReader} for closed caption tracks.
+ */
+ private UserDataReader buildUserDataReader(EsInfo esInfo) {
+ return new UserDataReader(getClosedCaptionFormats(esInfo));
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List} of {@link
+ * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link
+ * List} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is
+ * not present.
+ *
+ * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
+ * @return A {@link List} containing list of closed caption formats.
+ */
+ private List getClosedCaptionFormats(EsInfo esInfo) {
if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) {
- return new SeiReader(closedCaptionFormats);
+ return closedCaptionFormats;
}
ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes);
List closedCaptionFormats = this.closedCaptionFormats;
@@ -163,21 +189,42 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
mimeType = MimeTypes.APPLICATION_CEA608;
accessibilityChannel = 1;
}
- closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null,
- Format.NO_VALUE, 0, language, accessibilityChannel, null));
- // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14).
- scratchDescriptorData.skipBytes(2);
+
+ // easy_reader(1), wide_aspect_ratio(1), reserved(6).
+ byte flags = (byte) scratchDescriptorData.readUnsignedByte();
+ // Skip reserved (8).
+ scratchDescriptorData.skipBytes(1);
+
+ List initializationData = null;
+ // The wide_aspect_ratio flag only has meaning for CEA-708.
+ if (isDigital) {
+ boolean isWideAspectRatio = (flags & 0x40) != 0;
+ initializationData = Cea708InitializationData.buildData(isWideAspectRatio);
+ }
+
+ closedCaptionFormats.add(
+ Format.createTextSampleFormat(
+ /* id= */ null,
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* selectionFlags= */ 0,
+ language,
+ accessibilityChannel,
+ /* drmInitData= */ null,
+ Format.OFFSET_SAMPLE_RELATIVE,
+ initializationData));
}
} else {
// Unknown descriptor. Ignore.
}
scratchDescriptorData.setPosition(nextDescriptorPosition);
}
- return new SeiReader(closedCaptionFormats);
+
+ return closedCaptionFormats;
}
private boolean isSet(@Flags int flag) {
return (flags & flag) != 0;
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
index a3502a3242..e9827893ee 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -36,6 +36,7 @@ public final class H262Reader implements ElementaryStreamReader {
private static final int START_SEQUENCE_HEADER = 0xB3;
private static final int START_EXTENSION = 0xB5;
private static final int START_GROUP = 0xB8;
+ private static final int START_USER_DATA = 0xB2;
private String formatId;
private TrackOutput output;
@@ -48,9 +49,13 @@ public final class H262Reader implements ElementaryStreamReader {
private boolean hasOutputFormat;
private long frameDurationUs;
+ private final UserDataReader userDataReader;
+ private final ParsableByteArray userDataParsable;
+
// State that should be reset on seek.
private final boolean[] prefixFlags;
private final CsdBuffer csdBuffer;
+ private final NalUnitTargetBuffer userData;
private long totalBytesWritten;
private boolean startedFirstSample;
@@ -64,14 +69,29 @@ public final class H262Reader implements ElementaryStreamReader {
private boolean sampleHasPicture;
public H262Reader() {
+ this(null);
+ }
+
+ public H262Reader(UserDataReader userDataReader) {
+ this.userDataReader = userDataReader;
prefixFlags = new boolean[4];
csdBuffer = new CsdBuffer(128);
+ if (userDataReader != null) {
+ userData = new NalUnitTargetBuffer(START_USER_DATA, 128);
+ userDataParsable = new ParsableByteArray();
+ } else {
+ userData = null;
+ userDataParsable = null;
+ }
}
@Override
public void seek() {
NalUnitUtil.clearPrefixFlags(prefixFlags);
csdBuffer.reset();
+ if (userDataReader != null) {
+ userData.reset();
+ }
totalBytesWritten = 0;
startedFirstSample = false;
}
@@ -81,6 +101,9 @@ public final class H262Reader implements ElementaryStreamReader {
idGenerator.generateNewId();
formatId = idGenerator.getFormatId();
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ if (userDataReader != null) {
+ userDataReader.createTracks(extractorOutput, idGenerator);
+ }
}
@Override
@@ -106,16 +129,19 @@ public final class H262Reader implements ElementaryStreamReader {
if (!hasOutputFormat) {
csdBuffer.onData(dataArray, offset, limit);
}
+ if (userDataReader != null) {
+ userData.appendToNalUnit(dataArray, offset, limit);
+ }
return;
}
// We've found a start code with the following value.
int startCodeValue = data.data[startCodeOffset + 3] & 0xFF;
+ // This is the number of bytes from the current offset to the start of the next start
+ // code. It may be negative if the start code started in the previously consumed data.
+ int lengthToStartCode = startCodeOffset - offset;
if (!hasOutputFormat) {
- // This is the number of bytes from the current offset to the start of the next start
- // code. It may be negative if the start code started in the previously consumed data.
- int lengthToStartCode = startCodeOffset - offset;
if (lengthToStartCode > 0) {
csdBuffer.onData(dataArray, offset, startCodeOffset);
}
@@ -130,7 +156,24 @@ public final class H262Reader implements ElementaryStreamReader {
hasOutputFormat = true;
}
}
+ if (userDataReader != null) {
+ int bytesAlreadyPassed = 0;
+ if (lengthToStartCode > 0) {
+ userData.appendToNalUnit(dataArray, offset, startCodeOffset);
+ } else {
+ bytesAlreadyPassed = -lengthToStartCode;
+ }
+ if (userData.endNalUnit(bytesAlreadyPassed)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength);
+ userDataParsable.reset(userData.nalData, unescapedLength);
+ userDataReader.consume(sampleTimeUs, userDataParsable);
+ }
+
+ if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) {
+ userData.startNalUnit(startCodeValue);
+ }
+ }
if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) {
int bytesWrittenPastStartCode = limit - startCodeOffset;
if (startedFirstSample && sampleHasPicture && hasOutputFormat) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
index 3cde946ce3..45e094f69d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
@@ -180,9 +181,23 @@ public final class H264Reader implements ElementaryStreamReader {
initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength));
NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
- output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null,
- Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE,
- initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null));
+ output.format(
+ Format.createVideoSampleFormat(
+ formatId,
+ MimeTypes.VIDEO_H264,
+ CodecSpecificDataUtil.buildAvcCodecString(
+ spsData.profileIdc,
+ spsData.constraintsFlagsAndReservedZero2Bits,
+ spsData.levelIdc),
+ /* bitrate= */ Format.NO_VALUE,
+ /* maxInputSize= */ Format.NO_VALUE,
+ spsData.width,
+ spsData.height,
+ /* frameRate= */ Format.NO_VALUE,
+ initializationData,
+ /* rotationDegrees= */ Format.NO_VALUE,
+ spsData.pixelWidthAspectRatio,
+ /* drmInitData= */ null));
hasOutputFormat = true;
sampleReader.putSps(spsData);
sampleReader.putPps(ppsData);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java
new file mode 100644
index 0000000000..e8c207f75d
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * A seeker that supports seeking within PS stream using binary search.
+ *
+ * This seeker uses the first and last SCR values within the stream, as well as the stream
+ * duration to interpolate the SCR value of the seeking position. Then it performs binary search
+ * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from
+ * the target SCR.
+ */
+/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker {
+
+ private static final long SEEK_TOLERANCE_US = 100_000;
+ private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000;
+ private static final int TIMESTAMP_SEARCH_BYTES = 20000;
+
+ public PsBinarySearchSeeker(
+ TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) {
+ super(
+ new DefaultSeekTimestampConverter(),
+ new PsScrSeeker(scrTimestampAdjuster),
+ streamDurationUs,
+ /* floorTimePosition= */ 0,
+ /* ceilingTimePosition= */ streamDurationUs + 1,
+ /* floorBytePosition= */ 0,
+ /* ceilingBytePosition= */ inputLength,
+ /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
+ MINIMUM_SEARCH_RANGE_BYTES);
+ }
+
+ /**
+ * A seeker that looks for a given SCR timestamp at a given position in a PS stream.
+ *
+ *
Given a SCR timestamp, and a position within a PS stream, this seeker will try to read a
+ * range of up to {@link #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all
+ * packs in that range, and then compare the SCR timestamps (if available) of these packets vs the
+ * target timestamp.
+ */
+ private static final class PsScrSeeker implements TimestampSeeker {
+
+ private final TimestampAdjuster scrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+
+ private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
+ this.scrTimestampAdjuster = scrTimestampAdjuster;
+ packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES);
+ }
+
+ @Override
+ public TimestampSearchResult searchForTimestamp(
+ ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
+ throws IOException, InterruptedException {
+ long inputPosition = input.getPosition();
+ int bytesToRead =
+ (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
+ packetBuffer.reset(bytesToRead);
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
+
+ return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
+ }
+
+ private TimestampSearchResult searchForScrValueInBuffer(
+ ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
+ int startOfLastPacketPosition = C.POSITION_UNSET;
+ int endOfLastPacketPosition = C.POSITION_UNSET;
+ long lastScrTimeUsInRange = C.TIME_UNSET;
+
+ while (packetBuffer.bytesLeft() >= 4) {
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
+ if (nextStartCode != PsExtractor.PACK_START_CODE) {
+ packetBuffer.skipBytes(1);
+ continue;
+ } else {
+ packetBuffer.skipBytes(4);
+ }
+
+ // We found a pack.
+ long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer);
+ if (scrValue != C.TIME_UNSET) {
+ long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue);
+ if (scrTimeUs > targetScrTimeUs) {
+ if (lastScrTimeUsInRange == C.TIME_UNSET) {
+ // First SCR timestamp is already over target.
+ return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset);
+ } else {
+ // Last SCR timestamp < target timestamp < this timestamp.
+ return TimestampSearchResult.targetFoundResult(
+ bufferStartOffset + startOfLastPacketPosition);
+ }
+ } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) {
+ long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition();
+ return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
+ }
+
+ lastScrTimeUsInRange = scrTimeUs;
+ startOfLastPacketPosition = packetBuffer.getPosition();
+ }
+ skipToEndOfCurrentPack(packetBuffer);
+ endOfLastPacketPosition = packetBuffer.getPosition();
+ }
+
+ if (lastScrTimeUsInRange != C.TIME_UNSET) {
+ long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
+ return TimestampSearchResult.underestimatedResult(
+ lastScrTimeUsInRange, endOfLastPacketPositionInStream);
+ } else {
+ return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
+ }
+ }
+
+ /**
+ * Skips the buffer position to the position after the end of the current PS pack in the buffer,
+ * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in
+ * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer.
+ */
+ private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) {
+ int limit = packetBuffer.limit();
+
+ if (packetBuffer.bytesLeft() < 10) {
+ // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing
+ // length.
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ packetBuffer.skipBytes(9);
+
+ int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07;
+ if (packetBuffer.bytesLeft() < packStuffingLength) {
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ packetBuffer.skipBytes(packStuffingLength);
+
+ if (packetBuffer.bytesLeft() < 4) {
+ packetBuffer.setPosition(limit);
+ return;
+ }
+
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
+ if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) {
+ packetBuffer.skipBytes(4);
+ int systemHeaderLength = packetBuffer.readUnsignedShort();
+ if (packetBuffer.bytesLeft() < systemHeaderLength) {
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ packetBuffer.skipBytes(systemHeaderLength);
+ }
+
+ // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right
+ // after the end position of this pack.
+ // If we couldn't find these codes within the buffer, return the buffer limit, or return
+ // the first position which PES packets pattern does not match (some malformed packets).
+ while (packetBuffer.bytesLeft() >= 4) {
+ nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
+ if (nextStartCode == PsExtractor.PACK_START_CODE
+ || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) {
+ break;
+ }
+ if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) {
+ break;
+ }
+ packetBuffer.skipBytes(4);
+
+ if (packetBuffer.bytesLeft() < 2) {
+ // 2 bytes for PES_packet length.
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ int pesPacketLength = packetBuffer.readUnsignedShort();
+ packetBuffer.setPosition(
+ Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength));
+ }
+ }
+ }
+
+ private static int peekIntAtPosition(byte[] data, int position) {
+ return (data[position] & 0xFF) << 24
+ | (data[position + 1] & 0xFF) << 16
+ | (data[position + 2] & 0xFF) << 8
+ | (data[position + 3] & 0xFF);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java
new file mode 100644
index 0000000000..3b52206235
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * A reader that can extract the approximate duration from a given MPEG program stream (PS).
+ *
+ *
This reader extracts the duration by reading system clock reference (SCR) values from the
+ * header of a pack at the start and at the end of the stream, calculating the difference, and
+ * converting that into stream duration. This reader also handles the case when a single SCR
+ * wraparound takes place within the stream, which can make SCR values at the beginning of the
+ * stream larger than SCR values at the end. This class can only be used once to read duration from
+ * a given stream, and the usage of the class is not thread-safe, so all calls should be made from
+ * the same thread.
+ *
+ *
Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header.
+ */
+/* package */ final class PsDurationReader {
+
+ private static final int DURATION_READ_BYTES = 20000;
+
+ private final TimestampAdjuster scrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+
+ private boolean isDurationRead;
+ private boolean isFirstScrValueRead;
+ private boolean isLastScrValueRead;
+
+ private long firstScrValue;
+ private long lastScrValue;
+ private long durationUs;
+
+ /* package */ PsDurationReader() {
+ scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
+ firstScrValue = C.TIME_UNSET;
+ lastScrValue = C.TIME_UNSET;
+ durationUs = C.TIME_UNSET;
+ packetBuffer = new ParsableByteArray(DURATION_READ_BYTES);
+ }
+
+ /** Returns true if a PS duration has been read. */
+ public boolean isDurationReadFinished() {
+ return isDurationRead;
+ }
+
+ public TimestampAdjuster getScrTimestampAdjuster() {
+ return scrTimestampAdjuster;
+ }
+
+ /**
+ * Reads a PS duration from the input.
+ *
+ *
This reader reads the duration by reading SCR values from the header of a pack at the start
+ * and at the end of the stream, calculating the difference, and converting that into stream
+ * duration.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public @Extractor.ReadResult int readDuration(
+ ExtractorInput input, PositionHolder seekPositionHolder)
+ throws IOException, InterruptedException {
+ if (!isLastScrValueRead) {
+ return readLastScrValue(input, seekPositionHolder);
+ }
+ if (lastScrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+ if (!isFirstScrValueRead) {
+ return readFirstScrValue(input, seekPositionHolder);
+ }
+ if (firstScrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+
+ long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue);
+ long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue);
+ durationUs = maxScrPositionUs - minScrPositionUs;
+ return finishReadDuration(input);
+ }
+
+ /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the SCR value read from the next pack in the stream, given the buffer at the pack
+ * header start position (just behind the pack start code).
+ */
+ public static long readScrValueFromPack(ParsableByteArray packetBuffer) {
+ int originalPosition = packetBuffer.getPosition();
+ if (packetBuffer.bytesLeft() < 9) {
+ // We require at 9 bytes for pack header to read scr value
+ return C.TIME_UNSET;
+ }
+ byte[] scrBytes = new byte[9];
+ packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length);
+ packetBuffer.setPosition(originalPosition);
+ if (!checkMarkerBits(scrBytes)) {
+ return C.TIME_UNSET;
+ }
+ return readScrValueFromPackHeader(scrBytes);
+ }
+
+ private int finishReadDuration(ExtractorInput input) {
+ isDurationRead = true;
+ input.resetPeekPosition();
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
+ throws IOException, InterruptedException {
+ if (input.getPosition() != 0) {
+ seekPositionHolder.position = 0;
+ return Extractor.RESULT_SEEK;
+ }
+
+ int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesToRead);
+
+ firstScrValue = readFirstScrValueFromBuffer(packetBuffer);
+ isFirstScrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchStartPosition;
+ searchPosition < searchEndPosition - 3;
+ searchPosition++) {
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);
+ if (nextStartCode == PsExtractor.PACK_START_CODE) {
+ packetBuffer.setPosition(searchPosition + 4);
+ long scrValue = readScrValueFromPack(packetBuffer);
+ if (scrValue != C.TIME_UNSET) {
+ return scrValue;
+ }
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
+ throws IOException, InterruptedException {
+ int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
+ long bufferStartStreamPosition = input.getLength() - bytesToRead;
+ if (input.getPosition() != bufferStartStreamPosition) {
+ seekPositionHolder.position = bufferStartStreamPosition;
+ return Extractor.RESULT_SEEK;
+ }
+
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesToRead);
+
+ lastScrValue = readLastScrValueFromBuffer(packetBuffer);
+ isLastScrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchEndPosition - 4;
+ searchPosition >= searchStartPosition;
+ searchPosition--) {
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);
+ if (nextStartCode == PsExtractor.PACK_START_CODE) {
+ packetBuffer.setPosition(searchPosition + 4);
+ long scrValue = readScrValueFromPack(packetBuffer);
+ if (scrValue != C.TIME_UNSET) {
+ return scrValue;
+ }
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ private int peekIntAtPosition(byte[] data, int position) {
+ return (data[position] & 0xFF) << 24
+ | (data[position + 1] & 0xFF) << 16
+ | (data[position + 2] & 0xFF) << 8
+ | (data[position + 3] & 0xFF);
+ }
+
+ private static boolean checkMarkerBits(byte[] scrBytes) {
+ // Verify the 01xxx1xx marker on the 0th byte
+ if ((scrBytes[0] & 0xC4) != 0x44) {
+ return false;
+ }
+ // 1st byte belongs to scr field.
+ // Verify the xxxxx1xx marker on the 2nd byte
+ if ((scrBytes[2] & 0x04) != 0x04) {
+ return false;
+ }
+ // 3rd byte belongs to scr field.
+ // Verify the xxxxx1xx marker on the 4rd byte
+ if ((scrBytes[4] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxxxx1 marker on the 5th byte
+ if ((scrBytes[5] & 0x01) != 0x01) {
+ return false;
+ }
+ // 6th and 7th bytes belongs to program_max_rate field.
+ // Verify the xxxxxx11 marker on the 8th byte
+ return (scrBytes[8] & 0x03) == 0x03;
+ }
+
+ /**
+ * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring
+ * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in
+ * pack_header.
+ *
+ *
We ignore SCR Ext, because it's too small to have any significance.
+ */
+ private static long readScrValueFromPackHeader(byte[] scrBytes) {
+ return ((scrBytes[0] & 0b00111000L) >> 3) << 30
+ | (scrBytes[0] & 0b00000011L) << 28
+ | (scrBytes[1] & 0xFFL) << 20
+ | ((scrBytes[2] & 0b11111000L) >> 3) << 15
+ | (scrBytes[2] & 0b00000011L) << 13
+ | (scrBytes[3] & 0xFFL) << 5
+ | (scrBytes[4] & 0b11111000L) >> 3;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
index f3aad6ba6b..c7a082aeac 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -35,24 +35,20 @@ import java.io.IOException;
*/
public final class PsExtractor implements Extractor {
- /**
- * Factory for {@link PsExtractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+ /** Factory for {@link PsExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()};
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new PsExtractor()};
- }
-
- };
-
- private static final int PACK_START_CODE = 0x000001BA;
- private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
- private static final int PACKET_START_CODE_PREFIX = 0x000001;
- private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
+ /* package */ static final int PACK_START_CODE = 0x000001BA;
+ /* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
+ /* package */ static final int PACKET_START_CODE_PREFIX = 0x000001;
+ /* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
+
+ // Max search length for first audio and video track in input data.
private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
+ // Max search length for additional audio and video tracks in input data after at least one audio
+ // and video track has been found.
+ private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;
public static final int PRIVATE_STREAM_1 = 0xBD;
public static final int AUDIO_STREAM = 0xC0;
@@ -63,12 +59,17 @@ public final class PsExtractor implements Extractor {
private final TimestampAdjuster timestampAdjuster;
private final SparseArray psPayloadReaders; // Indexed by pid
private final ParsableByteArray psPacketBuffer;
+ private final PsDurationReader durationReader;
+
private boolean foundAllTracks;
private boolean foundAudioTrack;
private boolean foundVideoTrack;
+ private long lastTrackPosition;
// Accessed only by the loading thread.
+ private PsBinarySearchSeeker psBinarySearchSeeker;
private ExtractorOutput output;
+ private boolean hasOutputSeekMap;
public PsExtractor() {
this(new TimestampAdjuster(0));
@@ -78,6 +79,7 @@ public final class PsExtractor implements Extractor {
this.timestampAdjuster = timestampAdjuster;
psPacketBuffer = new ParsableByteArray(4096);
psPayloadReaders = new SparseArray<>();
+ durationReader = new PsDurationReader();
}
// Extractor implementation.
@@ -124,12 +126,27 @@ public final class PsExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
this.output = output;
- output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
}
@Override
public void seek(long position, long timeUs) {
- timestampAdjuster.reset();
+ boolean hasNotEncounteredFirstTimestamp =
+ timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;
+ if (hasNotEncounteredFirstTimestamp
+ || (timestampAdjuster.getFirstSampleTimestampUs() != 0
+ && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {
+ // - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to
+ // treat the first timestamp encountered as sample time 0, which is incorrect. In this case,
+ // we have to set the first sample timestamp manually.
+ // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
+ // different position, we need to set the first sample timestamp manually again.
+ timestampAdjuster.reset();
+ timestampAdjuster.setFirstSampleTimestampUs(timeUs);
+ }
+
+ if (psBinarySearchSeeker != null) {
+ psBinarySearchSeeker.setSeekTargetUs(timeUs);
+ }
for (int i = 0; i < psPayloadReaders.size(); i++) {
psPayloadReaders.valueAt(i).seek();
}
@@ -143,6 +160,24 @@ public final class PsExtractor implements Extractor {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
+
+ long inputLength = input.getLength();
+ boolean canReadDuration = inputLength != C.LENGTH_UNSET;
+ if (canReadDuration && !durationReader.isDurationReadFinished()) {
+ return durationReader.readDuration(input, seekPosition);
+ }
+ maybeOutputSeekMap(inputLength);
+ if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) {
+ return psBinarySearchSeeker.handlePendingSeek(
+ input, seekPosition, /* outputFrameHolder= */ null);
+ }
+
+ input.resetPeekPosition();
+ long peekBytesLeft =
+ inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET;
+ if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) {
+ return RESULT_END_OF_INPUT;
+ }
// First peek and check what type of start code is next.
if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
return RESULT_END_OF_INPUT;
@@ -188,18 +223,21 @@ public final class PsExtractor implements Extractor {
if (!foundAllTracks) {
if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null;
- if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) {
+ if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs.
elementaryStreamReader = new Ac3Reader();
foundAudioTrack = true;
- } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
elementaryStreamReader = new MpegAudioReader();
foundAudioTrack = true;
- } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
elementaryStreamReader = new H262Reader();
foundVideoTrack = true;
+ lastTrackPosition = input.getPosition();
}
if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
@@ -208,7 +246,11 @@ public final class PsExtractor implements Extractor {
psPayloadReaders.put(streamId, payloadReader);
}
}
- if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) {
+ long maxSearchPosition =
+ foundAudioTrack && foundVideoTrack
+ ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
+ : MAX_SEARCH_LENGTH;
+ if (input.getPosition() > maxSearchPosition) {
foundAllTracks = true;
output.endTracks();
}
@@ -237,6 +279,22 @@ public final class PsExtractor implements Extractor {
// Internals.
+ private void maybeOutputSeekMap(long inputLength) {
+ if (!hasOutputSeekMap) {
+ hasOutputSeekMap = true;
+ if (durationReader.getDurationUs() != C.TIME_UNSET) {
+ psBinarySearchSeeker =
+ new PsBinarySearchSeeker(
+ durationReader.getScrTimestampAdjuster(),
+ durationReader.getDurationUs(),
+ inputLength);
+ output.seekMap(psBinarySearchSeeker.getSeekMap());
+ } else {
+ output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
+ }
+ }
+ }
+
/**
* Parses PES packet data and extracts samples.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java
index 907419f8fc..895c224697 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java
@@ -52,9 +52,18 @@ import java.util.List;
|| MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
"Invalid closed caption mime type provided: " + channelMimeType);
String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId();
- output.format(Format.createTextSampleFormat(formatId, channelMimeType, null, Format.NO_VALUE,
- channelFormat.selectionFlags, channelFormat.language, channelFormat.accessibilityChannel,
- null));
+ output.format(
+ Format.createTextSampleFormat(
+ formatId,
+ channelMimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ channelFormat.selectionFlags,
+ channelFormat.language,
+ channelFormat.accessibilityChannel,
+ /* drmInitData= */ null,
+ Format.OFFSET_SAMPLE_RELATIVE,
+ channelFormat.initializationData));
outputs[i] = output;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java
new file mode 100644
index 0000000000..29aa0d55d2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * A seeker that supports seeking within TS stream using binary search.
+ *
+ * This seeker uses the first and last PCR values within the stream, as well as the stream
+ * duration to interpolate the PCR value of the seeking position. Then it performs binary search
+ * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the
+ * target PCR.
+ */
+/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {
+
+ private static final long SEEK_TOLERANCE_US = 100_000;
+ private static final int MINIMUM_SEARCH_RANGE_BYTES = TsExtractor.TS_PACKET_SIZE * 5;
+ private static final int TIMESTAMP_SEARCH_PACKETS = 200;
+ private static final int TIMESTAMP_SEARCH_BYTES =
+ TsExtractor.TS_PACKET_SIZE * TIMESTAMP_SEARCH_PACKETS;
+
+ public TsBinarySearchSeeker(
+ TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
+ super(
+ new DefaultSeekTimestampConverter(),
+ new TsPcrSeeker(pcrPid, pcrTimestampAdjuster),
+ streamDurationUs,
+ /* floorTimePosition= */ 0,
+ /* ceilingTimePosition= */ streamDurationUs + 1,
+ /* floorBytePosition= */ 0,
+ /* ceilingBytePosition= */ inputLength,
+ /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
+ MINIMUM_SEARCH_RANGE_BYTES);
+ }
+
+ /**
+ * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given
+ * position in a TS stream.
+ *
+ *
Given a PCR timestamp, and a position within a TS stream, this seeker will try to read up to
+ * {@link #TIMESTAMP_SEARCH_PACKETS} TS packets from that stream position, look for all packet
+ * with PID equals to PCR_PID, and then compare the PCR timestamps (if available) of these packets
+ * vs the target timestamp.
+ */
+ private static final class TsPcrSeeker implements TimestampSeeker {
+
+ private final TimestampAdjuster pcrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+ private final int pcrPid;
+
+ public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {
+ this.pcrPid = pcrPid;
+ this.pcrTimestampAdjuster = pcrTimestampAdjuster;
+ packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES);
+ }
+
+ @Override
+ public TimestampSearchResult searchForTimestamp(
+ ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
+ throws IOException, InterruptedException {
+ long inputPosition = input.getPosition();
+ int bytesToRead =
+ (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
+ packetBuffer.reset(bytesToRead);
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
+
+ return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
+ }
+
+ private TimestampSearchResult searchForPcrValueInBuffer(
+ ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) {
+ int limit = packetBuffer.limit();
+
+ long startOfLastPacketPosition = C.POSITION_UNSET;
+ long endOfLastPacketPosition = C.POSITION_UNSET;
+ long lastPcrTimeUsInRange = C.TIME_UNSET;
+
+ while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) {
+ int startOfPacket =
+ TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit);
+ int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE;
+ if (endOfPacket > limit) {
+ break;
+ }
+ long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid);
+ if (pcrValue != C.TIME_UNSET) {
+ long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue);
+ if (pcrTimeUs > targetPcrTimeUs) {
+ if (lastPcrTimeUsInRange == C.TIME_UNSET) {
+ // First PCR timestamp is already over target.
+ return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset);
+ } else {
+ // Last PCR timestamp < target timestamp < this timestamp.
+ return TimestampSearchResult.targetFoundResult(
+ bufferStartOffset + startOfLastPacketPosition);
+ }
+ } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) {
+ long startOfPacketInStream = bufferStartOffset + startOfPacket;
+ return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
+ }
+
+ lastPcrTimeUsInRange = pcrTimeUs;
+ startOfLastPacketPosition = startOfPacket;
+ }
+ packetBuffer.setPosition(endOfPacket);
+ endOfLastPacketPosition = endOfPacket;
+ }
+
+ if (lastPcrTimeUsInRange != C.TIME_UNSET) {
+ long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
+ return TimestampSearchResult.underestimatedResult(
+ lastPcrTimeUsInRange, endOfLastPacketPositionInStream);
+ } else {
+ return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
+ }
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java
new file mode 100644
index 0000000000..350337cc86
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * A reader that can extract the approximate duration from a given MPEG transport stream (TS).
+ *
+ *
This reader extracts the duration by reading PCR values of the PCR PID packets at the start
+ * and at the end of the stream, calculating the difference, and converting that into stream
+ * duration. This reader also handles the case when a single PCR wraparound takes place within the
+ * stream, which can make PCR values at the beginning of the stream larger than PCR values at the
+ * end. This class can only be used once to read duration from a given stream, and the usage of the
+ * class is not thread-safe, so all calls should be made from the same thread.
+ */
+/* package */ final class TsDurationReader {
+
+ private static final int DURATION_READ_PACKETS = 200;
+ private static final int DURATION_READ_BYTES = TsExtractor.TS_PACKET_SIZE * DURATION_READ_PACKETS;
+
+ private final TimestampAdjuster pcrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+
+ private boolean isDurationRead;
+ private boolean isFirstPcrValueRead;
+ private boolean isLastPcrValueRead;
+
+ private long firstPcrValue;
+ private long lastPcrValue;
+ private long durationUs;
+
+ /* package */ TsDurationReader() {
+ pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
+ firstPcrValue = C.TIME_UNSET;
+ lastPcrValue = C.TIME_UNSET;
+ durationUs = C.TIME_UNSET;
+ packetBuffer = new ParsableByteArray(DURATION_READ_BYTES);
+ }
+
+ /** Returns true if a TS duration has been read. */
+ public boolean isDurationReadFinished() {
+ return isDurationRead;
+ }
+
+ /**
+ * Reads a TS duration from the input, using the given PCR PID.
+ *
+ *
This reader reads the duration by reading PCR values of the PCR PID packets at the start and
+ * at the end of the stream, calculating the difference, and converting that into stream duration.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @param pcrPid The PID of the packet stream within this TS stream that contains PCR values.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public @Extractor.ReadResult int readDuration(
+ ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
+ throws IOException, InterruptedException {
+ if (pcrPid <= 0) {
+ return finishReadDuration(input);
+ }
+ if (!isLastPcrValueRead) {
+ return readLastPcrValue(input, seekPositionHolder, pcrPid);
+ }
+ if (lastPcrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+ if (!isFirstPcrValueRead) {
+ return readFirstPcrValue(input, seekPositionHolder, pcrPid);
+ }
+ if (firstPcrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+
+ long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue);
+ long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue);
+ durationUs = maxPcrPositionUs - minPcrPositionUs;
+ return finishReadDuration(input);
+ }
+
+ /**
+ * Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder, int)}.
+ */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the
+ * input TS stream.
+ */
+ public TimestampAdjuster getPcrTimestampAdjuster() {
+ return pcrTimestampAdjuster;
+ }
+
+ private int finishReadDuration(ExtractorInput input) {
+ isDurationRead = true;
+ input.resetPeekPosition();
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
+ throws IOException, InterruptedException {
+ if (input.getPosition() != 0) {
+ seekPositionHolder.position = 0;
+ return Extractor.RESULT_SEEK;
+ }
+
+ int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesToRead);
+
+ firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid);
+ isFirstPcrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchStartPosition;
+ searchPosition < searchEndPosition;
+ searchPosition++) {
+ if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
+ continue;
+ }
+ long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
+ if (pcrValue != C.TIME_UNSET) {
+ return pcrValue;
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
+ throws IOException, InterruptedException {
+ int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
+ long bufferStartStreamPosition = input.getLength() - bytesToRead;
+ if (input.getPosition() != bufferStartStreamPosition) {
+ seekPositionHolder.position = bufferStartStreamPosition;
+ return Extractor.RESULT_SEEK;
+ }
+
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesToRead);
+
+ lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);
+ isLastPcrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchEndPosition - 1;
+ searchPosition >= searchStartPosition;
+ searchPosition--) {
+ if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
+ continue;
+ }
+ long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
+ if (pcrValue != C.TIME_UNSET) {
+ return pcrValue;
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+}
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 50931e2d90..f677dc008f 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
@@ -50,17 +50,8 @@ import java.util.List;
*/
public final class TsExtractor implements Extractor {
- /**
- * Factory for {@link TsExtractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new TsExtractor()};
- }
-
- };
+ /** Factory for {@link TsExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()};
/**
* Modes for the extractor.
@@ -98,8 +89,9 @@ public final class TsExtractor implements Extractor {
public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86;
public static final int TS_STREAM_TYPE_DVBSUBS = 0x59;
- private static final int TS_PACKET_SIZE = 188;
- private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
+ public static final int TS_PACKET_SIZE = 188;
+ public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
+
private static final int TS_PAT_PID = 0;
private static final int MAX_PID_PLUS_ONE = 0x2000;
@@ -110,20 +102,26 @@ public final class TsExtractor implements Extractor {
private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50;
private static final int SNIFF_TS_PACKET_COUNT = 5;
- @Mode private final int mode;
+ private final @Mode int mode;
private final List timestampAdjusters;
private final ParsableByteArray tsPacketBuffer;
private final SparseIntArray continuityCounters;
private final TsPayloadReader.Factory payloadReaderFactory;
private final SparseArray tsPayloadReaders; // Indexed by pid
private final SparseBooleanArray trackIds;
+ private final SparseBooleanArray trackPids;
+ private final TsDurationReader durationReader;
// Accessed only by the loading thread.
+ private TsBinarySearchSeeker tsBinarySearchSeeker;
private ExtractorOutput output;
private int remainingPmts;
private boolean tracksEnded;
+ private boolean hasOutputSeekMap;
+ private boolean pendingSeekToStart;
private TsPayloadReader id3Reader;
private int bytesSinceLastSync;
+ private int pcrPid;
public TsExtractor() {
this(0);
@@ -144,18 +142,21 @@ public final class TsExtractor implements Extractor {
* {@code FLAG_*} values that control the behavior of the payload readers.
*/
public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) {
- this(mode, new TimestampAdjuster(0),
+ 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.
*/
- public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster,
+ public TsExtractor(
+ @Mode int mode,
+ TimestampAdjuster timestampAdjuster,
TsPayloadReader.Factory payloadReaderFactory) {
this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
this.mode = mode;
@@ -167,8 +168,11 @@ public final class TsExtractor implements Extractor {
}
tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0);
trackIds = new SparseBooleanArray();
+ trackPids = new SparseBooleanArray();
tsPayloadReaders = new SparseArray<>();
continuityCounters = new SparseIntArray();
+ durationReader = new TsDurationReader();
+ pcrPid = -1;
resetPayloadReaders();
}
@@ -178,16 +182,19 @@ public final class TsExtractor implements Extractor {
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
byte[] buffer = tsPacketBuffer.data;
input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT);
- for (int j = 0; j < TS_PACKET_SIZE; j++) {
- for (int i = 0; true; i++) {
- if (i == SNIFF_TS_PACKET_COUNT) {
- input.skipFully(j);
- return true;
- }
- if (buffer[j + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
+ for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) {
+ // Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE.
+ boolean isSyncBytePatternCorrect = true;
+ for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) {
+ if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
+ isSyncBytePatternCorrect = false;
break;
}
}
+ if (isSyncBytePatternCorrect) {
+ input.skipFully(startPosCandidate);
+ return true;
+ }
}
return false;
}
@@ -195,19 +202,36 @@ public final class TsExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
this.output = output;
- output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
}
@Override
public void seek(long position, long timeUs) {
+ Assertions.checkState(mode != MODE_HLS);
int timestampAdjustersCount = timestampAdjusters.size();
for (int i = 0; i < timestampAdjustersCount; i++) {
- timestampAdjusters.get(i).reset();
+ TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i);
+ boolean hasNotEncounteredFirstTimestamp =
+ timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;
+ if (hasNotEncounteredFirstTimestamp
+ || (timestampAdjuster.getTimestampOffsetUs() != 0
+ && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {
+ // - If a track in the TS stream has not encountered any sample, it's going to treat the
+ // first sample encountered as timestamp 0, which is incorrect. So we have to set the first
+ // sample timestamp for that track manually.
+ // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
+ // different position, we need to set the first sample timestamp manually again.
+ timestampAdjuster.reset();
+ timestampAdjuster.setFirstSampleTimestampUs(timeUs);
+ }
+ }
+ if (timeUs != 0 && tsBinarySearchSeeker != null) {
+ tsBinarySearchSeeker.setSeekTargetUs(timeUs);
}
tsPacketBuffer.reset();
continuityCounters.clear();
- // Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
- resetPayloadReaders();
+ for (int i = 0; i < tsPayloadReaders.size(); i++) {
+ tsPayloadReaders.valueAt(i).seek();
+ }
bytesSinceLastSync = 0;
}
@@ -217,48 +241,42 @@ public final class TsExtractor implements Extractor {
}
@Override
- public int read(ExtractorInput input, PositionHolder seekPosition)
+ public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- byte[] data = tsPacketBuffer.data;
-
- // Shift bytes to the start of the buffer if there isn't enough space left at the end.
- if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
- int bytesLeft = tsPacketBuffer.bytesLeft();
- if (bytesLeft > 0) {
- System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);
+ if (tracksEnded) {
+ long inputLength = input.getLength();
+ boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;
+ if (canReadDuration && !durationReader.isDurationReadFinished()) {
+ return durationReader.readDuration(input, seekPosition, pcrPid);
+ }
+ maybeOutputSeekMap(inputLength);
+
+ if (pendingSeekToStart) {
+ pendingSeekToStart = false;
+ seek(/* position= */ 0, /* timeUs= */ 0);
+ if (input.getPosition() != 0) {
+ seekPosition.position = 0;
+ return RESULT_SEEK;
+ }
+ }
+
+ if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {
+ return tsBinarySearchSeeker.handlePendingSeek(
+ input, seekPosition, /* outputFrameHolder= */ null);
}
- tsPacketBuffer.reset(data, bytesLeft);
}
- // Read more bytes until we have at least one packet.
- while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
- int limit = tsPacketBuffer.limit();
- int read = input.read(data, limit, BUFFER_SIZE - limit);
- if (read == C.RESULT_END_OF_INPUT) {
- return RESULT_END_OF_INPUT;
- }
- tsPacketBuffer.setLimit(limit + read);
+ if (!fillBufferWithAtLeastOnePacket(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ int endOfPacket = findEndOfFirstTsPacketInBuffer();
+ int limit = tsPacketBuffer.limit();
+ if (endOfPacket > limit) {
+ return RESULT_CONTINUE;
}
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
- int limit = tsPacketBuffer.limit();
- int position = tsPacketBuffer.getPosition();
- int searchStart = position;
- while (position < limit && data[position] != TS_SYNC_BYTE) {
- position++;
- }
- tsPacketBuffer.setPosition(position);
-
- int endOfPacket = position + TS_PACKET_SIZE;
- if (endOfPacket > limit) {
- bytesSinceLastSync += position - searchStart;
- if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) {
- throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream.");
- }
- return RESULT_CONTINUE;
- }
- bytesSinceLastSync = 0;
-
int tsPacketHeader = tsPacketBuffer.readInt();
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
// There are uncorrectable errors in this packet.
@@ -300,9 +318,18 @@ public final class TsExtractor implements Extractor {
}
// Read the payload.
- tsPacketBuffer.setLimit(endOfPacket);
- payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
- tsPacketBuffer.setLimit(limit);
+ boolean wereTracksEnded = tracksEnded;
+ if (shouldConsumePacketPayload(pid)) {
+ tsPacketBuffer.setLimit(endOfPacket);
+ payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
+ tsPacketBuffer.setLimit(limit);
+ }
+ if (mode != MODE_HLS && !wereTracksEnded && tracksEnded) {
+ // We have read all tracks from all PMTs in this stream. Now seek to the beginning and read
+ // again to make sure we output all media, including any contained in packets prior to those
+ // containing the track information.
+ pendingSeekToStart = true;
+ }
tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE;
@@ -310,6 +337,78 @@ public final class TsExtractor implements Extractor {
// Internals.
+ private void maybeOutputSeekMap(long inputLength) {
+ if (!hasOutputSeekMap) {
+ hasOutputSeekMap = true;
+ if (durationReader.getDurationUs() != C.TIME_UNSET) {
+ tsBinarySearchSeeker =
+ new TsBinarySearchSeeker(
+ durationReader.getPcrTimestampAdjuster(),
+ durationReader.getDurationUs(),
+ inputLength,
+ pcrPid);
+ output.seekMap(tsBinarySearchSeeker.getSeekMap());
+ } else {
+ output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
+ }
+ }
+ }
+
+ private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input)
+ throws IOException, InterruptedException {
+ byte[] data = tsPacketBuffer.data;
+ // Shift bytes to the start of the buffer if there isn't enough space left at the end.
+ if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
+ int bytesLeft = tsPacketBuffer.bytesLeft();
+ if (bytesLeft > 0) {
+ System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);
+ }
+ tsPacketBuffer.reset(data, bytesLeft);
+ }
+ // Read more bytes until we have at least one packet.
+ while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
+ int limit = tsPacketBuffer.limit();
+ int read = input.read(data, limit, BUFFER_SIZE - limit);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ tsPacketBuffer.setLimit(limit + read);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the position of the end of the first TS packet (exclusive) in the packet buffer.
+ *
+ * This may be a position beyond the buffer limit if the packet has not been read fully into
+ * the buffer, or if no packet could be found within the buffer.
+ */
+ private int findEndOfFirstTsPacketInBuffer() throws ParserException {
+ int searchStart = tsPacketBuffer.getPosition();
+ int limit = tsPacketBuffer.limit();
+ int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit);
+ // Discard all bytes before the sync byte.
+ // If sync byte is not found, this means discard the whole buffer.
+ tsPacketBuffer.setPosition(syncBytePosition);
+ int endOfPacket = syncBytePosition + TS_PACKET_SIZE;
+ if (endOfPacket > limit) {
+ bytesSinceLastSync += syncBytePosition - searchStart;
+ if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) {
+ throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream.");
+ }
+ } else {
+ // We have found a packet within the buffer.
+ bytesSinceLastSync = 0;
+ }
+ return endOfPacket;
+ }
+
+ private boolean shouldConsumePacketPayload(int packetPid) {
+ return mode == MODE_HLS
+ || tracksEnded
+ || !trackPids.get(packetPid, /* valueIfKeyNotFound= */ false); // It's a PSI packet
+ }
+
private void resetPayloadReaders() {
trackIds.clear();
tsPayloadReaders.clear();
@@ -422,9 +521,16 @@ public final class TsExtractor implements Extractor {
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(12)
sectionData.skipBytes(2);
int programNumber = sectionData.readUnsignedShort();
+
+ // Skip 3 bytes (24 bits), including:
// reserved (2), version_number (5), current_next_indicator (1), section_number (8),
- // last_section_number (8), reserved (3), PCR_PID (13)
- sectionData.skipBytes(5);
+ // last_section_number (8)
+ sectionData.skipBytes(3);
+
+ sectionData.readBytes(pmtScratch, 2);
+ // reserved (3), PCR_PID (13)
+ pmtScratch.skipBits(3);
+ pcrPid = pmtScratch.readBits(13);
// Read program_info_length.
sectionData.readBytes(pmtScratch, 2);
@@ -476,14 +582,16 @@ public final class TsExtractor implements Extractor {
int trackIdCount = trackIdToPidScratch.size();
for (int i = 0; i < trackIdCount; i++) {
int trackId = trackIdToPidScratch.keyAt(i);
+ int trackPid = trackIdToPidScratch.valueAt(i);
trackIds.put(trackId, true);
+ trackPids.put(trackPid, true);
TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
if (reader != null) {
if (reader != id3Reader) {
reader.init(timestampAdjuster, output,
new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));
}
- tsPayloadReaders.put(trackIdToPidScratch.valueAt(i), reader);
+ tsPayloadReaders.put(trackPid, reader);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
index efa764b572..2ea25bb2e0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -77,8 +77,10 @@ public interface TsPayloadReader {
byte[] descriptorBytes) {
this.streamType = streamType;
this.language = language;
- this.dvbSubtitleInfos = dvbSubtitleInfos == null ? Collections.emptyList()
- : Collections.unmodifiableList(dvbSubtitleInfos);
+ this.dvbSubtitleInfos =
+ dvbSubtitleInfos == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(dvbSubtitleInfos);
this.descriptorBytes = descriptorBytes;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java
new file mode 100644
index 0000000000..2a7a0d25ab
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/** Utilities method for extracting MPEG-TS streams. */
+public final class TsUtil {
+ /**
+ * Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition)
+ * from the provided data array, or returns limitPosition if sync byte could not be found.
+ */
+ public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) {
+ int position = startPosition;
+ while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) {
+ position++;
+ }
+ return position;
+ }
+
+ /**
+ * Returns the PCR value read from a given TS packet.
+ *
+ * @param packetBuffer The buffer that holds the packet.
+ * @param startOfPacket The starting position of the packet in the buffer.
+ * @param pcrPid The PID for valid packets that contain PCR values.
+ * @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it
+ * contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise.
+ */
+ public static long readPcrFromPacket(
+ ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {
+ packetBuffer.setPosition(startOfPacket);
+ if (packetBuffer.bytesLeft() < 5) {
+ // Header = 4 bytes, adaptationFieldLength = 1 byte.
+ return C.TIME_UNSET;
+ }
+ // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
+ int tsPacketHeader = packetBuffer.readInt();
+ if ((tsPacketHeader & 0x800000) != 0) {
+ // transport_error_indicator != 0 means there are uncorrectable errors in this packet.
+ return C.TIME_UNSET;
+ }
+ int pid = (tsPacketHeader & 0x1FFF00) >> 8;
+ if (pid != pcrPid) {
+ return C.TIME_UNSET;
+ }
+ boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;
+ if (!adaptationFieldExists) {
+ return C.TIME_UNSET;
+ }
+
+ int adaptationFieldLength = packetBuffer.readUnsignedByte();
+ if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {
+ int flags = packetBuffer.readUnsignedByte();
+ boolean pcrFlagSet = (flags & 0x10) == 0x10;
+ if (pcrFlagSet) {
+ byte[] pcrBytes = new byte[6];
+ packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);
+ return readPcrValueFromPcrBytes(pcrBytes);
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ /**
+ * Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes.
+ *
+ * We ignore PCR Ext, because it's too small to have any significance.
+ */
+ private static long readPcrValueFromPcrBytes(byte[] pcrBytes) {
+ return (pcrBytes[0] & 0xFFL) << 25
+ | (pcrBytes[1] & 0xFFL) << 17
+ | (pcrBytes[2] & 0xFFL) << 9
+ | (pcrBytes[3] & 0xFFL) << 1
+ | (pcrBytes[4] & 0xFFL) >> 7;
+ }
+
+ private TsUtil() {
+ // Prevent instantiation.
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java
new file mode 100644
index 0000000000..724eba1d9a
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.text.cea.CeaUtil;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.List;
+
+/** Consumes user data, outputting contained CEA-608/708 messages to a {@link TrackOutput}. */
+/* package */ final class UserDataReader {
+
+ private static final int USER_DATA_START_CODE = 0x0001B2;
+
+ private final List closedCaptionFormats;
+ private final TrackOutput[] outputs;
+
+ public UserDataReader(List closedCaptionFormats) {
+ this.closedCaptionFormats = closedCaptionFormats;
+ outputs = new TrackOutput[closedCaptionFormats.size()];
+ }
+
+ public void createTracks(
+ ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ idGenerator.generateNewId();
+ TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
+ Format channelFormat = closedCaptionFormats.get(i);
+ String channelMimeType = channelFormat.sampleMimeType;
+ Assertions.checkArgument(
+ MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
+ "Invalid closed caption mime type provided: " + channelMimeType);
+ output.format(
+ Format.createTextSampleFormat(
+ idGenerator.getFormatId(),
+ channelMimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ channelFormat.selectionFlags,
+ channelFormat.language,
+ channelFormat.accessibilityChannel,
+ /* drmInitData= */ null,
+ Format.OFFSET_SAMPLE_RELATIVE,
+ channelFormat.initializationData));
+ outputs[i] = output;
+ }
+ }
+
+ public void consume(long pesTimeUs, ParsableByteArray userDataPayload) {
+ if (userDataPayload.bytesLeft() < 9) {
+ return;
+ }
+ int userDataStartCode = userDataPayload.readInt();
+ int userDataIdentifier = userDataPayload.readInt();
+ int userDataTypeCode = userDataPayload.readUnsignedByte();
+ if (userDataStartCode == USER_DATA_START_CODE
+ && userDataIdentifier == CeaUtil.USER_DATA_IDENTIFIER_GA94
+ && userDataTypeCode == CeaUtil.USER_DATA_TYPE_CODE_MPEG_CC) {
+ CeaUtil.consumeCcData(pesTimeUs, userDataPayload, outputs);
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
index 4f2be71a69..7d6aa7024c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
@@ -32,17 +32,8 @@ import java.io.IOException;
*/
public final class WavExtractor implements Extractor {
- /**
- * Factory for {@link WavExtractor} instances.
- */
- public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
-
- @Override
- public Extractor[] createExtractors() {
- return new Extractor[] {new WavExtractor()};
- }
-
- };
+ /** Factory for {@link WavExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
/** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
private static final int MAX_INPUT_SIZE = 32 * 1024;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
index d0810a0629..284b750107 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.wav;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.audio.WavUtil;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -25,17 +26,10 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
-/*package*/ final class WavHeaderReader {
+/* package */ final class WavHeaderReader {
private static final String TAG = "WavHeaderReader";
- /** Integer PCM audio data. */
- private static final int TYPE_PCM = 0x0001;
- /** Float PCM audio data. */
- private static final int TYPE_FLOAT = 0x0003;
- /** Extended WAVE format. */
- private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
-
/**
* Peeks and returns a {@code WavHeader}.
*
@@ -54,21 +48,21 @@ import java.io.IOException;
// Attempt to read the RIFF chunk.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
- if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) {
+ if (chunkHeader.id != WavUtil.RIFF_FOURCC) {
return null;
}
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
int riffFormat = scratch.readInt();
- if (riffFormat != Util.getIntegerCodeForString("WAVE")) {
+ if (riffFormat != WavUtil.WAVE_FOURCC) {
Log.e(TAG, "Unsupported RIFF format: " + riffFormat);
return null;
}
// Skip chunks until we find the format chunk.
chunkHeader = ChunkHeader.peek(input, scratch);
- while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) {
+ while (chunkHeader.id != WavUtil.FMT_FOURCC) {
input.advancePeekPosition((int) chunkHeader.size);
chunkHeader = ChunkHeader.peek(input, scratch);
}
@@ -89,22 +83,9 @@ import java.io.IOException;
+ blockAlignment);
}
- @C.PcmEncoding int encoding;
- switch (type) {
- case TYPE_PCM:
- case TYPE_WAVE_FORMAT_EXTENSIBLE:
- encoding = Util.getPcmEncoding(bitsPerSample);
- break;
- case TYPE_FLOAT:
- encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
- break;
- default:
- Log.e(TAG, "Unsupported WAV format type: " + type);
- return null;
- }
-
+ @C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample);
if (encoding == C.ENCODING_INVALID) {
- Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type);
+ Log.e(TAG, "Unsupported WAV format: " + bitsPerSample + " bit/sample, type " + type);
return null;
}
@@ -158,6 +139,10 @@ import java.io.IOException;
wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
}
+ private WavHeaderReader() {
+ // Prevent instantiation.
+ }
+
/** Container for a WAV chunk header. */
private static final class ChunkHeader {
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 d822916bce..727dfaf1d5 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
@@ -30,10 +30,9 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
-/**
- * Information about a {@link MediaCodec} for a given mime type.
- */
+/** Information about a {@link MediaCodec} for a given mime type. */
@TargetApi(16)
+@SuppressWarnings("InlinedApi")
public final class MediaCodecInfo {
public static final String TAG = "MediaCodecInfo";
@@ -88,6 +87,8 @@ public final class MediaCodecInfo {
/** Whether this instance describes a passthrough codec. */
public final boolean passthrough;
+ private final boolean isVideo;
+
/**
* Creates an instance representing an audio passthrough decoder.
*
@@ -157,6 +158,12 @@ public final class MediaCodecInfo {
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
tunneling = capabilities != null && isTunneling(capabilities);
secure = forceSecure || (capabilities != null && isSecure(capabilities));
+ isVideo = MimeTypes.isVideo(mimeType);
+ }
+
+ @Override
+ public String toString() {
+ return name;
}
/**
@@ -182,6 +189,41 @@ public final class MediaCodecInfo {
: getMaxSupportedInstancesV23(capabilities);
}
+ /**
+ * Returns whether the decoder may support decoding the given {@code format}.
+ *
+ * @param format The input media format.
+ * @return Whether the decoder may support decoding the given {@code format}.
+ * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders.
+ */
+ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException {
+ if (!isCodecSupported(format.codecs)) {
+ return false;
+ }
+
+ if (isVideo) {
+ if (format.width <= 0 || format.height <= 0) {
+ return true;
+ }
+ if (Util.SDK_INT >= 21) {
+ return isVideoSizeAndRateSupportedV21(format.width, format.height, format.frameRate);
+ } else {
+ boolean isFormatSupported =
+ format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize();
+ if (!isFormatSupported) {
+ logNoSupport("legacyFrameSize, " + format.width + "x" + format.height);
+ }
+ return isFormatSupported;
+ }
+ } else { // Audio
+ return Util.SDK_INT < 21
+ || ((format.sampleRate == Format.NO_VALUE
+ || isAudioSampleRateSupportedV21(format.sampleRate))
+ && (format.channelCount == Format.NO_VALUE
+ || isAudioChannelCountSupportedV21(format.channelCount)));
+ }
+ }
+
/**
* Whether the decoder supports the given {@code codec}. If there is insufficient information to
* decide, returns true.
@@ -216,6 +258,63 @@ public final class MediaCodecInfo {
return false;
}
+ /**
+ * Returns whether it may be possible to adapt to playing a different format when the codec is
+ * configured to play media in the specified {@code format}. For adaptation to succeed, the codec
+ * must also be configured with appropriate maximum values and {@link
+ * #isSeamlessAdaptationSupported(Format, Format)} must return {@code true} for the old/new
+ * formats.
+ *
+ * @param format The format of media for which the decoder will be configured.
+ * @return Whether adaptation may be possible
+ */
+ public boolean isSeamlessAdaptationSupported(Format format) {
+ if (isVideo) {
+ return adaptive;
+ } else {
+ Pair codecProfileLevel =
+ MediaCodecUtil.getCodecProfileAndLevel(format.codecs);
+ return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE;
+ }
+ }
+
+ /**
+ * Returns whether it is possible to adapt the decoder seamlessly from {@code oldFormat} to {@code
+ * newFormat}.
+ *
+ * @param oldFormat The format being decoded.
+ * @param newFormat The new format.
+ * @return Whether it is possible to adapt the decoder seamlessly.
+ */
+ public boolean isSeamlessAdaptationSupported(Format oldFormat, Format newFormat) {
+ if (isVideo) {
+ return oldFormat.sampleMimeType.equals(newFormat.sampleMimeType)
+ && oldFormat.rotationDegrees == newFormat.rotationDegrees
+ && (adaptive
+ || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height))
+ && Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo);
+ } else {
+ if (!MimeTypes.AUDIO_AAC.equals(mimeType)
+ || !oldFormat.sampleMimeType.equals(newFormat.sampleMimeType)
+ || oldFormat.channelCount != newFormat.channelCount
+ || oldFormat.sampleRate != newFormat.sampleRate) {
+ return false;
+ }
+ // Check the codec profile levels support adaptation.
+ Pair oldCodecProfileLevel =
+ MediaCodecUtil.getCodecProfileAndLevel(oldFormat.codecs);
+ Pair newCodecProfileLevel =
+ MediaCodecUtil.getCodecProfileAndLevel(newFormat.codecs);
+ if (oldCodecProfileLevel == null || newCodecProfileLevel == null) {
+ return false;
+ }
+ int oldProfile = oldCodecProfileLevel.first;
+ int newProfile = newCodecProfileLevel.first;
+ return oldProfile == CodecProfileLevel.AACObjectXHE
+ && newProfile == CodecProfileLevel.AACObjectXHE;
+ }
+ }
+
/**
* Whether the decoder supports video with a given width, height and frame rate.
*
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 03a0b66661..3630977fca 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
@@ -21,8 +21,10 @@ import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto;
import android.media.MediaFormat;
+import android.os.Bundle;
import android.os.Looper;
import android.os.SystemClock;
+import android.support.annotation.CheckResult;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -46,6 +48,7 @@ import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
@@ -84,22 +87,64 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
public final String diagnosticInfo;
+ /**
+ * If the decoder failed to initialize and another decoder being used as a fallback also failed
+ * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if
+ * there was no fallback decoder or no suitable decoders were found.
+ */
+ public final @Nullable DecoderInitializationException fallbackDecoderInitializationException;
+
public DecoderInitializationException(Format format, Throwable cause,
boolean secureDecoderRequired, int errorCode) {
- super("Decoder init failed: [" + errorCode + "], " + format, cause);
- this.mimeType = format.sampleMimeType;
- this.secureDecoderRequired = secureDecoderRequired;
- this.decoderName = null;
- this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
+ this(
+ "Decoder init failed: [" + errorCode + "], " + format,
+ cause,
+ format.sampleMimeType,
+ secureDecoderRequired,
+ /* decoderName= */ null,
+ buildCustomDiagnosticInfo(errorCode),
+ /* fallbackDecoderInitializationException= */ null);
}
public DecoderInitializationException(Format format, Throwable cause,
boolean secureDecoderRequired, String decoderName) {
- super("Decoder init failed: " + decoderName + ", " + format, cause);
- this.mimeType = format.sampleMimeType;
+ this(
+ "Decoder init failed: " + decoderName + ", " + format,
+ cause,
+ format.sampleMimeType,
+ secureDecoderRequired,
+ decoderName,
+ Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null,
+ /* fallbackDecoderInitializationException= */ null);
+ }
+
+ private DecoderInitializationException(
+ String message,
+ Throwable cause,
+ String mimeType,
+ boolean secureDecoderRequired,
+ @Nullable String decoderName,
+ @Nullable String diagnosticInfo,
+ @Nullable DecoderInitializationException fallbackDecoderInitializationException) {
+ super(message, cause);
+ this.mimeType = mimeType;
this.secureDecoderRequired = secureDecoderRequired;
this.decoderName = decoderName;
- this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;
+ this.diagnosticInfo = diagnosticInfo;
+ this.fallbackDecoderInitializationException = fallbackDecoderInitializationException;
+ }
+
+ @CheckResult
+ private DecoderInitializationException copyWithFallbackException(
+ DecoderInitializationException fallbackException) {
+ return new DecoderInitializationException(
+ getMessage(),
+ getCause(),
+ mimeType,
+ secureDecoderRequired,
+ decoderName,
+ diagnosticInfo,
+ fallbackException);
}
@TargetApi(21)
@@ -117,6 +162,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
+ /** Indicates no codec operating rate should be set. */
+ protected static final float CODEC_OPERATING_RATE_UNSET = -1;
+
private static final String TAG = "MediaCodecRenderer";
/**
@@ -220,6 +268,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Nullable
private final DrmSessionManager drmSessionManager;
private final boolean playClearSamplesWithoutKeys;
+ private final float assumedMinimumCodecOperatingRate;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer flagsOnlyBuffer;
private final FormatHolder formatHolder;
@@ -230,7 +279,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private DrmSession drmSession;
private DrmSession pendingDrmSession;
private MediaCodec codec;
- private MediaCodecInfo codecInfo;
+ private float rendererOperatingRate;
+ private float codecOperatingRate;
+ private boolean codecConfiguredWithOperatingRate;
+ private @Nullable ArrayDeque availableCodecInfos;
+ private @Nullable DecoderInitializationException preferredDecoderInitializationException;
+ private @Nullable MediaCodecInfo codecInfo;
private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode;
private boolean codecNeedsDiscardToSpsWorkaround;
private boolean codecNeedsFlushWorkaround;
@@ -271,15 +325,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by
+ * this renderer are assumed to meet implicitly (i.e. without the operating rate being set
+ * explicitly using {@link MediaFormat#KEY_OPERATING_RATE}).
*/
- public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector,
+ public MediaCodecRenderer(
+ int trackType,
+ MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys) {
+ boolean playClearSamplesWithoutKeys,
+ float assumedMinimumCodecOperatingRate) {
super(trackType);
Assertions.checkState(Util.SDK_INT >= 16);
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate;
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
formatHolder = new FormatHolder();
@@ -287,6 +348,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputBufferInfo = new MediaCodec.BufferInfo();
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
+ codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
+ rendererOperatingRate = 1f;
}
@Override
@@ -318,18 +381,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throws DecoderQueryException;
/**
- * Returns a {@link MediaCodecInfo} for a given format.
+ * Returns a list of decoders that can decode media in the specified format, in priority order.
*
* @param mediaCodecSelector The decoder selector.
* @param format The format for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required.
- * @return A {@link MediaCodecInfo} describing the decoder to instantiate, or null if no
- * suitable decoder exists.
+ * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
- protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
- Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
- return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
+ protected List getDecoderInfos(
+ MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ return mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder);
}
/**
@@ -339,10 +402,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* @param codec The {@link MediaCodec} to configure.
* @param format The format for which the codec is being configured.
* @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
+ * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
+ * no codec operating rate should be set.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
- protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
- MediaCrypto crypto) throws DecoderQueryException;
+ protected abstract void configureCodec(
+ MediaCodecInfo codecInfo,
+ MediaCodec codec,
+ Format format,
+ MediaCrypto crypto,
+ float codecOperatingRate)
+ throws DecoderQueryException;
protected final void maybeInitCodec() throws ExoPlaybackException {
if (codec != null || format == null) {
@@ -369,78 +439,44 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
- }
-
- 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 + ".");
- }
+ if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
+ // Wait for keys.
+ return;
}
- } 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));
}
}
- if (!shouldInitCodec(codecInfo)) {
- return;
+ try {
+ if (!initCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder)) {
+ // We can't initialize a codec yet.
+ return;
+ }
+ } catch (DecoderInitializationException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
}
String codecName = codecInfo.name;
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
- codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
+ codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
- try {
- long codecInitializingTimestamp = SystemClock.elapsedRealtime();
- TraceUtil.beginSection("createCodec:" + codecName);
- codec = MediaCodec.createByCodecName(codecName);
- TraceUtil.endSection();
- TraceUtil.beginSection("configureCodec");
- configureCodec(codecInfo, codec, format, wrappedMediaCrypto);
- TraceUtil.endSection();
- TraceUtil.beginSection("startCodec");
- codec.start();
- TraceUtil.endSection();
- long codecInitializedTimestamp = SystemClock.elapsedRealtime();
- onCodecInitialized(codecName, codecInitializedTimestamp,
- codecInitializedTimestamp - codecInitializingTimestamp);
- getCodecBuffers();
- } catch (Exception e) {
- throwDecoderInitError(new DecoderInitializationException(format, e,
- drmSessionRequiresSecureDecoder, codecName));
- }
- codecHotswapDeadlineMs = getState() == STATE_STARTED
- ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET;
+ codecHotswapDeadlineMs =
+ getState() == STATE_STARTED
+ ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS)
+ : C.TIME_UNSET;
resetInputBuffer();
resetOutputBuffer();
waitingForFirstSyncFrame = true;
decoderCounters.decoderInitCount++;
}
- private void throwDecoderInitError(DecoderInitializationException e)
- throws ExoPlaybackException {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
- }
-
protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
return true;
}
@@ -467,9 +503,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
+ @Override
+ public final void setOperatingRate(float operatingRate) throws ExoPlaybackException {
+ rendererOperatingRate = operatingRate;
+ updateCodecOperatingRate();
+ }
+
@Override
protected void onDisabled() {
format = null;
+ availableCodecInfos = null;
try {
releaseCodec();
} finally {
@@ -512,6 +555,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecReceivedEos = false;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
+ codecConfiguredWithOperatingRate = false;
if (codec != null) {
decoderCounters.decoderReleaseCount++;
try {
@@ -622,6 +666,166 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
+ private boolean initCodecWithFallback(MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder)
+ throws DecoderInitializationException {
+ if (availableCodecInfos == null) {
+ try {
+ availableCodecInfos =
+ new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder));
+ preferredDecoderInitializationException = null;
+ } catch (DecoderQueryException e) {
+ throw new DecoderInitializationException(
+ format,
+ e,
+ drmSessionRequiresSecureDecoder,
+ DecoderInitializationException.DECODER_QUERY_ERROR);
+ }
+ }
+
+ if (availableCodecInfos.isEmpty()) {
+ throw new DecoderInitializationException(
+ format,
+ /* cause= */ null,
+ drmSessionRequiresSecureDecoder,
+ DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
+ }
+
+ while (true) {
+ MediaCodecInfo codecInfo = availableCodecInfos.peekFirst();
+ if (!shouldInitCodec(codecInfo)) {
+ return false;
+ }
+ try {
+ initCodec(codecInfo, crypto);
+ return true;
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to initialize decoder: " + codecInfo, e);
+ // This codec failed to initialize, so fall back to the next codec in the list (if any). We
+ // won't try to use this codec again unless there's a format change or the renderer is
+ // disabled and re-enabled.
+ availableCodecInfos.removeFirst();
+ DecoderInitializationException exception =
+ new DecoderInitializationException(
+ format, e, drmSessionRequiresSecureDecoder, codecInfo.name);
+ if (preferredDecoderInitializationException == null) {
+ preferredDecoderInitializationException = exception;
+ } else {
+ preferredDecoderInitializationException =
+ preferredDecoderInitializationException.copyWithFallbackException(exception);
+ }
+ if (availableCodecInfos.isEmpty()) {
+ throw preferredDecoderInitializationException;
+ }
+ }
+ }
+ }
+
+ private List getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder)
+ throws DecoderQueryException {
+ List codecInfos =
+ getDecoderInfos(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
+ if (codecInfos.isEmpty() && 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.
+ codecInfos = getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false);
+ if (!codecInfos.isEmpty()) {
+ Log.w(
+ TAG,
+ "Drm session requires secure decoder for "
+ + format.sampleMimeType
+ + ", but no secure decoder available. Trying to proceed with "
+ + codecInfos
+ + ".");
+ }
+ }
+ return codecInfos;
+ }
+
+ private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception {
+ long codecInitializingTimestamp;
+ long codecInitializedTimestamp;
+ MediaCodec codec = null;
+ String name = codecInfo.name;
+ updateCodecOperatingRate();
+ boolean configureWithOperatingRate = codecOperatingRate > assumedMinimumCodecOperatingRate;
+ try {
+ codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createCodec:" + name);
+ codec = MediaCodec.createByCodecName(name);
+ TraceUtil.endSection();
+ TraceUtil.beginSection("configureCodec");
+ configureCodec(
+ codecInfo,
+ codec,
+ format,
+ crypto,
+ configureWithOperatingRate ? codecOperatingRate : CODEC_OPERATING_RATE_UNSET);
+ codecConfiguredWithOperatingRate = configureWithOperatingRate;
+ TraceUtil.endSection();
+ TraceUtil.beginSection("startCodec");
+ codec.start();
+ TraceUtil.endSection();
+ codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ getCodecBuffers(codec);
+ } catch (Exception e) {
+ if (codec != null) {
+ resetCodecBuffers();
+ codec.release();
+ }
+ throw e;
+ }
+ this.codec = codec;
+ this.codecInfo = codecInfo;
+ long elapsed = codecInitializedTimestamp - codecInitializingTimestamp;
+ onCodecInitialized(name, codecInitializedTimestamp, elapsed);
+ }
+
+ private void getCodecBuffers(MediaCodec codec) {
+ if (Util.SDK_INT < 21) {
+ inputBuffers = codec.getInputBuffers();
+ outputBuffers = codec.getOutputBuffers();
+ }
+ }
+
+ private void resetCodecBuffers() {
+ if (Util.SDK_INT < 21) {
+ inputBuffers = null;
+ outputBuffers = null;
+ }
+ }
+
+ private ByteBuffer getInputBuffer(int inputIndex) {
+ if (Util.SDK_INT >= 21) {
+ return codec.getInputBuffer(inputIndex);
+ } else {
+ return inputBuffers[inputIndex];
+ }
+ }
+
+ private ByteBuffer getOutputBuffer(int outputIndex) {
+ if (Util.SDK_INT >= 21) {
+ return codec.getOutputBuffer(outputIndex);
+ } else {
+ return outputBuffers[outputIndex];
+ }
+ }
+
+ private boolean hasOutputBuffer() {
+ return outputIndex >= 0;
+ }
+
+ private void resetInputBuffer() {
+ inputIndex = C.INDEX_UNSET;
+ buffer.data = null;
+ }
+
+ private void resetOutputBuffer() {
+ outputIndex = C.INDEX_UNSET;
+ outputBuffer = null;
+ }
+
/**
* @return Whether it may be possible to feed more input data.
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
@@ -773,66 +977,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return true;
}
- private void getCodecBuffers() {
- if (Util.SDK_INT < 21) {
- inputBuffers = codec.getInputBuffers();
- outputBuffers = codec.getOutputBuffers();
- }
- }
-
- private void resetCodecBuffers() {
- if (Util.SDK_INT < 21) {
- inputBuffers = null;
- outputBuffers = null;
- }
- }
-
- private ByteBuffer getInputBuffer(int inputIndex) {
- if (Util.SDK_INT >= 21) {
- return codec.getInputBuffer(inputIndex);
- } else {
- return inputBuffers[inputIndex];
- }
- }
-
- private ByteBuffer getOutputBuffer(int outputIndex) {
- if (Util.SDK_INT >= 21) {
- return codec.getOutputBuffer(outputIndex);
- } else {
- return outputBuffers[outputIndex];
- }
- }
-
- private boolean hasOutputBuffer() {
- return outputIndex >= 0;
- }
-
- private void resetInputBuffer() {
- inputIndex = C.INDEX_UNSET;
- buffer.data = null;
- }
-
- private void resetOutputBuffer() {
- outputIndex = C.INDEX_UNSET;
- outputBuffer = null;
- }
-
- private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(DecoderInputBuffer buffer,
- int adaptiveReconfigurationBytes) {
- MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
- if (adaptiveReconfigurationBytes == 0) {
- return cryptoInfo;
- }
- // There must be at least one sub-sample, although numBytesOfClearData is permitted to be
- // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
- // bytes to the clear byte count of the first sub-sample.
- if (cryptoInfo.numBytesOfClearData == null) {
- cryptoInfo.numBytesOfClearData = new int[1];
- }
- cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
- return cryptoInfo;
- }
-
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
@@ -911,14 +1055,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
if (!keepingCodec) {
- if (codecReceivedBuffers) {
- // Signal end of stream and wait for any final output buffers before re-initialization.
- codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
- } else {
- // There aren't any final output buffers, so perform re-initialization immediately.
- releaseCodec();
- maybeInitCodec();
- }
+ reinitializeCodec();
+ } else {
+ updateCodecOperatingRate();
}
}
@@ -999,6 +1138,77 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return 0;
}
+ /**
+ * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate,
+ * current format and set of possible stream formats.
+ *
+ * The default implementation returns {@link #CODEC_OPERATING_RATE_UNSET}.
+ *
+ * @param operatingRate The renderer operating rate.
+ * @param format The format for which the codec is being configured.
+ * @param streamFormats The possible stream formats.
+ * @return The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if no codec operating
+ * rate should be set.
+ */
+ protected float getCodecOperatingRate(
+ float operatingRate, Format format, Format[] streamFormats) {
+ return CODEC_OPERATING_RATE_UNSET;
+ }
+
+ /**
+ * Updates the codec operating rate, and the codec itself if necessary.
+ *
+ * @throws ExoPlaybackException If an error occurs releasing or initializing a codec.
+ */
+ private void updateCodecOperatingRate() throws ExoPlaybackException {
+ if (format == null || Util.SDK_INT < 23) {
+ return;
+ }
+
+ float codecOperatingRate =
+ getCodecOperatingRate(rendererOperatingRate, format, getStreamFormats());
+ if (this.codecOperatingRate == codecOperatingRate) {
+ return;
+ }
+
+ this.codecOperatingRate = codecOperatingRate;
+ if (codec == null || codecReinitializationState != REINITIALIZATION_STATE_NONE) {
+ // Either no codec, or it's about to be reinitialized anyway.
+ } else if (codecOperatingRate == CODEC_OPERATING_RATE_UNSET
+ && codecConfiguredWithOperatingRate) {
+ // We need to clear the operating rate. The only way to do so is to instantiate a new codec
+ // instance. See [Internal ref: b/71987865].
+ reinitializeCodec();
+ } else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET
+ && (codecConfiguredWithOperatingRate
+ || codecOperatingRate > assumedMinimumCodecOperatingRate)) {
+ // We need to set the operating rate, either because we've set it previously or because it's
+ // above the assumed minimum rate.
+ Bundle codecParameters = new Bundle();
+ codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);
+ codec.setParameters(codecParameters);
+ codecConfiguredWithOperatingRate = true;
+ }
+ }
+
+ /**
+ * Starts the process of releasing the existing codec and initializing a new one. This may occur
+ * immediately, or be deferred until any final output buffers have been dequeued.
+ *
+ * @throws ExoPlaybackException If an error occurs releasing or initializing a codec.
+ */
+ private void reinitializeCodec() throws ExoPlaybackException {
+ availableCodecInfos = null;
+ if (codecReceivedBuffers) {
+ // Signal end of stream and wait for any final output buffers before re-initialization.
+ codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ // There aren't any final output buffers, so perform re-initialization immediately.
+ releaseCodec();
+ maybeInitCodec();
+ }
+ }
+
/**
* @return Whether it may be possible to drain more output data.
* @throws ExoPlaybackException If an error occurs draining the output buffer.
@@ -1209,6 +1419,32 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
+ private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(
+ DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) {
+ MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
+ if (adaptiveReconfigurationBytes == 0) {
+ return cryptoInfo;
+ }
+ // There must be at least one sub-sample, although numBytesOfClearData is permitted to be
+ // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
+ // bytes to the clear byte count of the first sub-sample.
+ if (cryptoInfo.numBytesOfClearData == null) {
+ cryptoInfo.numBytesOfClearData = new int[1];
+ }
+ cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
+ return cryptoInfo;
+ }
+
+ /**
+ * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before
+ * codec configuration.
+ */
+ private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() {
+ return "Amazon".equals(Util.MANUFACTURER)
+ && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
+ || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1
+ }
+
/**
* Returns whether the decoder is known to fail when flushed.
*
@@ -1272,20 +1508,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
- * Returns whether the decoder is known to handle the propagation of the
- * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
- *
- * If true is returned, the renderer will work around the issue by approximating end of stream
+ * Returns whether the decoder is known to handle the propagation of the {@link
+ * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
+ *
+ *
If true is returned, the renderer will work around the issue by approximating end of stream
* behavior without relying on the flag being propagated through to an output buffer by the
* underlying decoder.
*
- * @param name The name of the decoder.
+ * @param codecInfo Information about the {@link MediaCodec}.
* @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
* propagation incorrectly on the host device. False otherwise.
*/
- private static boolean codecNeedsEosPropagationWorkaround(String name) {
- return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name)
- || "OMX.allwinner.video.decoder.avc".equals(name));
+ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
+ String name = codecInfo.name;
+ return (Util.SDK_INT <= 17
+ && ("OMX.rk.video_decoder.avc".equals(name)
+ || "OMX.allwinner.video.decoder.avc".equals(name)))
+ || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
}
/**
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 1823c3a7ff..d92e93d45b 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
@@ -16,7 +16,10 @@
package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import java.util.Collections;
+import java.util.List;
/**
* Selector of {@link MediaCodec} instances.
@@ -24,32 +27,58 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep
public interface MediaCodecSelector {
/**
- * Default implementation of {@link MediaCodecSelector}.
+ * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for
+ * the given format.
*/
- MediaCodecSelector DEFAULT = new MediaCodecSelector() {
+ MediaCodecSelector DEFAULT =
+ new MediaCodecSelector() {
+ @Override
+ public List getDecoderInfos(String mimeType, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ List decoderInfos =
+ MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder);
+ return decoderInfos.isEmpty()
+ ? Collections.emptyList()
+ : Collections.singletonList(decoderInfos.get(0));
+ }
- @Override
- public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
- throws DecoderQueryException {
- return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
- }
-
- @Override
- public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
- return MediaCodecUtil.getPassthroughDecoderInfo();
- }
-
- };
+ @Override
+ public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
+ return MediaCodecUtil.getPassthroughDecoderInfo();
+ }
+ };
/**
- * Selects a decoder to instantiate for a given mime type.
+ * A {@link MediaCodecSelector} that returns a list of decoders in priority order, allowing
+ * fallback to less preferred decoders if initialization fails.
*
- * @param mimeType The mime type for which a decoder is required.
+ * Note: if a hardware-accelerated video decoder fails to initialize, this selector may provide
+ * a software video decoder to use as a fallback. Using software decoding can be inefficient, and
+ * the decoder may be too slow to keep up with the playback position.
+ */
+ MediaCodecSelector DEFAULT_WITH_FALLBACK =
+ new MediaCodecSelector() {
+ @Override
+ public List getDecoderInfos(String mimeType, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder);
+ }
+
+ @Override
+ public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
+ return MediaCodecUtil.getPassthroughDecoderInfo();
+ }
+ };
+
+ /**
+ * Returns a list of decoders that can decode media in the specified MIME type, in priority order.
+ *
+ * @param mimeType The MIME type for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required.
- * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
+ * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
- MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+ List getDecoderInfos(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException;
/**
@@ -58,6 +87,6 @@ public interface MediaCodecSelector {
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
+ @Nullable
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 347afe29fd..570d5074b7 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
@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -57,11 +58,9 @@ 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(GOOGLE_RAW_DECODER_NAME);
private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
+ private static final RawAudioCodecComparator RAW_AUDIO_CODEC_COMPARATOR =
+ new RawAudioCodecComparator();
private static final HashMap> decoderInfosCache = new HashMap<>();
@@ -75,6 +74,9 @@ public final class MediaCodecUtil {
private static final Map HEVC_CODEC_STRING_TO_PROFILE_LEVEL;
private static final String CODEC_ID_HEV1 = "hev1";
private static final String CODEC_ID_HVC1 = "hvc1";
+ // MP4A AAC.
+ private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE;
+ private static final String CODEC_ID_MP4A = "mp4a";
// Lazily initialized.
private static int maxH264DecodableFrameSize = -1;
@@ -103,22 +105,21 @@ 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.
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
+ * @throws DecoderQueryException If there was an error querying the available decoders.
*/
- public static MediaCodecInfo getPassthroughDecoderInfo() {
- // TODO: Return null if the raw decoder doesn't exist.
- return PASSTHROUGH_DECODER_INFO;
+ public static @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
+ MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false);
+ return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name);
}
/**
* Returns information about the preferred decoder for a given mime type.
*
- * @param mimeType The mime type.
+ * @param mimeType The MIME type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
- * @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 If there was an error querying the available decoders.
*/
public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
@@ -128,18 +129,18 @@ public final class MediaCodecUtil {
}
/**
- * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by
- * {@link MediaCodecList}.
+ * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link
+ * MediaCodecList}.
*
- * @param mimeType The mime type.
+ * @param mimeType The MIME type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
- * @return A list of all @{link MediaCodecInfo}s for the given mime type, in the order
- * given by {@link MediaCodecList}.
+ * @return A list of all {@link MediaCodecInfo}s for the given mime type, in the order given by
+ * {@link MediaCodecList}.
* @throws DecoderQueryException If there was an error querying the available decoders.
*/
- public static synchronized List getDecoderInfos(String mimeType,
- boolean secure) throws DecoderQueryException {
+ public static synchronized List getDecoderInfos(String mimeType, boolean secure)
+ throws DecoderQueryException {
CodecKey key = new CodecKey(mimeType, secure);
List cachedDecoderInfos = decoderInfosCache.get(key);
if (cachedDecoderInfos != null) {
@@ -165,7 +166,7 @@ public final class MediaCodecUtil {
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
decoderInfos.addAll(eac3DecoderInfos);
}
- applyWorkarounds(decoderInfos);
+ applyWorkarounds(mimeType, decoderInfos);
List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, unmodifiableDecoderInfos);
return unmodifiableDecoderInfos;
@@ -201,7 +202,7 @@ public final class MediaCodecUtil {
* @return A pair (profile constant, level constant) if {@code codec} is well-formed and
* recognized, or null otherwise
*/
- public static Pair getCodecProfileAndLevel(String codec) {
+ public static @Nullable Pair getCodecProfileAndLevel(String codec) {
if (codec == null) {
return null;
}
@@ -213,6 +214,8 @@ public final class MediaCodecUtil {
case CODEC_ID_AVC1:
case CODEC_ID_AVC2:
return getAvcProfileAndLevel(codec, parts);
+ case CODEC_ID_MP4A:
+ return getAacCodecProfileAndLevel(codec, parts);
default:
return null;
}
@@ -395,20 +398,12 @@ public final class MediaCodecUtil {
* Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the
* platform.
*
+ * @param mimeType The MIME type of input media.
* @param decoderInfos The list to modify.
*/
- 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;
- }
- }
+ private static void applyWorkarounds(String mimeType, List decoderInfos) {
+ if (MimeTypes.AUDIO_RAW.equals(mimeType)) {
+ Collections.sort(decoderInfos, RAW_AUDIO_CODEC_COMPARATOR);
}
}
@@ -455,8 +450,8 @@ public final class MediaCodecUtil {
return new Pair<>(profile, level);
}
- private static Pair getAvcProfileAndLevel(String codec, String[] codecsParts) {
- if (codecsParts.length < 2) {
+ private static Pair getAvcProfileAndLevel(String codec, String[] parts) {
+ if (parts.length < 2) {
// The codec has fewer parts than required by the AVC codec string format.
Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
return null;
@@ -464,14 +459,14 @@ public final class MediaCodecUtil {
Integer profileInteger;
Integer levelInteger;
try {
- if (codecsParts[1].length() == 6) {
+ if (parts[1].length() == 6) {
// Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal.
- profileInteger = Integer.parseInt(codecsParts[1].substring(0, 2), 16);
- levelInteger = Integer.parseInt(codecsParts[1].substring(4), 16);
- } else if (codecsParts.length >= 3) {
+ profileInteger = Integer.parseInt(parts[1].substring(0, 2), 16);
+ levelInteger = Integer.parseInt(parts[1].substring(4), 16);
+ } else if (parts.length >= 3) {
// Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal.
- profileInteger = Integer.parseInt(codecsParts[1]);
- levelInteger = Integer.parseInt(codecsParts[2]);
+ profileInteger = Integer.parseInt(parts[1]);
+ levelInteger = Integer.parseInt(parts[2]);
} else {
// We don't recognize the format.
Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
@@ -524,6 +519,31 @@ public final class MediaCodecUtil {
}
}
+ private static @Nullable Pair getAacCodecProfileAndLevel(
+ String codec, String[] parts) {
+ if (parts.length != 3) {
+ Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec);
+ return null;
+ }
+ try {
+ // Get the object type indication, which is a hexadecimal value (see RFC 6381/ISO 14496-1).
+ int objectTypeIndication = Integer.parseInt(parts[1], 16);
+ String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication);
+ if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
+ // For MPEG-4 audio this is followed by an audio object type indication as a decimal number.
+ int audioObjectTypeIndication = Integer.parseInt(parts[2]);
+ int profile = MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.get(audioObjectTypeIndication, -1);
+ if (profile != -1) {
+ // Level is set to zero in AAC decoder CodecProfileLevels.
+ return new Pair<>(profile, 0);
+ }
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec);
+ }
+ return null;
+ }
+
private interface MediaCodecListCompat {
/**
@@ -652,12 +672,41 @@ public final class MediaCodecUtil {
}
+ /**
+ * Comparator for ordering media codecs that handle {@link MimeTypes#AUDIO_RAW} to work around
+ * possible inconsistent behavior across different devices. A list sorted with this comparator has
+ * more preferred codecs first.
+ */
+ private static final class RawAudioCodecComparator implements Comparator {
+ @Override
+ public int compare(MediaCodecInfo a, MediaCodecInfo b) {
+ return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b);
+ }
+
+ private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) {
+ String name = mediaCodecInfo.name;
+ if (name.startsWith("OMX.google") || name.startsWith("c2.android")) {
+ // Prefer generic decoders over ones provided by the device.
+ return -1;
+ }
+ if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) {
+ // This decoder may modify the audio, so any other compatible decoders take precedence. See
+ // [Internal: b/62337687].
+ return 1;
+ }
+ return 0;
+ }
+ }
+
static {
AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);
AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain);
AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended);
AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh);
+ AVC_PROFILE_NUMBER_TO_CONST.put(110, CodecProfileLevel.AVCProfileHigh10);
+ AVC_PROFILE_NUMBER_TO_CONST.put(122, CodecProfileLevel.AVCProfileHigh422);
+ AVC_PROFILE_NUMBER_TO_CONST.put(244, CodecProfileLevel.AVCProfileHigh444);
AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray();
AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1);
@@ -706,6 +755,20 @@ public final class MediaCodecUtil {
HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H180", CodecProfileLevel.HEVCHighTierLevel6);
HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H183", CodecProfileLevel.HEVCHighTierLevel61);
HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H186", CodecProfileLevel.HEVCHighTierLevel62);
+
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray();
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(3, CodecProfileLevel.AACObjectSSR);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(4, CodecProfileLevel.AACObjectLTP);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(5, CodecProfileLevel.AACObjectHE);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(6, CodecProfileLevel.AACObjectScalable);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(17, CodecProfileLevel.AACObjectERLC);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(20, CodecProfileLevel.AACObjectERScalable);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(23, CodecProfileLevel.AACObjectLD);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(29, CodecProfileLevel.AACObjectHE_PS);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(39, CodecProfileLevel.AACObjectELD);
+ MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(42, CodecProfileLevel.AACObjectXHE);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
index 9137bad4fd..7e4861a8cb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata;
+import android.support.annotation.Nullable;
+
/**
* Decodes metadata from binary data.
*/
@@ -24,9 +26,8 @@ public interface MetadataDecoder {
* Decodes a {@link Metadata} element from the provided input buffer.
*
* @param inputBuffer The input buffer to decode.
- * @return The decoded metadata object.
- * @throws MetadataDecoderException If a problem occurred decoding the data.
+ * @return The decoded metadata object, or null if the metadata could not be decoded.
*/
- Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
-
+ @Nullable
+ Metadata decode(MetadataInputBuffer inputBuffer);
}
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 7d36d87a9e..152eb97e0c 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
@@ -19,12 +19,14 @@ import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
@@ -46,7 +48,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private final MetadataDecoderFactory decoderFactory;
private final MetadataOutput output;
- private final Handler outputHandler;
+ private final @Nullable Handler outputHandler;
private final FormatHolder formatHolder;
private final MetadataInputBuffer buffer;
private final Metadata[] pendingMetadata;
@@ -61,11 +63,11 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
- * looper associated with the application's main thread, which can be obtained using
- * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
- * called directly on the player's internal rendering thread.
+ * looper associated with the application's main thread, which can be obtained using {@link
+ * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
+ * directly on the player's internal rendering thread.
*/
- public MetadataRenderer(MetadataOutput output, Looper outputLooper) {
+ public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {
this(output, outputLooper, MetadataDecoderFactory.DEFAULT);
}
@@ -73,16 +75,17 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
- * looper associated with the application's main thread, which can be obtained using
- * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
- * called directly on the player's internal rendering thread.
+ * looper associated with the application's main thread, which can be obtained using {@link
+ * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
+ * directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
*/
- public MetadataRenderer(MetadataOutput output, Looper outputLooper,
- MetadataDecoderFactory decoderFactory) {
+ public MetadataRenderer(
+ MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output);
- this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
+ this.outputHandler =
+ outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = Assertions.checkNotNull(decoderFactory);
formatHolder = new FormatHolder();
buffer = new MetadataInputBuffer();
@@ -125,14 +128,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
} else {
buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
buffer.flip();
- try {
- int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;
- pendingMetadata[index] = decoder.decode(buffer);
- pendingMetadataTimestamps[index] = buffer.timeUs;
- pendingMetadataCount++;
- } catch (MetadataDecoderException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
- }
+ int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;
+ pendingMetadata[index] = decoder.decode(buffer);
+ pendingMetadataTimestamps[index] = buffer.timeUs;
+ pendingMetadataCount++;
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
index 5f521aada6..7d70d9de1c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.emsg;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -81,12 +83,12 @@ public final class EventMessage implements Metadata.Entry {
}
/* package */ EventMessage(Parcel in) {
- schemeIdUri = in.readString();
- value = in.readString();
+ schemeIdUri = castNonNull(in.readString());
+ value = castNonNull(in.readString());
presentationTimeUs = in.readLong();
durationMs = in.readLong();
id = in.readLong();
- messageData = in.createByteArray();
+ messageData = castNonNull(in.createByteArray());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
index ae78f712c7..53976da0d1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -29,11 +31,12 @@ public final class ApicFrame extends Id3Frame {
public static final String ID = "APIC";
public final String mimeType;
- public final String description;
+ public final @Nullable String description;
public final int pictureType;
public final byte[] pictureData;
- public ApicFrame(String mimeType, String description, int pictureType, byte[] pictureData) {
+ public ApicFrame(
+ String mimeType, @Nullable String description, int pictureType, byte[] pictureData) {
super(ID);
this.mimeType = mimeType;
this.description = description;
@@ -43,10 +46,10 @@ public final class ApicFrame extends Id3Frame {
/* package */ ApicFrame(Parcel in) {
super(ID);
- mimeType = in.readString();
- description = in.readString();
+ mimeType = castNonNull(in.readString());
+ description = castNonNull(in.readString());
pictureType = in.readInt();
- pictureData = in.createByteArray();
+ pictureData = castNonNull(in.createByteArray());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
index 129803299c..c48829ae54 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -33,8 +35,8 @@ public final class BinaryFrame extends Id3Frame {
}
/* package */ BinaryFrame(Parcel in) {
- super(in.readString());
- data = in.createByteArray();
+ super(castNonNull(in.readString()));
+ data = castNonNull(in.createByteArray());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
index aca530cdee..7ffb6d028c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -54,7 +56,7 @@ public final class ChapterFrame extends Id3Frame {
/* package */ ChapterFrame(Parcel in) {
super(ID);
- this.chapterId = in.readString();
+ this.chapterId = castNonNull(in.readString());
this.startTimeMs = in.readInt();
this.endTimeMs = in.readInt();
this.startOffset = in.readLong();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
index 56b08bbee3..c4a7c06e49 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
@@ -45,7 +47,7 @@ public final class ChapterTocFrame extends Id3Frame {
/* package */ ChapterTocFrame(Parcel in) {
super(ID);
- this.elementId = in.readString();
+ this.elementId = castNonNull(in.readString());
this.isRoot = in.readByte() != 0;
this.isOrdered = in.readByte() != 0;
this.children = in.createStringArray();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
index e84b776790..5666e48939 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -40,9 +42,9 @@ public final class CommentFrame extends Id3Frame {
/* package */ CommentFrame(Parcel in) {
super(ID);
- language = in.readString();
- description = in.readString();
- text = in.readString();
+ language = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ text = castNonNull(in.readString());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
index 8b665fce00..990d8f2e48 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -43,10 +45,10 @@ public final class GeobFrame extends Id3Frame {
/* package */ GeobFrame(Parcel in) {
super(ID);
- mimeType = in.readString();
- filename = in.readString();
- description = in.readString();
- data = in.createByteArray();
+ mimeType = castNonNull(in.readString());
+ filename = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ data = castNonNull(in.createByteArray());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
index ad24bac6c4..914fca5eef 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -88,7 +89,7 @@ public final class Id3Decoder implements MetadataDecoder {
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
- private final FramePredicate framePredicate;
+ private final @Nullable FramePredicate framePredicate;
public Id3Decoder() {
this(null);
@@ -97,12 +98,12 @@ public final class Id3Decoder implements MetadataDecoder {
/**
* @param framePredicate Determines which frames are decoded. May be null to decode all frames.
*/
- public Id3Decoder(FramePredicate framePredicate) {
+ public Id3Decoder(@Nullable FramePredicate framePredicate) {
this.framePredicate = framePredicate;
}
@Override
- public Metadata decode(MetadataInputBuffer inputBuffer) {
+ public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) {
ByteBuffer buffer = inputBuffer.data;
return decode(buffer.array(), buffer.limit());
}
@@ -112,9 +113,10 @@ public final class Id3Decoder implements MetadataDecoder {
*
* @param data The bytes to decode ID3 tags from.
* @param size Amount of bytes in {@code data} to read.
- * @return A {@link Metadata} object containing the decoded ID3 tags.
+ * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could
+ * not be decoded.
*/
- public Metadata decode(byte[] data, int size) {
+ public @Nullable Metadata decode(byte[] data, int size) {
List id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size);
@@ -156,7 +158,7 @@ public final class Id3Decoder implements MetadataDecoder {
* @param data A {@link ParsableByteArray} from which the header should be read.
* @return The parsed header, or null if the ID3 tag is unsupported.
*/
- private static Id3Header decodeHeader(ParsableByteArray data) {
+ private static @Nullable Id3Header decodeHeader(ParsableByteArray data) {
if (data.bytesLeft() < ID3_HEADER_LENGTH) {
Log.w(TAG, "Data too short to be an ID3 tag");
return null;
@@ -270,8 +272,12 @@ public final class Id3Decoder implements MetadataDecoder {
}
}
- private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
- boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) {
+ private static @Nullable Id3Frame decodeFrame(
+ int majorVersion,
+ ParsableByteArray id3Data,
+ boolean unsignedIntFrameSizeHack,
+ int frameHeaderSize,
+ @Nullable FramePredicate framePredicate) {
int frameId0 = id3Data.readUnsignedByte();
int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Data.readUnsignedByte();
@@ -399,8 +405,8 @@ public final class Id3Decoder implements MetadataDecoder {
}
}
- private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
- throws UnsupportedEncodingException {
+ private static @Nullable TextInformationFrame decodeTxxxFrame(
+ ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException {
if (frameSize < 1) {
// Frame is malformed.
return null;
@@ -422,8 +428,8 @@ public final class Id3Decoder implements MetadataDecoder {
return new TextInformationFrame("TXXX", description, value);
}
- private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
- int frameSize, String id) throws UnsupportedEncodingException {
+ private static @Nullable TextInformationFrame decodeTextInformationFrame(
+ ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException {
if (frameSize < 1) {
// Frame is malformed.
return null;
@@ -441,7 +447,7 @@ public final class Id3Decoder implements MetadataDecoder {
return new TextInformationFrame(id, null, value);
}
- private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)
+ private static @Nullable UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException {
if (frameSize < 1) {
// Frame is malformed.
@@ -552,7 +558,7 @@ public final class Id3Decoder implements MetadataDecoder {
return new ApicFrame(mimeType, description, pictureType, pictureData);
}
- private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
+ private static @Nullable CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException {
if (frameSize < 4) {
// Frame is malformed.
@@ -579,9 +585,14 @@ public final class Id3Decoder implements MetadataDecoder {
return new CommentFrame(language, description, text);
}
- private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
- int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize,
- FramePredicate framePredicate) throws UnsupportedEncodingException {
+ private static ChapterFrame decodeChapterFrame(
+ ParsableByteArray id3Data,
+ int frameSize,
+ int majorVersion,
+ boolean unsignedIntFrameSizeHack,
+ int frameHeaderSize,
+ @Nullable FramePredicate framePredicate)
+ throws UnsupportedEncodingException {
int framePosition = id3Data.getPosition();
int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition,
@@ -614,9 +625,14 @@ public final class Id3Decoder implements MetadataDecoder {
return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray);
}
- private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize,
- int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize,
- FramePredicate framePredicate) throws UnsupportedEncodingException {
+ private static ChapterTocFrame decodeChapterTOCFrame(
+ ParsableByteArray id3Data,
+ int frameSize,
+ int majorVersion,
+ boolean unsignedIntFrameSizeHack,
+ int frameHeaderSize,
+ @Nullable FramePredicate framePredicate)
+ throws UnsupportedEncodingException {
int framePosition = id3Data.getPosition();
int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
index 433c52bdcc..27ea833deb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.metadata.id3;
import com.google.android.exoplayer2.metadata.Metadata;
-import com.google.android.exoplayer2.util.Assertions;
/**
* Base class for ID3 frames.
@@ -29,7 +28,7 @@ public abstract class Id3Frame implements Metadata.Entry {
public final String id;
public Id3Frame(String id) {
- this.id = Assertions.checkNotNull(id);
+ this.id = id;
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
new file mode 100644
index 0000000000..c191676ce2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.id3;
+
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.util.Util;
+
+/** Internal ID3 frame that is intended for use by the player. */
+public final class InternalFrame extends Id3Frame {
+
+ public static final String ID = "----";
+
+ public final String domain;
+ public final String description;
+ public final String text;
+
+ public InternalFrame(String domain, String description, String text) {
+ super(ID);
+ this.domain = domain;
+ this.description = description;
+ this.text = text;
+ }
+
+ /* package */ InternalFrame(Parcel in) {
+ super(ID);
+ domain = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ text = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ InternalFrame other = (InternalFrame) obj;
+ return Util.areEqual(description, other.description)
+ && Util.areEqual(domain, other.domain)
+ && Util.areEqual(text, other.text);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (domain != null ? domain.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (text != null ? text.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": domain=" + domain + ", description=" + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(domain);
+ dest.writeString(text);
+ }
+
+ public static final Creator CREATOR =
+ new Creator() {
+
+ @Override
+ public InternalFrame createFromParcel(Parcel in) {
+ return new InternalFrame(in);
+ }
+
+ @Override
+ public InternalFrame[] newArray(int size) {
+ return new InternalFrame[size];
+ }
+ };
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
index 1b5ba67c11..a10ce229d9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -39,8 +41,8 @@ public final class PrivFrame extends Id3Frame {
/* package */ PrivFrame(Parcel in) {
super(ID);
- owner = in.readString();
- privateData = in.createByteArray();
+ owner = castNonNull(in.readString());
+ privateData = castNonNull(in.createByteArray());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
index dbab4ca7a8..62175ee90c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -25,19 +27,19 @@ import com.google.android.exoplayer2.util.Util;
*/
public final class TextInformationFrame extends Id3Frame {
- public final String description;
+ public final @Nullable String description;
public final String value;
- public TextInformationFrame(String id, String description, String value) {
+ public TextInformationFrame(String id, @Nullable String description, String value) {
super(id);
this.description = description;
this.value = value;
}
/* package */ TextInformationFrame(Parcel in) {
- super(in.readString());
+ super(castNonNull(in.readString()));
description = in.readString();
- value = in.readString();
+ value = castNonNull(in.readString());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
index f657eefc30..4b35131bea 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.metadata.id3;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -25,19 +27,19 @@ import com.google.android.exoplayer2.util.Util;
*/
public final class UrlLinkFrame extends Id3Frame {
- public final String description;
+ public final @Nullable String description;
public final String url;
- public UrlLinkFrame(String id, String description, String url) {
+ public UrlLinkFrame(String id, @Nullable String description, String url) {
super(id);
this.description = description;
this.url = url;
}
/* package */ UrlLinkFrame(Parcel in) {
- super(in.readString());
+ super(castNonNull(in.readString()));
description = in.readString();
- url = in.readString();
+ url = castNonNull(in.readString());
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
index 4050daa1cb..d6fc4f6c19 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.metadata.scte35;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
-import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -46,7 +45,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
}
@Override
- public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
// Internal timestamps adjustment.
if (timestampAdjuster == null
|| inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
index 98360b909c..20b7860784 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.util.Assertions;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -24,6 +25,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
/** Contains the necessary parameters for a download or remove action. */
public abstract class DownloadAction {
@@ -50,6 +53,48 @@ public abstract class DownloadAction {
throws IOException;
}
+ private static @Nullable Deserializer[] defaultDeserializers;
+
+ /** Returns available default {@link Deserializer}s. */
+ public static synchronized Deserializer[] getDefaultDeserializers() {
+ if (defaultDeserializers != null) {
+ return defaultDeserializers;
+ }
+ Deserializer[] deserializers = new Deserializer[4];
+ int count = 0;
+ deserializers[count++] = ProgressiveDownloadAction.DESERIALIZER;
+ Class> clazz;
+ // Full class names used for constructor args so the LINT rule triggers if any of them move.
+ try {
+ // LINT.IfChange
+ clazz = Class.forName("com.google.android.exoplayer2.source.dash.offline.DashDownloadAction");
+ // LINT.ThenChange(../../../../../../../../../dash/proguard-rules.txt)
+ deserializers[count++] = getDeserializer(clazz);
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ try {
+ // LINT.IfChange
+ clazz = Class.forName("com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction");
+ // LINT.ThenChange(../../../../../../../../../hls/proguard-rules.txt)
+ deserializers[count++] = getDeserializer(clazz);
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ try {
+ // LINT.IfChange
+ clazz =
+ Class.forName(
+ "com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction");
+ // LINT.ThenChange(../../../../../../../../../smoothstreaming/proguard-rules.txt)
+ deserializers[count++] = getDeserializer(clazz);
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ defaultDeserializers = Arrays.copyOf(Assertions.checkNotNull(deserializers), count);
+ return defaultDeserializers;
+ }
+
/**
* Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction,
* OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the
@@ -132,11 +177,16 @@ public abstract class DownloadAction {
return uri.equals(other.uri);
}
+ /** Returns keys of tracks to be downloaded. */
+ public List getKeys() {
+ return Collections.emptyList();
+ }
+
/** Serializes itself into the {@code output}. */
protected abstract void writeToStream(DataOutputStream output) throws IOException;
/** Creates a {@link Downloader} with the given parameters. */
- protected abstract Downloader createDownloader(
+ public abstract Downloader createDownloader(
DownloaderConstructorHelper downloaderConstructorHelper);
@Override
@@ -160,4 +210,9 @@ public abstract class DownloadAction {
return result;
}
+ private static Deserializer getDeserializer(Class> clazz)
+ throws NoSuchFieldException, IllegalAccessException {
+ Object value = clazz.getDeclaredField("DESERIALIZER").get(null);
+ return (Deserializer) Assertions.checkNotNull(value);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
index 0e2c5874b1..3b825bb14a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -108,7 +108,8 @@ public final class DownloadManager {
* @param upstreamDataSourceFactory A {@link DataSource.Factory} for creating data sources for
* downloading upstream data.
* @param actionSaveFile File to save active actions.
- * @param deserializers Used to deserialize {@link DownloadAction}s.
+ * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link
+ * DownloadAction#getDefaultDeserializers()} is used instead.
*/
public DownloadManager(
Cache cache,
@@ -127,7 +128,8 @@ public final class DownloadManager {
* @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
* for downloading data.
* @param actionFile The file in which active actions are saved.
- * @param deserializers Used to deserialize {@link DownloadAction}s.
+ * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link
+ * DownloadAction#getDefaultDeserializers()} is used instead.
*/
public DownloadManager(
DownloaderConstructorHelper constructorHelper,
@@ -149,7 +151,8 @@ public final class DownloadManager {
* @param maxSimultaneousDownloads The maximum number of simultaneous download tasks.
* @param minRetryCount The minimum number of times a task must be retried before failing.
* @param actionFile The file in which active actions are saved.
- * @param deserializers Used to deserialize {@link DownloadAction}s.
+ * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link
+ * DownloadAction#getDefaultDeserializers()} is used instead.
*/
public DownloadManager(
DownloaderConstructorHelper constructorHelper,
@@ -157,13 +160,12 @@ public final class DownloadManager {
int minRetryCount,
File actionFile,
Deserializer... deserializers) {
- Assertions.checkArgument(deserializers.length > 0, "At least one Deserializer is required.");
-
this.downloaderConstructorHelper = constructorHelper;
this.maxActiveDownloadTasks = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount;
this.actionFile = new ActionFile(actionFile);
- this.deserializers = deserializers;
+ this.deserializers =
+ deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers();
this.downloadsStopped = true;
tasks = new ArrayList<>();
@@ -262,12 +264,23 @@ public final class DownloadManager {
return task.id;
}
- /** Returns the current number of tasks. */
+ /** Returns the number of tasks. */
public int getTaskCount() {
Assertions.checkState(!released);
return tasks.size();
}
+ /** Returns the number of download tasks. */
+ public int getDownloadCount() {
+ int count = 0;
+ for (int i = 0; i < tasks.size(); i++) {
+ if (!tasks.get(i).action.isRemoveAction) {
+ count++;
+ }
+ }
+ return count;
+ }
+
/** Returns the state of a task, or null if no such task exists */
public @Nullable TaskState getTaskState(int taskId) {
Assertions.checkState(!released);
@@ -717,6 +730,11 @@ public final class DownloadManager {
return "CANCELING";
case STATE_STARTED_STOPPING:
return "STOPPING";
+ case STATE_QUEUED:
+ case STATE_STARTED:
+ case STATE_COMPLETED:
+ case STATE_CANCELED:
+ case STATE_FAILED:
default:
return TaskState.getStateString(currentState);
}
@@ -729,6 +747,11 @@ public final class DownloadManager {
case STATE_STARTED_CANCELING:
case STATE_STARTED_STOPPING:
return STATE_STARTED;
+ case STATE_QUEUED:
+ case STATE_STARTED:
+ case STATE_COMPLETED:
+ case STATE_CANCELED:
+ case STATE_FAILED:
default:
return currentState;
}
@@ -762,7 +785,7 @@ public final class DownloadManager {
private void stop() {
if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_STOPPING)) {
logd("Stopping", this);
- thread.interrupt();
+ cancelDownload();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
index 908aae481a..f7ca793b22 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
@@ -44,21 +44,20 @@ public abstract class DownloadService extends Service {
/** Starts a download service, adding a new {@link DownloadAction} to be executed. */
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
+ /** Reloads the download requirements. */
+ public static final String ACTION_RELOAD_REQUIREMENTS =
+ "com.google.android.exoplayer.downloadService.action.RELOAD_REQUIREMENTS";
+
/** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
private static final String ACTION_RESTART =
"com.google.android.exoplayer.downloadService.action.RESTART";
- /** Starts download tasks. */
- private static final String ACTION_START_DOWNLOADS =
- "com.google.android.exoplayer.downloadService.action.START_DOWNLOADS";
-
- /** Stops download tasks. */
- private static final String ACTION_STOP_DOWNLOADS =
- "com.google.android.exoplayer.downloadService.action.STOP_DOWNLOADS";
-
/** Key for the {@link DownloadAction} in an {@link #ACTION_ADD} intent. */
public static final String KEY_DOWNLOAD_ACTION = "download_action";
+ /** Invalid foreground notification id which can be used to run the service in the background. */
+ public static final int FOREGROUND_NOTIFICATION_ID_NONE = 0;
+
/**
* Key for a boolean flag in any intent to indicate whether the service was started in the
* foreground. If set, the service is guaranteed to call {@link #startForeground(int,
@@ -77,8 +76,10 @@ public abstract class DownloadService extends Service {
// tasks the resume more quickly than when relying on the scheduler alone.
private static final HashMap, RequirementsHelper>
requirementsHelpers = new HashMap<>();
+ private static final Requirements DEFAULT_REQUIREMENTS =
+ new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
- private final ForegroundNotificationUpdater foregroundNotificationUpdater;
+ private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater;
private final @Nullable String channelId;
private final @StringRes int channelName;
@@ -86,18 +87,31 @@ public abstract class DownloadService extends Service {
private DownloadManagerListener downloadManagerListener;
private int lastStartId;
private boolean startedInForeground;
+ private boolean taskRemoved;
/**
- * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
+ * Creates a DownloadService.
*
- * @param foregroundNotificationId The notification id for the foreground notification, must not
- * be 0.
+ * If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value
+ * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) then the service runs in the background. No
+ * foreground notification is displayed and {@link #getScheduler()} isn't called.
+ *
+ *
If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value
+ * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link
+ * #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link
+ * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ *
+ * @param foregroundNotificationId The notification id for the foreground notification, or {@link
+ * #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE})
*/
protected DownloadService(int foregroundNotificationId) {
this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL);
}
/**
+ * Creates a DownloadService which will run in the foreground. {@link
+ * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ *
* @param foregroundNotificationId The notification id for the foreground notification, must not
* be 0.
* @param foregroundNotificationUpdateInterval The maximum interval to update foreground
@@ -113,6 +127,9 @@ public abstract class DownloadService extends Service {
}
/**
+ * Creates a DownloadService which will run in the foreground. {@link
+ * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ *
* @param foregroundNotificationId The notification id for the foreground notification. Must not
* be 0.
* @param foregroundNotificationUpdateInterval The maximum interval between updates to the
@@ -130,8 +147,10 @@ public abstract class DownloadService extends Service {
@Nullable String channelId,
@StringRes int channelName) {
foregroundNotificationUpdater =
- new ForegroundNotificationUpdater(
- foregroundNotificationId, foregroundNotificationUpdateInterval);
+ foregroundNotificationId == 0
+ ? null
+ : new ForegroundNotificationUpdater(
+ foregroundNotificationId, foregroundNotificationUpdateInterval);
this.channelId = channelId;
this.channelName = channelName;
}
@@ -150,8 +169,7 @@ public abstract class DownloadService extends Service {
Class extends DownloadService> clazz,
DownloadAction downloadAction,
boolean foreground) {
- return new Intent(context, clazz)
- .setAction(ACTION_ADD)
+ return getIntent(context, clazz, ACTION_ADD)
.putExtra(KEY_DOWNLOAD_ACTION, downloadAction.toByteArray())
.putExtra(KEY_FOREGROUND, foreground);
}
@@ -160,9 +178,9 @@ public abstract class DownloadService extends Service {
* Starts the service, adding an action to be executed.
*
* @param context A {@link Context}.
- * @param clazz The concrete download service being targeted by the intent.
+ * @param clazz The concrete download service to be started.
* @param downloadAction The action to be executed.
- * @param foreground Whether this intent will be used to start the service in the foreground.
+ * @param foreground Whether the service is started in the foreground.
*/
public static void startWithAction(
Context context,
@@ -177,6 +195,32 @@ public abstract class DownloadService extends Service {
}
}
+ /**
+ * Starts the service without adding a new action. If there are any not finished actions and the
+ * requirements are met, the service resumes executing actions. Otherwise it stops immediately.
+ *
+ * @param context A {@link Context}.
+ * @param clazz The concrete download service to be started.
+ * @see #startForeground(Context, Class)
+ */
+ public static void start(Context context, Class extends DownloadService> clazz) {
+ context.startService(getIntent(context, clazz, ACTION_INIT));
+ }
+
+ /**
+ * Starts the service in the foreground without adding a new action. If there are any not finished
+ * actions and the requirements are met, the service resumes executing actions. Otherwise it stops
+ * immediately.
+ *
+ * @param context A {@link Context}.
+ * @param clazz The concrete download service to be started.
+ * @see #start(Context, Class)
+ */
+ public static void startForeground(Context context, Class extends DownloadService> clazz) {
+ Intent intent = getIntent(context, clazz, ACTION_INIT).putExtra(KEY_FOREGROUND, true);
+ Util.startForegroundService(context, intent);
+ }
+
@Override
public void onCreate() {
logd("onCreate");
@@ -187,33 +231,27 @@ public abstract class DownloadService extends Service {
downloadManager = getDownloadManager();
downloadManagerListener = new DownloadManagerListener();
downloadManager.addListener(downloadManagerListener);
-
- RequirementsHelper requirementsHelper;
- synchronized (requirementsHelpers) {
- Class extends DownloadService> clazz = getClass();
- requirementsHelper = requirementsHelpers.get(clazz);
- if (requirementsHelper == null) {
- requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz);
- requirementsHelpers.put(clazz, requirementsHelper);
- }
- }
- requirementsHelper.start();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
lastStartId = startId;
+ taskRemoved = false;
String intentAction = null;
if (intent != null) {
intentAction = intent.getAction();
startedInForeground |=
intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
}
+ // intentAction is null if the service is restarted or no action is specified.
+ if (intentAction == null) {
+ intentAction = ACTION_INIT;
+ }
logd("onStartCommand action: " + intentAction + " startId: " + startId);
switch (intentAction) {
case ACTION_INIT:
case ACTION_RESTART:
- // Do nothing. The RequirementsWatcher will start downloads when possible.
+ // Do nothing.
break;
case ACTION_ADD:
byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION);
@@ -227,35 +265,42 @@ public abstract class DownloadService extends Service {
}
}
break;
- case ACTION_STOP_DOWNLOADS:
- downloadManager.stopDownloads();
- break;
- case ACTION_START_DOWNLOADS:
- downloadManager.startDownloads();
+ case ACTION_RELOAD_REQUIREMENTS:
+ stopWatchingRequirements();
break;
default:
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
break;
}
+
+ Requirements requirements = getRequirements();
+ if (requirements.checkRequirements(this)) {
+ downloadManager.startDownloads();
+ } else {
+ downloadManager.stopDownloads();
+ }
+ maybeStartWatchingRequirements(requirements);
+
if (downloadManager.isIdle()) {
stop();
}
return START_STICKY;
}
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ logd("onTaskRemoved rootIntent: " + rootIntent);
+ taskRemoved = true;
+ }
+
@Override
public void onDestroy() {
logd("onDestroy");
- foregroundNotificationUpdater.stopPeriodicUpdates();
- downloadManager.removeListener(downloadManagerListener);
- if (downloadManager.getTaskCount() == 0) {
- synchronized (requirementsHelpers) {
- RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
- if (requirementsHelper != null) {
- requirementsHelper.stop();
- }
- }
+ if (foregroundNotificationUpdater != null) {
+ foregroundNotificationUpdater.stopPeriodicUpdates();
}
+ downloadManager.removeListener(downloadManagerListener);
+ maybeStopWatchingRequirements();
}
@Nullable
@@ -284,11 +329,13 @@ public abstract class DownloadService extends Service {
* device has network connectivity.
*/
protected Requirements getRequirements() {
- return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
+ return DEFAULT_REQUIREMENTS;
}
/**
- * Returns a notification to be displayed when this service running in the foreground.
+ * Should be overridden in the subclass if the service will be run in the foreground.
+ *
+ *
Returns a notification to be displayed when this service running in the foreground.
*
*
This method is called when there is a task state change and periodically while there are
* active tasks. The periodic update interval can be set using {@link #DownloadService(int,
@@ -301,7 +348,11 @@ public abstract class DownloadService extends Service {
* @param taskStates The states of all current tasks.
* @return The foreground notification to display.
*/
- protected abstract Notification getForegroundNotification(TaskState[] taskStates);
+ protected Notification getForegroundNotification(TaskState[] taskStates) {
+ throw new IllegalStateException(
+ getClass().getName()
+ + " is started in the foreground but getForegroundNotification() is not implemented.");
+ }
/**
* Called when the state of a task changes.
@@ -312,14 +363,50 @@ public abstract class DownloadService extends Service {
// Do nothing.
}
- private void stop() {
- foregroundNotificationUpdater.stopPeriodicUpdates();
- // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
- if (startedInForeground && Util.SDK_INT >= 26) {
- foregroundNotificationUpdater.showNotificationIfNotAlready();
+ private void maybeStartWatchingRequirements(Requirements requirements) {
+ if (downloadManager.getDownloadCount() == 0) {
+ return;
+ }
+ Class extends DownloadService> clazz = getClass();
+ RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz);
+ if (requirementsHelper == null) {
+ requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz);
+ requirementsHelpers.put(clazz, requirementsHelper);
+ requirementsHelper.start();
+ logd("started watching requirements");
+ }
+ }
+
+ private void maybeStopWatchingRequirements() {
+ if (downloadManager.getDownloadCount() > 0) {
+ return;
+ }
+ stopWatchingRequirements();
+ }
+
+ private void stopWatchingRequirements() {
+ RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
+ if (requirementsHelper != null) {
+ requirementsHelper.stop();
+ logd("stopped watching requirements");
+ }
+ }
+
+ private void stop() {
+ if (foregroundNotificationUpdater != null) {
+ foregroundNotificationUpdater.stopPeriodicUpdates();
+ // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
+ if (startedInForeground && Util.SDK_INT >= 26) {
+ foregroundNotificationUpdater.showNotificationIfNotAlready();
+ }
+ }
+ if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
+ stopSelf();
+ logd("stopSelf()");
+ } else {
+ boolean stopSelfResult = stopSelfResult(lastStartId);
+ logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
- boolean stopSelfResult = stopSelfResult(lastStartId);
- logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
private void logd(String message) {
@@ -328,19 +415,26 @@ public abstract class DownloadService extends Service {
}
}
+ private static Intent getIntent(
+ Context context, Class extends DownloadService> clazz, String action) {
+ return new Intent(context, clazz).setAction(action);
+ }
+
private final class DownloadManagerListener implements DownloadManager.Listener {
@Override
public void onInitialized(DownloadManager downloadManager) {
- // Do nothing.
+ maybeStartWatchingRequirements(getRequirements());
}
@Override
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
DownloadService.this.onTaskStateChanged(taskState);
- if (taskState.state == TaskState.STATE_STARTED) {
- foregroundNotificationUpdater.startPeriodicUpdates();
- } else {
- foregroundNotificationUpdater.update();
+ if (foregroundNotificationUpdater != null) {
+ if (taskState.state == TaskState.STATE_STARTED) {
+ foregroundNotificationUpdater.startPeriodicUpdates();
+ } else {
+ foregroundNotificationUpdater.update();
+ }
}
}
@@ -430,7 +524,12 @@ public abstract class DownloadService extends Service {
@Override
public void requirementsMet(RequirementsWatcher requirementsWatcher) {
- startServiceWithAction(DownloadService.ACTION_START_DOWNLOADS);
+ try {
+ notifyService();
+ } catch (Exception e) {
+ /* If we can't notify the service, don't stop the scheduler. */
+ return;
+ }
if (scheduler != null) {
scheduler.cancel();
}
@@ -438,7 +537,11 @@ public abstract class DownloadService extends Service {
@Override
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) {
- startServiceWithAction(DownloadService.ACTION_STOP_DOWNLOADS);
+ try {
+ notifyService();
+ } catch (Exception e) {
+ /* Do nothing. The service isn't running anyway. */
+ }
if (scheduler != null) {
String servicePackage = context.getPackageName();
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
@@ -448,10 +551,14 @@ public abstract class DownloadService extends Service {
}
}
- private void startServiceWithAction(String action) {
- Intent intent =
- new Intent(context, serviceClass).setAction(action).putExtra(KEY_FOREGROUND, true);
- Util.startForegroundService(context, intent);
+ private void notifyService() throws Exception {
+ Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
+ try {
+ context.startService(intent);
+ } catch (IllegalStateException e) {
+ /* startService will fail if the app is in the background and the service isn't running. */
+ throw new Exception(e);
+ }
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java
index 35d05fd43b..e688b7216f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java
@@ -22,9 +22,8 @@ import java.util.List;
* keys.
*
* @param The manifest type.
- * @param The stream key type.
*/
-public interface FilterableManifest {
+public interface FilterableManifest