mirror of
https://github.com/samsonjs/media.git
synced 2026-04-05 11:15:46 +00:00
DataSources: Enforce that opening at end-of-resource succeeds
- Update the three `HttpDataSource` implementations to use the Content-Range response header to determine when this is the case. The Content-Range header is included when the status code is 416. See [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416). - Update `ByteArrayDataSource` to conform to the requirement. - Update `DataSourceContractTest` to enforce the requirement. PiperOrigin-RevId: 363642114
This commit is contained in:
parent
8337991be3
commit
1affbf9357
8 changed files with 105 additions and 52 deletions
|
|
@ -565,6 +565,16 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
int responseCode = responseInfo.getHttpStatusCode();
|
||||
Map<String, List<String>> 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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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<String, List<String>> 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue