mirror of
https://github.com/androidx/media.git
synced 2025-05-06 23:20:42 +08:00
parent
4527539efe
commit
54b71a5743
@ -130,8 +130,6 @@ import java.util.Locale;
|
|||||||
public static final Sample[] MISC = new Sample[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
||||||
DemoUtil.TYPE_MP4),
|
DemoUtil.TYPE_MP4),
|
||||||
new Sample("Dizzy (https->http redirect)", "https://goo.gl/MtUDEj",
|
|
||||||
DemoUtil.TYPE_MP4),
|
|
||||||
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
|
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
|
||||||
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
||||||
DemoUtil.TYPE_AAC),
|
DemoUtil.TYPE_AAC),
|
||||||
|
@ -28,6 +28,8 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.NoRouteToHostException;
|
||||||
|
import java.net.ProtocolException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -38,17 +40,30 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
|
* A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
|
||||||
|
* <p>
|
||||||
|
* By default this implementation will not follow cross-protocol redirects (i.e. redirects from
|
||||||
|
* HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the
|
||||||
|
* {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)}
|
||||||
|
* constructor and passing {@code true} as the final argument.
|
||||||
*/
|
*/
|
||||||
public class DefaultHttpDataSource implements HttpDataSource {
|
public class DefaultHttpDataSource implements HttpDataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default connection timeout, in milliseconds.
|
||||||
|
*/
|
||||||
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
|
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
|
||||||
|
/**
|
||||||
|
* The default read timeout, in milliseconds.
|
||||||
|
*/
|
||||||
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
|
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
|
||||||
|
|
||||||
|
private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
|
||||||
private static final String TAG = "HttpDataSource";
|
private static final String TAG = "HttpDataSource";
|
||||||
private static final Pattern CONTENT_RANGE_HEADER =
|
private static final Pattern CONTENT_RANGE_HEADER =
|
||||||
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
|
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
|
||||||
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
|
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
|
||||||
|
|
||||||
|
private final boolean allowCrossProtocolRedirects;
|
||||||
private final int connectTimeoutMillis;
|
private final int connectTimeoutMillis;
|
||||||
private final int readTimeoutMillis;
|
private final int readTimeoutMillis;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
@ -103,12 +118,33 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
*/
|
*/
|
||||||
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
|
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
|
||||||
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
|
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
|
||||||
|
this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param userAgent The User-Agent string that should be used.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is
|
||||||
|
* rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is
|
||||||
|
* thrown from {@link #open(DataSpec)}.
|
||||||
|
* @param listener An optional listener.
|
||||||
|
* @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
|
||||||
|
* interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
|
||||||
|
* the default value.
|
||||||
|
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
|
||||||
|
* as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
|
||||||
|
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
|
||||||
|
* to HTTPS and vice versa) are enabled.
|
||||||
|
*/
|
||||||
|
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
|
||||||
|
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis,
|
||||||
|
boolean allowCrossProtocolRedirects) {
|
||||||
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.requestProperties = new HashMap<String, String>();
|
this.requestProperties = new HashMap<String, String>();
|
||||||
this.connectTimeoutMillis = connectTimeoutMillis;
|
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||||
this.readTimeoutMillis = readTimeoutMillis;
|
this.readTimeoutMillis = readTimeoutMillis;
|
||||||
|
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -283,8 +319,58 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
|
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a connection, following redirects to do so where permitted.
|
||||||
|
*/
|
||||||
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
|
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
|
||||||
URL url = new URL(dataSpec.uri.toString());
|
URL url = new URL(dataSpec.uri.toString());
|
||||||
|
long position = dataSpec.position;
|
||||||
|
long length = dataSpec.length;
|
||||||
|
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
|
||||||
|
|
||||||
|
if (!allowCrossProtocolRedirects) {
|
||||||
|
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
|
||||||
|
// automatically. This is the behavior we want, so use it.
|
||||||
|
HttpURLConnection connection = configureConnection(url, position, length, allowGzip);
|
||||||
|
connection.connect();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to handle redirects ourselves to allow cross-protocol redirects.
|
||||||
|
int redirectCount = 0;
|
||||||
|
while (redirectCount++ <= MAX_REDIRECTS) {
|
||||||
|
HttpURLConnection connection = configureConnection(url, position, length, allowGzip);
|
||||||
|
connection.setInstanceFollowRedirects(false);
|
||||||
|
connection.connect();
|
||||||
|
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
|
||||||
|
|| responseCode == 307 /* HTTP_TEMP_REDIRECT */
|
||||||
|
|| responseCode == 308 /* HTTP_PERM_REDIRECT */) {
|
||||||
|
String location = connection.getHeaderField("Location");
|
||||||
|
connection.disconnect();
|
||||||
|
url = handleRedirect(url, location);
|
||||||
|
} else {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here we've been redirected more times than are permitted.
|
||||||
|
throw new NoRouteToHostException("Too many redirects: " + redirectCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures a connection, but does not open it.
|
||||||
|
*
|
||||||
|
* @param url The url to connect to.
|
||||||
|
* @param position The byte offset of the requested data.
|
||||||
|
* @param length The length of the requested data, or {@link C#LENGTH_UNBOUNDED}.
|
||||||
|
* @param allowGzip Whether to allow the use of gzip.
|
||||||
|
*/
|
||||||
|
private HttpURLConnection configureConnection(URL url, long position, long length,
|
||||||
|
boolean allowGzip) throws IOException {
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setConnectTimeout(connectTimeoutMillis);
|
connection.setConnectTimeout(connectTimeoutMillis);
|
||||||
connection.setReadTimeout(readTimeoutMillis);
|
connection.setReadTimeout(readTimeoutMillis);
|
||||||
@ -294,28 +380,56 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
connection.setRequestProperty(property.getKey(), property.getValue());
|
connection.setRequestProperty(property.getKey(), property.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setRangeHeader(connection, dataSpec);
|
if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) {
|
||||||
|
String rangeRequest = "bytes=" + position + "-";
|
||||||
|
if (length != C.LENGTH_UNBOUNDED) {
|
||||||
|
rangeRequest += (position + length - 1);
|
||||||
|
}
|
||||||
|
connection.setRequestProperty("Range", rangeRequest);
|
||||||
|
}
|
||||||
connection.setRequestProperty("User-Agent", userAgent);
|
connection.setRequestProperty("User-Agent", userAgent);
|
||||||
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
|
if (!allowGzip) {
|
||||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||||
}
|
}
|
||||||
connection.connect();
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) {
|
/**
|
||||||
if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) {
|
* Handles a redirect.
|
||||||
// Not required.
|
*
|
||||||
return;
|
* @param originalUrl The original URL.
|
||||||
|
* @param location The Location header in the response.
|
||||||
|
* @return The next URL.
|
||||||
|
* @throws IOException If redirection isn't possible.
|
||||||
|
*/
|
||||||
|
private static URL handleRedirect(URL originalUrl, String location) throws IOException {
|
||||||
|
if (location == null) {
|
||||||
|
throw new ProtocolException("Null location redirect");
|
||||||
}
|
}
|
||||||
String rangeRequest = "bytes=" + dataSpec.position + "-";
|
// Form the new url.
|
||||||
if (dataSpec.length != C.LENGTH_UNBOUNDED) {
|
URL url = new URL(originalUrl, location);
|
||||||
rangeRequest += (dataSpec.position + dataSpec.length - 1);
|
// Check that the protocol of the new url is supported.
|
||||||
|
String protocol = url.getProtocol();
|
||||||
|
if (!"https".equals(protocol) && !"http".equals(protocol)) {
|
||||||
|
throw new ProtocolException("Unsupported protocol redirect: " + protocol);
|
||||||
}
|
}
|
||||||
connection.setRequestProperty("Range", rangeRequest);
|
// Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
|
||||||
|
// below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
|
||||||
|
// redirects are disabled, we'll need to uncomment this block of code.
|
||||||
|
// if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
|
||||||
|
// throw new ProtocolException("Disallowed cross-protocol redirect ("
|
||||||
|
// + originalUrl.getProtocol() + " to " + protocol + ")");
|
||||||
|
// }
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getContentLength(HttpURLConnection connection) {
|
/**
|
||||||
|
* 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_UNBOUNDED}.
|
||||||
|
*/
|
||||||
|
private static long getContentLength(HttpURLConnection connection) {
|
||||||
long contentLength = C.LENGTH_UNBOUNDED;
|
long contentLength = C.LENGTH_UNBOUNDED;
|
||||||
String contentLengthHeader = connection.getHeaderField("Content-Length");
|
String contentLengthHeader = connection.getHeaderField("Content-Length");
|
||||||
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
||||||
@ -429,6 +543,9 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||||||
return read;
|
return read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the current connection, if there is one.
|
||||||
|
*/
|
||||||
private void closeConnection() {
|
private void closeConnection() {
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
|
@ -36,15 +36,36 @@ public final class DefaultUriDataSource implements UriDataSource {
|
|||||||
private UriDataSource dataSource;
|
private UriDataSource dataSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and an
|
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a
|
||||||
* {@link DefaultHttpDataSource} for other URIs.
|
* {@link DefaultHttpDataSource} for other URIs.
|
||||||
|
* <p>
|
||||||
|
* The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to
|
||||||
|
* HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by
|
||||||
|
* using the {@link #DefaultUriDataSource(String, TransferListener, boolean)} constructor and
|
||||||
|
* passing {@code true} as the final argument.
|
||||||
*
|
*
|
||||||
* @param userAgent The User-Agent string that should be used when requesting remote data.
|
* @param userAgent The User-Agent string that should be used when requesting remote data.
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
*/
|
*/
|
||||||
public DefaultUriDataSource(String userAgent, TransferListener transferListener) {
|
public DefaultUriDataSource(String userAgent, TransferListener transferListener) {
|
||||||
|
this(userAgent, transferListener, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a
|
||||||
|
* {@link DefaultHttpDataSource} for other URIs.
|
||||||
|
*
|
||||||
|
* @param userAgent The User-Agent string that should be used when requesting remote data.
|
||||||
|
* @param transferListener An optional listener.
|
||||||
|
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
|
||||||
|
* to HTTPS and vice versa) are enabled when fetching remote data..
|
||||||
|
*/
|
||||||
|
public DefaultUriDataSource(String userAgent, TransferListener transferListener,
|
||||||
|
boolean allowCrossProtocolRedirects) {
|
||||||
this(new FileDataSource(transferListener),
|
this(new FileDataSource(transferListener),
|
||||||
new DefaultHttpDataSource(userAgent, null, transferListener));
|
new DefaultHttpDataSource(userAgent, null, transferListener,
|
||||||
|
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user