系列文章目录
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
ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod
ExoPlayer架构详解与源码分析(15)——Renderer
ExoPlayer架构详解与源码分析(16)——LoadControl
ExoPlayer架构详解与源码分析(17)——TrackSelector
文章目录
前言
上篇说完了Extractor的整体结构,本篇将详细讲解Extractor的实现,主要通过TsExtractor这个实现类来讲解,顾名思义TsExtractor是用于TS容器格式的解析器。
TS(Transport Stream,传输流)是一种封装的格式,它的全称为MPEG2-TS。
MPEG组织于1994年推出MPEG-2压缩标准,以实现视/音频服务与应用互操作的可能性,MPEG-2标准是针对标准数字电视和高清晰度电视在各种应用下的压缩方案和系统层的详细规定。在MPEG-2标准中,为了将一个或更多的音频、视频或其他的基本数据流合成单个或多个数据流,以适应于存储和传送,必须对其重新进行打包编码,在码流中还需插入各种时间标记、系统控制等信息,最后送到信道编码与调制器。这样可以形成两种数据流——传送流(TS)和节目流(PS),分别适用于不同的应用。
MPEG2-TS是一种标准数据容器格式,传输与存储音视频、节目与系统信息协议数据,主要应用于数字广播系统,譬如DVB、ATSC与IPTV。TS流是将视频、音频、PSI等数据打包成传输包进行传送。其整体的设计充分考虑了传输过程中的丢包,数据干扰等问题,特别适合用于节目传输。
科普时间结束,回归主线,看下ExoPlayer 是如何解析TS结构的
TsExtractor
在看ExoPlayer 源码前,必须先来了解下TS的整体结构,然后再结合源码,看下ExoPlayer是如何实现TS的解析的。
首先看下TS容器的结构(网图,侵删)。
可以看到每个TS包大小为188,包含一个header和payload,这种固定块大小的结果特别适合网络传输的场景,运用的也比较多。
-
header
名称 大小(b) 说明 sync_byte 8 同步标记占1个字节,固定为0x47,当解析器读取到这个字节的时候就知道这是一个包开始位置 transport_error_indicator 1 传输错误指示符,1’表示在相关的传输包中至少有一个不可纠正的错误位。当被置1后,在错误被纠正之前不能重置为0 payload_unit_start_indicator 1 负载单元起始标示符,一个完整的数据包开始时标记为1 transport_priority 1 传输优先级,0为低优先级,1为高优先级,通常取0 pid 13 包的 ID,用于区分不同的包,注意这个不是唯一的,可能相同类型的包都对应同一个PID,其中PID有几个固定值用于指定类型的包,如PAT包固定值为0x0000 transport_scrambling_control 2 传输加扰控制,00表示未加密 adaptation_field_control 2 是否包含自适应区,‘00’保留;‘01’为无自适应域,仅含有效负载;‘10’为仅含自适应域,无有效负载;‘11’为同时带有自适应域和有效负载。 continuity_counter 4 递增计数器,从0-f,起始值不一定取0,但必须是连续的,随着每一个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。接收端可判断是否有包丢失及包传送顺序错误 adaptation_field_length 8 自适应域长度,包含在上图的PRC分段中 flag 8 取0x50表示包含PCR或0x40表示不包含PCR,包含在上图的PRC分段中 PCR 40 Program Clock Reference,节目时钟参考,用于恢复出与编码端一致的系统时序时钟STC(System Time Clock)。可以理解为当前包的时间戳,时间戳一般是以90 kHz 为单位的时间戳,所以转化成正常时间戳得除以90000,这段同样包含在上图的PRC分段中 -
payload
payload里主要包含2种类型数据PES和PSI(Program Specific Information:由对于传输流的多路分解以及节目成功再现所必要的标准数据组成)
PSI 有6种类型或者说6种表:- 节目相关表(PAT)
- TS 节目映射表(PMT)
- 网络信息表(NIT)
- 有条件访问表(CAT)
- 传输流描述表
- IPMP 控制信息表。
主要介绍下下面3种
-
PAT表,包头的PID固定为0x0000,包含一个header和一个body,一个payload前3字节为header,包含了body的长度,后面则为body,主要列出所有的PMT表ID,可以通过它确定哪些PID的包是PMT表主要要来查询PMT,下面是PAT的表结构,section_length前为header,后面的属于body
名称 大小(b) 说明 table_id 8 PAT表固定为0x00 section_syntax_indicator 1 固定为1 zero 1 固定为0 reserved 2 固定为11 section_length 12 后面数据的长度 transport_stream_id 16 传输流ID,固定为0x0001 reserved 2 固定为11 version_number 5 版本号,固定为00000,如果PAT有变化则版本号加1 current_next_indicator 1 固定为1,表示这个PAT表可以用,如果为0则要等待下一个PAT表 section_number 8 固定为0x00 last_section_number 8 固定为0x00 开始循环 program_number 16 为0x0000时表示这是NIT网络信息表,节目号为0x0001时,表示这是PMT reserved 3 固定为111 PID 13 PAT对应PMT的包PID值 结束循环 CRC32 32 前面数据的CRC32校验码 -
PMT表,包头的PID不固定,需要通过PAT获取,主要列出了包含的所有流类型及其对于的PID,通过它可以确定当前的包对应的是哪种流,然后针对性的解析,下面是PMT的表结构
名称 大小(b) 说明 table_id 8 PMT表固定为0x02 section_syntax_indicator 1 固定为1 zero 1 固定为0 reserved 2 固定为11 section_length 12 后面数据的长度 program_number 16 频道号码,表示当前的PMT关联到的频道,取值0x0001 reserved 2 固定为11 version_number 5 版本号,固定为00000,如果PAT有变化则版本号加1 current_next_indicator 1 固定为1 section_number 8 固定为0x00 last_section_number 8 固定为0x00 reserved 3 固定为111 PCR_PID 13 PCR(节目参考时钟)所在TS分组的PID,指定为视频PID reserved 3 固定为111 program_info_length 12 描述信息,指定为0x000表示没有 开始循环 stream_type 8 流类型,标志是Video还是Audio还是其他数据,h.264编码对应0x1b,aac编码对应0x0f,mp3编码对应0x03 reserved 3 固定为111 elementary_PID 13 与stream_type对应的PID reserved 4 固定为1111 ES_info_length 12 描述信息,指定为0x000表示没有 结束循环 CRC32 32 前面数据的CRC32校验码 -
PES 用于承载基本流数据的数据结构,可以理解成具体的媒体流数据,同样包含header和body,看下包结构图
PES的Header结构很复杂,这里我们说明下重要的几个名称 大小(b) 说明 packet_start_code_prefix 24 固定为0x000001,同跟随它的 stream_id 一起组成标识包起始端的包起始码 stream_id 16 流ID,音频取值(0xc0-0xdf),通常为0xc0视频取值(0xe0-0xef),通常为0xe0具体参照ISO/IEC 13818-1 2.4.3.7 PES_packet_length 24 后面pes数据的长度,0表示长度不限制,只有视频数据长度会超过0xffff PTS_DTS_flags 2 当 PTS_DTS_flags 字段设置为‘10’时,PES 包头中 PTS 字段存在。设置为‘11’时,PES 包头中 PTS 字段和 DTS 字段均存在。设置为‘00’时,PES 包头中既无任何 PTS 字段也无任何 DTS 字段存在。值‘01’禁用 PES_header_data_length 8 额外包含的数据长度,包含的PTS或者DTS数据 PTS 33 presentation time stamp,显示时间戳,具体参考ISO/IEC 13818-1 2.4.3.7 DTS 33 decoding time stamp,解码时间戳,具体参考ISO/IEC 13818-1 2.4.3.7
好了有了上面的知识,我们一起来看下源码是如何解析的
首先从初始化看起
public TsExtractor(
@Mode int mode, @Flags int defaultTsPayloadReaderFlags, int timestampSearchBytes) {
this(
mode,
new TimestampAdjuster(0),
//创建默认的payload的解析工厂类
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags),
timestampSearchBytes);
}
public TsExtractor(
@Mode int mode,
TimestampAdjuster timestampAdjuster,
TsPayloadReader.Factory payloadReaderFactory,
int timestampSearchBytes) {
...
//初始化缓存数据大小为50个TS包大小
tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0);
...
//用于从PRC中计算时长
durationReader = new TsDurationReader(timestampSearchBytes);
...
resetPayloadReaders();
}
//初始化payload解析器
private void resetPayloadReaders() {
trackIds.clear();
tsPayloadReaders.clear();
SparseArray<TsPayloadReader> initialPayloadReaders =
payloadReaderFactory.createInitialPayloadReaders();
int initialPayloadReadersSize = initialPayloadReaders.size();
//添加初始化默认的解析器,如果没有自定义工厂会使用DefaultTsPayloadReaderFactory此时不包含任何初始化的解析器
for (int i = 0; i < initialPayloadReadersSize; i++) {
tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i));
}
//添加包头PAT解析器
tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));
id3Reader = null;
}
看下初始化后调用的第一个方法,主要用来确定当前Extractor是否适用
//在确定使用哪种解析器时会先调用Extractor.sniff决定当前解析器是否可以用于解析,上文中也提到了调用点
@Override
public boolean sniff(ExtractorInput input) throws IOException {
byte[] buffer = tsPacketBuffer.getData();
input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT);//填充5*118个数据
for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) {
// Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE.
boolean isSyncBytePatternCorrect = true;
//是否有5个0x47字节连续的间隔188的数据
for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) {
if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
isSyncBytePatternCorrect = false;
break;
}
}
if (isSyncBytePatternCorrect) {
input.skipFully(startPosCandidate);
return true;
}
}
return false;
}
然后就开始执行主要的方法read
@Override
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
long inputLength = input.getLength();
if (tracksEnded) {
//所有PMT表都解析完 tracksEnded
//如果tracksEnded了,此时数据的总长度已知,且不为HLS(hls有多个ts,时长记录在m3u8文件里)
boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;
if (canReadDuration && !durationReader.isDurationReadFinished()) {
//开始读取时长,后面会具体讲到读取方式
return durationReader.readDuration(input, seekPosition, pcrPid);
}
//输出SeekMap表,这里保存播放时间戳和数据位置的对应关系,可以通过时间戳快速定位到数据位置
//这里不深入了
maybeOutputSeekMap(inputLength);
//是否从头开始,这里作用是当Tarck信息解析完毕的时候会返回RESULT_SEEK
//回到上面讲的外循环再次从头加载数据
if (pendingSeekToStart) {
pendingSeekToStart = false;
seek(/* position= */ 0, /* timeUs= */ 0);
if (input.getPosition() != 0) {
seekPosition.position = 0;
return RESULT_SEEK;
}
}
if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {
return tsBinarySearchSeeker.handlePendingSeek(input, seekPosition);
}
}
//读取至少一段Ts包
if (!fillBufferWithAtLeastOnePacket(input)) {
return RESULT_END_OF_INPUT;
}
//从上次读取位置查找第一个包的结束位置
int endOfPacket = findEndOfFirstTsPacketInBuffer();
int limit = tsPacketBuffer.limit();
//如果超过limit,其实就是包不足188字节,这里会继续加载
if (endOfPacket > limit) {
return RESULT_CONTINUE;
}
@TsPayloadReader.Flags int packetHeaderFlags = 0;
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
//读取4字节,也就是包头的长度