让 exoplayer2 支持播放 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支持多种网络协议,包括但不限于以下几种:

  1. HTTP和HTTPS:ExoPlayer可以通过HTTP和HTTPS协议从远程服务器下载和播放媒体内容。

  2. RTMP(Real-Time Messaging Protocol):RTMP是一种实时流媒体传输协议,ExoPlayer可以通过RTMP协议从服务器下载和播放媒体内容。

  3. udp

其中 exoplayer 支持的stream格式有:

  1. DASH(Dynamic Adaptive Streaming over HTTP):DASH是一种自适应流媒体传输协议,ExoPlayer可以解析和播放使用DASH协议传输的媒体内容。

  2. HLS(HTTP Live Streaming)(M3U8):HLS是苹果公司开发的一种流媒体传输协议,ExoPlayer可以解析和播放使用HLS协议传输的媒体内容。

  3. SmoothStreaming:Smooth Streaming是微软开发的一种流媒体传输协议,ExoPlayer可以解析和播放使用Smooth Streaming协议传输的媒体内容。

  4. RTSP | Android Developers

  5. ……

需要注意的是,ExoPlayer的网络协议支持可以通过自定义扩展来进行扩展,因此还可以支持其他自定义的网络协议。

扩展 exoplayer 支持 ftp

可见支持的网络协议很多,唯独没有 ftp。有两种方法可以扩展 exoplayer 支持 ftp 协议:

一、使用 Url.openConnection

URLConnection connection = url.openConnection();

除了 HTTPUrlConnection,其实部分安卓的 url.openConnection 还支持 FTPUrlConnection,不过是隐藏功能:

让 exoplayer2 支持播放 ftp ( 扩展 exoplayer 支持 ftp 协议 ) 的两种方式_第1张图片

代码

修改自 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> 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}.
	 * 

* 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:[email protected]:3001/test.mp4"); URLConnection connection = url.openConnection(); // connection.setConnectTimeout(connectTimeoutMillis); // connection.setReadTimeout(readTimeoutMillis); // if (defaultRequestProperties != null) { // for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { // connection.setRequestProperty(property.getKey(), property.getValue()); // } // } // for (Map.Entry 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. *

* 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}. *

* 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

参考资料1:exoPlayer/library/src/main/java/com/google/android/exoplayer/upstream/FtpDataSource.java at master · uplusplus/exoPlayer

参考资料2:apache/commons-net: Apache Commons Net

参考资料1是基于 exoplayer 1 扩展的,需要升级。

其实无需改动 exoplayer 源代码 ——

如何扩展 exoplayer 的 DefaultDataSource? 扩展 exoplayer 的正确方式:

DefaultDataSource 是 final 实现,无法直接扩展。

至于如何扩展,可以将源码复制出来,然后想怎么扩展,就怎么扩展!

让 exoplayer2 支持播放 ftp ( 扩展 exoplayer 支持 ftp 协议 ) 的两种方式_第2张图片

你可能感兴趣的:(编译奇兵,安卓,安卓,音视频,视频)