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一起看。

@Override
  public void consume(ParsableByteArray data) {
    assertTracksCreated();

    int offset = data.getPosition();
    int limit = data.limit();
    byte[] dataArray = data.getData();

    // 将当前数据长度计入总长度,此时总数据的尾部和当前数据的尾部就是对齐的
    totalBytesWritten += data.bytesLeft();
    //到这里已经是解复用后的数据了,将数据发给SampleQueue
    output.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都已经endNalUnit
          List<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输出给SampleQueue
          output.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.endNalUnit
 public 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长度,一般都是5
          int 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的ID

    int chromaFormatIdc = 1; // Default is 4:2:0
    boolean 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_minus8
      data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
      data.skipBit(); // qpprime_y_zero_transform_bypass_flag
      boolean 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 + 4
    int picOrderCntType = data.readUnsignedExpGolombCodedInt();//pic_order_cnt_type
    int picOrderCntLsbLength = 0;
    boolean deltaPicOrderAlwaysZeroFlag = false;
    if (picOrderCntType == 0) {
      // log2_max_pic_order_cnt_lsb_minus4 + 4
      picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4;
    } else if (picOrderCntType == 1) {
      deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flag
      data.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic
      data.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field
      long numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt();
      for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
        data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
      }
    }
    int maxNumRefFrames = data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
    data.skipBit(); // gaps_in_frame_num_value_allowed_flag

    int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;//pic_width_in_mbs_minus1
    int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1;//pic_height_in_map_units_minus1
    boolean frameMbsOnlyFlag = data.readBit();//frame_mbs_only_flag
    int 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_flag
        int aspectRatioIdc = data.readBits(8);//aspect_ratio_idc
        if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {//自定义了宽高比
          int sarWidth = data.readBits(16);//sar_width
          int sarHeight = data.readBits(16);//sar_height
          if (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_flag
        data.skipBit(); // overscan_appropriate_flag
      }
      if (data.readBit()) { // video_signal_type_present_flag
        data.skipBits(3); // video_format
        colorRange =
            data.readBit() ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; // video_full_range_flag
        if (data.readBit()) { // colour_description_present_flag
          int colorPrimaries = data.readBits(8); // colour_primaries
          int transferCharacteristics = data.readBits(8); // transfer_characteristics
          data.skipBits(8); // matrix_coeffs

          colorSpace = 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的ID
    int seqParameterSetId = data.readUnsignedExpGolombCodedInt();//seq_parameter_set_id SPS的ID
    data.skipBit(); // entropy_coding_mode_flag
    boolean bottomFieldPicOrderInFramePresentFlag = data.readBit();//bottom_field_pic_order_in_frame_present_flag
    return 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);//发送数据到SamleQueue
      if (presentationTimeUs != C.TIME_UNSET) {
        output.sampleMetadata(//字幕轨道sampleMetadata
            presentationTimeUs,
            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_bit
      int 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_slice
      if (!bitArray.canReadExpGolombCodedNum()) {
        return;
      }
      int sliceType = bitArray.readUnsignedExpGolombCodedInt();//slice_type
      if (!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 ID
      if (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 获取到PPS
      NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId);//再通过PPS的ID获取到SPS数据
      if (spsData.separateColorPlaneFlag) {//separate_colour_plane_flag为1说明存在colour_plane_id
        if (!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 类型的NAL
      int 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_lsb
        if (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作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI大模型:启动参数总结整理

虽然通过调整启动大模型的参数&#xff0c;对生成效果的提升是有限的&#xff0c;但适当的调整&#xff0c;还是能满足一些常用的场景的~ 一. 【max_length】 令牌生成最大数 用于控制生成文本的最大长度&#xff0c;默认为 20。它的值对应于输入提示的长度加上max_new_token…

10_企业架构NOSQL数据库之MongoDB

企业架构NOSQL数据库之MongoDB 学习目标和内容 1、能够简单描述MongoDB的使用特点 2、能够安装配置启动MongoDB 3、能够使用命令行客户端简单操作MongoDB 4、能够实现基本的数据操作 5、能够实现MongoDB基本安全设置 6、能够操作安装php的MongoDB扩展 一、背景描述及其方案设计…

【AI】以大厂PaaS为例,看人工智能技术方案服务能力的方向(1/2)

目录 一、深度学习能力 二、计算框架 2.1 语音 2.2 OCR 2.3 人脸/体识别 2.4 图像审核 2.5 图像识别 2.6 视频 2.7 自然语言理解 2.8 知识图谱 今天以百度智能云为例&#xff0c;梳理下人工智能技术方案服务能力&#xff0c;主要有哪些方向的应用和拓展。 纯属学习&…

JDK 9 模块化系统 (Module System) 和 多版本兼容 Jar (Multi-Release Jar)

博文目录 文章目录 Module System原因JDK 模块化模块描述文件关键字 启用模块化测试结论 Multi-Release jar (MRJAR)原因原理结论用 IDEA 创建多版本兼容 Jar项目结构pom.xml测试 Module System 原因 Java 9引入了模块化系统的主要原因是为了解决Java平台面临的复杂性和可维…

OpenCV图像相似性比对算法

背景 在做图像处理或者计算机视觉相关的项目的时候&#xff0c;很多时候需要我们对当前获得的图像和上一次的图像做相似性比对&#xff0c;从而找出当前图像针对上一次的图像的差异性和变化点&#xff0c;这需要用到OpenCV中的一些图像相似性和差异性的比对算法&#xff0c;在O…

C练习题13

单项选择题(本大题共20小题,每小题2分,共40分。在每小题给出的四个备选项中,选出一个正确的答案,并将所选项前的字母填写在答题纸的相应位置上。) 1.结构化程序由三种基本结构组成、三种基本结构组成的算法是() A.可以完成任何复杂的任务 B. 只能完成部分复杂的任务 C. 只能完…

打破常规思维:Scrapy处理豆瓣视频下载的方式

概述 Scrapy是一个强大的Python爬虫框架&#xff0c;它可以帮助我们快速地开发和部署各种类型的爬虫项目。Scrapy提供了许多方便的功能&#xff0c;例如请求调度、数据提取、数据存储、中间件、管道、信号等&#xff0c;让我们可以专注于业务逻辑&#xff0c;而不用担心底层的…

TrustZone之物理地址空间

除了两个安全状态外&#xff0c;该体系结构还提供了两个物理地址空间&#xff1a;安全和非安全。 在非安全状态下&#xff0c;虚拟地址始终转换为非安全物理地址。这意味着在非安全状态下的软件只能看到非安全资源&#xff0c;但永远看不到安全资源。如图所示&#xff1a; 在安…

大数据Doris(三十三):Doris高级设置

文章目录 Doris高级设置 一、增大内存

准确!!!在 CentOS 8 上配置 PostgreSQL 14 的主从复制

在 CentOS 8 上配置 PostgreSQL 14 的主从复制&#xff0c;并设置 WAL 归档到特定路径 /home/postgres/archive 的步骤如下&#xff1a; 主服务器配置&#xff08;主机&#xff09; 配置 PostgreSQL&#xff1a; 编辑 postgresql.conf 文件&#xff1a; vim /data/postgres/p…

Java二阶知识点总结(一)Maven

一、Maven概念 Maven是一个项目管理工具&#xff0c;其主要作用有2点 依赖管理&#xff1a;管理项目依赖的各种jar包自动构建&#xff1a;项目构建的过程&#xff0c;从编译、测试、运行、打包到安装的过程可以一键执行 二、Maven工程的目录结构 src/main/java&#xff1a;…

H5ke13-1浏览器处理异常

window对应的error没有event对象 window对应的error他接收三个参数,msg,url,行号 return false return true 1就不会返回错误 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>Title</title>&…

使用 MITRE ATTCK® 框架缓解网络安全威胁

什么是MITRE ATT&CK框架 MITRE Adversarial Tactics&#xff0c; Techniques&#xff0c; and Common Knowledge&#xff08;ATT&CK&#xff09;是一个威胁建模框架&#xff0c;用于对攻击者用来入侵企业、云和工业控制系统&#xff08;ICS&#xff09;并发起网络攻击…

探索人工智能领域——每日20个名词详解【day10】

目录 前言 正文 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da;。 &#x1f4e3;如需转载&#xff0c;请事先与我联系以…

正则表达式(5):常用符号

正则表达式&#xff08;5&#xff09;&#xff1a;常用符号 小结 本博文转载自 在本博客中&#xff0c;”正则表达式”为一系列文章&#xff0c;如果你想要从头学习怎样在Linux中使用正则&#xff0c;可以参考此系列文章&#xff0c;直达链接如下&#xff1a; 在Linux中使用正…

Leetcode刷题详解——最长湍流子数组

1. 题目链接&#xff1a;978. 最长湍流子数组 2. 题目描述&#xff1a; 给定一个整数数组 arr &#xff0c;返回 arr 的 最大湍流子数组的长度 。 如果比较符号在子数组中的每个相邻元素对之间翻转&#xff0c;则该子数组是 湍流子数组 。 更正式地来说&#xff0c;当 arr 的子…

简单易懂:Axios 如何取消请求的两种方法

在前端开发中&#xff0c;网络请求是非常常见的操作。而有时候&#xff0c;我们可能需要在发送请求后取消它&#xff0c;比如用户在请求还未完成时离开了当前页面或者执行了其他操作&#xff0c;本文将介绍如何在使用 Axios 发送请求时取消这些请求。 基本概念 在 Axios 中&am…

WindowsServer服务器系列:定时备份 MySQL

一、编写脚本 echo 取日期、时间变量值 set yy%date:~0,4% set mm%date:~5,2% set dd%date:~8,2% if /i %time:~0,2% lss 10 set hh0%time:~1,1% if /i %time:~0,2% geq 10 set hh%time:~0,2% set mn%time:~3,2% set ss%time:~6,2% set date%yy%%mm%%dd% set time%hh%%mn%%ss…

DAPP开发【10】express.js的使用

Express.js 是一种流行、轻量级的开源 Web 应用程序框架&#xff0c;用于开发基于 Node.js 的服务器端 Web 应用程序。它提供了强大的功能集&#xff0c;适用于 Web 和移动应用程序。Express.js 旨在支持单页、多页和混合式 Web 应用程序的开发。Express.js 提供了广泛的功能&a…

2022年第十一届数学建模国际赛小美赛A题翼龙如何飞行解题全过程文档及程序

2022年第十一届数学建模国际赛小美赛 A题 翼龙如何飞行 原题再现&#xff1a; 翼龙是翼龙目中一个已灭绝的飞行爬行动物分支。它们存在于中生代的大部分时期&#xff1a;从三叠纪晚期到白垩纪末期。翼龙是已知最早进化出动力飞行的脊椎动物。它们的翅膀是由皮肤、肌肉和其他组…