让 exoplayer2 支持播放 ftp ( 扩展 exoplayer 支持 ftp 协议 ) 的两种方式
exoplayer 是安卓开源播放器组件库,由谷歌开发维护。它提供了一个可高度扩展的音视频播放框架,支持许多媒体格式与高级媒体功能,比如 adaptive streaming,DRM,以及安卓media session 集成。但是不支持 ftp ,有两种方式可以扩展 exoplayer 支持 ftp 协议。
exoplayer 是安卓开源播放器组件库,由谷歌开发维护。它提供了一个可高度扩展的音视频播放框架,支持许多媒体格式与高级媒体功能,比如 adaptive streaming,DRM,以及安卓 media session 集成。
但是不支持 ftp ,有两种方式可以扩展 exoplayer 支持 ftp 协议。一种是调用系统隐藏功能,一种是使用 apache/commons-net 网络库。
如何查看exoplayer支持哪些网络协议?
简单,直接打开源码 com.google.android.exoplayer2.upstream.DefaultDataSource
@Override
public long open(DataSpec dataSpec) throws IOException {
Assertions.checkState(dataSource == null);
// Choose the correct source for the scheme.
String scheme = dataSpec.uri.getScheme();
if (Util.isLocalFileUri(dataSpec.uri)) {
String uriPath = dataSpec.uri.getPath();
if (uriPath != null && uriPath.startsWith("/android_asset/")) {
dataSource = getAssetDataSource();
} else {
dataSource = getFileDataSource();
}
} else if (SCHEME_ASSET.equals(scheme)) {
dataSource = getAssetDataSource();
} else if (SCHEME_CONTENT.equals(scheme)) {
dataSource = getContentDataSource();
} else if (SCHEME_RTMP.equals(scheme)) {
dataSource = getRtmpDataSource();
} else if (SCHEME_UDP.equals(scheme)) {
dataSource = getUdpDataSource();
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
dataSource = getDataSchemeDataSource();
} else if (SCHEME_RAW.equals(scheme)) {
dataSource = getRawResourceDataSource();
} else {
dataSource = baseDataSource;
}
// Open the source and return.
return dataSource.open(dataSpec);
}
可见,ExoPlayer支持多种网络协议,包括但不限于以下几种:
-
HTTP和HTTPS:ExoPlayer可以通过HTTP和HTTPS协议从远程服务器下载和播放媒体内容。
-
RTMP(Real-Time Messaging Protocol):RTMP是一种实时流媒体传输协议,ExoPlayer可以通过RTMP协议从服务器下载和播放媒体内容。
-
udp
其中 exoplayer 支持的stream格式有:
-
DASH(Dynamic Adaptive Streaming over HTTP):DASH是一种自适应流媒体传输协议,ExoPlayer可以解析和播放使用DASH协议传输的媒体内容。
-
HLS(HTTP Live Streaming)(M3U8):HLS是苹果公司开发的一种流媒体传输协议,ExoPlayer可以解析和播放使用HLS协议传输的媒体内容。
-
SmoothStreaming:Smooth Streaming是微软开发的一种流媒体传输协议,ExoPlayer可以解析和播放使用Smooth Streaming协议传输的媒体内容。
-
……
需要注意的是,ExoPlayer的网络协议支持可以通过自定义扩展来进行扩展,因此还可以支持其他自定义的网络协议。
扩展 exoplayer 支持 ftp
可见支持的网络协议很多,唯独没有 ftp。有两种方法可以扩展 exoplayer 支持 ftp 协议:
一、使用 Url.openConnection
URLConnection connection = url.openConnection();
除了 HTTPUrlConnection,其实部分安卓的 url.openConnection
还支持 FTPUrlConnection,不过是隐藏功能:
代码
修改自 exoplayer 源码 UriDataSource.java
,将之泛化,不局限于 HTTPUrlConnection:
public class UriDataSource extends BaseDataSource /*implements HttpDataSource*/ {
/**
* The default connection timeout, in milliseconds.
*/
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;
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+)$");
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis;
private final int readTimeoutMillis;
private final String userAgent;
private final @Nullable Predicate<String> contentTypePredicate;
private final @Nullable RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private @Nullable
DataSpec dataSpec;
private @Nullable URLConnection connection;
private @Nullable InputStream inputStream;
private boolean opened;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesRead;
/** @param userAgent The User-Agent string that should be used. */
public UriDataSource(String userAgent) {
this(userAgent, /* contentTypePredicate= */ null);
}
/**
* @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)}.
*/
public UriDataSource(String userAgent, @Nullable Predicate<String> contentTypePredicate) {
this(
userAgent,
contentTypePredicate,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS);
}
/**
* @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 connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
* interpreted as an infinite timeout.
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as
* an infinite timeout.
*/
public UriDataSource(
String userAgent,
@Nullable Predicate<String> contentTypePredicate,
int connectTimeoutMillis,
int readTimeoutMillis) {
this(
userAgent,
contentTypePredicate,
connectTimeoutMillis,
readTimeoutMillis,
/* allowCrossProtocolRedirects= */ false,
/* defaultRequestProperties= */ null);
}
/**
* @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 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.
* @param defaultRequestProperties The default request properties to be sent to the server as HTTP
* headers or {@code null} if not required.
*/
public UriDataSource(
String userAgent,
@Nullable Predicate<String> contentTypePredicate,
int connectTimeoutMillis,
int readTimeoutMillis,
boolean allowCrossProtocolRedirects,
@Nullable RequestProperties defaultRequestProperties) {
super(/* isNetwork= */ true);
this.userAgent = (userAgent);
this.contentTypePredicate = contentTypePredicate;
this.requestProperties = new RequestProperties();
this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis;
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
this.defaultRequestProperties = defaultRequestProperties;
}
/**
* @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.
* @deprecated Use {@link #UriDataSource(String, Predicate)} and {@link
* #addTransferListener(TransferListener)}.
*/
@Deprecated
@SuppressWarnings("deprecation")
public UriDataSource(
String userAgent,
@Nullable Predicate<String> contentTypePredicate,
@Nullable TransferListener listener) {
this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS);
}
/**
* @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.
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as
* an infinite timeout.
* @deprecated Use {@link #UriDataSource(String, Predicate, int, int)} and {@link
* #addTransferListener(TransferListener)}.
*/
@Deprecated
@SuppressWarnings("deprecation")
public UriDataSource(
String userAgent,
@Nullable Predicate<String> contentTypePredicate,
@Nullable TransferListener listener,
int connectTimeoutMillis,
int readTimeoutMillis) {
this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false,
null);
}
/**
* @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.
* @param defaultRequestProperties The default request properties to be sent to the server as HTTP
* headers or {@code null} if not required.
* @deprecated Use {@link #UriDataSource(String, Predicate, int, int, boolean,
* RequestProperties)} and {@link #addTransferListener(TransferListener)}.
*/
@Deprecated
public UriDataSource(
String userAgent,
@Nullable Predicate<String> contentTypePredicate,
@Nullable TransferListener listener,
int connectTimeoutMillis,
int readTimeoutMillis,
boolean allowCrossProtocolRedirects,
@Nullable RequestProperties defaultRequestProperties) {
this(
userAgent,
contentTypePredicate,
connectTimeoutMillis,
readTimeoutMillis,
allowCrossProtocolRedirects,
defaultRequestProperties);
if (listener != null) {
addTransferListener(listener);
}
}
@Override
public @Nullable Uri getUri() {
return connection == null ? null : Uri.parse(connection.getURL().toString());
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return connection == null ? Collections.emptyMap() : connection.getHeaderFields();
}
//@Override
public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
requestProperties.set(name, value);
}
//@Override
public void clearRequestProperty(String name) {
Assertions.checkNotNull(name);
requestProperties.remove(name);
}
//@Override
public void clearAllRequestProperties() {
requestProperties.clear();
}
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
this.bytesRead = 0;
this.bytesSkipped = 0;
transferInitializing(dataSpec);
try {
connection = makeConnection(dataSpec);
} catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
dataSpec, HttpDataSourceException.TYPE_OPEN);
}
// int responseCode;
// String responseMessage;
// try {
// responseCode = connection.getResponseCode();
// responseMessage = connection.getResponseMessage();
// } catch (IOException e) {
// closeConnectionQuietly();
// throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
// dataSpec, HttpDataSourceException.TYPE_OPEN);
// }
// Check for a valid response code.
// if (responseCode < 200 || responseCode > 299) {
// Map<String, List<String>> headers = connection.getHeaderFields();
// closeConnectionQuietly();
// InvalidResponseCodeException exception =
// new InvalidResponseCodeException(responseCode, responseMessage, headers, dataSpec);
// if (responseCode == 416) {
// exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
// }
// throw exception;
// }
// Check for a valid content type.
String contentType = connection.getContentType();
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
closeConnectionQuietly();
throw new InvalidContentTypeException(contentType, dataSpec);
}
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
bytesToSkip = /*responseCode == 200 &&*/ dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
long contentLength = getContentLength(connection);
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
: C.LENGTH_UNSET;
}
} else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a
// reliable way to determine whether the gzip was used or not. Always use the dataSpec length
// in this case.
bytesToRead = dataSpec.length;
}
try {
inputStream = connection.getInputStream();
} catch (IOException e) {
closeConnectionQuietly();
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
opened = true;
transferStarted(dataSpec);
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
}
}
@Override
public void close() throws HttpDataSourceException {
try {
if (inputStream != null) {
maybeTerminateInputStream(connection, bytesRemaining());
try {
inputStream.close();
} catch (IOException e) {
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE);
}
}
} finally {
inputStream = null;
closeConnectionQuietly();
if (opened) {
opened = false;
transferEnded();
}
}
}
/**
* Returns the current connection, or null if the source is not currently opened.
*
* @return The current open connection, or null.
*/
protected final @Nullable URLConnection getConnection() {
return connection;
}
/**
* Returns the number of bytes that have been skipped since the most recent call to
* {@link #open(DataSpec)}.
*
* @return The number of bytes skipped.
*/
protected final long bytesSkipped() {
return bytesSkipped;
}
/**
* Returns the number of bytes that have been read since the most recent call to
* {@link #open(DataSpec)}.
*
* @return The number of bytes read.
*/
protected final long bytesRead() {
return bytesRead;
}
/**
* Returns the number of bytes that are still to be read for the current {@link DataSpec}.
* <p>
* If the total length of the data being read is known, then this length minus {@code bytesRead()}
* is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.
*
* @return The remaining length, or {@link C#LENGTH_UNSET}.
*/
protected final long bytesRemaining() {
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
}
/**
* Establishes a connection, following redirects to do so where permitted.
*/
private URLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString());
@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);
boolean allowIcyMetadata = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA);
if (!allowCrossProtocolRedirects) {
// URLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it.
return makeConnection(
url,
httpMethod,
httpBody,
position,
length,
allowGzip,
allowIcyMetadata,
/* followRedirects= */ true);
}
// We need to handle redirects ourselves to allow cross-protocol redirects.
// int redirectCount = 0;
// while (redirectCount++ <= MAX_REDIRECTS) {
// URLConnection connection =
// makeConnection(
// url,
// httpMethod,
// httpBody,
// position,
// length,
// allowGzip,
// allowIcyMetadata,
// /* followRedirects= */ false);
// int responseCode = connection.getResponseCode();
// String location = connection.getHeaderField("Location");
// if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
// && (responseCode == URLConnection.HTTP_MULT_CHOICE
// || responseCode == URLConnection.HTTP_MOVED_PERM
// || responseCode == URLConnection.HTTP_MOVED_TEMP
// || responseCode == URLConnection.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 == URLConnection.HTTP_MULT_CHOICE
// || responseCode == URLConnection.HTTP_MOVED_PERM
// || responseCode == URLConnection.HTTP_MOVED_TEMP
// || responseCode == URLConnection.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;
// }
// }
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 and opens it.
*
* @param url The url to connect to.
* @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 allowIcyMetadata Whether to allow ICY metadata.
* @param followRedirects Whether to follow redirects.
*/
private URLConnection makeConnection(
URL url,
@HttpMethod int httpMethod,
byte[] httpBody,
long position,
long length,
boolean allowGzip,
boolean allowIcyMetadata,
boolean followRedirects)
throws IOException {
// url = new URL("ftp://x:s@192.168.0.101:3001/test.mp4");
URLConnection connection = url.openConnection();
// connection.setConnectTimeout(connectTimeoutMillis);
// connection.setReadTimeout(readTimeoutMillis);
// if (defaultRequestProperties != null) {
// for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {
// connection.setRequestProperty(property.getKey(), property.getValue());
// }
// }
// for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {
// connection.setRequestProperty(property.getKey(), property.getValue());
// }
// if (!(position == 0 && length == C.LENGTH_UNSET)) {
// String rangeRequest = "bytes=" + position + "-";
// if (length != C.LENGTH_UNSET) {
// rangeRequest += (position + length - 1);
// }
// connection.setRequestProperty("Range", rangeRequest);
// }
// connection.setRequestProperty("User-Agent", userAgent);
// if (!allowGzip) {
// connection.setRequestProperty("Accept-Encoding", "identity");
// }
// if (allowIcyMetadata) {
// connection.setRequestProperty(
// IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
// IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
// }
connection.setInstanceFollowRedirects(followRedirects);
// connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
// if (httpBody != null && false) {
connection.setFixedLengthStreamingMode(httpBody.length);
// connection.connect();
// OutputStream os = connection.getOutputStream();
// os.write(httpBody);
// os.close();
// } else {
// connection.connect();
// }
connection.connect();
return connection;
}
/**
* Handles a redirect.
*
* @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");
}
// Form the new url.
URL url = new URL(originalUrl, location);
// 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);
}
// 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;
}
/**
* 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(URLConnection 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(matcher.group(2)) - Long.parseLong(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 = Math.max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
/**
* Skips any bytes that need skipping. Else does nothing.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
*
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
*/
private void skipInternal() throws IOException {
if (bytesSkipped == bytesToSkip) {
return;
}
// Acquire the shared skip buffer.
byte[] skipBuffer = skipBufferReference.getAndSet(null);
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = inputStream.read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
}
bytesSkipped += read;
bytesTransferred(read);
}
// Release the shared skip buffer.
skipBufferReference.set(skipBuffer);
}
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}.
* <p>
* This method blocks until at least one byte of data can be read, the end of the opened range is
* detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
* range is reached.
* @throws IOException If an error occurs reading from the source.
*/
private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
if (readLength == 0) {
return 0;
}
if (bytesToRead != C.LENGTH_UNSET) {
long bytesRemaining = bytesToRead - bytesRead;
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = (int) Math.min(readLength, bytesRemaining);
}
int read = inputStream.read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
bytesTransferred(read);
return read;
}
/**
* On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
* block for a long time if the stream has a lot of data remaining. Call this method before
* closing the input stream to make a best effort to cause the input stream to encounter an
* unexpected end of input, working around this issue. On other platform API levels, the method
* does nothing.
*
* @param connection The connection whose {@link InputStream} should be terminated.
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNSET} otherwise.
*/
private static void maybeTerminateInputStream(URLConnection connection, long bytesRemaining) {
if (Util.SDK_INT != 19 && Util.SDK_INT != 20) {
return;
}
try {
InputStream inputStream = connection.getInputStream();
if (bytesRemaining == C.LENGTH_UNSET) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return;
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return;
}
String className = inputStream.getClass().getName();
if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className)
|| "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream"
.equals(className)) {
Class<?> superclass = inputStream.getClass().getSuperclass();
Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
} catch (Exception e) {
// If an IOException then the connection didn't ever have an input stream, or it was closed
// already. If another type of exception then something went wrong, most likely the device
// isn't using okhttp.
}
}
/**
* Closes the current connection quietly, if there is one.
*/
private void closeConnectionQuietly() {
// if (connection != null) {
// try {
// connection.disconnect();
// } catch (Exception e) {
// Log.e(TAG, "Unexpected error while disconnecting", e);
// }
// connection = null;
// }
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
Log.e(TAG, "Unexpected error while disconnecting", e);
}
inputStream = null;
}
}
}
二、使用 apache ftpclient
参考资料2:apache/commons-net: Apache Commons Net
参考资料1是基于 exoplayer 1 扩展的,需要升级。
其实无需改动 exoplayer 源代码 ——
如何扩展 exoplayer 的 DefaultDataSource? 扩展 exoplayer 的正确方式:
DefaultDataSource 是 final 实现,无法直接扩展。
至于如何扩展,可以将源码复制出来,然后想怎么扩展,就怎么扩展!
更多推荐
所有评论(0)