Commit b563b827 by olly Committed by Ian Baker

Add HttpUtil for tasks common to HttpDataSource implementations

Part of aligning HttpDataSource behavior will require adding
logic that's common across the DataSource implementations. This
change establishes a util class to house it, and moves a bit of
existing logic that's related and can be easily shared into it.

There is one small behavior change in this CL, which is that our
handling of Content-Range response headers can now parse the body
length if the "document size" part of the Content-Range is unknown,
for example "bytes 5-9/*". Previously the pattern we were matching
to required the "size" part to be set, for example "bytes 5-9/100",
despite the fact we don't need or use it.

PiperOrigin-RevId: 362396976
parent 93cc9164
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -29,13 +29,14 @@ import com.google.android.exoplayer2.upstream.DataSourceException; ...@@ -29,13 +29,14 @@ import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpUtil;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.net.HttpHeaders;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
...@@ -49,9 +50,6 @@ import java.util.List; ...@@ -49,9 +50,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.chromium.net.CronetEngine; import org.chromium.net.CronetEngine;
import org.chromium.net.CronetException; import org.chromium.net.CronetException;
import org.chromium.net.NetworkException; import org.chromium.net.NetworkException;
...@@ -295,13 +293,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -295,13 +293,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
/* package */ final UrlRequest.Callback urlRequestCallback; /* 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";
private static final String COOKIE = "Cookie";
private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
// The size of read buffer passed to cronet UrlRequest.read(). // The size of read buffer passed to cronet UrlRequest.read().
private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024; private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024;
...@@ -572,6 +563,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -572,6 +563,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Check for a valid response code. // Check for a valid response code.
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo); UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode(); int responseCode = responseInfo.getHttpStatusCode();
Map<String, List<String>> responseHeaders = responseInfo.getAllHeaders();
if (responseCode < 200 || responseCode > 299) { if (responseCode < 200 || responseCode > 299) {
byte[] responseBody; byte[] responseBody;
try { try {
...@@ -584,7 +576,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -584,7 +576,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
new InvalidResponseCodeException( new InvalidResponseCodeException(
responseCode, responseCode,
responseInfo.getHttpStatusText(), responseInfo.getHttpStatusText(),
responseInfo.getAllHeaders(), responseHeaders,
dataSpec, dataSpec,
responseBody); responseBody);
if (responseCode == 416) { if (responseCode == 416) {
...@@ -596,8 +588,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -596,8 +588,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Check for a valid content type. // Check for a valid content type.
Predicate<String> contentTypePredicate = this.contentTypePredicate; Predicate<String> contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) { if (contentTypePredicate != null) {
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE); @Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
if (contentType != null && !contentTypePredicate.apply(contentType)) { if (contentType != null && !contentTypePredicate.apply(contentType)) {
throw new InvalidContentTypeException(contentType, dataSpec); throw new InvalidContentTypeException(contentType, dataSpec);
} }
...@@ -613,7 +604,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -613,7 +604,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;
} else { } else {
long contentLength = getContentLength(responseInfo); long contentLength =
HttpUtil.getContentLength(
getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),
getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
bytesRemaining = bytesRemaining =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET; contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
} }
...@@ -811,23 +805,16 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -811,23 +805,16 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
requestBuilder.addHeader(key, value); requestBuilder.addHeader(key, value);
} }
if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) { if (dataSpec.httpBody != null && !requestHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
throw new IOException("HTTP request with non-empty body must set Content-Type"); throw new IOException("HTTP request with non-empty body must set Content-Type");
} }
// Set the Range header. @Nullable String rangeHeader = buildRangeRequestHeader(dataSpec.position, dataSpec.length);
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) { if (rangeHeader != null) {
StringBuilder rangeValue = new StringBuilder(); requestBuilder.addHeader(HttpHeaders.RANGE, rangeHeader);
rangeValue.append("bytes=");
rangeValue.append(dataSpec.position);
rangeValue.append("-");
if (dataSpec.length != C.LENGTH_UNSET) {
rangeValue.append(dataSpec.position + dataSpec.length - 1);
}
requestBuilder.addHeader("Range", rangeValue.toString());
} }
if (userAgent != null) { if (userAgent != null) {
requestBuilder.addHeader("User-Agent", userAgent); requestBuilder.addHeader(HttpHeaders.USER_AGENT, userAgent);
} }
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
// (adjusting the code as necessary). // (adjusting the code as necessary).
...@@ -973,52 +960,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -973,52 +960,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return false; return false;
} }
private static long getContentLength(UrlResponseInfo info) {
long contentLength = C.LENGTH_UNSET;
Map<String, List<String>> headers = info.getAllHeaders();
List<String> contentLengthHeaders = headers.get("Content-Length");
String contentLengthHeader = null;
if (!isEmpty(contentLengthHeaders)) {
contentLengthHeader = contentLengthHeaders.get(0);
if (!TextUtils.isEmpty(contentLengthHeader)) {
try {
contentLength = Long.parseLong(contentLengthHeader);
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
}
}
}
List<String> contentRangeHeaders = headers.get("Content-Range");
if (!isEmpty(contentRangeHeaders)) {
String contentRangeHeader = contentRangeHeaders.get(0);
Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader);
if (matcher.find()) {
try {
long contentLengthFromRange =
Long.parseLong(Assertions.checkNotNull(matcher.group(2)))
- Long.parseLong(Assertions.checkNotNull(matcher.group(1)))
+ 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
} else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody
// would increase it.
Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ "]");
contentLength = max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
private static String parseCookies(List<String> setCookieHeaders) { private static String parseCookies(List<String> setCookieHeaders) {
return TextUtils.join(";", setCookieHeaders); return TextUtils.join(";", setCookieHeaders);
} }
...@@ -1027,7 +968,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -1027,7 +968,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (TextUtils.isEmpty(cookies)) { if (TextUtils.isEmpty(cookies)) {
return; return;
} }
requestBuilder.addHeader(COOKIE, cookies); requestBuilder.addHeader(HttpHeaders.COOKIE, cookies);
} }
private static int getStatus(UrlRequest request) throws InterruptedException { private static int getStatus(UrlRequest request) throws InterruptedException {
...@@ -1044,9 +985,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -1044,9 +985,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return statusHolder[0]; return statusHolder[0];
} }
@EnsuresNonNullIf(result = false, expression = "#1") @Nullable
private static boolean isEmpty(@Nullable List<?> list) { private static String getFirstHeader(Map<String, List<String>> allHeaders, String headerName) {
return list == null || list.isEmpty(); @Nullable List<String> headers = allHeaders.get(headerName);
return headers != null && !headers.isEmpty() ? headers.get(0) : null;
} }
// Copy as much as possible from the src buffer into dst buffer. // Copy as much as possible from the src buffer into dst buffer.
...@@ -1094,8 +1036,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -1094,8 +1036,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return; return;
} }
List<String> setCookieHeaders = info.getAllHeaders().get(SET_COOKIE); @Nullable List<String> setCookieHeaders = info.getAllHeaders().get(HttpHeaders.SET_COOKIE);
if (isEmpty(setCookieHeaders)) { if (setCookieHeaders == null || setCookieHeaders.isEmpty()) {
request.followRedirect(); request.followRedirect();
return; return;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.okhttp; package com.google.android.exoplayer2.ext.okhttp;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.min; import static java.lang.Math.min;
...@@ -31,6 +32,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; ...@@ -31,6 +32,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.net.HttpHeaders;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
...@@ -397,18 +399,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -397,18 +399,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
builder.header(header.getKey(), header.getValue()); builder.header(header.getKey(), header.getValue());
} }
if (!(position == 0 && length == C.LENGTH_UNSET)) { @Nullable String rangeHeader = buildRangeRequestHeader(position, length);
String rangeRequest = "bytes=" + position + "-"; if (rangeHeader != null) {
if (length != C.LENGTH_UNSET) { builder.addHeader(HttpHeaders.RANGE, rangeHeader);
rangeRequest += (position + length - 1);
}
builder.addHeader("Range", rangeRequest);
} }
if (userAgent != null) { if (userAgent != null) {
builder.addHeader("User-Agent", userAgent); builder.addHeader(HttpHeaders.USER_AGENT, userAgent);
} }
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
builder.addHeader("Accept-Encoding", "identity"); builder.addHeader(HttpHeaders.ACCEPT_ENCODING, "identity");
} }
@Nullable RequestBody requestBody = null; @Nullable RequestBody requestBody = null;
......
...@@ -15,13 +15,12 @@ ...@@ -15,13 +15,12 @@
*/ */
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -29,6 +28,7 @@ import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; ...@@ -29,6 +28,7 @@ import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.net.HttpHeaders;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
...@@ -43,8 +43,6 @@ import java.util.Collections; ...@@ -43,8 +43,6 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
/** /**
...@@ -206,8 +204,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -206,8 +204,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307; private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308; private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
private static final long MAX_BYTES_TO_DRAIN = 2048; private static final long MAX_BYTES_TO_DRAIN = 2048;
private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private final boolean allowCrossProtocolRedirects; private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis; private final int connectTimeoutMillis;
...@@ -401,7 +397,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -401,7 +397,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length; bytesToRead = dataSpec.length;
} else { } else {
long contentLength = getContentLength(connection); long contentLength =
HttpUtil.getContentLength(
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
: C.LENGTH_UNSET; : C.LENGTH_UNSET;
} }
...@@ -577,17 +576,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -577,17 +576,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
connection.setRequestProperty(property.getKey(), property.getValue()); connection.setRequestProperty(property.getKey(), property.getValue());
} }
if (!(position == 0 && length == C.LENGTH_UNSET)) { @Nullable String rangeHeader = buildRangeRequestHeader(position, length);
String rangeRequest = "bytes=" + position + "-"; if (rangeHeader != null) {
if (length != C.LENGTH_UNSET) { connection.setRequestProperty(HttpHeaders.RANGE, rangeHeader);
rangeRequest += (position + length - 1);
}
connection.setRequestProperty("Range", rangeRequest);
} }
if (userAgent != null) { if (userAgent != null) {
connection.setRequestProperty("User-Agent", userAgent); connection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent);
} }
connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity"); connection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING, allowGzip ? "gzip" : "identity");
connection.setInstanceFollowRedirects(followRedirects); connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(httpBody != null); connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
...@@ -640,52 +636,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -640,52 +636,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
} }
/** /**
* Attempts to extract the length of the content from the response headers of an open connection.
*
* @param connection The open connection.
* @return The extracted length, or {@link C#LENGTH_UNSET}.
*/
private static long getContentLength(HttpURLConnection connection) {
long contentLength = C.LENGTH_UNSET;
String contentLengthHeader = connection.getHeaderField("Content-Length");
if (!TextUtils.isEmpty(contentLengthHeader)) {
try {
contentLength = Long.parseLong(contentLengthHeader);
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
}
}
String contentRangeHeader = connection.getHeaderField("Content-Range");
if (!TextUtils.isEmpty(contentRangeHeader)) {
Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
if (matcher.find()) {
try {
long contentLengthFromRange =
Long.parseLong(checkNotNull(matcher.group(2)))
- Long.parseLong(checkNotNull(matcher.group(1)))
+ 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
} else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody would
// increase it.
Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ "]");
contentLength = max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
/**
* Attempts to skip the specified number of bytes in full. * Attempts to skip the specified number of bytes in full.
* *
* @param bytesToSkip The number of bytes to skip. * @param bytesToSkip The number of bytes to skip.
......
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Log;
import com.google.common.net.HttpHeaders;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Utility methods for HTTP. */
public final class HttpUtil {
private static final String TAG = "HttpUtil";
private static final Pattern CONTENT_RANGE_WITH_START_AND_END =
Pattern.compile("bytes (\\d+)-(\\d+)/(?:\\d+|\\*)");
/** Class only contains static methods. */
private HttpUtil() {}
/**
* Builds a {@link HttpHeaders#RANGE Range header} for the given position and length.
*
* @param position The request position.
* @param length The request length, or {@link C#LENGTH_UNSET} if the request is unbounded.
* @return The corresponding range header, or {@code null} if a header is unnecessary because the
* whole resource is being requested.
*/
@Nullable
public static String buildRangeRequestHeader(long position, long length) {
if (position == 0 && length == C.LENGTH_UNSET) {
return null;
}
StringBuilder rangeValue = new StringBuilder();
rangeValue.append("bytes=");
rangeValue.append(position);
rangeValue.append("-");
if (length != C.LENGTH_UNSET) {
rangeValue.append(position + length - 1);
}
return rangeValue.toString();
}
/**
* Attempts to parse the length of a response body from the corresponding response headers.
*
* @param contentLengthHeader The {@link HttpHeaders#CONTENT_LENGTH Content-Length header}, or
* {@code null} if not set.
* @param contentRangeHeader The {@link HttpHeaders#CONTENT_RANGE Content-Range header}, or {@code
* null} if not set.
* @return The length of the response body, or {@link C#LENGTH_UNSET} if it could not be
* determined.
*/
public static long getContentLength(
@Nullable String contentLengthHeader, @Nullable String contentRangeHeader) {
long contentLength = C.LENGTH_UNSET;
if (!TextUtils.isEmpty(contentLengthHeader)) {
try {
contentLength = Long.parseLong(contentLengthHeader);
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
}
}
if (!TextUtils.isEmpty(contentRangeHeader)) {
Matcher matcher = CONTENT_RANGE_WITH_START_AND_END.matcher(contentRangeHeader);
if (matcher.matches()) {
try {
long contentLengthFromRange =
Long.parseLong(checkNotNull(matcher.group(2)))
- Long.parseLong(checkNotNull(matcher.group(1)))
+ 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
} else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody would
// increase it.
Log.w(
TAG,
"Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader + "]");
contentLength = max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.upstream.HttpUtil.getContentLength;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link DefaultHttpDataSource}. */
@RunWith(AndroidJUnit4.class)
public class HttpUtilTest {
@Test
public void buildRangeRequestHeader_buildsHeader() {
assertThat(buildRangeRequestHeader(0, C.LENGTH_UNSET)).isNull();
assertThat(buildRangeRequestHeader(1, C.LENGTH_UNSET)).isEqualTo("bytes=1-");
assertThat(buildRangeRequestHeader(0, 5)).isEqualTo("bytes=0-4");
assertThat(buildRangeRequestHeader(5, 15)).isEqualTo("bytes=5-19");
}
@Test
public void getContentLength_bothHeadersMissing_returnsUnset() {
assertThat(getContentLength(null, null)).isEqualTo(C.LENGTH_UNSET);
assertThat(getContentLength("", "")).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void getContentLength_onlyContentLengthHeaderSet_returnsCorrectValue() {
assertThat(getContentLength("5", null)).isEqualTo(5);
assertThat(getContentLength("5", "")).isEqualTo(5);
}
@Test
public void getContentLength_onlyContentRangeHeaderSet_returnsCorrectValue() {
assertThat(getContentLength(null, "bytes 5-9/100")).isEqualTo(5);
assertThat(getContentLength("", "bytes 5-9/100")).isEqualTo(5);
assertThat(getContentLength("", "bytes 5-9/*")).isEqualTo(5);
}
@Test
public void getContentLength_bothHeadersSet_returnsCorrectValue() {
assertThat(getContentLength("5", "bytes 5-9/100")).isEqualTo(5);
}
@Test
public void getContentLength_headersInconsistent_returnsLargerValue() {
assertThat(getContentLength("10", "bytes 0-4/100")).isEqualTo(10);
assertThat(getContentLength("5", "bytes 0-9/100")).isEqualTo(10);
}
@Test
public void getContentLength_ignoresInvalidValues() {
assertThat(getContentLength("Invalid", "Invalid")).isEqualTo(C.LENGTH_UNSET);
assertThat(getContentLength("Invalid", "bytes 5-9/100")).isEqualTo(5);
assertThat(getContentLength("5", "Invalid")).isEqualTo(5);
}
@Test
public void getContentLength_ignoresUnhandledRangeUnits() {
assertThat(getContentLength(null, "unhandled 5-9/100")).isEqualTo(C.LENGTH_UNSET);
assertThat(getContentLength("10", "unhandled 0-4/100")).isEqualTo(10);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment