让 exoplayer2 支持播放 ftp ( 扩展 exoplayer 支持 ftp 协议 ) 的两种方式

news2025/1/11 22:37:57

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

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

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/912425.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

罗德与施瓦茨频谱分析仪RSFSUP50

FSUP50 R&S FSUP50 信号源分析仪&#xff0c;20Hz到50GHz 壹捌叁贰零玖壹捌陆伍叁 R&S FSUP 是一款测量功能丰富、高度灵活的相位噪声测试仪&#xff0c;它兼具***信号和频谱分析仪及单纯相位噪声测试仪两者的功能。 主要特点 频率范围高达 8 GHz、26.5 GHz 或 50…

网络原理详解(图文结合)

目录 ​编辑一、应用层 1、请求和响应 2、通用的协议格式 &#xff08;1&#xff09;xml &#xff08;2&#xff09;json &#xff08;3&#xff09;protobuffer 二、传输层 1、UDP 2、TCP &#xff08;1&#xff09;TCP 协议段格式&#xff1a; &#xff08;2&am…

js中 0==‘0‘、0==[] 为 true ,‘0‘==[] 为false

文章目录 问题分析 问题 js中 0‘0’、0[] 为 true &#xff0c;为什么 ‘0’[] 为false 分析 是弱类型比较 当两者类型不同时会发生类型转换0 “0”&#xff1a;先把“0”转为number类型再比较&#xff1b;0 []&#xff1a;有对象的话&#xff0c;先获取对象的原始值&#…

WPF中的效果Effect

WPF中的效果Effect 前言 WPF提供了可应用于任何元素的可视化效果。效果的目标是提供一种简便的声明式方法&#xff0c;从而改进文本、图像、按钮以及其他控件的外观。不是编写自己的绘图代码&#xff0c;而是使用某个继承自Effect的类&#xff0c;以立即获得诸如模糊、光辉以…

问道管理:数字经济概念走势强劲,竞业达、久其软件等涨停,观想科技等大涨

信创、智慧政务等数字经济概念22日盘中走势微弱&#xff0c;截至发稿&#xff0c;观想科技、慧博云通涨超15%&#xff0c;竞业达、中远海科、久其软件等涨停&#xff0c;云赛智联、延华智能、汇纳科技涨约9%&#xff0c;天玑科技、安硕信息、思特奇、零点稀有涨逾7%。 音讯面上…

虚幻官方项目《CropOut》技术解析 之 在实战中理解Enhanced Input系统

文章目录 概要Enhanced Input系统基础回顾旧版输入系统定义物理按键和Action/Axis的映射输入事件 Enhanced Input系统统一的ActionInput Mapping Context输入事件 《Crop Out》《Crop Out》中基于Enhanced Input的输入控制系统Input Mapping Context分层管理输入修改器(Input M…

Ubuntu系统全盘备份——TimeShift的安装与使用

Timeshift&#xff0c;它使用完全备份的方式&#xff0c;直接将整个操作系统包括个人配置全部备份打包。也就是全盘备份。 操作过程 依次执行命令&#xff1a; sudo apt-add-repository -y ppa:teejee2008/ppa sudo apt update sudo apt install timeshift打开软件后&#x…

【ES6】—【必备知识】—函数的参数

一、参数的默认值 1. ES5 设置默认值 function foo (x, y) {y y || worldconsole.log(x, y) } foo(hello, xiaoxiao) foo(hello, 0) // hello xiaoxiao // hello worldPS&#xff1a; 使用 || 的方式设置默认值不严谨&#xff0c; 0、undefined、‘’ 这些参数都会被判定为f…

安装VSCA 过程中的报错

安装VSCA 过程中的报错&#xff1a;无法获取目标服务器证书的SSL指纹&#xff1a; 解决方案三点&#xff1a; 防火墙 网络IP&#xff0c;网关 使用ping命令或者telnet检查网络是否可达 网络端口没有开启 有无启用 另外最新版本有漏洞 建议换一个常用版本

NestJS 中的 gRPC 微服务通信

想象一下&#xff0c;你回家过节&#xff0c;你的家人决定聚会。而不是让一个人做所有的烹饪&#xff0c;每个人都同意带上他们擅长制作的特色菜。目标是通过组合所有这些菜肴来制作一顿完整的饭菜。你同意做鸡肉炒饭&#xff0c;你哥哥做甜点蛋糕&#xff0c;妹妹做沙拉。 每…

SV-7101T网络音频广播终端使用手册

1.1、产品简介 感谢你使用我司的SV-7101T网络音频播放终端&#xff0c;SV-7101T能处理tcp/ip网络音频流&#xff0c;提供一路线路输出。主要用于公共数字广播&#xff0c;媒体教学&#xff0c;报警等需要数字音频的领域。 SV-7101T具有10/100M以太网接口&#xff0c;支持最高4…

高忆管理:巨无霸IPO来了!年内全球最大?英伟达曾400亿美金“求亲”

当地时间8月21日&#xff0c;美股三大股指涨跌纷歧&#xff0c;纳指连跌四日之后反弹。截至收盘&#xff0c;道指跌0.11%&#xff0c;标普500指数涨0.69%&#xff0c;纳指涨1.56%。本周市场关注美联储主席鲍威尔周五在杰克逊霍尔央行年会上的说话。 周一美国国债收益率攀升。美…

Power BI 如何做页面权限控制

同一个PBI报告中有多页, 有时我们需要将其中一些页面开给一部分人, 一些页面开给另一部分人 比如A用户只允许查看报告的第1,2页&#xff0c;B用户只能查看第3页等 1 导入页面控制权限表 2 创建角色 3 设计封面页 选择筛选器视图, 将页面字段拖进去, 将筛选器设置成单选选择, …

2023年上半年,吉利汽车销量增长13.1%,同比增长38%

根据吉利汽车发布的中期业绩报告&#xff0c;2023年上半年&#xff0c;吉利汽车取得了令人瞩目的销售和收入增长。该公司在中国乘用车市场表现出色&#xff0c;销量增长了13.1%&#xff0c;达到了69.4万辆&#xff0c;超过了整体市场增长率&#xff08;8.8%&#xff09;。 这主…

ebay小夜灯亚马逊UL1786测试报告

小夜灯UL报告UL1786测试标准亚马逊美国站销售办理 UL认证&#xff0c;很多人对它熟悉却又陌生。出口美国的时候&#xff0c;很多人都听过UL认证。但是因为UL认证在美国属于非强制性的认证&#xff0c;对于清关没有影响&#xff0c;所以有很多卖家不会深入了解它。其实相关产品…

Python实现SSA智能麻雀搜索算法优化随机森林分类模型(RandomForestClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法&#xff0c;在2020年提出&a…

​LeetCode解法汇总849. 到最近的人的最大距离

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个数…

互联网医院开发|医院叫号系统提升就医效率

在这个数字化时代&#xff0c;互联网医院不仅改变了我们的生活方式&#xff0c;也深刻影响着医疗行业。医院叫号系统应运而生&#xff0c;它能够有效解决患者管理和服务方面的难题。不再浪费大量时间在排队上&#xff0c;避免患者错过重要信息。同时&#xff0c;医护工作效率得…

QT基础教程之三信号和槽机制

QT基础教程之三信号和槽机制 信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽&#xff0c;实际就是观察者模式。当某个事件发生之后&#xff0c;比如&#xff0c;按钮检测到自己被点击了一下&#xff0c;它就会发出一个信号&#xff08;signal&#xff09;。这种发出是没有目…

Python实现SSA智能麻雀搜索算法优化Catboost回归模型(CatBoostRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法&#xff0c;在2020年提出&a…