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 61b287e5fd..e15c5180aa 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 @@ -565,6 +565,16 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int responseCode = responseInfo.getHttpStatusCode(); Map> responseHeaders = responseInfo.getAllHeaders(); if (responseCode < 200 || responseCode > 299) { + if (responseCode == 416) { + long documentSize = + HttpUtil.getDocumentSize(getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE)); + if (dataSpec.position == documentSize) { + opened = true; + transferStarted(dataSpec); + return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0; + } + } + byte[] responseBody; try { responseBody = readResponseBody(); 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 e095465043..c2dadc4521 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 @@ -28,6 +28,7 @@ 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.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpUtil; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -273,7 +274,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; - this.bytesRead = 0; + bytesRead = 0; + bytesToRead = 0; transferInitializing(dataSpec); Request request = makeRequest(dataSpec); @@ -298,6 +300,16 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { // Check for a valid response code. if (!response.isSuccessful()) { + if (responseCode == 416) { + long documentSize = + HttpUtil.getDocumentSize(response.headers().get(HttpHeaders.CONTENT_RANGE)); + if (dataSpec.position == documentSize) { + opened = true; + transferStarted(dataSpec); + return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0; + } + } + byte[] errorResponseBody; try { errorResponseBody = Util.toByteArray(Assertions.checkNotNull(responseByteStream)); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 15a79c5677..8c8b07607a 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -330,7 +330,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; - this.bytesRead = 0; + bytesRead = 0; + bytesToRead = 0; transferInitializing(dataSpec); try { @@ -359,6 +360,16 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou // Check for a valid response code. if (responseCode < 200 || responseCode > 299) { Map> headers = connection.getHeaderFields(); + if (responseCode == 416) { + long documentSize = + HttpUtil.getDocumentSize(connection.getHeaderField(HttpHeaders.CONTENT_RANGE)); + if (dataSpec.position == documentSize) { + opened = true; + transferStarted(dataSpec); + return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0; + } + } + @Nullable InputStream errorStream = connection.getErrorStream(); byte[] errorResponseBody; try { @@ -371,7 +382,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou InvalidResponseCodeException exception = new InvalidResponseCodeException( responseCode, responseMessage, headers, dataSpec, errorResponseBody); - if (responseCode == 416) { exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java index 01eed29e84..ac433009a7 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java @@ -32,6 +32,8 @@ 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+|\\*)"); + private static final Pattern CONTENT_RANGE_WITH_SIZE = + Pattern.compile("bytes (?:(?:\\d+-\\d+)|\\*)/(\\d+)"); /** Class only contains static methods. */ private HttpUtil() {} @@ -59,6 +61,22 @@ public final class HttpUtil { return rangeValue.toString(); } + /** + * Attempts to parse the document size from a {@link HttpHeaders#CONTENT_RANGE Content-Range + * header}. + * + * @param contentRangeHeader The {@link HttpHeaders#CONTENT_RANGE Content-Range header}, or {@code + * null} if not set. + * @return The document size, or {@link C#LENGTH_UNSET} if it could not be determined. + */ + public static long getDocumentSize(@Nullable String contentRangeHeader) { + if (TextUtils.isEmpty(contentRangeHeader)) { + return C.LENGTH_UNSET; + } + Matcher matcher = CONTENT_RANGE_WITH_SIZE.matcher(contentRangeHeader); + return matcher.matches() ? Long.parseLong(checkNotNull(matcher.group(1))) : C.LENGTH_UNSET; + } + /** * Attempts to parse the length of a response body from the corresponding response headers. * diff --git a/library/common/src/test/java/com/google/android/exoplayer2/upstream/HttpUtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/upstream/HttpUtilTest.java index cb4cc0b623..52c605bd05 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/upstream/HttpUtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/upstream/HttpUtilTest.java @@ -17,6 +17,7 @@ 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.android.exoplayer2.upstream.HttpUtil.getDocumentSize; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -78,4 +79,22 @@ public class HttpUtilTest { assertThat(getContentLength(null, "unhandled 5-9/100")).isEqualTo(C.LENGTH_UNSET); assertThat(getContentLength("10", "unhandled 0-4/100")).isEqualTo(10); } + + @Test + public void getDocumentSize_noHeader_returnsUnset() { + assertThat(getDocumentSize(null)).isEqualTo(C.LENGTH_UNSET); + assertThat(getDocumentSize("")).isEqualTo(C.LENGTH_UNSET); + } + + @Test + public void getDocumentSize_returnsSize() { + assertThat(getDocumentSize("bytes */20")).isEqualTo(20); + assertThat(getDocumentSize("bytes 0-4/20")).isEqualTo(20); + } + + @Test + public void getDocumentSize_ignoresUnhandledRangeUnits() { + assertThat(getDocumentSize("unhandled */20")).isEqualTo(C.LENGTH_UNSET); + assertThat(getDocumentSize("unhandled 0-4/20")).isEqualTo(C.LENGTH_UNSET); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index 9f9f11b67d..24d2c728a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -47,16 +47,17 @@ public final class ByteArrayDataSource extends BaseDataSource { public long open(DataSpec dataSpec) throws IOException { uri = dataSpec.uri; transferInitializing(dataSpec); - if (dataSpec.position >= data.length) { + if (dataSpec.position > data.length) { throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } readPosition = (int) dataSpec.position; - bytesRemaining = - (int) - (dataSpec.length == C.LENGTH_UNSET ? data.length - dataSpec.position : dataSpec.length); + bytesRemaining = data.length - (int) dataSpec.position; + if (dataSpec.length != C.LENGTH_UNSET) { + bytesRemaining = (int) min(bytesRemaining, dataSpec.length); + } opened = true; transferStarted(dataSpec); - return bytesRemaining; + return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : bytesRemaining; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java index c370630815..21c942974a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java @@ -74,17 +74,17 @@ public final class ByteArrayDataSourceTest { // And with bound. readTestData(TEST_DATA, 1, 6, 1, 0, 1, false); // Read from the last possible offset without bound. - readTestData(TEST_DATA, TEST_DATA.length - 1, C.LENGTH_UNSET, 1, 0, 1, false); + readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNSET, 1, 0, 1, false); // And with bound. - readTestData(TEST_DATA, TEST_DATA.length - 1, 1, 1, 0, 1, false); + readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, false); } @Test public void readFromInvalidOffsets() { // Read from first invalid offset and check failure without bound. - readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNSET, 1, 0, 1, true); + readTestData(TEST_DATA, TEST_DATA.length + 1, C.LENGTH_UNSET, 1, 0, 1, true); // And with bound. - readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true); + readTestData(TEST_DATA, TEST_DATA.length + 1, 1, 1, 0, 1, true); } /** @@ -100,8 +100,10 @@ public final class ByteArrayDataSourceTest { */ private void readTestData(byte[] testData, int dataOffset, int dataLength, int outputBufferLength, int writeOffset, int maxReadLength, boolean expectFailOnOpen) { - int expectedFinalBytesRead = - dataLength == C.LENGTH_UNSET ? (testData.length - dataOffset) : dataLength; + int expectedFinalBytesRead = testData.length - dataOffset; + if (dataLength != C.LENGTH_UNSET) { + expectedFinalBytesRead = min(expectedFinalBytesRead, dataLength); + } ByteArrayDataSource dataSource = new ByteArrayDataSource(testData); boolean opened = false; try { @@ -111,7 +113,8 @@ public final class ByteArrayDataSourceTest { assertThat(expectFailOnOpen).isFalse(); // Verify the resolved length is as we expect. - assertThat(length).isEqualTo(expectedFinalBytesRead); + assertThat(length) + .isEqualTo(dataLength != C.LENGTH_UNSET ? dataLength : expectedFinalBytesRead); byte[] outputBuffer = new byte[outputBufferLength]; int accumulatedBytesRead = 0; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java index bf39e8e309..857b22a5e7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java @@ -232,27 +232,17 @@ public abstract class DataSourceContractTest { DataSpec dataSpec = new DataSpec.Builder().setUri(resource.getUri()).setPosition(resourceLength).build(); try { - try { - long length = dataSource.open(dataSpec); - // The DataSource.open() contract requires the returned length to equal the length in the - // DataSpec if set. This is true even though the DataSource implementation may know that - // fewer bytes will be read in this case. - if (length != C.LENGTH_UNSET) { - assertThat(length).isEqualTo(0); - } + long length = dataSource.open(dataSpec); + byte[] data = + unboundedReadsAreIndefinite() ? Util.EMPTY_BYTE_ARRAY : Util.readToEnd(dataSource); - try { - byte[] data = - unboundedReadsAreIndefinite() ? Util.EMPTY_BYTE_ARRAY : Util.readToEnd(dataSource); - assertThat(data).isEmpty(); - } catch (IOException e) { - // TODO: Remove this catch once the one below is removed. - throw new RuntimeException(e); - } - } catch (IOException e) { - // TODO: Remove this catch and require that implementations do not throw. - assertThat(DataSourceException.isCausedByPositionOutOfRange(e)).isTrue(); + // The DataSource.open() contract requires the returned length to equal the length in the + // DataSpec if set. This is true even though the DataSource implementation may know that + // fewer bytes will be read in this case. + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(0); } + assertThat(data).isEmpty(); } finally { dataSource.close(); } @@ -278,25 +268,15 @@ public abstract class DataSourceContractTest { .setLength(1) .build(); try { - try { - long length = dataSource.open(dataSpec); - // The DataSource.open() contract requires the returned length to equal the length in the - // DataSpec if set. This is true even though the DataSource implementation may know that - // fewer bytes will be read in this case. - assertThat(length).isEqualTo(1); + long length = dataSource.open(dataSpec); + byte[] data = + unboundedReadsAreIndefinite() ? Util.EMPTY_BYTE_ARRAY : Util.readToEnd(dataSource); - try { - byte[] data = - unboundedReadsAreIndefinite() ? Util.EMPTY_BYTE_ARRAY : Util.readToEnd(dataSource); - assertThat(data).isEmpty(); - } catch (IOException e) { - // TODO: Remove this catch once the one below is removed. - throw new RuntimeException(e); - } - } catch (IOException e) { - // TODO: Remove this catch and require that implementations do not throw. - assertThat(DataSourceException.isCausedByPositionOutOfRange(e)).isTrue(); - } + // The DataSource.open() contract requires the returned length to equal the length in the + // DataSpec if set. This is true even though the DataSource implementation may know that + // fewer bytes will be read in this case. + assertThat(length).isEqualTo(1); + assertThat(data).isEmpty(); } finally { dataSource.close(); }