ExoPlayer里里外外之:流媒体播放与数据结构

2024-03-16 00:18

本文主要是介绍ExoPlayer里里外外之:流媒体播放与数据结构,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

播放器中的Buffer(从source读到视频数据经过处理送给Decoder解码之前存放视频数据的缓冲,“source->Buffer->decoder”)设计往往很重要,涉及读、写、跳转等操作的效率,流媒体播放器更是如此,经典的设计比如rolling buffer,还有叫ring-buffer的,简单理解就是一个数组然后让首、尾连起来,通过读指针和写指针的移动来控制读写的位置更新。


ExoPlayer这个Buffer实做是怎么样的?上一篇文章我们有提到DataQueue和InfoQueue,就是说这个Buffer是个Queue,就是先入先出(FIFO)的队列,下面会具体描述下这个Queue的细节。ExoPlayer中除了用到这个Queue,还有用到别的数据结构,也结合流媒体的相关知识一并描述。


本文包括如下部分:

1.基于Queue实现的Buffer

2.SparseArray与HLS TS的解析

3.Dash fMP4与Stack

4.HLS的演进:fMP4


基于Queue实现的Buffer

DefaultTrackOutput的DataQueue的定义如下:

private final LinkedBlockingDeque<Allocation> dataQueue;

LinkedBlockingDeque是Android SDK中提供的使用双向链表实现的Deque,Node的类型使用ExoPlayer自己定义的Allocation,定义如下:

public final class Allocation {

  public final byte[] data;

  private final int offset;

}

每个Allocation的大小都是64K bytes,也就是说链表里面的每个Node的大小是64K bytes(一个Allocation对象),DataQueue是由一连串64K bytes大小的Allocation对象构成的,示意图如下:

DefaultTrackOutput::sampleData在往DataQueue写入数据的时候,prepareForAppend判断当前allocation(lastAllocation)是否已经满了,不满就把当前Sample写入,如果满了,就重新new一个64K bytes的Allocation,lastAllocation重新指向刚才新建的Allocation,继续写入数据。

把一个个64K bytes大小的小buffer串联成一个大buffer后,还需要InfoQueue的配合来管理这个DataQueue;InfoQueue存放Sample的metaData信息,通过DefaultTrackOutput::sampleMetadata调用infoQueue.commitSample写入每个Sample的timeUs,size,offset,encryptionKey以及是否Key Frame等信息。

InfoQueue初始容量为SAMPLE_CAPACITY_INCREMENT=1000,timesUs[], offsets[], sizes[]数组大小均为1000, 通过relativeWriteIndex来管理写,每写入一个Sample,relativeWriteIndex++; offset是重要的参数,需要通过它去DataQueue找到sample的绝对位置。

通过relativeReadIndex来管理读,解码器通过DefaultTrackOutput::readData读数据,首先调用infoQueue.readData,根据relativeReadIndex获得timeUs,size,offset; 根据offset算出DataQueue里前面可以remove的Allocation数据并remove,dataQueue.peek()获取当前offset对应数据节点Allocation,并写入DecoderInputBuffer.data。

queueSize不超过SAMPLE_CAPACITY的情况下,relativeWriteIndex和relativeReadIndex环形读写,index到CAPACITY(1000)再从0开始循环;这样以来,InfoQueue就成了rolling buffer。

如果queueSize超出了SAMPLE_CAPACITY_INCREMENT,queueSize扩大一倍。

这么设计buffer的好处

1.DataQueue和InfoQueue分开,通过InfoQueue来管理DataQueue

2.DataQueue是动态的,要多少分配多少,不用了就收回;比给定一个const的bufferSize大小要灵活的多

3.DataQueue只在被读取的时候根据offset做queue的remove,DataQueue.add的时候只管往队尾放;那有人要问了,queue不就是往队尾写,从队头读吗,这有什么奇怪的?这时候你考虑下,自适应码率切换,码率切换的时候,为了尽早切换,相同sequenceNum不同码率的chunk会重复下载一次,这时候前面已经下载成功的这个sequenceNum的数据肯定有部分不会被解码播放,那这部分的数据是不是可以提前从queue队尾remove掉,然后补上新的数据?不用考虑这么多,你尽管往队尾写好了,它的管理者InfoQueue会帮你搞定这些事情,relativeReadIndex的更新安全地把这些不用的数据在readData的时候remove掉。seek的情形也是一样的。预告下,下期写自适应码率切换的实现,到时候可以看看这个DataQueue是如何被巧妙摆布的。

4.还有一点,关于rolling buffer的读写要不要加锁?ExoPlayer的做法是:InfoQueue相关的调用都会加锁(使用Java的synchronized方法);也就是说DataQueue读写不受锁的控制;只有操作存放metaData以及read、write index信息的InfoQueue时才会加锁,这些数据跟ES数据比起来,读、写量太小了,以至于这个锁根本不会影响视频数据读写的效率。


2. SparseArray与HLS TS的解析

ExoPlayer中TsExtractor相关类关系如下:

PesReader处理ES数据,SectionReader处理各种表,对HLS来数,TS简单的多,主要是PAT和PMT。

Ts解析用到了SparseArray,SparseArray是Android SDK提供的integers to Objects的Map,详细的可以查看Android SDK中SparseArray的实做,总体来数,设计它的目的是在某些条件下性能更好,尤其是在获取数据的时候,看到下面的代码的时候你大概明白了:

public void put(int key, E value) {

       int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

...

}

public E get(int key, E valueIfKeyNotFound) {

       int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

...

}

SparseArray存储的元素都是按元素的key值从小到大排列好的,二分查找在获取数据的时候,某些情况下比需要遍历的HashMap快的多。

回到ts的解析,SparseArray在TsExtractor被用作为pid与相应Reader的Map,如下:

private final SparseArray<TsPayloadReader> tsPayloadReaders;

TsExtractor构造函数首先会把PAT表的pid TS_PAT_PID(0)加到Map里:

tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));

 

TsExtractor从defaultExtractorInput读到的TS数据,首先拿到的包是PAT,根据PAT的pid从SparseArray返回处理PAT的Reader如下:

TsPayloadReader payloadReader = tsPayloadReaders.get(pid); 

payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);

在PatReader::consume()中,PatReader会把PmtReader放到SparseArray中:

for (int i = 0; i < programCount; i++) {

    ...

    tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));

}

当解析到PMT的时候:

PmtReader::consume(),PmtReader会把真正audio/video对应的PesReader放到SparseArray中tsPayloadReaders.put(elementaryPid, reader); 

这样,所有的pid以及对应的reader都放到了SparseArray中,之后TsExtractor read数据,根据ts包里面的pid通过:

TsPayloadReader payloadReader = tsPayloadReaders.get(pid);

直接就可以找到处理数据的reader,如下调用consume就能分别处理:

payloadReader.consume(tsPacketBuffer); 

这样就完成了ts的demux,a/v数据最终被分别存到不同的defaultTrackOutput dataQueue中,等待MediaCodecRenderer来readData。

再回顾下上篇文章提到的HLS的数据流,如下:

ExoPlayer的实做针对HLS的ts数据包,已经算是相当不错了,然而,不比不知道,接下来再看看Dash fMP4的实现,看完后你会发现,码农真的很可怜。


3.Dash fMP4与Stack

直接上图,下图是Dash的数据流:

看到跟HLS数据流的对比,就是上面少了好几个框框,是的,HLS解析里面放在SparseArray里面的Reader全都不见了,FragmentedMp4Extractor直接往DefaultTrackOutput的DataQueue里面写数据。

FragmentedMp4Extractor中只用到了Stack一个数据结构,主要用来协助做moov和moof的解析,Android SDK中Stack的实做是这样的:

public class Stack<E> extends Vector<E>{};

是继承Vector而来的,所以实现非常简单;C++熟悉的马上会想到,C++ STL中Stack的实现默认是基于std::deque,在C++中把其称为allocator,这个allocator也可以是std::vector,一点问题没有。所以语言层面的东西,也都是通的。

FragmentedMp4Extractor除了包含这个Stack外,剩下的就是MP4各种box的数据解析,如此简单的封装设计,你的代码效率想不高都不行。

HLS解析里面牛掰的码农们使用的SparseArray在Dash根本不需要,为什么?Dash的协议就是这么定的:manifest文件里不仅把A/V分开了,还会告诉播放器codec的类型、mimeType、resolution、sampleRate等等信息,它把复杂的工作留给了码流生产端,那里有性能强劲的服务器集群,干这点事分分钟搞定;客户端只需要简单的解析下box就可以了。

就是这样的一个标准上的差别,实际应用中我们发现,即使在性能优越的电视芯片上,播放同样4K的影片,HLS的流播放丢帧非常明显,而同样的Dash流却是异常的流畅,为此你需要为ExoPlayer HLS流畅播放做很多优化的工作。关于优化的思路,后续会有一遍专门探讨。

感谢牛掰的标准制定者,你们的简单标准改进就能解放码农一大部分的工作

4.HLS的演进: fMP4

上面我们看到Dash相比于HLS(或者fMP4比TS)的优点,但是Apple也在往前走,从HLS v7(https://tools.ietf.org/html/draft-pantos-http-live-streaming-20)开始,HLS正式引入了MPEG-4 Fragmented,playlist示例如下:

#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="init.mp4"#EXTINF:4.0
segment_0.m4s
#EXTINF:4.0
segment_1.m4s
...
#EXT-X-ENDLIST

长远来讲,CP(Content Provider)们是最开心的,之后不需要encode content in different formats,fMP4就能满足所有需求,come on。。。

ExoPlayer官方的Demo已经有HLS master playlist advanced(fMP4)的支持,只是例子中所用的m3u8实在是有点过于复杂了,有兴趣的可以先行研究下,url:https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8


总结

前面写了ExoPlayer里用到的三个重要的数据结构:Queue、SparseArray、Stack,都是Android SDK提供的基本的数据结构。

最后说一下HashMap,HashMap在ExoPlayer的HttpDataSource和MediaCodec相关模块都有使用;HashMap感觉上是网络世界中最重要的数据结构了,由于太经典,就不废话了。

还有最后提到了Dash和HLS在协议上的差别的导致的Performance问题,后续会有详细篇幅说明;以及HLS对fMP4的支持。

下一篇写自适应码率切换,感受下本文开始描述的ExoPlayer这个Queue的灵活。

这篇关于ExoPlayer里里外外之:流媒体播放与数据结构的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/813736

相关文章

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

rtmp流媒体编程相关整理2013(crtmpserver,rtmpdump,x264,faac)

转自:http://blog.163.com/zhujiatc@126/blog/static/1834638201392335213119/ 相关资料在线版(不定时更新,其实也不会很多,也许一两个月也不会改) http://www.zhujiatc.esy.es/crtmpserver/index.htm 去年在这进行rtmp相关整理,其实内容早有了,只是整理一下看着方

RTMP流媒体服务器 crtmpserver

http://www.oschina.net/p/crtmpserver crtmpserver又称rtmpd是Evostream Media Server(www.evostream.com)的社区版本采用GPLV3授权 其主要作用为一个高性能的RTMP流媒体服务器,可以实现直播与点播功能多终端支持功能,在特定情况下是FMS的良好替代品。 支持RTMP的一堆协议(RT

【408数据结构】散列 (哈希)知识点集合复习考点题目

苏泽  “弃工从研”的路上很孤独,于是我记下了些许笔记相伴,希望能够帮助到大家    知识点 1. 散列查找 散列查找是一种高效的查找方法,它通过散列函数将关键字映射到数组的一个位置,从而实现快速查找。这种方法的时间复杂度平均为(

浙大数据结构:树的定义与操作

四种遍历 #include<iostream>#include<queue>using namespace std;typedef struct treenode *BinTree;typedef BinTree position;typedef int ElementType;struct treenode{ElementType data;BinTree left;BinTre

Python 内置的一些数据结构

文章目录 1. 列表 (List)2. 元组 (Tuple)3. 字典 (Dictionary)4. 集合 (Set)5. 字符串 (String) Python 提供了几种内置的数据结构来存储和操作数据,每种都有其独特的特点和用途。下面是一些常用的数据结构及其简要说明: 1. 列表 (List) 列表是一种可变的有序集合,可以存放任意类型的数据。列表中的元素可以通过索