视频播放器Media3数据源 CronetDataSource 源码分析

259 阅读9分钟

前言

CronetDataSource 是 Media3 中用于基于 Chrome 网络库(Cronet)进行网络数据传输的数据源。Cronet 是 Chrome 浏览器网络堆栈的 Android 版本,它提供了与原生 Android 网络库相比的更多功能和更好的性能。顺手一提,ExoPlayer 是 Media3 中 Player 接口的默认实现。

CronetDataSource入口

public class CronetDataSource extends BaseDataSource implements HttpDataSource {
  static {
    MediaLibraryInfo.registerModule("media3.datasource.cronet");
  }
 }  

进入 CronetDataSource 映入眼帘的是 这个静态模块的注册,在MediaLibraryInfo注入模块,我想应该主要是用于调试打印。

成员变量

public static final class Factory implements HttpDataSource.Factory {

  @Nullable private final CronetEngine cronetEngine;
  private final Executor executor;
  private final RequestProperties defaultRequestProperties;
  @Nullable private final DefaultHttpDataSource.Factory internalFallbackFactory;
  @Nullable private HttpDataSource.Factory fallbackFactory;
  @Nullable private Predicate<String> contentTypePredicate;
  @Nullable private TransferListener transferListener;
  @Nullable private String userAgent;
  private int requestPriority;
  private int connectTimeoutMs;
  private int readTimeoutMs;
  private boolean resetTimeoutOnRedirects;
  private boolean handleSetCookieRequests;
  private boolean keepPostFor302Redirects;
 
 }
  
  1. cronetEngine:
    这是一个 CronetEngine 实例的引用。CronetEngine 是 Cronet 库的核心组件,负责执行网络请求。这个变量被标记为 @Nullable,意味着它可能为空,这通常是因为 CronetEngine 的初始化不是由这个工厂类直接负责的,或者在一些情况下可能不需要使用 CronetEngine。

  2. executor:
    这是一个 Executor 实例,这里提供自定义线程池。

  3. defaultRequestProperties:
    这些是默认的 HTTP 请求属性,如请求头。这些属性会被应用到所有通过这个数据源发出的 HTTP 请求上。

  4. internalFallbackFactory:
    这是一个 DefaultHttpDataSource.Factory 的实例,作为内部回退机制。当 CronetEngine 无法使用时,这个工厂会被用来创建备用的数据源实例。这个变量也被标记为 @Nullable ,表明当 CronetEngineWrapper 被删除时,这个回退机制可能也会被移除。

  5. fallbackFactory:
    这是一个更通用的 HttpDataSource.Factory 实例,作为外部回退机制。当主要的数据源(即基于 Cronet 的数据源)无法使用时,这个工厂会被用来创建备用的数据源实例。同样,这个变量也被标记为 @Nullable 。

  6. contentTypePredicate:
    这是一个 Predicate<String> 实例,用于过滤或选择特定内容类型的请求。只有满足这个条件的请求才会被这个数据源处理。

  7. transferListener:
    这是一个 TransferListener 实例,用于监听数据传输过程中的事件,如传输开始、传输完成、传输失败等。

  8. userAgent:
    这是一个字符串,表示 HTTP 请求中的 User-Agent 头部。它用于标识发出请求的客户端类型和版本。

  9. requestPriority:
    这表示请求的优先级。在某些情况下,网络库可能会根据优先级来决定先处理哪些请求。

  10. connectTimeoutMs:
    这是连接超时的时间(以毫秒为单位)。如果在指定的时间内无法建立连接,请求将会失败。这里默认是8 * 1000,即八秒 

  11. readTimeoutMs:
    这是读取超时的时间(以毫秒为单位)。如果在指定的时间内无法从连接中读取数据,请求将会失败。这里默认是8 * 1000,即八秒 

  12. resetTimeoutOnRedirects:
    这是一个布尔值,表示是否在重定向时重置超时设置。如果为 true,每次重定向都会重新计算连接和读取超时。

  13. handleSetCookieRequests:
    这是一个布尔值,表示是否处理 Set-Cookie 响应头。如果为 true,数据源会处理服务器返回的 Set-Cookie 头部,并可能将其存储在本地以供后续请求使用。

  14. keepPostFor302Redirects:
    这是一个布尔值,表示在遇到 302 重定向时是否保持原始的 POST 请求方法。在某些情况下,服务器可能会用302 重定向来指示客户端用 GET 方法重新发送请求,但这个选项允许客户端保持原始的 POST 方法。

创建CronetDataSource实例


public Factory(CronetEngine cronetEngine, Executor executor) {
 this.cronetEngine = Assertions.checkNotNull(cronetEngine);
 this.executor = executor;
 defaultRequestProperties = new RequestProperties();
 internalFallbackFactory = null;
 requestPriority = REQUEST_PRIORITY_MEDIUM;
 connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
 readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
}

这个Factory构造器是用于创建CronetDataSource实例。这里至少需要传进来两个参数,CronetEngine 和 线程池。

open 函数

接下来直达 public long open(DataSpec dataSpec)这个函数有点长我们,一点点分析,头发又掉了几根了。

aa4b588714bee7013b1cdf670e1fd52b.jpg

@UnstableApi
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
  Assertions.checkNotNull(dataSpec);
  Assertions.checkState(!opened);
 // 重置同步条件
  operation.close();
  //重置连接超时
  resetConnectTimeout();
  //赋值当前数据源
  currentDataSpec = dataSpec;
  UrlRequest urlRequest;
  try {
    //构建一个 文件下载请求,如https://xxxx.com/1.ts ,这里面是靠CronetEngine去请求,并且添加监听请求回调
    urlRequest = buildRequestBuilder(dataSpec).build();
    //赋值当前网络请求
    currentUrlRequest = urlRequest;
  } catch (IOException e) {
    if (e instanceof HttpDataSourceException) {
      throw (HttpDataSourceException) e;
    } else {
      throw new OpenException(
          e, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, Status.IDLE);
    }
  }
  
  //发起请求
  urlRequest.start();

 //回调 TransferListener
  transferInitializing(dataSpec);
  1. 检查数据
  2. 重置同步条件
  3. 重置连接超时
  4. 赋值当前数据源
  5. 构建一个文件下载请求
  6. 发起请求
  7. 回调TransferListener

处理网络连接和打开网络连接时可能发生的异常。

try {
 //这里开始阻塞,直到网络回调成功或者超时,当operation.open()后,这里会继续往下走。
  boolean connectionOpened = blockUntilConnectTimeout();
  @Nullable IOException connectionOpenException = exception;
  if (connectionOpenException != null) {
    @Nullable String message = connectionOpenException.getMessage();
    if (message != null && Ascii.toLowerCase(message).contains("err_cleartext_not_permitted")) {
      throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
    }
    throw new OpenException(
        connectionOpenException,
        dataSpec,
        PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
        getStatus(urlRequest));
  } else if (!connectionOpened) {
    throw new OpenException(
        new SocketTimeoutException(),
        dataSpec,
        PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
        getStatus(urlRequest));
  }
} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  throw new OpenException(
      new InterruptedIOException(),
      dataSpec,
      PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,
      Status.INVALID);
}

这块主要用于处理网络连接和尝试打开网络连接时可能出现的异常。 特别注意一下 , boolean connectionOpened = blockUntilConnectTimeout()这里会开始阻塞,直到网络回调成功或者超时,当在请求监听中调用 operation.open()后,这里会继续往下走。

处理异常的HTTP响应

// Check for a valid response code.
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
Map<String, List<String>> responseHeaders = responseInfo.getAllHeaders();
if (responseCode < 200 || responseCode > 299) {
  if (responseCode == 416) {
    long documentSize =
        HttpUtil.getDocumentSize(getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
    if (dataSpec.position == documentSize) {
      opened = true;
      transferStarted(dataSpec);
      return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
    }
  }

  byte[] responseBody;
  try {
    responseBody = readResponseBody();
  } catch (IOException e) {
    responseBody = Util.EMPTY_BYTE_ARRAY;
  }

  @Nullable
  IOException cause =
      responseCode == 416
          ? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
          : null;
  throw new InvalidResponseCodeException(
      responseCode,
      responseInfo.getHttpStatusText(),
      cause,
      responseHeaders,
      dataSpec,
      responseBody);
}

主要是处理HTTP响应的。它首先检查HTTP响应的状态码是否在200-299之间,这是HTTP协议中定义的成功的状态码范围。如果状态码不在这个范围内,它将根据状态码的不同情况进行不同的处理。

如果状态码是416(表示请求的Range不合法),它将尝试获取文档的大小,并与数据规范(dataSpec)中的位置进行比较。如果位置与文档大小相同,那么它认为连接已经打开,并且可能已经读取了所有的数据,所以返回数据的长度。

如果状态码不是416,或者获取文档大小或比较位置时发生异常,它将尝试读取响应的主体内容。如果读取过程中发生IO异常,那么它将使用一个空的字节数组作为响应的主体。

最后,如果状态码不是416,会抛出一个InvalidResponseCodeException,这个异常包含了响应的状态码、状态文本、可能的原因、响应头、数据规范和响应的主体内容。

检查内容类型

// Check for a valid content type.
Predicate<String> contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) {
  @Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);
  if (contentType != null && !contentTypePredicate.apply(contentType)) {
    throw new InvalidContentTypeException(contentType, dataSpec);
  }
}

这段代码的目的是确保HTTP响应的内容类型符合特定的要求或标准.

获取数据长度( Content Length)

经历的层层大关,终于获取到数据长度,一睹庐山真面目了。


long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;

if (!isCompressed(responseInfo)) {
  if (dataSpec.length != C.LENGTH_UNSET) {
    bytesRemaining = dataSpec.length;
  } else {
    long contentLength =
        HttpUtil.getContentLength(
            getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),
            getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
    bytesRemaining =
        contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
  }
} else {.
  bytesRemaining = dataSpec.length;
}

opened = true;
transferStarted(dataSpec);

skipFully(bytesToSkip, dataSpec);
  1. 计算需要跳过的字节数

bytesToSkip 如果我们请求了一个从非零位置开始的范围,并且收到了200而不是206,那么服务器不支持部分请求。我们需要手动跳到请求的位置。(这里似乎有点费解),由于服务器不支持部分请求,客户端需要手动跳过到请求的位置来开始读取数据。

注:206 是对资源某一部分的请求,该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的GET请求。响应报文中包含由Content-Range指定范围的实体内容。

  1. 计算内容长度
  • 首先,代码检查响应是否已压缩(isCompressed(responseInfo))。

  • 如果响应未压缩,它将尝试获取内容长度:

  • 如果dataSpec.length已设置(即不是C.LENGTH_UNSET),则使用dataSpec.length作为剩余字节数(bytesRemaining)。 - 否则,从HTTP响应头中获取Content-LengthContent-Range来计算内容长度,并减去需要跳过的字节数(bytesToSkip)。

  • 如果响应被压缩,使用dataSpec长度。。

读数据 函数 read(byte[] buffer, int offset, int length)

这里主要调用 Android 自带的 ByteBuffer去读数据,比较简单一些,还是拆开一点点分析吧!!!

检查数据状态

  Assertions.checkState(opened);
  if (length == 0) {
    return 0;
  } else if (bytesRemaining == 0) {
    return C.RESULT_END_OF_INPUT;
  }

1,先检查数据是否打开状态

2,检查数据长度

3,bytesRemaining == 0,即已经读取完所有数据。返回 C.RESULT_END_OF_INPUT

重置,阻塞和处理读异常情况

  ByteBuffer readBuffer = getOrCreateReadBuffer();
  if (!readBuffer.hasRemaining()) {
    operation.close();
    readBuffer.clear();
    readInternal(readBuffer, castNonNull(currentDataSpec));

    //finished 在 UrlRequestCallback onSucceeded  设置为true ,代表 读取操作已成功完成
    if (finished) {
      bytesRemaining = 0;
      return C.RESULT_END_OF_INPUT;
    }
    readBuffer.flip();
    Assertions.checkState(readBuffer.hasRemaining());
  }


如果读取缓冲区没有剩余的数据,同步条件重置,清空readBuffer, readBuffer.flip() 将 Buffer 从写模式切换到读模式。readInternal 主要是阻塞和处理异常。

// Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
// the server does not support Range requests and transmitted the entire resource.
  int bytesRead =
      (int)
          Longs.min(
              bytesRemaining != C.LENGTH_UNSET ? bytesRemaining : Long.MAX_VALUE,
              readBuffer.remaining(),
              length);

  readBuffer.get(buffer, offset, bytesRead);

  if (bytesRemaining != C.LENGTH_UNSET) {
    bytesRemaining -= bytesRead;
  }
  bytesTransferred(bytesRead);
  return bytesRead;

  • 从注释我们可以看到 支持Range请求,可以很大程度加快播放速度,特别是视频拖拽快进的时候。

  • bytesRead 取的 bytesRemaining , Long.MAX_VALUE, readBuffer.remaining(), length 这几个参数里面最小的值。

  • 使用 readBuffer.get() 方法将数据从读取缓冲区复制到提供的 buffer 数组的指定偏移量处

  • 调用 bytesTransferred() 方法来通知已传输的字节数。

  • 最后实际读取的字节数

请求回调 UrlRequestCallback

onRedirectReceived

服务器可能会发送重定向响应,该响应会将流程转到 [onRedirectReceived()] 方法。

onResponseStarted

public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
  if (request != currentUrlRequest) {
    return;
  }
  responseInfo = info;
  operation.open();
}

这里的 operation.open(),会唤醒 open 方法中 的 blockUntilConnectTimeout。 应用跟踪所有重定向后,服务器会发送响应标头并调用 [onResponseStarted()]方法。请求处于 Waiting for read()  状态。应用应调用 [read()] 方法来尝试读取部分响应正文。调用 read() 后,请求处于读取状态,可能出现以下结果:

  • onReadCompleted 读取操作已成功完成,但还存在更多可用数据。系统会调用 [onReadCompleted()],并且请求再次处于正在等待 read()  状态。应用会再次调用 [read()]方法,以继续读取响应正文。应用还可以使用 [cancel()]方法停止读取请求。

  • onSucceeded 读取操作已成功,没有更多数据了。

  • onFailed 读取操作失败。

释放资源 close()

public synchronized void close() {
  if (currentUrlRequest != null) {
    currentUrlRequest.cancel();
    currentUrlRequest = null;
  }
  if (readBuffer != null) {
    readBuffer.limit(0);
  }
  currentDataSpec = null;
  responseInfo = null;
  exception = null;
  finished = false;
  if (opened) {
    opened = false;
    transferEnded();
  }
}

这里主要是释放资源,重置参数,和处理回调transferEnded()。

到此,全文结束了,谢谢大家。

参考资料

Cronet 请求生命周期 developer.android.google.cn/develop/con…

本人知识有限,如有描述错误之处,望虎正。

你的赞就像冬日暖阳,温暖心窝。