基于ExoPlayer 2.17.1源码分析,带着问题看代码,主要解决以下几点问题:

  • 缓存是如何存取管理的
  • 如何获取HLS的已缓存大小

首先回顾下上一篇文章《ExoPlayer 源码阅读小记–HLS播放带缓存加载M38U文件过程》里第一次涉及到缓存的地方:
调用StatsDataSource封装的CacheDataSource,这里会调用TeeDataSource 的open,Tees翻译就是三通的意思,这是个一进二出的三通,一个水龙头冷水进,一路进小厨宝缓存加热后出,一路直接水龙出

//TeeDataSource
public long open(DataSpec dataSpec) throws IOException {bytesRemaining = upstream.open(dataSpec);//调用OkHttpDataSource open打开冷水进水阀门,准备一路上出水到水龙头,一路到小厨宝缓存if (bytesRemaining == 0) {return 0;}if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) {// Reconstruct dataSpec in order to provide the resolved length to the sink.dataSpec = dataSpec.subrange(0, bytesRemaining);}dataSinkNeedsClosing = true;dataSink.open(dataSpec);//dataSink由CacheDataSource设置,调用CacheDataSink open打开厨宝的进水阀,准备下出水到小厨宝缓存return bytesRemaining;}

open的流也没有立即读取而是缓存在DataSource里,当调用TeeDataSource read方法时才会写入缓存,这里可以看出缓存是播多少缓存多少,并不会提前预缓存,只有在读取到这个hls分段的时候才会去加载缓存,exoplayer缓存的目的也只是为了下次再加载时可以直接从缓存中获取数据

//TeeDataSourcepublic int read(byte[] buffer, int offset, int length) throws IOException {if (bytesRemaining == 0) {return C.RESULT_END_OF_INPUT;}int bytesRead = upstream.read(buffer, offset, length);if (bytesRead > 0) {// TODO: Consider continuing even if writes to the sink fail.这里还有个TODO作者想要缓存写入失败的时候也不要影响读取dataSink.write(buffer, offset, bytesRead);if (bytesRemaining != C.LENGTH_UNSET) {bytesRemaining -= bytesRead;}}return bytesRead;}

TeeDataSource open是在CacheDataSource open函数中调用的,来看下CacheDataSource open函数,这里主要关注openNextSource这个函数,这里包含了缓存的整个初始化过程,详细看下面注释

//CacheDataSource
public long open(DataSpec dataSpec) throws IOException {....if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {openNextSource(requestDataSpec, false);}return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : bytesRemaining;....}private void openNextSource(DataSpec requestDataSpec, boolean checkCache) throws IOException {@Nullable CacheSpan nextSpan;
....
//这里如果是首次会将当前url加入到索引,但只是缓存在内存中,并未持久化到文件,如果非首次会直接读取之前的索引返回Span,
//索引文件包含唯一id和文件的url,当初始化的时候会线读取索引文件中所有的id和url
//然后扫描缓存目录将通过文件名中的id将索引和对应的文件建立起映射关系,进而可以通过索引文件查询到所有的缓存文件nextSpan = cache.startReadWriteNonBlocking(key, readPosition, bytesRemaining);}DataSpec nextDataSpec;DataSource nextDataSource;if (nextSpan == null) {// The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read// from upstream.nextDataSource = upstreamDataSource;nextDataSpec =requestDataSpec.buildUpon().setPosition(readPosition).setLength(bytesRemaining).build();} else if (nextSpan.isCached) {//如果获取到的Span是已有的已经缓存的走这里// Data is cached in a span file starting at nextSpan.position.Uri fileUri = Uri.fromFile(castNonNull(nextSpan.file));long filePositionOffset = nextSpan.position;long positionInFile = readPosition - filePositionOffset;long length = nextSpan.length - positionInFile;if (bytesRemaining != C.LENGTH_UNSET) {length = min(length, bytesRemaining);}nextDataSpec =requestDataSpec.buildUpon().setUri(fileUri).setUriPositionOffset(filePositionOffset).setPosition(positionInFile).setLength(length).build();//创建供缓存文件读取Datasource的Spec//这里使用DefaultDataSource,它会根据文件的SCHEME使用合适的DataSoucrce打开读取文件,//如果是本地缓存文件会使用FileDataSource来读取缓存nextDataSource = cacheReadDataSource;} else {//首次的情况为被缓存会走这里// Data is not cached, and data is not locked, read from upstream with cache backing.long length;if (nextSpan.isOpenEnded()) {length = bytesRemaining;} else {length = nextSpan.length;if (bytesRemaining != C.LENGTH_UNSET) {length = min(length, bytesRemaining);}}nextDataSpec =requestDataSpec.buildUpon().setPosition(readPosition).setLength(length).build();if (cacheWriteDataSource != null) {nextDataSource = cacheWriteDataSource;//这里设置使用TeeDataSource获取数据
....currentDataSource = nextDataSource;currentDataSpec = nextDataSpec;currentDataSourceBytesRead = 0;long resolvedLength = nextDataSource.open(nextDataSpec);//调用事先设定的DataSource去open资源
....}

当第一次访问没有被缓存的资源时候会调用TeeDataSource的open,最终会调用dataSink.open(dataSpec)我看看下做了什么

//CacheDataSink
public void open(DataSpec dataSpec) throws CacheDataSinkException {....try {openNextOutputStream(dataSpec);} catch (IOException e) {throw new CacheDataSinkException(e);}}private void openNextOutputStream(DataSpec dataSpec) throws IOException {....//这里主要就是创建缓存的文件并打开文件输入流,这个文件对缓存目录下的一个随机数字文件夹下的一个.exo文件里file =cache.startFile(castNonNull(dataSpec.key), dataSpec.position + dataSpecBytesWritten, length);FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);if (bufferSize > 0) {if (bufferedOutputStream == null) {bufferedOutputStream =new ReusableBufferedOutputStream(underlyingFileOutputStream, bufferSize);} else {bufferedOutputStream.reset(underlyingFileOutputStream);}outputStream = bufferedOutputStream;} else {outputStream = underlyingFileOutputStream;}outputStreamBytesWritten = 0;}
//SimpleCachepublic synchronized File startFile(String key, long position, long length) throws CacheException {....//contentIndex是一个所有已通过url查询索引,在open前会//调用SimpleCache的startReadWriteNonBlocking方法将当前url添加到索引中同时生成一个唯一ID供下面使用,但只是缓存在内存中,并未持久化到文件CachedContent cachedContent = contentIndex.get(key);....if (!cacheDir.exists()) {//检测缓存目录是否存在// The cache directory has been deleted from underneath us. Recreate it, and remove in-memory// spans corresponding to cache files that no longer exist.createCacheDirectories(cacheDir);removeStaleSpans();}evictor.onStartFile(this, key, position, length);// Randomly distribute files into subdirectories with a uniform distribution.File cacheSubDir = new File(cacheDir, Integer.toString(random.nextInt(SUBDIRECTORY_COUNT)));//创建随机数字文件夹if (!cacheSubDir.exists()) {createCacheDirectories(cacheSubDir);}long lastTouchTimestamp = System.currentTimeMillis();return SimpleCacheSpan.getCacheFile(cacheSubDir, cachedContent.id, position, lastTouchTimestamp);//构建文件名}
//SimpleCacheSpanpublic static File getCacheFile(File cacheDir, int id, long position, long timestamp) {//文件名规则是索引的ID+资源读取位置+当前时间戳return new File(cacheDir, id + "." + position + "." + timestamp + SUFFIX);}

到这里/sdcard/Android/data/com.xxxx.xxx/cache/exo/0/0.0.1653091779124.v3.exo(各个项目会不同,此处为举例)文件已经创建流已打开,下面来看读数据同时写入缓存的逻辑还是从CacheDataSource看起

//CacheDataSourcepublic int read(byte[] buffer, int offset, int length) throws IOException {if (length == 0) {return 0;}if (bytesRemaining == 0) {return C.RESULT_END_OF_INPUT;}DataSpec requestDataSpec = checkNotNull(this.requestDataSpec);DataSpec currentDataSpec = checkNotNull(this.currentDataSpec);try {if (readPosition >= checkCachePosition) {openNextSource(requestDataSpec, true);}//currentDataSource在上面open时设置和上面一样如果是首次会使用TeeDataSource,已缓存会使用FileDataSourceint bytesRead = checkNotNull(currentDataSource).read(buffer, offset, length);....return bytesRead;} catch (Throwable e) {handleBeforeThrow(e);throw e;}}

如果是首次的情况会使用TeeDataSource获取OkhttpDataSource里的网络数据,同时通过dataSink.write(buffer, offset, bytesRead);写入缓存,代码一开始已经贴过,这里直接进入到dataSink.write

//CacheDataSink
public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {@Nullable DataSpec dataSpec = this.dataSpec;if (dataSpec == null) {return;}try {int bytesWritten = 0;while (bytesWritten < length) {if (outputStreamBytesWritten == dataSpecFragmentSize) {closeCurrentOutputStream();openNextOutputStream(dataSpec);}int bytesToWrite =(int) min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten);//这里的outputStream就是上面open时打开的文件流castNonNull(outputStream).write(buffer, offset + bytesWritten, bytesToWrite);bytesWritten += bytesToWrite;outputStreamBytesWritten += bytesToWrite;dataSpecBytesWritten += bytesToWrite;}} catch (IOException e) {throw new CacheDataSinkException(e);}}

写入完成后会关闭当前流开始请求下一个片段,关闭流的时候会同时将内存中的索引同步到本地索引文件中,位置在/sdcard/Android/data/com.xxxx.xxx/cache/exo/cached_content_index.exi,这个索引文件里包含当前url的索引和ID,通过缓存文件名中的ID建立起关联

通过上面的分析我们知道一个ts文件缓存时会在cached_content_index.exi中建立索引,通过ID关联到缓存的文件,当获取数据时会查询索引文件进而获取到对应的缓存文件读取,一段缓存在EXO里对应一个Span,一个ts文件可能有多个Span缓存,好了第一个问题已经解决,下面学以致用来解决第二个问题

如何如何获取HLS的已缓存大小?

由这篇文章知道,一个m3u8文件每个分段都有各自的url,这些分段在缓存后会将url和生成的唯一ID记录到索引文件中,而缓存文件通过ID和索引建立联系,文件名中除了ID+postion+时间戳,就没有其他信息了,索引文件也只记录了ID和分段的URL,所以各个分段之间是独立的,并不知道他们属于哪个m3u8,所以要回答上面的问题需要先获取m3u8和各分段的关系,也就是获取到解析后当前播放的m3u8列表(EXO叫snapshots快照),通过上一篇文章可知HlsPlaylistTracker就是管理列表的类,好了问题变为如何获取到HlsPlaylistTracker?

可以在设置HlsMediaSource时通过setPlaylistTrackerFactory时缓存当前的HlsPlaylistTracker:

mediaSource = new HlsMediaSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir, uerAgent)).setPlaylistTrackerFactory(new HlsPlaylistTracker.Factory() {@Overridepublic HlsPlaylistTracker createTracker(final HlsDataSourceFactory hlsDataSourceFactory,final LoadErrorHandlingPolicy loadErrorHandlingPolicy, final HlsPlaylistParserFactory playlistParserFactory) {currentHlsTracker = new DefaultHlsPlaylistTracker(hlsDataSourceFactory,loadErrorHandlingPolicy, playlistParserFactory);return currentHlsTracker;}}).createMediaSource(mediaItem);

下面就可以利用这些来查询缓存了,这里列下大致代码:


//使用上面缓存的currentHlsTracker获取所有的播放列表final HlsMultivariantPlaylist multivariantPlaylist = currentHlsTracker.getMultivariantPlaylist();//获取第一个m3u8的url地址final Uri url = multivariantPlaylist.variants.get(0).url;//获取当前播放的列表快照final HlsMediaPlaylist playlistSnapshot = currentHlsTracker.getPlaylistSnapshot(multivariantPlaylist.variants.get(0).url, false);//获取到所有的分段开始循环for (final Segment segment : playlistSnapshot.segments) {//cacheSingleInstance为创建CacheDataSource时setCache使用的SimpleCache单例//buildCacheKey通过CacheKeyFactory.DEFAULT.buildCacheKey获取,其实就是当前分段的url全路径NavigableSet<CacheSpan> cachedSpans = cacheSingleInstance.getCachedSpans(buildCacheKey);//获取到当前分段url对应的所有缓存Span,通过isCached判断是否已缓存if (cachedSpans.size() != 0) {for (CacheSpan cachedSpan : cachedSpans) {if (cachedSpan.isCached) {....//判断完成后可以通过//segment.relativeStartTimeUs获取当前分段对应播放列表的开始时间点//segment.relativeStartTimeUs + segment.durationUs获取当前分段对应播放列表的结束时间点}}}}

好了缓存模块就分析到这里,默认的缓存还不够强大,大家可以思考下如何实现预加载更多的缓存甚至实现边下边播

ExoPlayer 源码阅读小记--缓存模块及获取HLS已缓存大小相关推荐

  1. ExoPlayer 源码阅读小记--HLS播放带缓存加载M38U文件过程

    基于ExoPlayer 2.17.1源码分析,基本是一边看一边写的流水账,记录下防止以后忘了: 第一步createMediaSource创建HlsMediaSource对象时同时会实例化出HlsPla ...

  2. FreeSWITCH 1.10 源码阅读(3)-sofia 模块原理及其呼入处理流程

    文章目录 1. 前言 2. 源码分析 2.1 sofia 模块的加载 2.2 呼入的处理流程 1. 前言 SIP(Session Initiation Protocol) 是应用层的信令控制协议,有许 ...

  3. sfm三维重建源码_OpenMVG源码阅读小记

    "读一份好源码,就是和许多智慧的人谈话". 本文记录了笔者学习 openMVG 开源软件的一些初步经验和心得.如果你对计算机视觉和摄影测量有兴趣,需要用到相关技术,这篇文章正好就是 ...

  4. java中talent-aio_talent-aio源码阅读小记(一)

    近来在oschina上看到一个很火的java 即时通讯项目talent-aio,恰巧想了解一下这方面的东西,就阅读了一下项目的源码,这里对自己阅读源码后的一些心得体会做一下备忘,也希望能够对其他项目中 ...

  5. vue data 值如何渲染_vue源码阅读复盘-watcher模块

    回顾目标 数据驱动视图: 理解watcher.dep.observer这三个对象之间的关系 这和VUE对象又有什么关系? 这和视图又有什么关系? 叙述过程 先彻底理解了一下VUE的简介,并写出了一份建 ...

  6. freeswitch源码阅读 之 sofia模块

    sofia模块在freeswitch中的位置非常重要, 所有的sip通话都和它有关, 那么我们就看一下该模块的执行流程. 一. 实现的功能: 1. sip注册; 2. 呼叫; 3. Presence; ...

  7. openedge-hub模块请求处理源码浅析——百度BIE边缘侧openedge项目源码阅读(2)

    前言 在openedge-hub模块启动源码浅析--百度BIE边缘侧openedge项目源码阅读(1)一文中浅析了openedge-hub模块的启动过程,openedge-hub为每一个连接的请求创建 ...

  8. Nginx模块开发:模块结构的源码阅读以及过滤器(Filter)模块的实现

    Nginx模块开发:模块结构的源码阅读以及过滤器(Filter)模块的实现 一.Nignx中的模块是什么? 二.模块的基本结构 `ngx_module_s` `ngx_command_s` `ngx_ ...

  9. cocos creator 游戏源码_Cocos Creator 3D引擎源码阅读之授之以渔 源码阅读

    源码阅读 动静之法 静 找到引擎源码的所在 在编辑器的右上角有一个大按钮 在VSCode里开打engine目录 引擎源码就在红色标中的cocos文件夹里,如下图 让我们来看一下引擎的目录结构 可以看到 ...

  10. SpringMVC源码阅读系列汇总

    1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...

最新文章

  1. 【进大厂大数据爬虫技术核心难点】纯前端开发的爬虫程序,很多BAT技术大咖都为之惊叹
  2. 通过memcached来实现对tomcat集群中Session的共享策略 .
  3. 【五线谱】Sibelius 7.5.1 打谱软件安装 ( 软件下载 | 软件安装 )
  4. jsp页面返回文本时产生大量空格的解决办法
  5. SQL:使用备份向导、SQL命令、导出数据三种方式对已建立的数据库进行备份
  6. 从淘特升级,看电商特别模式的特别价值
  7. linux 查看进程id对应的路径,Linux中怎么通过PID号找到对应的进程名及所在目录方法...
  8. TMS320 C6000系列 DSP之 CCS5.5 仿真调试
  9. 第53天:鼠标事件、event事件对象
  10. python学习笔记——类
  11. C++一天一个程序(七)
  12. T-SQL LIKE子句 模糊查询
  13. LightGCN:用于推荐任务的简化并增强的图卷积网络 SIGIR 2020
  14. IT的2017,面临数字生态系统新挑战,该怎么办?
  15. C和指针 第五章 习题
  16. 转换器(Converter)——Struts 2.0中的魔术师
  17. 计算机网络基础系列(五)Socket与TCP/IP编程
  18. 几行python代码实现Windows软件卸载
  19. 有没有什么好用的pdf编辑软件?3款App轻松编辑所有PDF文件
  20. cin cin.get cin.getlin

热门文章

  1. 2022-2028全球与中国以太网控制器市场现状及未来发展趋势
  2. 为 昂达 v891 安装上了 remix OS 了
  3. ModelAndView 详解
  4. VB6 TCP通讯服务端、客户端源码
  5. java运行 .class文件_运行java的class文件方法详解
  6. 由粒子加速器产生的反中子形成的白洞
  7. 判断拐点的条件_拐点的判断
  8. java计算机毕业设计网络教学系统源码+系统+数据库+lw文档
  9. 计算机桌面图标被挡怎么办,win7电脑桌面图标被挡住怎么恢复 - 卡饭网
  10. 如何写一个简单的爬虫程序