济南网站制作运营/html网页模板
系列文章目录
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
文章目录
- 系列文章目录
- 前言
- H264结构
- H264Reader
- SPS的解析
- PPS的解析
- SEI的解析
- Slice的解析
- 总结
前言
TsExtractor解封完TS数据后,会根据payload中的视频类型使用指定Reader继续解析,如果payload是H.264格式,就会使用H264Reader来继续解析PES payload部分视频数据流。先上下ProgressiveMediaPeriod的万年老图:
这部分已经可以和SampQueue关联起来了,也就是说图中sampleData的地方就发生在H264Reader中。
H264结构
在看代码前老规矩,先简单了解下H264的码流结构
H264都是由一个个的NAL基本单元组成的,每个NAL由包含一个HEADER和一个DATA,如下图
这些基本的NAL可能为多种类型如上图的SPS,PPS,SLICE,这些类型就定义在NAL的Header之中,Header的结构很简单就一个字节,如下表
名称 | 大小(b) | 说明 |
---|---|---|
forbidden_zero_bit | 1 | 禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误,告诉接收方丢掉该单元,否则为0 |
nal_ref_idc | 2 | 指示当前NALU的优先级,或者说重要性,数值越大表明越重要 |
nal_unit_type | 5 | 表示NALU的类型 |
那么nal_unit_type不同值对应什么类型呢看下表
nal_unit_type | NAL类型 |
---|---|
0 | 未使用 |
1 | 不分区、非 IDR 图像的片 |
2 | SLICE A 片分区 A |
3 | SLICE B 片分区 B |
4 | SLICE C 片分区 C |
5 | IDR 图像中的片 |
6 | Supplemental Enhancement Information(SEI ) 补充增强信息单元 |
7 | Sequence Paramater Set(SPS) 序列参数集 |
8 | Picture Paramater Set(PPS) 图像参数集 |
9 | Access Unit Delimiter(AUD) 分界符 |
10 | End Of Seq 序列结束 |
11 | End Of Stream 码流结束 |
12 | Filler Data 填充 |
13…23 | 保留 |
24…31 | 未使用 |
下面看下几个重要的unitType结构
-
Sequence Paramater Set(SPS) 序列参数集
SPS结构比较复杂这里挑几个用到的名称 大小(b) 说明 profile_idc 8 本视频编码时遵循的profile,profile分为Baseline,Main,Extended等,主要用来规定编码时是否采用某些特性,比如说Baseline profile就规定了只能使用I、P slice进行编码,关于profile的说明可以去查看标准的Annex A。 constraint_set0_flag 1 强制使用Baseline profile进行编码 constraint_set1_flag 1 强制使用Main profile进行编码 constraint_set2_flag 1 强制使用Extended profile进行编码 level_idc 8 本视频遵循的level,level主要规定了每秒最多能处理多少个宏块,最大的帧大小,最大的解码缓存,最大比特率等这些性能相关的东西,如果是硬解码,则比较容易出现由于视频level太高而不能解码的情况。 seq_parameter_set_id ue(v) 本SPS的ID,这个ID主要是给PPS用的 separate_colour_plane_flag 1 separate_colour_plane_flag 等于 1 表示对 4:4:4 色度格式中的三个色彩分量分别进行编码。 如果 separate_colour_plane_flag 的值为 0,则表示不对色彩成分进行单独编码,separate_colour_plane_flag 等于 1 时,主编码图像由三个独立的分量组成,每个分量由一个颜色平面(Y、Cb 或 Cr)的编码采样组成,每个采样使用单色编码语法。在这种情况下,每个色彩平面都与特定的 color_plane_id 值相关联 log2_max_frame_num_minus4 ue(v) 指定了变量 MaxFrameNum 的值,值范围应为 0 至 12(含 12), M a x F r a m e N u m = 2 ( l o g 2 m a x f r a m e n u m m i n u s 4 + 4 ) MaxFrameNum = 2^{(log2maxframenumminus4 +4)} MaxFrameNum=2(log2maxframenumminus4+4) pic_order_cnt_type ue(v) 指定解码图片顺序计数的方法,pic_order_cnt_type 的值范围应为 0 至 2(含 2) pic_width_in_mbs_minus1 ue(v) 图片宽度 pic_height_in_map_units_minus1 ue(v) 图片高度 frame_mbs_only_flag 1 是否只进行帧编码 vui_parameters_present_flag 1 SPS是否包含vui参数, video usability information,在标准的Annex E中有描述,主要包含了视频的比例调整,overscan,视频格式,timing,比特率等信息 aspect_ratio_info_present_flag 1 等于 1 表示存在 aspect_ratio_idc,等于 0 表示不存在 aspect_ratio_idc aspect_ratio_idc 8 指定样本的采样纵横比值。当 aspect_ratio_idc 表示 Extended_SAR(扩展 SAR)时,采样纵横比用 sar_width : sar_height 表示,当没有 aspect_ratio_idc 语法元素时,aspect_ratio_idc 值为 0 sar_width 16 表示样本纵横比的水平尺寸 sar_height 16 表示样本纵横比的垂直尺寸(单位与 sar_width 相同) ue(v)、se(v)表示以哥伦布编码的一种变长压缩算法
-
Picture Paramater Set(PPS) 图像参数集
这里也挑几个用到的讲下名称 大小(b) 说明 pic_parameter_set_id ue(v) 当前PPS的ID,供slice RBSP使用 seq_parameter_set_id ue(v) 当前PPS所属的SPS的ID bottom_field_pic_order_in_frame_present_flag 1 用于POC计算,请参考h.264的POC计算中的bottom_field_flag -
Supplemental Enhancement Information(SEI ) 补充增强信息单元
集成在音视频码流中,用于在音视频内部传递消息,可以保证信息与直播音视频数据的同步,SEI并不是解码过程的必须项,有可能对解码过程(容错、纠错)有帮助,视频传输过程、解封装、解码环节,都可能因为某种原因丢弃SEI ,在视频内容的生成端、传输过程中,都可以插入SEI 信息。插入的信息,和其他视频内容一起经过传输链路到达了消费端。那么在SEI 中可以添加哪些信息呢?传递编码器参数、传递视频版权信息、传递摄像头参数、当然也可以传输字幕信息,后面我们会看到。 -
Slice
视频中的一帧图像可以理解成由一个或多个Slice组成,每一个Slice总体来看都由两部分组成-
Slice header,包含着分片类型、分片中的宏块类型、分片帧的数量以及对应的帧的设置和参数等信息,slice body中的宏块在进行解码时需依赖这些信息
来看下Header 的结构名称 大小(b) 说明 first_mb_in_slice ue(v) 当前slice中包含的第一个宏块在整帧中的位置 slice_type ue(v) 当前slice的类型参照下表 pic_parameter_set_id ue(v) 当前slice所依赖的pps的id;范围 0 到 255 colour_plane_id 2 当标识位separate_colour_plane_flag为true时,colour_plane_id表示当前的颜色分量,0、1、2分别表示Y、U、V分量 frame_num ue(v) 表示当前帧序号,数据长度参考上面的log2_max_frame_num_minus4 field_pic_flag 1 场编码标识位。当该标识位为1时表示当前slice按照场进行编码;该标识位为0时表示当前 slice按照帧进行编码 bottom_field_flag 1 底场标识位。该标志位为1表示当前slice是某一帧的底场;为0表示当前slice为某一帧的顶场 idr_pic_id ue(v) 表示IDR帧的序号。某一个IDR帧所属的所有slice,其idr_pic_id应保持一致。该值的取值范围为[0,65535]。 pic_order_cnt_lsb ue(v) 表示当前帧序号的另一种计量方式 delta_pic_order_cnt_bottom se(v) 表示顶场与底场POC差值的计算方法,不存在则默认为0 delta_pic_order_cnt[0] se(v) 指定编码帧顶部字段的图片顺序计数与预期图片顺序计数的差值 delta_pic_order_cnt[1] se(v) 指定图像顺序计数与编码帧底层字段的预期图像顺序计数的差值 slice_type Name of slice_type 0 P (P slice) 1 B (B slice) 2 I (I slice) 3 SP (SP slice) 4 SI (SI slice) 5 P (P slice) 6 B (B slice) 7 I (I slice) 8 SP (SP slice) 9 SI (SI slice) -
Slice body,通常是一组连续的宏块结构(参照上图),这里就是最终存储像素数据的地方了。宏块中还包含了宏块类型、预测类型、Coded Block Pattern、Quantization Parameter、像素的亮度和色度数据集等等信息。具体结构这个里不是重点不展开。
一个视频由多个帧组成,一帧由多个Slice(片)组成,一个Slice由多个宏块组成,一个宏块又由多个(如4X4)的YUV像素数据组成。
-
看完了这些SPS、PPS、SLICE他们之间关系是怎么样的呢
Slice里的pic_parameter_set_id指向了PPS里的pic_parameter_set_id,而PPS里的seq_parameter_set_id又指向了SPS(序列参数集)里的seq_parameter_set_id,这样一个SPS关联多个PPS,而一个PPS又关联了多个Slice;解码器解码Slice时就通过这些ID查询相关的PPS、SPS获取解码所需的必要信息。
在网络传输流的过程中编码器可能会将每个NAL单元放入到单个独立的网络传输块中,如TS中可能一个包中之包含一个NAL,解码器可以很容易的检测出NAL的分界,然后依次取出NAL来解码,但是实际可能一个包里会包含一个PES头这个头后面跟随了多个NAL单元这种情况该如何找到这些NAL单元的分界呢?
很简单,给NAL前添加0x000001头3个字节,某些情况下会要求NAL长度对齐不足的部分填充0,所以H.264规定当检测到0x000000这3个字节的时候也表示当前NAL结束,这样感觉已经可以解决分界问题了。
但是如果NAL内部数据出现0x000001或者0x000000字段怎么办呢,解码器会误以为这里是新的NAL的开始,导致数据解码出错,于是H.264规定了另一个规则 emulation prevention,在编码器编码完一个NAL时,会再去检测当前NAL中是否包含上述2种字节序列,如果检测出则在最后一个字节前插入一个新字节0x03,当解码器在NAL内部检测到有0x000003 字节序列时,就会把0x03丢弃,恢复数据。
如0x000001 最后一个字节添加0x03 变成0x00000301,解码器丢弃后又变成0x000001。
源码里的ParsableNalUnitBitArray 和NalUnitUtil.unescapeStream方法就是用来丢弃0x03的。
H264Reader
了解了上面的知识,基本就可以开始看代码实现了,这部分最好联系上文ExoPlayer架构详解与源码分析(7)——SampleQueue一起看。
@Overridepublic void consume(ParsableByteArray data) {assertTracksCreated();int offset = data.getPosition();int limit = data.limit();byte[] dataArray = data.getData();// 将当前数据长度计入总长度,此时总数据的尾部和当前数据的尾部就是对齐的totalBytesWritten += data.bytesLeft();//到这里已经是解复用后的数据了,将数据发给SampleQueueoutput.sampleData(data, data.bytesLeft());// 循环读取到NAL单元结束while (true) {//通过判断是否为0x000001 3字节,确定NAL开始位置,prefixFlags用于保存上一次循环里的3字节信息,防止目标字节被循环分割int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);if (nalUnitOffset == limit) {// 读取到最后一个字节,循环结束nalUnitData(dataArray, offset, limit);return;}// 知道起始位置后,获取第四个字节后5位就是NAL的类型int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);//获取NAL开始位置到当前位置的偏移量,当NAL单元开始位置在上一段数据中时,这个值为负值int lengthToNalUnit = nalUnitOffset - offset;if (lengthToNalUnit > 0) {//将当前位置到下一个NAL开始位置的数据输入nalUnitData(dataArray, offset, nalUnitOffset);}//用当前数据的结束位置-相对于当前数据的NAL开始位置,得到就是当前NAL开始位置到当前数据的结束距离int bytesWrittenPastPosition = limit - nalUnitOffset;//由于当前的结束位置和整个的结束位置是对齐的,用整个数据的长度减轻到结尾的距离,就是这个NAL相对于整个数据的绝对位置long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;// 如果到下一个单元开始的长度为负,那么我们向 NAL 缓冲区写入了过多字节。当通知NAL结束时丢弃多余的字节。endNalUnit(absolutePosition,bytesWrittenPastPosition,lengthToNalUnit < 0 ? -lengthToNalUnit : 0,pesTimeUs);// 下个NAL单元开始startNalUnit(absolutePosition, nalUnitType, pesTimeUs);// 从NAL单元开始位置读取3个字节offset = nalUnitOffset + 3;}}//结束NAL单元private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {if (!hasOutputFormat || sampleReader.needsSpsPps()) {sps.endNalUnit(discardPadding);pps.endNalUnit(discardPadding);if (!hasOutputFormat) {//保证只执行一次if (sps.isCompleted() && pps.isCompleted()) {//sps和pps都已经endNalUnitList<byte[]> initializationData = new ArrayList<>();initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength));initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength));//解析出SPS数据NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);//解析出PPS数据NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);//构建codecs 参数,最终用于 MediaCodec 的 configure,确定解码器String codecs =CodecSpecificDataUtil.buildAvcCodecString(spsData.profileIdc,spsData.constraintsFlagsAndReservedZero2Bits,spsData.levelIdc);//通过SPS和PPS构建Format输出给SampleQueueoutput.format(new Format.Builder().setId(formatId).setSampleMimeType(MimeTypes.VIDEO_H264).setCodecs(codecs).setWidth(spsData.width).setHeight(spsData.height).setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio).setInitializationData(initializationData).build());hasOutputFormat = true;sampleReader.putSps(spsData);sampleReader.putPps(ppsData);sps.reset();pps.reset();}} else if (sps.isCompleted()) {NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);sampleReader.putSps(spsData);sps.reset();} else if (pps.isCompleted()) {NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);sampleReader.putPps(ppsData);pps.reset();}}if (sei.endNalUnit(discardPadding)) {//丢弃0x03字节int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);seiWrapper.reset(sei.nalData, unescapedLength);seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.//解析SEI,解析这部分主演是防止SEI中包含字幕信息,将SEI中的字幕轨道提取出来seiReader.consume(pesTimeUs, seiWrapper);}boolean sampleIsKeyFrame =sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator);if (sampleIsKeyFrame) {//这要么是 IDR 帧,要么是自随机访问指示符以来的第一个 I 帧,因此将其标记为关键帧。清除该标志,以便后续的非 IDR I 帧不会被标记为关键帧,直到我们看到另一个随机访问指示符。randomAccessIndicator = false;}}//sampleReader.endNalUnitpublic boolean endNalUnit(long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) {if (nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_AUD//遇到一个AUD就sample一次Metadata|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {// If the NAL unit ending is the start of a new sample, output the previous one.if (hasOutputFormat && readingSample) {//Fromat未解析出也就是SPS PPS未解析完成,跳过//position为当前AUD结束位置,nalUnitLength 就是AUD长度,一般都是5int nalUnitLength = (int) (position - nalUnitStartPosition);//这里的offset 为当前AUD结尾到sampleData结尾的距离//offset +nalUnitLength后相当于AUD开始位置到sampleData结尾的距离,相当于当前Metadata 结束位置到SampleData结尾的距离outputSample(offset + nalUnitLength);}samplePosition = nalUnitStartPosition;//标记当前Metadata 开始位置sampleTimeUs = nalUnitTimeUs;//标记当前Metadata 开始时间sampleIsKeyframe = false;readingSample = true;//标记当前Metadata 开始}boolean treatIFrameAsKeyframe =allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;sampleIsKeyframe |=nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_IDR|| (treatIFrameAsKeyframe && nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_NON_IDR);return sampleIsKeyframe;}private void outputSample(int offset) {if (sampleTimeUs == C.TIME_UNSET) {return;}@C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;//计算当前Metadata 的有效长度,从第一个AUD开始到下一个AUD的开始位置长度int size = (int) (nalUnitStartPosition - samplePosition);//将Metadata Sample,计算Metadata 在SampleData中起始位置时,就可以用SampleData总长度-Metadata 的长度-Metadata 结束位置到SampleData结尾的距离output.sampleMetadata(sampleTimeUs, flags, size, offset, null);}
SPS的解析
public static SpsData parseSpsNalUnitPayload(byte[] nalData, int nalOffset, int nalLimit) {ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);int profileIdc = data.readBits(8);//获取profileIdc 主要用于Codec的构建int constraintsFlagsAndReservedZero2Bits = data.readBits(8);//获取后面几个Flag主要用于Codec的构建int levelIdc = data.readBits(8);//获取levelIdc 主要用于Codec的构建int seqParameterSetId = data.readUnsignedExpGolombCodedInt();//获取SPS的IDint chromaFormatIdc = 1; // Default is 4:2:0boolean separateColorPlaneFlag = false;if (profileIdc == 100|| profileIdc == 110|| profileIdc == 122|| profileIdc == 244|| profileIdc == 44|| profileIdc == 83|| profileIdc == 86|| profileIdc == 118|| profileIdc == 128|| profileIdc == 138) {chromaFormatIdc = data.readUnsignedExpGolombCodedInt();if (chromaFormatIdc == 3) {separateColorPlaneFlag = data.readBit();//获取separate_colour_plane_flag}data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8data.skipBit(); // qpprime_y_zero_transform_bypass_flagboolean seqScalingMatrixPresentFlag = data.readBit();if (seqScalingMatrixPresentFlag) {int limit = (chromaFormatIdc != 3) ? 8 : 12;for (int i = 0; i < limit; i++) {boolean seqScalingListPresentFlag = data.readBit();if (seqScalingListPresentFlag) {skipScalingList(data, i < 6 ? 16 : 64);}}}}int frameNumLength = data.readUnsignedExpGolombCodedInt() + 4; // log2_max_frame_num_minus4 + 4int picOrderCntType = data.readUnsignedExpGolombCodedInt();//pic_order_cnt_typeint picOrderCntLsbLength = 0;boolean deltaPicOrderAlwaysZeroFlag = false;if (picOrderCntType == 0) {// log2_max_pic_order_cnt_lsb_minus4 + 4picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4;} else if (picOrderCntType == 1) {deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flagdata.readSignedExpGolombCodedInt(); // offset_for_non_ref_picdata.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_fieldlong numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt();for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]}}int maxNumRefFrames = data.readUnsignedExpGolombCodedInt(); // max_num_ref_framesdata.skipBit(); // gaps_in_frame_num_value_allowed_flagint picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;//pic_width_in_mbs_minus1int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1;//pic_height_in_map_units_minus1boolean frameMbsOnlyFlag = data.readBit();//frame_mbs_only_flagint frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;if (!frameMbsOnlyFlag) {data.skipBit(); // mb_adaptive_frame_field_flag}data.skipBit(); // direct_8x8_inference_flag//下面确定视频帧的高宽int frameWidth = picWidthInMbs * 16;int frameHeight = frameHeightInMbs * 16;boolean frameCroppingFlag = data.readBit();if (frameCroppingFlag) {//获取裁剪后的高宽int frameCropLeftOffset = data.readUnsignedExpGolombCodedInt();int frameCropRightOffset = data.readUnsignedExpGolombCodedInt();int frameCropTopOffset = data.readUnsignedExpGolombCodedInt();int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt();int cropUnitX;int cropUnitY;if (chromaFormatIdc == 0) {cropUnitX = 1;cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0);} else {int subWidthC = (chromaFormatIdc == 3) ? 1 : 2;int subHeightC = (chromaFormatIdc == 1) ? 2 : 1;cropUnitX = subWidthC;cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0));}frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;}@C.ColorSpace int colorSpace = Format.NO_VALUE;@C.ColorRange int colorRange = Format.NO_VALUE;@C.ColorTransfer int colorTransfer = Format.NO_VALUE;//确定宽高比float pixelWidthHeightRatio = 1;boolean vuiParametersPresentFlag = data.readBit();if (vuiParametersPresentFlag) {//vui_parameters_present_flag包含VUI数据boolean aspectRatioInfoPresentFlag = data.readBit();if (aspectRatioInfoPresentFlag) {//aspect_ratio_info_present_flagint aspectRatioIdc = data.readBits(8);//aspect_ratio_idcif (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {//自定义了宽高比int sarWidth = data.readBits(16);//sar_widthint sarHeight = data.readBits(16);//sar_heightif (sarWidth != 0 && sarHeight != 0) {pixelWidthHeightRatio = (float) sarWidth / sarHeight;}} else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {//无自定义获取已定义的宽高比pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];} else {Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);}}if (data.readBit()) { // overscan_info_present_flagdata.skipBit(); // overscan_appropriate_flag}if (data.readBit()) { // video_signal_type_present_flagdata.skipBits(3); // video_formatcolorRange =data.readBit() ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; // video_full_range_flagif (data.readBit()) { // colour_description_present_flagint colorPrimaries = data.readBits(8); // colour_primariesint transferCharacteristics = data.readBits(8); // transfer_characteristicsdata.skipBits(8); // matrix_coeffscolorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries);colorTransfer =ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics);}}}return new SpsData(profileIdc,constraintsFlagsAndReservedZero2Bits,levelIdc,seqParameterSetId,maxNumRefFrames,frameWidth,frameHeight,pixelWidthHeightRatio,separateColorPlaneFlag,frameMbsOnlyFlag,frameNumLength,picOrderCntType,picOrderCntLsbLength,deltaPicOrderAlwaysZeroFlag,colorSpace,colorRange,colorTransfer);}
SPS这主要获取的SPS 的ID,编码的profile,帧宽高,以及宽高比,到这里基本可以确定解码器,确定出视频的宽高等全局参数。
PPS的解析
public static PpsData parsePpsNalUnitPayload(byte[] nalData, int nalOffset, int nalLimit) {ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);int picParameterSetId = data.readUnsignedExpGolombCodedInt();//pic_parameter_set_id PPS的IDint seqParameterSetId = data.readUnsignedExpGolombCodedInt();//seq_parameter_set_id SPS的IDdata.skipBit(); // entropy_coding_mode_flagboolean bottomFieldPicOrderInFramePresentFlag = data.readBit();//bottom_field_pic_order_in_frame_present_flagreturn new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag);}
PPS的解析就简单多了主要或bottom_field_pic_order_in_frame_present_flag这一个值。
SEI的解析
public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, TrackOutput[] outputs) {while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {int payloadType = readNon255TerminatedValue(seiBuffer);//SEI的类型int payloadSize = readNon255TerminatedValue(seiBuffer);//SEI大小int nextPayloadPosition = seiBuffer.getPosition() + payloadSize;// Process the payload.if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) {// This might occur if we're trying to read an encrypted SEI NAL unit.Log.w(TAG, "Skipping remainder of malformed SEI NAL unit.");nextPayloadPosition = seiBuffer.limit();} else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) {//字幕类型的数据int countryCode = seiBuffer.readUnsignedByte();//获取国家int providerCode = seiBuffer.readUnsignedShort();//获取地区int userIdentifier = 0;if (providerCode == PROVIDER_CODE_ATSC) {userIdentifier = seiBuffer.readInt();}int userDataTypeCode = seiBuffer.readUnsignedByte();if (providerCode == PROVIDER_CODE_DIRECTV) {seiBuffer.skipBytes(1); // user_data_length.}boolean messageIsSupportedCeaCaption =countryCode == COUNTRY_CODE&& (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV)&& userDataTypeCode == USER_DATA_TYPE_CODE_MPEG_CC;if (providerCode == PROVIDER_CODE_ATSC) {messageIsSupportedCeaCaption &= userIdentifier == USER_DATA_IDENTIFIER_GA94;}if (messageIsSupportedCeaCaption) {//开始解析字幕consumeCcData(presentationTimeUs, seiBuffer, outputs);}}seiBuffer.setPosition(nextPayloadPosition);}}public static void consumeCcData(long presentationTimeUs, ParsableByteArray ccDataBuffer, TrackOutput[] outputs) {// First byte contains: reserved (1), process_cc_data_flag (1), zero_bit (1), cc_count (5).int firstByte = ccDataBuffer.readUnsignedByte();boolean processCcDataFlag = (firstByte & 0x40) != 0;if (!processCcDataFlag) {// No need to process.return;}int ccCount = firstByte & 0x1F;ccDataBuffer.skipBytes(1); // Ignore em_data// Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)// + cc_data_1 (8) + cc_data_2 (8).int sampleLength = ccCount * 3;int sampleStartPosition = ccDataBuffer.getPosition();for (TrackOutput output : outputs) {ccDataBuffer.setPosition(sampleStartPosition);output.sampleData(ccDataBuffer, sampleLength);//发送数据到SamleQueueif (presentationTimeUs != C.TIME_UNSET) {output.sampleMetadata(//字幕轨道sampleMetadatapresentationTimeUs,C.BUFFER_FLAG_KEY_FRAME,sampleLength,/* offset= */ 0,/* cryptoData= */ null);}}}
可以看到这里解析SEI主要是为了获取其中的字幕信息,如果没有字幕信息,SEI可以直接忽略
Slice的解析
定义在SampleReader中。
public void appendToNalUnit(byte[] data, int offset, int limit) {if (!isFilling) {//数据还没有填充足够,返回继续填充return;}int readLength = limit - offset;if (buffer.length < bufferLength + readLength) {buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2);}System.arraycopy(data, offset, buffer, bufferLength, readLength);bufferLength += readLength;bitArray.reset(buffer, 0, bufferLength);if (!bitArray.canReadBits(8)) {return;}bitArray.skipBit(); // forbidden_zero_bitint nalRefIdc = bitArray.readBits(2);//nal_ref_idc优先级bitArray.skipBits(5); // nal_unit_type// Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013)// subsection 7.3.3.if (!bitArray.canReadExpGolombCodedNum()) {return;}bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_sliceif (!bitArray.canReadExpGolombCodedNum()) {return;}int sliceType = bitArray.readUnsignedExpGolombCodedInt();//slice_typeif (!detectAccessUnits) {// There are AUDs in the stream so the rest of the header can be ignored.isFilling = false;sliceHeader.setSliceType(sliceType);return;}if (!bitArray.canReadExpGolombCodedNum()) {return;}int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt();//获取 PPS IDif (pps.indexOfKey(picParameterSetId) < 0) {// We have not seen the PPS yet, so don't try to decode the slice header.isFilling = false;return;}NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId);//首先通过当前Slice的PPS ID 获取到PPSNalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId);//再通过PPS的ID获取到SPS数据if (spsData.separateColorPlaneFlag) {//separate_colour_plane_flag为1说明存在colour_plane_idif (!bitArray.canReadBits(2)) {return;}bitArray.skipBits(2); // 跳过colour_plane_id}if (!bitArray.canReadBits(spsData.frameNumLength)) {return;}boolean fieldPicFlag = false;boolean bottomFieldFlagPresent = false;boolean bottomFieldFlag = false;//通过SPS 获取到的帧序号长度读取帧序号int frameNum = bitArray.readBits(spsData.frameNumLength);if (!spsData.frameMbsOnlyFlag) {//frame_mbs_only_flag 不只进行帧编码if (!bitArray.canReadBits(1)) {return;}fieldPicFlag = bitArray.readBit();if (fieldPicFlag) {//field_pic_flag 还存在场编码if (!bitArray.canReadBits(1)) {return;}bottomFieldFlag = bitArray.readBit();//bottom_field_flag 底场标识位bottomFieldFlagPresent = true;}}boolean idrPicFlag = nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_IDR;//IDR 类型的NALint idrPicId = 0;if (idrPicFlag) {if (!bitArray.canReadExpGolombCodedNum()) {return;}idrPicId = bitArray.readUnsignedExpGolombCodedInt();//idr_pic_id IDR帧的序号}int picOrderCntLsb = 0;int deltaPicOrderCntBottom = 0;int deltaPicOrderCnt0 = 0;int deltaPicOrderCnt1 = 0;if (spsData.picOrderCountType == 0) {if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) {return;}picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength);//pic_order_cnt_lsbif (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {if (!bitArray.canReadExpGolombCodedNum()) {return;}deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt();//delta_pic_order_cnt_bottom}} else if (spsData.picOrderCountType == 1 && !spsData.deltaPicOrderAlwaysZeroFlag) {if (!bitArray.canReadExpGolombCodedNum()) {return;}deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt();//delta_pic_order_cnt[0]if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {if (!bitArray.canReadExpGolombCodedNum()) {return;}deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt();//delta_pic_order_cnt[1]}}sliceHeader.setAll(spsData,nalRefIdc,sliceType,frameNum,picParameterSetId,fieldPicFlag,bottomFieldFlagPresent,bottomFieldFlag,idrPicFlag,idrPicId,picOrderCntLsb,deltaPicOrderCntBottom,deltaPicOrderCnt0,deltaPicOrderCnt1);isFilling = false;}
Slice解析主要是解析了Slice的Header,用于判断是否为I帧以及判断当前NAL是否为图像的第一个VCL类的NAL,这些数据主要用于没有AUD时确定SampleMetadata的时机。
下面我们来动态看下SampleMetadata基于流的时序关系图:
对照上图可以看出,H264Reader 一开始就会将所有数据Sample到SampleQueue,接下来会查找第一个NAL开始位置,如果读取到第一个AUD,记录AUD开始位置为samplePosition,作为这段SampleData的有效开始位置,下面数据序列首先会将SPS,PPS这2个索引的NAL放在前面,等解码器获取了SPS和PPS基本上就能确定解码器的具体配置,这个时候会调用SampleQueue的format方法将解码器格式输出,用于解码器的初始化等。当读取到下一个AUD的时候将这个AUD的开始位置作为有效SampleData的结束位置,通过有效结束位置(absolutePosition)-有效开始位置(samplePosition)获得当前有效数据长度(size),同时通过当前AUD的长度(nalUnitLength)+下一个NAL头到数据段末尾的距离(bytesWrittenPastPosition)得到offset,将size和offset传给SampleQueue的sampleMetadata方法,sampleMetadata里通过数据总长度-size-offset确定当前SampleData有效数据的开始位置,这样就记录了每个Sample的开始位置和长度,当Rendere读取数据用于解码时,就可以查询这个开始位置和长度读取有效的视频数据给解码器。
总结
到这里ProgressiveMediaPeriod的数据解析部分终于讲完,那么这些解析的数据是如何加载的呢,这就是ProgressiveMediaPeriod整体架构右半部分的最后一块拼图——DataSource,这也是我们后面要讲的内容了。
版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持
相关文章:

ExoPlayer架构详解与源码分析(10)——H264Reader
系列文章目录 ExoPlayer架构详解与源码分析(1)——前言 ExoPlayer架构详解与源码分析(2)——Player ExoPlayer架构详解与源码分析(3)——Timeline ExoPlayer架构详解与源码分析(4)—…...

智能优化算法应用:基于粒子群算法3D无线传感器网络(WSN)覆盖优化 - 附代码
智能优化算法应用:基于粒子群算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于粒子群算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.粒子群算法4.实验参数设定5.算法结果6.参考文…...

微积分-序言
大家好,这里我将为大家带来一个全新的专栏“微积分”。在这里我将为大家讲解微积分的内容,我会从最基础的内容开始讲解。争取让零基础的人也可以看懂和学会。 我也会在后续出一些微积分的题,让大家可以进行巩固和提高。 学习微积分那么就需要…...

ArchLinux安装详细步骤
下载(略)安装VirtualBox(略)新建虚拟机(略)启动 进入提示符 进入安装设置界面 archinstall出现界面: 逐项设置。 Disk我选择了ext4 在Profile中 我选择了KDE作为桌面(选择后按回车…...

react 学习笔记 李立超老师 | (学习中~)
文章目录 react学习笔记01入门概述React 基础案例HelloWorld三个API介绍 JSXJSX 解构数组 创建react项目(手动)创建React项目(自动) | create-react-app事件处理React中的CSS样式内联样式 | 内联样式中使用state (不建议使用)外部样式表 | CSS Module React组件函数式组件和类组…...

Docker镜像和容器的简单操作
1.镜像管理 搜索镜像: 这种方法只能用于官方镜像库 搜索基于 centos 操作系统的镜像 # docker search centos 按星级搜索镜像: 查找 star 数至少为 100 的镜像,默认不加 s 选项找出所有相关 ubuntu 镜像…...

章鱼网络进展月报 | 2023.11.1-11.30
章鱼网络大事摘要 1、2023年12月,Octopus 2.0 将会正式启动。 2、隐私协议 Secret Network 宣布使用 Octopus Network 构建的 NEAR-IBC 连接 NEAR 生态。 3、Louis 受邀作为嘉宾,在 NEARCON2023 的多链网络主题沙龙中发言:我们依然处于区…...

基于Maven构建OSGI应用(Maven和OSGI结合)
基于Maven构建OSGI应用。 使用Maven来构建项目,包括项目的创建、子模块buldle的创建等。使用OSGI来实现动态模块化管理,实现模块的热插拔效果(即插即用)。 创建一个Maven项目:helloworld,并在该项目下创建…...

oracle分组排序后取第一条
在 Oracle 中,可以使用「ROW_NUMBER」函数对某个列进行分组并排序,然后通过「WHERE」语句取第一条记录。 假设有一张「USERS」表,其中包含「ID」、「NAME」、「AGE」和「COUNTRY」列,您可以使用以下 SQL 语句对「AGE」列进行分组…...

MAMBA介绍:一种新的可能超过Transformer的AI架构
有人说,“理解了人类的语言,就理解了世界”。一直以来,人工智能领域的学者和工程师们都试图让机器学习人类的语言和说话方式,但进展始终不大。因为人类的语言太复杂,太多样,而组成它背后的机制,…...

win系统一台电脑安装两个不同版本的mysql教程
文章目录 1.mysql下载zip包(地址)2.解压在你的电脑上(不要再C盘和带中文的路径)3.创建my.ini文件4.更改环境变量(方便使用, 可选)5.打包mysql服务6.初始化mysql的data7.启动刚刚打包的服务8.更改密码 1.mys…...

esp32-s3部署yolox_nano进行目标检测
ESP32-S3部署yolox_nano进行目标检测 一、生成模型部署项目01 环境02 配置TVM包03 模型量化3.1预处理3.2 量化 04 生成项目 二、烧录程序 手上的是ESP32-S3-WROOM-1 N8R8芯片,整个链路跑通了,但是识别速度太慢了,20秒一张图,所以暂…...

TCP传输数据的确认机制
实际的TCP收发数据的过程是双向的。 TCP采用这样的方式确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。如果对方没有返回某些包对应的ACK号,那么就重新发送这些包。 这一机制非常强大。通过这一机制…...

使用Ansible Expect模块实现自动化交互式任务
Ansible是一种功能强大的自动化工具,可用于自动化配置管理、部署和任务执行。其中的Expect模块是Ansible的一个重要组件,它允许我们自动化处理需要与交互式命令行进行交互的任务。本文将介绍如何使用Ansible的Expect模块,并提供一些示例来说明…...

51单片机独立按键以及矩阵按键的使用以及其原理--独立按键 K1 控制 D1 指示灯亮灭以及数码管显示矩阵按键 S1-S16 按下后键值 0-F
IO 的使用–按键 本文主要涉及8051单片机按键的使用,包括独立按键以及矩阵按键的使用以及其原理,其中代码实例包括: 1.独立按键 K1 控制 D1 指示灯亮灭 2.通过数码管显示矩阵按键 S1-S16 按下后键值 0-F 文章目录 IO 的使用--按键一、按键消抖二、独立按…...

chrome安装jsonview
写在前面 通过jsonview可以实现,当http响应时application/json时直接在浏览器格式化显示,增加可读性。本文看下如何安装该插件到chrome中。 1:安装 首先在这里 下载插件包,然后解压备用。接着在chrome按照如下步骤操作…...

使用TouchSocket适配一个c++的自定义协议
这里写目录标题 说明一、新建项目二、创建适配器三、创建服务器和客户端3.1 服务器3.2 客户端3.3 客户端发送3.4 客户端接收3.5 服务器接收与发送 四、关于同步Send 说明 今天有小伙伴咨询我,他和同事(c端)协商了一个协议,如果使…...

VSC改造MD编辑器及图床方案分享
VSC改造MD编辑器及图床方案分享 用了那么多md编辑器,到头来还是觉得VSC最好用。这次就来分享一下我的blog文件编辑流吧。 这篇文章包括:VSC下md功能扩展插件推荐、图床方案、blog文章管理方案 VSC插件 Markdown All in One Markdown Image - 粘粘图片…...

SpringBoot的依赖管理和自动配置
与其明天开始,不如现在行动! 文章目录 1 依赖管理机制2 自动配置机制2.1 初步理解2.2 完整流程 💎总结 1 依赖管理机制 为什么导入starter-web后所有相关依赖都会导入进来? 开发什么场景,导入什么场景启动器-spring-bo…...

linux 定时任务
使用 crontab Usage: crontab [-u user] [-e|-l|-r] Crontab 的格式说明如下: * 逗号(‘,’) 指定列表值。如: “1,3,4,7,8″ * 中横线(‘-’) 指定范围值 如 “1-6″, 代表 “1,2,3,4,5,6″ * 星号 (‘*’) 代表所有可能的值 */15 表示每 15 分钟执行一次 # Use the ha…...

增强现实中的真实人/机/环与虚拟人/机/环
在增强现实中,真实人与虚拟人、真实机器与虚拟机器、真实环境与虚拟环境之间有着密切的关系。增强现实技术通过将真实与虚拟相结合,打破了传统的现实世界与虚拟世界的界限,创造出了一种新的体验方式。真实人、真实机器和真实环境与其对应的虚…...

Python网络爬虫环境的安装指南
网络爬虫是一种自动化的网页数据抓取技术,广泛用于数据挖掘、信息搜集和互联网研究等领域。Python作为一种强大的编程语言,拥有丰富的库支持网络爬虫的开发。本文将为你详细介绍如何在你的计算机上安装Python网络爬虫环境。 一、安装python开发环境 进…...

【MyBatis系列】MyBatis字符串问题
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

【Java】构建表达式二叉树和表达式二叉树求值
问题背景 1. 实现一个简单的计算器。通过键盘输入一个包含圆括号、加减乘除等符号组成的算术表达式字符串,输出该算术表达式的值。要求: (1)系统至少能实现加、减、乘、除等运算; (2)利用二叉…...

采用Python 将PDF文件按照页码进行切分并保存
工作中经常会遇到 需要将一个大的PDF文件 进行切分,比如仅需要大PDF文件的某几页 或者连续几页,一开始都是用会员版本的WPS,但是对于程序员,就是要采用技术白嫖 这里就介绍一个 python的PDF 包 PyPDF2 其安装方式也很简单 p…...

H264视频编码原理
说到视频,我们首先想到的可能就是占内存。我们知道一个视频是由一连串图像序列组成的,视频中图像一般是 YUV 格式。假设有一个电影视频,分辨率是 1080P,帧率是 25fps,并且时长是 2 小时,如果不做视频压缩的…...

UDP实现群聊
代码: import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.net.*; import java.io.IOException; import java.lang.String;public class liaotian extends JFrame{private static final int DEFAULT_PORT8899;private JLabel stateLB…...

服务器部署网易开源TTS | EmotiVoice部署教程
一、环境 ubuntu 20.04 python 3.8 cuda 11.8二、部署 1、docker方式部署 1.1、安装docker 如何安装docker,可以参考这篇文章 1.2、拉取镜像 docker run -dp 127.0.0.1:8501:8501 syq163/emoti-voice:latest2、完整安装 安装python依赖 conda create -n Emo…...

贪心算法和动态规划
目录 一、简介 二、贪心算法案例:活动选择问题 1.原理介绍 三、动态规划案例:背包问题 1.原理介绍 四、贪心算法与动态规划的区别 五、总结 作者其他文章链接 正则表达式-CSDN博客 深入理解HashMap:Java中的键值对存储利器-CSDN博客…...

jsp 设备预约管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
一、源码特点 JSP 设备预约管理系统是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql5.0…...