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,不过是隐藏功能:

请添加图片描述

代码

修改自 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

参考资料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 实现,无法直接扩展。

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

请添加图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐