diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 952dc85643..d9e375171d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -92,6 +92,8 @@ ([#273](https://github.com/google/ExoPlayer/issues/273)). * Fix where transitions to clipped media sources happened too early ([#4583](https://github.com/google/ExoPlayer/issues/4583)). +* Add `DataSpec.httpMethod` and update `HttpDataSource` implemenations to support + http HEAD method. Previously, only GET and POST were supported. ### 2.8.3 ### 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 dd8b6e0bed..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 @@ -473,8 +473,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { 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) { @@ -494,12 +494,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // 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; } 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 7cfac378f0..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 @@ -80,6 +80,7 @@ public final class CronetDataSourceTest { private DataSpec testDataSpec; private DataSpec testPostDataSpec; + private DataSpec testHeadDataSpec; private Map testResponseHeader; private UrlResponseInfo testUrlResponseInfo; @@ -120,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. @@ -332,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(); 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 60aa19e364..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 @@ -296,9 +296,14 @@ public class OkHttpDataSource extends BaseDataSource 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(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 9f95fc662c..366b6d8c67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -54,14 +54,33 @@ public final class DataSpec { */ public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; // 2 + /** The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. */ + @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD}) + public @interface HttpMethod {} + + public static final int HTTP_METHOD_GET = 1; + public static final int HTTP_METHOD_POST = 2; + public static final int HTTP_METHOD_HEAD = 3; + /** * The source from which data should be read. */ public final Uri uri; + /** - * Body for a POST request, null otherwise. + * The HTTP method, which will be used by {@link HttpDataSource} when requesting this DataSpec. + * This value will be ignored by non-http {@link DataSource}s. */ - public final @Nullable byte[] postBody; + public final @HttpMethod int httpMethod; + + /** + * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. + */ + public final @Nullable byte[] httpBody; + + /** @deprecated Use {@link #httpBody} instead. */ + @Deprecated public final @Nullable byte[] postBody; + /** * The absolute position of the data in the full stream. */ @@ -155,11 +174,13 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} may differ from {@link - * #absoluteStreamPosition}. + * Construct a {@link DataSpec} by inferring the {@link #httpMethod} based on the {@code postBody} + * parameter. If postBody is non-null, then httpMethod is set to {@link #HTTP_METHOD_POST}. If + * postBody is null, then httpMethod is set to {@link #HTTP_METHOD_GET}. * * @param uri {@link #uri}. - * @param postBody {@link #postBody}. + * @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the + * {@link #httpMethod}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}. * @param position {@link #position}. * @param length {@link #length}. @@ -174,11 +195,46 @@ public final class DataSpec { long length, @Nullable String key, @Flags int flags) { + this( + uri, + /* httpMethod= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET, + /* httpBody= */ postBody, + absoluteStreamPosition, + position, + length, + key, + flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} may differ from {@link + * #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); this.uri = uri; - this.postBody = postBody; + this.httpMethod = httpMethod; + this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; + this.postBody = this.httpBody; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; @@ -197,8 +253,48 @@ public final class DataSpec { @Override public String toString() { - return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition - + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; + return "DataSpec[" + + getHttpMethodString() + + " " + + uri + + ", " + + Arrays.toString(httpBody) + + ", " + + absoluteStreamPosition + + ", " + + position + + ", " + + length + + ", " + + key + + ", " + + flags + + "]"; + } + + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@link + * #httpMethod}. + */ + public final String getHttpMethodString() { + return getStringForHttpMethod(httpMethod); + } + + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@code + * httpMethod}. + */ + public static String getStringForHttpMethod(@HttpMethod int httpMethod) { + switch (httpMethod) { + case HTTP_METHOD_GET: + return "GET"; + case HTTP_METHOD_POST: + return "POST"; + case HTTP_METHOD_HEAD: + return "HEAD"; + default: + throw new AssertionError(httpMethod); + } } /** @@ -223,8 +319,15 @@ public final class DataSpec { if (offset == 0 && this.length == length) { return this; } else { - return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length, - key, flags); + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition + offset, + position + offset, + length, + key, + flags); } } @@ -235,6 +338,7 @@ public final class DataSpec { * @return The copied {@link DataSpec} with the specified Uri. */ public DataSpec withUri(Uri uri) { - return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags); + return new DataSpec( + uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 0cb7bac2b3..87ea36bd18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; @@ -61,6 +62,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private static final String TAG = "DefaultHttpDataSource"; private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. + private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307; + private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308; private static final long MAX_BYTES_TO_DRAIN = 2048; private static final Pattern CONTENT_RANGE_HEADER = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); @@ -417,7 +420,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou */ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { URL url = new URL(dataSpec.uri.toString()); - byte[] postBody = dataSpec.postBody; + @HttpMethod int httpMethod = dataSpec.httpMethod; + byte[] httpBody = dataSpec.httpBody; long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); @@ -425,28 +429,37 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou if (!allowCrossProtocolRedirects) { // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection // automatically. This is the behavior we want, so use it. - return makeConnection(url, postBody, position, length, allowGzip, true /* followRedirects */); + return makeConnection( + url, httpMethod, httpBody, position, length, allowGzip, true /* followRedirects */); } // We need to handle redirects ourselves to allow cross-protocol redirects. int redirectCount = 0; while (redirectCount++ <= MAX_REDIRECTS) { - HttpURLConnection connection = makeConnection( - url, postBody, position, length, allowGzip, false /* followRedirects */); + HttpURLConnection connection = + makeConnection( + url, httpMethod, httpBody, position, length, allowGzip, false /* followRedirects */); int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE - || responseCode == HttpURLConnection.HTTP_MOVED_PERM - || responseCode == HttpURLConnection.HTTP_MOVED_TEMP - || responseCode == HttpURLConnection.HTTP_SEE_OTHER - || (postBody == null - && (responseCode == 307 /* HTTP_TEMP_REDIRECT */ - || responseCode == 308 /* HTTP_PERM_REDIRECT */))) { - // For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into - // GET requests. For 307 and 308 POST requests are not redirected. - postBody = null; - String location = connection.getHeaderField("Location"); + String location = connection.getHeaderField("Location"); + if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) + && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER + || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT + || responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) { connection.disconnect(); url = handleRedirect(url, location); + } else if (httpMethod == DataSpec.HTTP_METHOD_POST + && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) { + // POST request follows the redirect and is transformed into a GET request. + connection.disconnect(); + httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; + url = handleRedirect(url, location); } else { return connection; } @@ -460,14 +473,22 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * Configures a connection and opens it. * * @param url The url to connect to. - * @param postBody The body data for a POST request. + * @param httpMethod The http method. + * @param httpBody The body data. * @param position The byte offset of the requested data. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param allowGzip Whether to allow the use of gzip. * @param followRedirects Whether to follow redirects. */ - private HttpURLConnection makeConnection(URL url, byte[] postBody, long position, - long length, boolean allowGzip, boolean followRedirects) throws IOException { + private HttpURLConnection makeConnection( + URL url, + @HttpMethod int httpMethod, + byte[] httpBody, + long position, + long length, + boolean allowGzip, + boolean followRedirects) + throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); @@ -491,18 +512,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou connection.setRequestProperty("Accept-Encoding", "identity"); } connection.setInstanceFollowRedirects(followRedirects); - connection.setDoOutput(postBody != null); - if (postBody != null) { - connection.setRequestMethod("POST"); - if (postBody.length == 0) { - connection.connect(); - } else { - connection.setFixedLengthStreamingMode(postBody.length); - connection.connect(); - OutputStream os = connection.getOutputStream(); - os.write(postBody); - os.close(); - } + connection.setDoOutput(httpBody != null); + connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); + if (httpBody != null) { + connection.setFixedLengthStreamingMode(httpBody.length); + connection.connect(); + OutputStream os = connection.getOutputStream(); + os.write(httpBody); + os.close(); } else { connection.connect(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 9a116b08a6..222d5385d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource; import com.google.android.exoplayer2.upstream.TransferListener; @@ -133,6 +134,7 @@ public final class CacheDataSource implements DataSource { private boolean currentDataSpecLengthUnset; private @Nullable Uri uri; private @Nullable Uri actualUri; + private @HttpMethod int httpMethod; private int flags; private @Nullable String key; private long readPosition; @@ -269,6 +271,7 @@ public final class CacheDataSource implements DataSource { key = cacheKeyFactory.buildCacheKey(dataSpec); uri = dataSpec.uri; actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); + httpMethod = dataSpec.httpMethod; flags = dataSpec.flags; readPosition = dataSpec.position; @@ -353,6 +356,7 @@ public final class CacheDataSource implements DataSource { public void close() throws IOException { uri = null; actualUri = null; + httpMethod = DataSpec.HTTP_METHOD_GET; notifyBytesRead(); try { closeCurrentSource(); @@ -397,7 +401,9 @@ public final class CacheDataSource implements DataSource { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. nextDataSource = upstreamDataSource; - nextDataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); + nextDataSpec = + new DataSpec( + uri, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags); } else if (nextSpan.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(nextSpan.file); @@ -419,7 +425,8 @@ public final class CacheDataSource implements DataSource { length = Math.min(length, bytesRemaining); } } - nextDataSpec = new DataSpec(uri, readPosition, length, key, flags); + nextDataSpec = + new DataSpec(uri, httpMethod, null, readPosition, readPosition, length, key, flags); if (cacheWriteDataSource != null) { nextDataSource = cacheWriteDataSource; } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 9052aceb93..1bdaa8e3fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -264,10 +264,16 @@ public final class CacheUtil { throwExceptionIfInterruptedOrCancelled(isCanceled); // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in // case the given length exceeds the end of input. - dataSpec = new DataSpec(dataSpec.uri, dataSpec.postBody, absoluteStreamPosition, - dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, - C.LENGTH_UNSET, dataSpec.key, - dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + dataSpec = + new DataSpec( + dataSpec.uri, + dataSpec.httpMethod, + dataSpec.httpBody, + absoluteStreamPosition, + dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, + C.LENGTH_UNSET, + dataSpec.key, + dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); long resolvedLength = dataSource.open(dataSpec); if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { counters.contentLength = dataSpec.absoluteStreamPosition + resolvedLength;