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

2023-12-10 10:01

本文主要是介绍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作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

这篇关于ExoPlayer架构详解与源码分析(10)——H264Reader的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Debezium 与 Apache Kafka 的集成方式步骤详解

《Debezium与ApacheKafka的集成方式步骤详解》本文详细介绍了如何将Debezium与ApacheKafka集成,包括集成概述、步骤、注意事项等,通过KafkaConnect,D... 目录一、集成概述二、集成步骤1. 准备 Kafka 环境2. 配置 Kafka Connect3. 安装 D

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SQL 中多表查询的常见连接方式详解

《SQL中多表查询的常见连接方式详解》本文介绍SQL中多表查询的常见连接方式,包括内连接(INNERJOIN)、左连接(LEFTJOIN)、右连接(RIGHTJOIN)、全外连接(FULLOUTER... 目录一、连接类型图表(ASCII 形式)二、前置代码(创建示例表)三、连接方式代码示例1. 内连接(I

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

Java中八大包装类举例详解(通俗易懂)

《Java中八大包装类举例详解(通俗易懂)》:本文主要介绍Java中的包装类,包括它们的作用、特点、用途以及如何进行装箱和拆箱,包装类还提供了许多实用方法,如转换、获取基本类型值、比较和类型检测,... 目录一、包装类(Wrapper Class)1、简要介绍2、包装类特点3、包装类用途二、装箱和拆箱1、装

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动