ExoPlayer架构详解与源码分析(10)——H264Reader

系列文章目录

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_bit1禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误,告诉接收方丢掉该单元,否则为0
nal_ref_idc2指示当前NALU的优先级,或者说重要性,数值越大表明越重要
nal_unit_type5表示NALU的类型

那么nal_unit_type不同值对应什么类型呢看下表

nal_unit_typeNAL类型
0未使用
1不分区、非 IDR 图像的片
2SLICE A 片分区 A
3SLICE B 片分区 B
4SLICE C 片分区 C
5IDR 图像中的片
6Supplemental Enhancement Information(SEI ) 补充增强信息单元
7Sequence Paramater Set(SPS) 序列参数集
8Picture Paramater Set(PPS) 图像参数集
9Access Unit Delimiter(AUD) 分界符
10End Of Seq 序列结束
11End Of Stream 码流结束
12Filler Data 填充
13…23保留
24…31未使用

下面看下几个重要的unitType结构

  • Sequence Paramater Set(SPS) 序列参数集
    SPS结构比较复杂这里挑几个用到的

    名称大小(b)说明
    profile_idc8本视频编码时遵循的profile,profile分为Baseline,Main,Extended等,主要用来规定编码时是否采用某些特性,比如说Baseline profile就规定了只能使用I、P slice进行编码,关于profile的说明可以去查看标准的Annex A。
    constraint_set0_flag1强制使用Baseline profile进行编码
    constraint_set1_flag1强制使用Main profile进行编码
    constraint_set2_flag1强制使用Extended profile进行编码
    level_idc8本视频遵循的level,level主要规定了每秒最多能处理多少个宏块,最大的帧大小,最大的解码缓存,最大比特率等这些性能相关的东西,如果是硬解码,则比较容易出现由于视频level太高而不能解码的情况。
    seq_parameter_set_idue(v)本SPS的ID,这个ID主要是给PPS用的
    separate_colour_plane_flag1separate_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_minus4ue(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_typeue(v)指定解码图片顺序计数的方法,pic_order_cnt_type 的值范围应为 0 至 2(含 2)
    pic_width_in_mbs_minus1ue(v)图片宽度
    pic_height_in_map_units_minus1ue(v)图片高度
    frame_mbs_only_flag1是否只进行帧编码
    vui_parameters_present_flag1SPS是否包含vui参数, video usability information,在标准的Annex E中有描述,主要包含了视频的比例调整,overscan,视频格式,timing,比特率等信息
    aspect_ratio_info_present_flag1等于 1 表示存在 aspect_ratio_idc,等于 0 表示不存在 aspect_ratio_idc
    aspect_ratio_idc8指定样本的采样纵横比值。当 aspect_ratio_idc 表示 Extended_SAR(扩展 SAR)时,采样纵横比用 sar_width : sar_height 表示,当没有 aspect_ratio_idc 语法元素时,aspect_ratio_idc 值为 0
    sar_width16表示样本纵横比的水平尺寸
    sar_height16表示样本纵横比的垂直尺寸(单位与 sar_width 相同)

    ue(v)、se(v)表示以哥伦布编码的一种变长压缩算法

  • Picture Paramater Set(PPS) 图像参数集
    这里也挑几个用到的讲下

    名称大小(b)说明
    pic_parameter_set_idue(v)当前PPS的ID,供slice RBSP使用
    seq_parameter_set_idue(v)当前PPS所属的SPS的ID
    bottom_field_pic_order_in_frame_present_flag1用于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_sliceue(v)当前slice中包含的第一个宏块在整帧中的位置
      slice_typeue(v)当前slice的类型参照下表
      pic_parameter_set_idue(v)当前slice所依赖的pps的id;范围 0 到 255
      colour_plane_id2当标识位separate_colour_plane_flag为true时,colour_plane_id表示当前的颜色分量,0、1、2分别表示Y、U、V分量
      frame_numue(v)表示当前帧序号,数据长度参考上面的log2_max_frame_num_minus4
      field_pic_flag1场编码标识位。当该标识位为1时表示当前slice按照场进行编码;该标识位为0时表示当前 slice按照帧进行编码
      bottom_field_flag1底场标识位。该标志位为1表示当前slice是某一帧的底场;为0表示当前slice为某一帧的顶场
      idr_pic_idue(v)表示IDR帧的序号。某一个IDR帧所属的所有slice,其idr_pic_id应保持一致。该值的取值范围为[0,65535]。
      pic_order_cnt_lsbue(v)表示当前帧序号的另一种计量方式
      delta_pic_order_cnt_bottomse(v)表示顶场与底场POC差值的计算方法,不存在则默认为0
      delta_pic_order_cnt[0]se(v)指定编码帧顶部字段的图片顺序计数与预期图片顺序计数的差值
      delta_pic_order_cnt[1]se(v)指定图像顺序计数与编码帧底层字段的预期图像顺序计数的差值
      slice_typeName of slice_type
      0P (P slice)
      1B (B slice)
      2I (I slice)
      3SP (SP slice)
      4SI (SI slice)
      5P (P slice)
      6B (B slice)
      7I (I slice)
      8SP (SP slice)
      9SI (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作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/203015.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构初阶之二叉树性质练习与代码练习

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 数据结构初阶 Linux 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力,共赴大厂。 目录 1.前言 2.性质练习 3…

Python中的匿名函数是什么

匿名函数 lambda x , y : xy 1.匿名的目的就是要没有名字&#xff0c;给匿名函数赋给一个名字是没有意义的。 2.匿名函数的参数规则、作用域关系与有名函数是一样的。 3.匿名函数的函数体通常应该是 一个表达式,该表达式必须要有一个返回值。 flambda x,n:x ** n print(f…

我把springboot项目从Java 8 升级 到了Java 17 的过程总结,愿为君提前踩坑!

项目从jdk8升级到jdk17&#xff0c;我不是为了追求java 17的新特性&#xff08;准确来说也还没有去了解有什么新特性&#xff09;&#xff0c;也不是为了准确与时俱进&#xff0c;永远走在java行列的最前端&#xff0c;纯粹因为项目需要&#xff0c;因为我们都知道&#xff0c;…

【C++】:set和map

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关多态的知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结…

深入理解 Java 虚拟机(JVM)从入门到精通

目录 一、JVM内存结构1、堆&#xff08;Heap&#xff09;&#xff08;1&#xff09;特点&#xff08;2&#xff09;堆内存分配&#xff08;3&#xff09;晋升到老年代的方式&#xff08;4&#xff09;堆内存检验方式2、虚拟机栈&#xff08;VM Stack&#xff09;&#xff08;1&…

OpenHarmony北向-让更广泛的应用开发者更容易参与

一、标准系统的体验 按照官方文档指导&#xff0c;这样操作&#xff0c;OH标准系统开发板就可以运行开发者开发的OpenHarmony应用了。 二、实际情况 按照开发文档上的说明&#xff0c;肯定是装不上的。因为OH不同的发行版&#xff0c;不同发行板不同的设备&#xff0c;IDE&…

uni-app中vue3表单校验失败

目录 1.问题 2.原因及解决方式 3.表单校验方式&#xff08;vue3&#xff09; 1.问题 在app中使用uni-forms表单&#xff0c;并添加校验规则&#xff0c;问题是即使输入内容&#xff0c;表单校验依然失败。 代码&#xff1a; <template><view><uni-forms r…

vue中shift+alt+f格式化防止格式掉其它内容

好处就是使得提交记录干净&#xff0c;否则修改一两行代码&#xff0c;习惯性按了一下格式化快捷键&#xff0c;遍地飘红&#xff0c;下次找修改就费时间 1.点击设置图标-设置 2.点击这个转成配置文件 {"extensions.ignoreRecommendations": true,"[vue]":…

Android Glide自定义AppCompatImageView切分成若干小格子,每个小格子onDraw绘制Bitmap,Kotlin(1)

Android Glide自定义AppCompatImageView切分成若干小格子&#xff0c;每个小格子onDraw绘制Bitmap&#xff0c;Kotlin&#xff08;1&#xff09; 垂直方向的RecyclerView&#xff0c;每行一个AppCompatImageView&#xff0c;每个AppCompatImageView被均匀切割成n个小格子&#…

Games 103 作业三

Games 103 作业三 作业三的内容主要就是实现一下FVM。我们按照文档中的步骤&#xff0c;第一步就是去独立地更新mesh的速度和位置&#xff0c;在初始化每个顶点的受力时&#xff0c;需要考虑到重力的影响。 for(int i0 ;i<number; i) {//TODO: Add gravity to Force.Force[…

wsl安装虚拟机平台报错“无法解析服务器的名称或地址

wsl安装虚拟机平台报错“无法解析服务器的名称或地址” 1.问题 利用wsl安装拟机平台报错“无法解析服务器的名称或地址” 2.解决方案 修改DNS即可 控制面板->网络和Internet&#xff0c;选择查看网络状态和任务 选择更改适配器设置 选择所连接的网络&#xff0c;选择属性…

我不是DBA之慢SQL诊断方式

最近经常遇到技术开发跑来问我慢SQL优化相关工作&#xff0c;所以干脆出几篇SQL相关优化技术月报&#xff0c;我这里就以公司mysql一致的5.7版本来说明下。 在企业中慢SQL问题进场会遇到&#xff0c;尤其像我们这种ERP行业。 成熟的公司企业都会有晚上的慢SQL监控和预警机制。…

C++ Core Guidelines解析 ( 好书推荐 )

C Core Guidelines是Bjarne和 Herb Sutter发起编写的一个开源项目&#xff0c;汇聚了 C社区多年来积累的宝贵经验&#xff0c;是非常全面的编程最佳实践指导&#xff0c;包括代码风格、函数、类、错误处理、性能优化等&#xff0c;可以说是C社区的集大成者。用Effective Modern…

网络安全缓冲区溢出实验

实验要求实验步骤函数 f00()函数 f01()函数 f02() 实验要求 C 程序 homework08.c 的主函数如下&#xff1a; int main(int argc, char * argv[]) { init_buf(Lbuffer, LEN);switch(argc) {case 1: f00(); break;case 2: f01(); break;case 3: f02(); break; default: f00(); …

国内AI大模型已近80个,哪个最有前途?

目前&#xff0c;国内已经推出了近80个人工智能大模型&#xff0c;这些大模型各有优势&#xff0c;难以直接判断哪个最有前途。然而&#xff0c;以下几个大模型值得关注&#xff1a; 1、华为云盘古大模型&#xff1a;盘古大模型聚焦于为行业服务&#xff0c;包括自然语言、视觉…

出错:I/O文件读取JAVA

I/O文件读取 /** author:xiaowang* date:2023/12/6* demand:读取java1班的数据* * */ package homework;import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException;public class FileReadTest {public static void main(String[] args) …

月入9K, 95后夫妻转行网优:人生路远,我们只愿携手前行!

用心专注一门课&#xff0c;让教育改变更多人是优橙教育成立的初心与使命。 在优小橙的教育生涯里&#xff0c;见过许多学生&#xff0c;他们或是因为追求一份美好的前程来优橙&#xff1b;或是想要改变泥泞的现状来优橙&#xff1b;或是因为想要深耕通信行业来优橙……理由很多…

Linux--程序地址空间

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 [TOC](文章目录) 一、程序地址空间回顾 我们在讲C语言的时候&#xff0c;老师给大家画过这样的空间布局…

Qt-Q_OBJECT宏使用与“无法解析的外部符号qt_metacall/metaObject/qt_metacast“

有时候我们编写Qt类的时候&#xff0c;修改代码时直接加上Q_OBJECT宏&#xff0c;然后直接构建&#xff0c;会报如下错误&#xff1a; 这里的几个函数的声明是由Q_OBJECT宏引入的&#xff0c;而其对应的实现是由moc实现的&#xff0c;如果我们更新了代码但是没有执行qmake&…

基于FPGA的图像RGB转CIE-Lab实现,包含testbench和MATLAB辅助验证程序

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1RGB颜色空间 4.2CIE-Lab颜色空间 4.3RGB转CIE-Lab算法原理 5.算法完整程序工程 1.算法运行效果图预览 将FPGA的结果导入到matlab&#xff0c;并和matlab的仿真结果进行对比&#xff1a…