ExoPlayer架构详解与源码分析(7)——SampleQueue,零基础也能看得懂

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注运维)
img

正文


前言

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接口,对外提供sampleMetadataformat 函数使得调用者可以输入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) {
      // 保证最后一个的end位置要小于等于下一个的开始位置
      int previousSampleRelativeIndex = getRelativeIndex(length - 1);
      checkArgument(
          offsets[previousSampleRelativeIndex] + sizes[previousSampleRelativeIndex] <= offset);
    }

    isLastSampleQueued = (sampleFlags & C.BUFFER\_FLAG\_LAST\_SAMPLE) != 0;
    largestQueuedTimestampUs = max(largestQueuedTimestampUs, timeUs);

    int relativeEndIndex = getRelativeIndex(length);//获取Info里的下一个位置索引
    timesUs[relativeEndIndex] = timeUs;//开始赋值
    offsets[relativeEndIndex] = offset;
    sizes[relativeEndIndex] = size;
    flags[relativeEndIndex] = sampleFlags;
    cryptoDatas[relativeEndIndex] = cryptoData;
    sourceIds[relativeEndIndex] = upstreamSourceId;

    if (sharedSampleMetadata.isEmpty()
        || !sharedSampleMetadata.getEndValue().format.equals(upstreamFormat)) {
        //开始写入Metadata
      sharedSampleMetadata.appendSpan(
          getWriteIndex(),
          new SharedSampleMetadata(checkNotNull(upstreamFormat), drmSessionReference));
    }

    length++;//有效长度++
    if (length == capacity) {//如果写入数据已经超过Info的最大长度
      // Increase the capacity.
      int newCapacity = capacity + SAMPLE\_CAPACITY\_INCREMENT;//则将Info数组长度扩展至2倍
      long[] newSourceIds = new long[newCapacity];
      long[] newOffsets = new long[newCapacity];
      long[] newTimesUs = new long[newCapacity];
      int[] newFlags = new int[newCapacity];
      int[] newSizes = new int[newCapacity];
      CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
      //将旧的数据,移入新的数组,将相对开始位置作为新数组的第一个位置
      int beforeWrap = capacity - relativeFirstIndex;
      System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap);
      System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap);
      System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap);
      System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap);
      System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap);
      System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap);
      int afterWrap = relativeFirstIndex;
      System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
      System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
      System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
      System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
      System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);
      System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
      offsets = newOffsets;
      timesUs = newTimesUs;
      flags = newFlags;
      sizes = newSizes;
      cryptoDatas = newCryptoDatas;
      sourceIds = newSourceIds;
      relativeFirstIndex = 0;
      capacity = newCapacity;
    }
  }
 
  //获取当前Info数组的相对位置,传入相对第一个位置的偏移量
  private int getRelativeIndex(int offset) {
    int relativeIndex = relativeFirstIndex + offset;
    return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;//环形指针
  }
  //获取当前写入MetaData的绝对位置
  public final int getWriteIndex() {
    return absoluteFirstIndex + length;//等于当前绝开始位置+有效的长度
  }  
  //输入Format
  @Override
  public final void format(Format format) {
    Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(format);
    upstreamFormatAdjustmentRequired = false;
    unadjustedUpstreamFormat = format;
    boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat);
    if (upstreamFormatChangeListener != null && upstreamFormatChanged) {
      upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat);
    }
  }

Metadata输入主要分3部分:

1. 确保当前为关键帧。
2. 获取Info环形数组的下一个索引,将当前Sampledata的数据保存到Info数组并记录Meta信息。
3. 确保Info数组足够大可以容纳足够多的数据。下面看下sampleData部分
  @Override
  public final void sampleData(
      ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
    sampleDataQueue.sampleData(data, length);
  }

没了就这么多。😂,你只管告诉sampleDataQueue数据的大小和长度,sampleDataQueue来添加,具体sampleDataQueue是怎么有效管理数据的后面会讲到,现在不是重点

通过sampleDataQueue和sampleMetadata对比你会发现sampleMetadata比sampleDataQueue复杂多个,而且sampleMetadata方法添加了synchronized 同步块,多线程的时候会阻塞,而sampleDataQueue没有任何同步代码包括到sampleDataQueue里也一样。这样做是因为sampleMetadata的数据量很少,即使阻塞也能很高效的执行。而sampleData数据量往往比较大,写入的时间也比较长,所以不能阻塞。那么为什么要这么做呢,后面我们看到数据的读取部分就能理解了。

  • 输出 SampleQueue提供了read方法输出数据
    分析下对应源码:
public int read(
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      @ReadFlags int readFlags,
      boolean loadingFinished) {
      //首先读取Metadata
    int result =
        peekSampleMetadata(
            formatHolder,
            buffer,
            /\* formatRequired= \*/ (readFlags & FLAG\_REQUIRE\_FORMAT) != 0,
            loadingFinished,
            extrasHolder);
    if (result == C.RESULT\_BUFFER\_READ && !buffer.isEndOfStream()) {
      boolean peek = (readFlags & FLAG\_PEEK) != 0;
      if ((readFlags & FLAG\_OMIT\_SAMPLE\_DATA) == 0) {
        if (peek) {
          sampleDataQueue.peekToBuffer(buffer, extrasHolder);
        } else {
          sampleDataQueue.readToBuffer(buffer, extrasHolder);//将sampleData的位置信息通过extrasHolder传递给sampleDataQueue读取数据
        }
      }
      if (!peek) {
        readPosition++;//读取位置++
      }
    }
    return result;
  }
  //读取Metadata
 private synchronized int peekSampleMetadata(
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      boolean formatRequired,
      boolean loadingFinished,
      SampleExtrasHolder extrasHolder) {
    ...
    Format format = sharedSampleMetadata.get(getReadIndex()).format; //用绝对开始位置+读取位置(absoluteFirstIndex + readPosition)获取读取的绝对位置
    ...

    int relativeReadIndex = getRelativeIndex(readPosition);//获取当前Info数组的相对位置
    ...
    extrasHolder.size = sizes[relativeReadIndex];//取出Info数据
    extrasHolder.offset = offsets[relativeReadIndex];
    extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];

    return C.RESULT\_BUFFER\_READ;
  }
  //获取读取的绝对位置
  public final int getReadIndex() {
    return absoluteFirstIndex + readPosition;
  }

要读取sampleData和sampleMetadata的数据,首先要确定当前读取点的Info的位置,然后通过Info数组才能知道读取的位置和长度,最后读取sampleData,同样sampleMetadata读取是加锁的,而sampleData没有。可以看出sampleData的读写是不受线程限制的,通过SampleQueue内部维护的Info数组来维护sampleData,可以最大化保证多线程下sampleData读写的效率。

  • 释放 通过discardSamples等释放不需要的数据
    分析下对应源码:
 public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
    sampleDataQueue.discardDownstreamTo(
        discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition));//先释放Metadata
  }
  
 private synchronized long discardSampleMetadataTo(
      long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
    if (length == 0 || timeUs < timesUs[relativeFirstIndex]) {
      return C.INDEX\_UNSET;
    }
    int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
    //根据时间戳来确定要释放数据块的数量
    int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe);
    if (discardCount == -1) {
      return C.INDEX\_UNSET;
    }
    return discardSamples(discardCount);
}

 private long discardSamples(int discardCount) {
    largestDiscardedTimestampUs =
        max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount));
    length -= discardCount;//有效长度=有效长度-释放数量
    absoluteFirstIndex += discardCount;//绝对开始位置后移
    relativeFirstIndex += discardCount;//相对开始位置后移
    if (relativeFirstIndex >= capacity) {//环形数组
      relativeFirstIndex -= capacity;
    }
    readPosition -= discardCount;//因为relativeFirstIndex后移,相对它的位置在缩小
    if (readPosition < 0) {
      readPosition = 0;
    }
    sharedSampleMetadata.discardTo(absoluteFirstIndex);//释放Metadata

    if (length == 0) {
      int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;
      return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
    } else {
      return offsets[relativeFirstIndex];//返回SampleData的释放偏移量
    }
}

同样数据的释放和读写一样,通过内部的Info来管理,释放后会更新Info上的相关位置。

SampleQueue动态分析

上面一直在说环形数组,静态的看代码可能感受不到这点,下面我们通过几个图来动态分析下SampleQueue的运作原理,这部分需要结合上面的源码一起看才好理解。

首先为了方便分析假设数组最大长度为capacity=6,当前已经写入了4段sampleData对应图中的sample0-3,同时也写入了4个数据到Info数组对应图中的0-2的size、offset、time结构,每个结构的size、offset、time在图的最下方都有标记,这里可以看下加深下对这个结构数值的含义的理解,所以此时有效长度length=4,由于是刚开始读写这个时候的relativeFirstIndex=absoluteFirstIndex=0处于开始位置,当前的读取位置相对于relativeFirstIndex也就是和relativeFirstIndex差值,readPosition=2。接下来sample会从右侧箭头处不断写入。同样上方的Info数组随着读取也会不断变化。
在这里插入图片描述
好了,此时先向Sample队列写入一个Sample length+1,然后同时读取2个Sample readPosition+2=4
在这里插入图片描述
随着数据被使用(已经播放)之前的数据需要丢弃,以便下次写入,释放3个Sample,可以看到relativeFirstIndex和absoluteFirstIndex同时前移,虽然readPosiition的位置没有移动,也就是没有读取新的数据,但是readPosiition的值变小,有效长度缩小为2。到这里relativeFirstIndex还是和absoluteFirstIndex相等的还看不出环形的特性。
在这里插入图片描述
此时开始写入3个Sample,这个时候就可以看出Info环形特性,之前释放的0号和1号会重新指向数据队列的最前端,同时更新offset,size相关数据,有效数据长度增加到5,length=5。
在这里插入图片描述
这个时候再读取3个Sample时,readPosiition的值增加3此时指向Info数组的下标1,readPosiition=4。
在这里插入图片描述
好了我们继续释放数据,这次再次释放3个,可以看到relativeFirstIndex和absoluteFirstIndex值开始不一样了,由于又回到了Info数组的开始位置所以relativeFirstIndex=0,readPosiition缩短为1,有效长度length=2,而absoluteFirstIndex是相对于Smaple的绝对位置,这个时候absoluteFirstIndex继续后移到6号位置的sample6。
在这里插入图片描述
把上面的图连续不断的执行,可以想象出,Info数组像一个不断前行的履带行驶在sample铺平的道路上(读取)。被履带压过的道路(已经读取过的数据)就会被释放,此时路还在不断的向前铺设(新的sample数据在不断的写入到SampleQueen中),整个过程中如果新增数据比释放数据快,履带的大小会动态的扩充变长,图中为了方便理解并没有体现这点。

至此我们可以总结出几个规律:

  • 当前写入数据的绝对位置永远等于absoluteFirstIndex+length
  • 当前读取数据的绝对位置永远等于absoluteFirstIndex + readPosition
  • Info数组可以理解成一个首尾相连的环形数组,数组最后一个位置的下个位置就是数组的开始位置
    这几个规律在源码中经常被用到,感兴趣的同学可以深入阅读下。

下面来分析下实际存储数据的2个结构

SpannedData< SharedSampleMetadata >

本质是一个Android里实现SparseArray的map,通过int 类型key可以快速向指定key存入数据或者取出数据,这里数据跟随SampleQueue里的Info来管理,添加或者释放指定位置的Metadata数据。

SampleDataQueue

重点来说下SampleDataQueue,由于SampleData的数据量要远远大于Metadata,而且还需要频繁的读写释放,所以向SpannedData那样简单粗暴的管理数据,效率会非常低,因为JVM要频繁的申请内存GC释放内存。为了解决这个问题SampleDataQueue内部维护了一个链表,同时维护了链表中3个重要的节点,firstAllocationNode,readAllocationNode,writeAllocationNode,用于快速获取读写点。

  private AllocationNode firstAllocationNode;//第一个节点位置
  private AllocationNode readAllocationNode;//当前读取节点位置
  private AllocationNode writeAllocationNode;//当前写入节点位置

这里顺带提下AllocationNode的数据结构,主要就是封装了Allocation,allocation才是实际存储数据的部分,同时提供了next AllocationNode 提供下一个AllocationNode 的指针,形成一个链表结构。

 private static final class AllocationNode implements Allocator.AllocationNode {
    public long startPosition;//此段allocation的开始位置
    public long endPosition;//此段allocation的结束位置
    public Allocation allocation;//实际缓存数据部分


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)**
![img](https://img-blog.csdnimg.cn/img_convert/6705fae8db3971445a33eb521301e57e.jpeg)

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
mplements Allocator.AllocationNode {
    public long startPosition;//此段allocation的开始位置
    public long endPosition;//此段allocation的结束位置
    public Allocation allocation;//实际缓存数据部分


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)**
[外链图片转存中...(img-dsprzmx0-1713131697843)]

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
  • 77
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值