系列文章目录
ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor
ExoPlayer架构详解与源码分析(10)——H264Reader
ExoPlayer架构详解与源码分析(11)——DataSource
ExoPlayer架构详解与源码分析(12)——Cache
ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource
文章目录
前言
ProgressiveMediaPeriod中的SampleQueue部分相对其他部分,结构相对完整独立,没有像加载媒体那部分拆分出很多其他的概念,所以优先了解下SampleQueue。本篇主要解答媒体数据是如何在播放器内部缓存的,以及ExoPlayer是如何保证这些数据稳定高效的读写。
ProgressiveMediaPeriod
先预习下上篇的整体结构,本篇主要分析左半部分的SampleQueue:
SampleQueue
这是一个保存Sample的队列。MediaoPeriod向外提供的SampleStream其实就是从SampleQueue中读取的数据,一个SampleQueue就对应一个SampleStream。
SampleQueue主要有3大功能:
-
管理 通过内部的一个环形Info数组(包含offsets数组、sizes数等sampleData数据)管理SampleDataQueue和SharedSampleMetadata这2个数据源。SampleQueue实际的数据其实是保存在SampleDataQueue和SharedSampleMetadata中的,数据的管理实现在SampleQueue里。
这部分可以从SampleQueue初始化部分源码看出来:private final SampleDataQueue sampleDataQueue;//用于播放的数据 private final SampleExtrasHolder extrasHolder; private final SpannedData<SharedSampleMetadata> sharedSampleMetadata;//Meta数据 private int capacity;//Info数组的总长度 private long[] offsets;//每段SampleData的数据偏移量 private int[] sizes;//每段SampleData的数据大小 private int[] flags;//每段SampleData flags 数据 private long[] timesUs;//每段SampleData 时间戳 private int length;//有效的(没有被释放且已分配的数据)Info数组数据的长度 private int absoluteFirstIndex;//绝对的开始位置,指向数据段的开始位置,+readPosition就是当前读取的绝对位置 private int relativeFirstIndex;//一个在Info数据上循环的相对位置 private int readPosition;//当前的读取位置,这个值是相对relativeFirstIndex的位置偏移量 protected SampleQueue( Allocator allocator, @Nullable DrmSessionManager drmSessionManager, @Nullable DrmSessionEventListener.EventDispatcher drmEventDispatcher) { ... sampleDataQueue = new SampleDataQueue(allocator);//内存分配器供SampleDataQueue使用 extrasHolder = new SampleExtrasHolder(); capacity = SAMPLE_CAPACITY_INCREMENT;//默认的分段属是1000 sourceIds = new long[capacity]; offsets = new long[capacity]; timesUs = new long[capacity]; flags = new int[capacity]; sizes = new int[capacity]; sharedSampleMetadata = new SpannedData<>(/* removeCallback= */ metadata -> metadata.drmSessionReference.release()); ... }
上面主要是初始化出一个SampleDataQueue和一个sharedSampleMetadata数据集,然后初始化出一个1000个块的Info数组,用于管理这2块数据。这里将offsets、timesUs、sourceIds 、flags、sizes 几个数组统称为 Info数组,因为这里面共同保存着每个Sample的信息。
-
输入 同时SampleQueue实现了TrackOutput接口,对外提供sampleMetadata、format 函数使得调用者可以输入Meta信息,sampleData函数可以输入播放数据。这里的输入调用者主要是后面要说的ProgressiveMediaPeriod另一部分。
下面分析下源码数据是如何输入的://输入Metadata @Override public void sampleMetadata( long timeUs,//与当前数据关联的媒体时间戳 @C.BufferFlags int flags,//是否关键帧 int size,//样本数据大小 int offset,//块间偏移量,距离上一次已经SmapleMeta的SampleData的偏移量,我们知道媒体文件中用于播放数据不一定是连续的,其中可能包含一些其他数据,这些数据可以看成是之间的偏移量 @Nullable CryptoData cryptoData) { if (upstreamFormatAdjustmentRequired) { format(Assertions.checkStateNotNull(unadjustedUpstreamFormat)); } boolean isKeyframe = (flags & C.BUFFER_FLAG_KEY_FRAME) != 0; if (upstreamKeyframeRequired) { //从关键帧开始Sample if (!isKeyframe) { return; } upstreamKeyframeRequired = false; } timeUs += sampleOffsetUs; if (upstreamAllSamplesAreSyncSamples) { if (timeUs < startTimeUs) { // 如果所有轨道都是同步的,那么在当前Smaple点之前的时间数据就可以丢弃了 return; } if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { if (!loggedUnexpectedNonSyncSample) { Log.w(TAG, "Overriding unexpected non-sync sample for format: " + upstreamFormat); loggedUnexpectedNonSyncSample = true; } flags |= C.BUFFER_FLAG_KEY_FRAME;//保证设置为关键帧 } } if (pendingSplice) { //判断是否是拼接数据,如HLS切换流的时候就会用到 if (!isKeyframe || !attemptSplice(timeUs)) { return; } pendingSplice = false; } //当前Info的偏移量=数据总长度-样本数据长度-块间偏移量 long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset; commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } private synchronized void commitSample( long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, @Nullable CryptoData cryptoData) { if (length > 0)