基于ExoPlayer 2.17.1源码分析,基本是一边看一边写的流水账,记录下防止以后忘了:

第一步createMediaSource创建HlsMediaSource对象时同时会实例化出HlsPlaylistTracker.Factory

mediaSource = new HlsMediaSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir, uerAgent)).setAllowChunklessPreparation(true).setPlaylistParserFactory(new HlsPlaylistParserFactory()).createMediaSource(mediaItem);
    public HlsMediaSource createMediaSource(MediaItem mediaItem) {checkNotNull(mediaItem.localConfiguration);HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory;List<StreamKey> streamKeys = mediaItem.localConfiguration.streamKeys;if (!streamKeys.isEmpty()) {playlistParserFactory =new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);}return new HlsMediaSource(mediaItem,hlsDataSourceFactory,extractorFactory,compositeSequenceableLoaderFactory,drmSessionManagerProvider.get(mediaItem),loadErrorHandlingPolicy,playlistTrackerFactory.createTracker(hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),//实例化TrackerFactoryelapsedRealTimeOffsetMs,allowChunklessPreparation,metadataType,useSessionKeys);}

第二步调用prepare(),在准备阶段将m3u8文件下载并解析

     player.prepare();
//ExoPlayerImpl
public void prepare() {....internalPlayer.prepare();......}
//ExoPlayerImplInternalprivate void prepareInternal() {....mediaSourceList.prepare(bandwidthMeter.getTransferListener());....}
//MediaSourceListpublic void prepare(@Nullable TransferListener mediaTransferListener) {....for (int i = 0; i < mediaSourceHolders.size(); i++) {MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);prepareChildSource(mediaSourceHolder);enabledMediaSourceHolders.add(mediaSourceHolder);}....}private void prepareChildSource(MediaSourceHolder holder) {.....mediaSource.prepareSource(caller, mediaTransferListener, playerId);}
//BaseMediaSource
public final void prepareSource(MediaSourceCaller caller,@Nullable TransferListener mediaTransferListener,PlayerId playerId) {....prepareSourceInternal(mediaTransferListener);....}
//HlsMediaSource
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {....playlistTracker.start(localConfiguration.uri, eventDispatcher, /* primaryPlaylistListener= */ this);}

到这里使用HlsPlaylistTracker,这个类主要是管理播放列表(控制加载解析流程),主列表叫primary playlist,另外该类还能获取当前播放的列表快照,该类入口就在这个start方法

//DefaultHlsPlaylistTracker
public void start(Uri initialPlaylistUri,EventDispatcher eventDispatcher,PrimaryPlaylistListener primaryPlaylistListener) {....ParsingLoadable<HlsPlaylist> multivariantPlaylistLoadable =new ParsingLoadable<>(dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),initialPlaylistUri,C.DATA_TYPE_MANIFEST,playlistParserFactory.createPlaylistParser());Assertions.checkState(initialPlaylistLoader == null);initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MultivariantPlaylist");//创建m3u8文件的网络加载解析线程long elapsedRealtime =initialPlaylistLoader.startLoading(multivariantPlaylistLoadable,this,loadErrorHandlingPolicy.getMinimumLoadableRetryCount(multivariantPlaylistLoadable.type));.....}

ParsingLoadable主要代码就这个方法,先通过DataSource获取需要加载的文件流,将文件流传入解析器解析,这里说下DataSourceInputStream本质是InputStream子类,但是里面包含一个datasSource和dataSpec,datasSource可以理解为数据源访问工具(如网络数据源访问Okhttp)获取到数据流后会将流保存在datasSource以供读取,dataSpec用来标记当前数据范围,里面还封装了读取数据的方式参数(如http请求url和header)

//ParsingLoadablepublic final void load() throws IOException {// We always load from the beginning, so reset bytesRead to 0.dataSource.resetBytesRead();DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);try {inputStream.open();Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri());result = parser.parse(dataSourceUri, inputStream);} finally {Util.closeQuietly(inputStream);}}

当DataSourceInputStream调用open时,就会通过 dataSource.open(dataSpec)
当DataSourceInputStream,reader时就读取dataSource的数据流

//DataSourceInputStreamprivate void checkOpened() throws IOException {if (!opened) {dataSource.open(dataSpec);opened = true;}}public int read(byte[] buffer, int offset, int length) throws IOException {Assertions.checkState(!closed);checkOpened();int bytesRead = dataSource.read(buffer, offset, length);if (bytesRead == C.RESULT_END_OF_INPUT) {return -1;} else {totalBytesRead += bytesRead;return bytesRead;}}

好了我们继续open流程,StatsDataSource是一个封装DataSource的DataSource,里面包含一个DataSource 重定向的 uri 和响应标头等数据

//StatsDataSourcepublic long open(DataSpec dataSpec) throws IOException {// Reassign defaults in case dataSource.open throws an exception.lastOpenedUri = dataSpec.uri;lastResponseHeaders = Collections.emptyMap();long availableBytes = dataSource.open(dataSpec);lastOpenedUri = Assertions.checkNotNull(getUri());lastResponseHeaders = getResponseHeaders();return availableBytes;}

继续调用StatsDataSource封装的DataSource,继续套娃,套娃一般是为了分层,因为设置的是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;}

OkHttpDataSource 就忽略了,就是访问网络获取m3u8的文件流,这里看下CacheDataSink ,openNextOutputStream主要就是创建缓存的目录,创建缓存文件并获取输出流,这里并没有写入数据

//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 {....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;}....}

那么在哪里什么时机写入缓存文件了呢,通过上面open的流程知道,open的流也没有立即读取而是缓存在DataSource里,当调用DataSource的read方法才会读取,所以理所应当写入缓存的时机就在读取的时候
,回到TeeDataSource read方法,具体的read时间点在下面会讲到,我们继续往下看

//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;}

缓存相关先不展开说了,读取看完继续看解析,这里没啥好说的需要熟悉m3u8文件的格式,返回一个解析后的数据结构

//HlsPlaylistParser
public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));Queue<String> extraLines = new ArrayDeque<>();String line;try {if (!checkPlaylistHeader(reader)) {//这里校验M3U8文件头,有些不规范的M3U8文件会包含BOM头校验会不通过,可以在创建MediaSource时通过setPlaylistParserFactory设置自定义HlsPlaylistParser修改支持BOM头throw ParserException.createForMalformedManifest(/* message= */ "Input does not start with the #EXTM3U header.", /* cause= */ null);}while ((line = reader.readLine()) != null) {line = line.trim();if (line.isEmpty()) {// Do nothing.} else if (line.startsWith(TAG_STREAM_INF)) {extraLines.add(line);return parseMultivariantPlaylist(new LineIterator(extraLines, reader), uri.toString());} else if (line.startsWith(TAG_TARGET_DURATION)|| line.startsWith(TAG_MEDIA_SEQUENCE)|| line.startsWith(TAG_MEDIA_DURATION)|| line.startsWith(TAG_KEY)|| line.startsWith(TAG_BYTERANGE)|| line.equals(TAG_DISCONTINUITY)|| line.equals(TAG_DISCONTINUITY_SEQUENCE)|| line.equals(TAG_ENDLIST)) {extraLines.add(line);return parseMediaPlaylist(multivariantPlaylist,previousMediaPlaylist,new LineIterator(extraLines, reader),uri.toString());} else {extraLines.add(line);}}} finally {Util.closeQuietly(reader);}throw ParserException.createForMalformedManifest("Failed to parse the playlist, could not identify any tags.", /* cause= */ null);}

解析完成又会回到DefaultHlsPlaylistTracker的onLoadCompleted,会获取刚才的额解析结果

//DefaultHlsPlaylistTracker
public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {HlsPlaylist result = loadable.getResult();//获取解析结果.......MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);if (isMediaPlaylist) {// We don't need to load the playlist again. We can use the same result.primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo);} else {primaryBundle.loadPlaylist();}....}private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) {@Nullable HlsMediaPlaylist oldPlaylist = playlistSnapshot;....if (playlistSnapshot != oldPlaylist) {playlistError = null;lastSnapshotChangeMs = currentTimeMs;onPlaylistUpdated(playlistUrl, playlistSnapshot);} ....}private void onPlaylistUpdated(Uri url, HlsMediaPlaylist newSnapshot) {if (url.equals(primaryMediaPlaylistUrl)) {....primaryMediaPlaylistSnapshot = newSnapshot;primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);//首次加载primary}
....}

playlist获取后就会交友MediaSource类处理,MediaSource 有两个主要职责:

  1. 为播放器提供Timeline(定义了媒体结构),当在媒体结构发生变化时提供新的Timeline。MediaSource 通过调用MediaSource.MediaSourceCaller.onSourceInfoRefreshed来提供这些时间线。
  2. 为其时间线中的Period提供MediaPeriod实例。MediaPeriods 是通过调用createPeriod获得的,并为播放器提供了一种加载和读取媒体的方式。关于Timeline
//HlsMediaSource
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist) {....SinglePeriodTimeline timeline =playlistTracker.isLive()? createTimelineForLive(mediaPlaylist, presentationStartTimeMs, windowStartTimeMs, manifest): createTimelineForOnDemand(mediaPlaylist, presentationStartTimeMs, windowStartTimeMs, manifest);//Timeline在这里创建refreshSourceInfo(timeline);}
//BaseMediaSourceprotected final void refreshSourceInfo(Timeline timeline) {this.timeline = timeline;for (MediaSourceCaller caller : mediaSourceCallers) {caller.onSourceInfoRefreshed(/* source= */ this, timeline);}}
//CompositeMediaSource
protected final void prepareChildSource(@UnknownNull T id, MediaSource mediaSource) {,....MediaSourceCaller caller =(source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline);//调用此处prepare时注册的caller....}
//MaskingMediaSource
protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline newTimeline) {@Nullable MediaPeriodId idForMaskingPeriodPreparation = null;....if (idForMaskingPeriodPreparation != null) {Assertions.checkNotNull(unpreparedMaskingMediaPeriod).createPeriod(idForMaskingPeriodPreparation);//创建HlsMediaPeriod}}
public void createPeriod(MediaPeriodId id) {....if (callback != null) {mediaPeriod.prepare(/* callback= */ this, preparePositionUs);}}

HlsMediaPeriod用来加载Timeline里的Period

//HlsMediaPeriodpublic void prepare(Callback callback, long positionUs) {this.callback = callback;playlistTracker.addListener(this);buildAndPrepareSampleStreamWrappers(positionUs);}private void buildAndPrepareSampleStreamWrappers(long positionUs) {....for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) {sampleStreamWrapper.continuePreparing();}....}

HlsSampleStreamWrapper包含一个HlsChunkSource用来获取HlsMediaChunk进行加载提供出SampleStream,
Chunk可以理解为数据块,这里对应一个ts文件,通过ChunkSource获取Chunk执行加载

//HlsSampleStreamWrapperpublic void continuePreparing() {if (!prepared) {continueLoading(lastSeekPositionUs);}}
public boolean continueLoading(long positionUs) {....chunkSource.getNextChunk(positionUs,loadPositionUs,chunkQueue,/* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(),nextChunkHolder);
....@Nullable Chunk loadable = nextChunkHolder.chunk;....long elapsedRealtimeMs =loader.startLoading(//加载获取到的chunk,这里可以看到将回调函数设置在了HlsSampleStreamWrapper,后面加载完成后会回调它的onLoadCompleted方法loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));....return true;}

这里看下获取下一个chunk 的代码,其实就是从解析的playerlist里通过获取Segment创建出对应的chunk

//HlsChunkSource
public void getNextChunk(long playbackPositionUs,long loadPositionUs,List<HlsMediaChunk> queue,boolean allowEndOfStream,HlsChunkHolder out) {....// Select the track.MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);....@NullableHlsMediaPlaylist playlist =playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);//通过Tracker获取当前播放的列表
....Pair<Long, Integer> nextMediaSequenceAndPartIndex =getNextMediaSequenceAndPartIndex(previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);//获取下一个Sequence也就是m3u8里的下一个片段索引long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;int partIndex = nextMediaSequenceAndPartIndex.second;....@NullableSegmentBaseHolder segmentBaseHolder =getNextSegmentHolder(playlist, chunkMediaSequence, partIndex);//获取信息....//创建通过获取的Segment创建出对应的chunkout.chunk =HlsMediaChunk.createInstance(extractorFactory,mediaDataSource,playlistFormats[selectedTrackIndex],startOfPlaylistInPeriodUs,playlist,segmentBaseHolder,selectedPlaylistUrl,muxedCaptionFormats,trackSelection.getSelectionReason(),trackSelection.getSelectionData(),isTimestampMaster,timestampAdjusterProvider,previous,/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),shouldSpliceIn,playerId);}

MediaChunk执行chunk加载,首先通过prepareExtraction创建出input数据流和HlsMediaChunkExtractor执行器,然后通过执行器执行相应的流,流的这块后续处理还是比较复杂的不是本文重点,主要目的就是ts文件的Demux,解析出Demux(对应代码里的Sample,相当于一帧)后的数据发送到一个DataQuene里供Renderer获取,Renderer最终调用系统的MediaCodec渲染到surface上

//HlsMediaChunk
public void load() throws IOException {....if (!loadCanceled) {if (!hasGapTag) {loadMedia();}loadCompleted = !loadCanceled;}
}private void loadMedia() throws IOException {feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted, /* initializeTimestampAdjuster= */ true);
}private void feedDataToExtractor(DataSource dataSource,DataSpec dataSpec,boolean dataIsEncrypted,boolean initializeTimestampAdjuster)throws IOException {....try {ExtractorInput input =prepareExtraction(dataSource, loadDataSpec, initializeTimestampAdjuster);if (skipLoadedBytes) {input.skipFully(nextLoadPosition);}try {while (!loadCanceled && extractor.read(input)) {}//通过执行器执行相应的流} catch (EOFException e) {....
}private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec, boolean initializeTimestampAdjuster)throws IOException {long bytesToRead = dataSource.open(dataSpec);//这里又看到熟悉的代码类似ParsingLoadable加载m3u8文件一样,调用我们的三通TeeDataSource从网络获取ts文件,数据流会保存在datasource等待读取,读取时缓存文件....DefaultExtractorInput extractorInput =new DefaultExtractorInput(dataSource, dataSpec.position, bytesToRead);//数据流if (extractor == null) {....extractor =//执行器previousExtractor != null? previousExtractor.recreate(): extractorFactory.createExtractor(dataSpec.uri,trackFormat,muxedCaptionFormats,timestampAdjuster,dataSource.getResponseHeaders(),extractorInput,playerId);....return extractorInput;
}

到这里第一个ts文件已经加载,读取流,完成后会回到HlsSampleStreamWrapper这个类的onLoadCompleted方法,后续的ts文件加载会在播放器主loop里循环判断是否要继续加载,如果判断需要继续加载,最终调用continueLoading实现加载下一个ts文件。最具调用判断过程后续会讲到。

//HlsSampleStreamWrapper
public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {loadingChunk = null;chunkSource.onChunkLoadCompleted(loadable);
....if (!prepared) {continueLoading(lastSeekPositionUs);} else {callback.onContinueLoadingRequested(this);}}

至此HLS的m3u8文件加载解析以及ts文件读取的大致流程就完成了

ExoPlayer 源码阅读小记--HLS播放带缓存加载M38U文件过程相关推荐

  1. Soul网关源码阅读(九)插件配置加载初探

    Soul网关源码阅读(九)插件配置加载初探 简介     今日来探索一下插件的初始化,及相关的配置的加载 源码Debug 插件初始化     首先来到我们非常熟悉的插件链调用的类: SoulWebHa ...

  2. 【GStreamer源码分析】playbin播放test.wav加载插件过程分析

    playbin播放test.wav加载插件过程分析 一.前言 二.playbin 播放 .wav 音频插件加载一览 三.测试代码 3.1 gst_init 3.2 gst_element_set_st ...

  3. php源码自动识别文本中的链接,自动加载识别文件Auto.php

    用于本应用的控制器自动加载类设置,用法如同\CodeIgniter\Config\AutoloadConfig 自动加载识别文件:dayrui/App/应用目录/Config/Auto.php 语法格 ...

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

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

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

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

  6. Spring Ioc 源码分析(一)--Spring Ioc容器的加载

    1.目标:熟练使用spring,并分析其源码,了解其中的思想.这篇主要介绍spring ioc 容器的加载 2.前提条件:会使用debug 3.源码分析方法:Intellj idea debug 模式 ...

  7. linux dlopen 源码,采用dlopen、dlsym、dlclose加载动态链接库

    采用dlopen.dlsym.dlclose加载动态链接库 转载请标注,熬夜写的文章,挺辛苦 ... 环境 系统: 16.04.1-Ubuntu 编译器: gnu 5.4.0 python: 2.7. ...

  8. HTML卡片式布局源码,html5自适应卡片式设计动态加载整站源码_

    html5自适应卡片式设计动态加载整站源码 该模板是非常容易存活的,这样的程序很容易吸引访客点击,提升ip流量和pv是非常有利的,随意挂点联盟广告都能养活程序. 本套整站源码采使用现在非常流行的全屏自 ...

  9. wemall app商城源码中基于JAVA的Android异步加载图片管理器代码

    wemall doraemon是Android客户端程序,服务端采用wemall微信商城,不对原商城做任何修改,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可随意定制修改.本文分享其中 ...

最新文章

  1. LabVIEW设置应用程序显示标签透明
  2. Java培训完可以应用在什么领域
  3. JavaEE基本了解
  4. Azure Backup和Azure Site Recovery的区别是什么
  5. Android中使用SeekBar拖动条实现改变图片透明度
  6. ITK:通过指定区域裁剪图像
  7. 新手制作bom表格教程_抖音短视频怎么制作?这里有全部最新教程+指导,新手0基础上手!...
  8. php 实现进制相互转换
  9. 面试题鬼的很:Class.forName 和 ClassLoader 有什么区别?
  10. python 函数、面向对象
  11. sql中“delete from 表名”表示_SQL查询语句知识点总结
  12. LeetCode: Word Ladder
  13. 如何自定义容器网络?- 每天5分钟玩转 Docker 容器技术(33)
  14. membercache java_Java开发中的Memcache原理及实现
  15. android手机黑科技软件,安卓党福利!10款黑科技APP,让你的手机更好用
  16. Java学习 DAY18 Map、File、IO流
  17. 网页 游戏服务器连接超时,连接游戏服务器超时怎么解决
  18. Windows上搭建PHP开发环境
  19. 3、git 暂存区撤销与删除
  20. 卷积神经网络(一)- 卷积神经网络

热门文章

  1. umi创建简单的登录界面
  2. JSP编辑工具的介绍
  3. GridView分页的实现以及自定义分页样式功能实例
  4. expect使用总结
  5. 如何手工制作html网站地图,提升网站收录率简单的方法是手工制作网站地图与工具生成网站地图两种...
  6. [宋史学习] 谁制造了“天子为点检”的木牌?
  7. scp(安全拷贝)和rsync(增量复制)
  8. html 无缝轮播图完整代码
  9. 美柚“姨妈假”上头条,App事件营销怎么做
  10. Vue 播放rtmp直播流