视频基础学习六——视频编码基础三(h264框架配合图文+具体抓包分析 万字)

系列文章目录

视频基础学习一------色立体、三原色以及像素
视频基础学习二------图像深度与格式(RGB与YUV)
视频基础学习三------视频帧率、码率与分辨率
视频基础学习四------视频编码基础一(冗余信息)
视频基础学习五------视频编码基础二(编码参数帧、GOP、码率等)
视频基础学习六------视频编码基础三(h264框架配合图文+具体抓包分析 万字)

文章目录


前言

上一篇文章中有提到视频编码有两个目的:1.压缩视频体积 2.提高对于网络传输的亲和性。而h264作为当下最流行的编码器,也是本人一直在学习的。

本章内容就是结合笔者自己抓包和阅读文章,根据实际包结合深入刨析h264数据流的格式是怎样的,这篇文章会很长。

|版本声明:山河君,未经博主允许,禁止转载


一、h264架构

1.h264裸流

首先,需要知道的是h264裸流是由一个个NALU(Network Abstract Layer Unit)单元组成,就像是这样:

那么为什么需要NALU?

NALU又是什么东西?

NALU长什么样子?

这个就需要我们对h264的架构有一定的了解,相信接下来的内容能够回答这三个问题。

2.h264架构

我们不止一次提到过,h264编码目的地是为了高效压缩视频和亲和网络。为了完成这两个目标,h264将系统框架分为了两个层面:【视频编码层面(VCL)】、【网络抽象层面(NAL)】,如下图:

  • 视频编码层(VCL) :是对视频编码核心算法过程、子宏块、宏块、片等概念的定义。这层主要是为了尽可能的独立于网络来高效的对视频内容进行编码。编码完成后,输出的数据是 SODB(String Of Data Bits)

  • 网络适配层(NAL) ,是对图像序列、图像等片级别以上的概念的定义。这层负责将 VCL 产生的比特字符串适配到各种各样的网络和多元环境中。该层将 VCL 层输出的 SODB 数据打包成 RBSP(Raw Byte Sequence Payload)

总的来说,VLC负责表示视频数据内容,NAL负责格式化数据并提供头信息,保证数据适合各种存储和信道上的传输

VCL

其实在上一篇文章有说过,现在重新复习一下:

一帧是由若干个片(Slice)组成,一个片是由若干个宏块组成,而每一个宏块的大小是16x16的数据,一个宏块又可以分为1616,16 8,816,8 8,84,4 8,4*4,等大小不等的子块,具体关系如同下图:

扩展:

H264宏块是16x16的,H265宏块会根据宏块的变化幅度来进行宏块划分。而H265的编码单位可以选择从最小的8x8到最大的64x64。

H265压缩率相对于H264压缩率理论上要少百分之50。

NALU

一个视频帧可以被分成诺干个Slice,也就会产生对应的NALU。而一个NALU的组成如同下图:

  • StartCode:通常为0x000001或者0x00000001(一个完整帧产生多个Slice时使用三字节起始码,否则是四字节),表示这是一个NALU单元的开始,直到遇到下一个StartCode结束。
  • NAL Unit header:占用一字节,用来表示NALU类型
  • NAL Unit Body:可能是包含SPS、PPS、SEI、RBSP(Raw Byte Sequence Payload)等等其中一种信息,可以根据header获知。

二、NALU深入了解

上文说过NAL负责格式化数据并提供头信息,现在就来看看NAL层是如何提供头信息和格式化数据的。

1.NAL Unit header

NAL Unit header:NALU头信息,占用一字节,而这一字节的组成为

  • forbidden_bit(1bit) :禁止位,初始为0,占用NAL头的第一个位,当禁止位值为1时表示接收方需要纠错。
  • nal_reference_bit(2bits):表示优先级,取值越⼤,表示当前NAL越重要,需要优先受到保护。
  • nal_unit_type(5bits):表示当前NALU的类型有多少种

在这里引用网上做好的表格来介绍NAL的类型:

然后我们结合实际的抓包来进行分析其中几种类型

2.NAL Unit Body

上文说过,NAL单元的body会有多种,具体需要header进行判断,可能是SPS、PPS、SEI或者RBSP等,那么我们就以比较重要的几种先来看看

1)SPS、PPS

h264编码需要SPS和PPS中的信息才能进行解码,一般会在最开始进行发送一次,也有的会在每次I帧编出的时候发送,但是如果当编码器发生改变时候例如:视频分辨率改变,一定需要发送SPS和PPS信息。

SPS

那么SPS和PPS中到底有什么信息是解码必须要的呢?下面我们先看看SPS抓包后的信息

先分析第一个NAL unit ,对照NAL Unit header

6764001facd9405005bb011000000300100000030320f1831960取第一个字节

0x67:01100111

forbidden_bitL: 0 值为0

nal_reference_bit:11 值为3,表示受到最高保护

nal_unit_type:00111 值为7,对照表格可知为SPS(序列参数集)

下面是对整个SPS的解析

bash 复制代码
NAL unit: 6764001facd9405005bb011000000300100000030320f1831960
    0... .... = Forbidden_zero_bit: 0
    .11. .... = Nal_ref_idc: 3 #受到最高保护
    ...0 0111 = Nal_unit_type: Sequence parameter set (7) #对照nal_unit_type为sps
    Profile_idc: High profile (100)
    0... .... = Constraint_set0_flag: 0
    .0.. .... = Constraint_set1_flag: 0
    ..0. .... = Constraint_set2_flag: 0
    ...0 .... = Constraint_set3_flag: 0
    .... 0... = Constraint_set4_flag: 0
    .... .0.. = Constraint_set5_flag: 0
    .... ..00 = Reserved_zero_2bits: 0
    Level_id: 31 [Level 3.1 14 Mb/s] #码率等级
    1... .... = seq_parameter_set_id: 0 #序列参数集标志
    .010 .... = chroma_format_id: 1
    .... 1... = bit_depth_luma_minus8: 0
    .... .1.. = bit_depth_chroma_minus8: 0
    .... ..0. = qpprime_y_zero_transform_bypass_flag: 0
    .... ...0 = seq_scaling_matrix_present_flag: 0
    1... .... = log2_max_frame_num_minus4: 0
    .1.. .... = pic_order_cnt_type: 0
    ..01 1... = log2_max_pic_order_cnt_lsb_minus4: 2
    .... .001  01.. .... = num_ref_frames: 4
    ..0. .... = gaps_in_frame_num_value_allowed_flag: 0
    ...0 0000  0101 0000 = pic_width_in_mbs_minus1: 79 #宽
    0000 0101  101. .... = pic_height_in_map_units_minus1: 44 #长
    ...1 .... = frame_mbs_only_flag: 1
    .... 1... = direct_8x8_inference_flag: 1
    .... .0.. = frame_cropping_flag: 0
    .... ..1. = vui_parameters_present_flag: 1
    .... ...1 = aspect_ratio_info_present_flag: 1
    0000 0001 = aspect_ratio_idc: 1
    0... .... = overscan_info_present_flag: 0
    .0.. .... = video_signal_type_present_flag: 0
    ..0. .... = chroma_loc_info_present_flag: 0
    ...1 .... = timing_info_present_flag: 1
    .... 0000  0000 0000  0000 0000  0000 0011  0000 .... = num_units_in_tick: 48
    .... 0000  0001 0000  0000 0000  0000 0000  0000 .... = time_scale: 16777216
    .... 0... = fixed_frame_rate_flag: 0
    .... .0.. = nal_hrd_parameters_present_flag: 0
    .... ..1. = vcl_hrd_parameters_present_flag: 1
    .... ...1 = cpb_cnt_minus1: 0
    0000 .... = bit_rate_scale: 0
    .... 0011 = cpb_size_scale: 3
    0010 0... = bit_rate_value_minus1: 3
    .... .000  1111 .... = cpb_size_value_minus1: 14
    .... 0... = cbr_flag: 0
    .... .001  10.. .... = initial_cpb_removal_delay_length_minus1: 6
    ..00 001. = cpb_removal_delay_length_minus1: 1
    .... ...1  0001 .... = dpb_output_delay_length_minus11: 17
    .... 1001  0... .... = time_offset_length: 18
    .1.. .... = low_delay_hrd_flag: 1
    ..1. .... = pic_struct_present_flag: 1
    ...0 .... = bitstream_restriction_flag: 0
    .... 0... = rbsp_stop_bit: 0
    .... .000 = rbsp_trailing_bits: 0
长宽

从上面我们看到,有两个重要的参数------ pic_width_in_mbs_minus1: 79 和pic_height_in_map_units_minus1: 44

通过这两个参数我们就可以算出视频的长宽

width = 16 * (pic_width_in_mbs_minus1 + 1) = 1280

height = 16 * (pic_height_in_map_units_minus1 + 1) = 720

是16的原因是在h264中一个宏块的长宽就是16

压缩级别

Profile_idc: High profile (100)

标识当前H.264码流的profile。H.264中定义了三种常用的档次profile:

  • 基准档次:baseline profile;
  • 主要档次:main profile;
  • 扩展档次:extended profile;
    新版中,还包括igh、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等
序列参数集

seq_parameter_set_id:表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。

PPS

接着来看看PPS中的信息

现在看第二个NAL unit ,对照NAL Unit header

68ebe3cb22c0取第一个字节

0x68:01101000

forbidden_bitL: 0 值为0

nal_reference_bit:11 值为3,表示受到最高保护

nal_unit_type:00111 值为8,对照表格可知为PPS(图像参数集)

再来看一下具体内容

bash 复制代码
NAL unit: 68ebe3cb22c0
    0... .... = Forbidden_zero_bit: 0
    .11. .... = Nal_ref_idc: 3 #受到保护最高
    ...0 1000 = Nal_unit_type: Picture parameter set (8) #对照nal_unit_type为pps
    1... .... = pic_parameter_set_id: 0
    .1.. .... = seq_parameter_set_id: 0
    ..1. .... = entropy_coding_mode_flag: 1
    ...0 .... = pic_order_present_flag: 0
    .... 1... = num_slice_groups_minus1: 0
    .... .011 = num_ref_idx_l0_active_minus1: 2
    1... .... = num_ref_idx_l1_active_minus1: 0
    .1.. .... = weighted_pred_flag: 1 #是否加权预测
    ..10 .... = weighted_bipred_idc: 2 #隐式预测
    .... 0011  1... .... = pic_init_qp_minus26(se(v)): -3
    .1.. .... = pic_init_qs_minus26: 0
    ..00 101. = chroma_qp_index_offset(se(v)): -2
    .... ...1 = deblocking_filter_control_present_flag: 1
    0... .... = constrained_intra_pred_flag: 0
    .0.. .... = redundant_pic_cnt_present_flag: 0
    ..1. .... = transform_8x8_mode_flag: 1
    ...0 .... = pic_scaling_matrix_present_flag: 0
    .... 0010  1... .... = second_chroma_qp_index_offset(se(v)): -2
    .1.. .... = rbsp_stop_bit: 1
    ..00 0000 = rbsp_trailing_bits: 0
pic_parameter_set_id

表示当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值。该值的取值范围为[0,255]

seq_parameter_set_id

表示当前PPS所引用的激活的SPS的id。通过这种方式,PPS中也可以取到对应SPS中的参数。该值的取值范围为[0,31]。

weighted_pred_flag

标识位,表示在P/SP slice中是否开启加权预测。

weighted_bipred_idc

表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测。

扩展(图像与序列参数集)

SPS称为序列参数集,PPS称为图像参数集,那么到底什么是参数集?参数集又有什么作用?

前面说过,帧就是对于Slice的编码,在一段视频中,有些编码参数像上述的加权、长宽等等都是不变的,这些就形成了序列参数集,而图像参数集就是对于序列参数集中的参数做了引用,看起来关系如下:

2)SEI

我们先看一下抓包的情况

SEI实际上是对于编码参数的一种扩展,从抓包中数据分析

0x06:00000110

forbidden_bitL: 0 值为0

nal_reference_bit:00 值为3,表示不受保护

nal_unit_type:00110 值为6,对照表格可知为SEI

从nal_reference_bit中不难看出,这种类型的包基本不受保护,也就是说,实际上该包丢弃之后,依旧可以正常解码,所以这里不过多赘述。

3)IDR和非IDR

从nal_unit_type表格中可以看出,type为1, 2, 3, 4, 5及12的NAL单元保存的是VCL数据,也被称为VCL的NAL单元。

但是通常来说,一张图片经过编码压缩后依旧占据一定体积,所以就需要分为多个NAL进行发送,也被称为切片(FU-A),而切片后的数据nal_unit_type为28也就是自定义数据。

例如:

从上图可以看到实际一帧被分为了多个FU-A,并不是一个NAL单元就拥有一张图片

分析一下7C

0x7C:01111100

forbidden_bitL: 0 值为0

nal_reference_bit:11 值为3,表示受到最高保护

nal_unit_type:11100 值为28,对照表格可知为自定义数据

三、FU-A分片

1.分片

当视频帧大小超过一定范围(MTU ),则h264中视频帧将会进行分片处理,其实分片处理特别简单,如下图:

  • StartCode:------FU-A包中还是叫做StartCode
  • NAL Unit header------改名叫做FU indicator,只不过nal_unit_type一定是28
  • NAL Unit Body------被分为FU header和playload两部分

FU Header

总体大小为1字节,其中:

  • 第一位:Start标记位,为1表示分片起始位置
  • 第二位:end标记为,为1表示分片结束位置
  • 第三位:保留
  • 第四~八位:type,和 nal_unit_type表格一样解析

下面我们看一下I帧起始和结束实际包样子

起始帧

bash 复制代码
H.264
    FU identifier
        0... .... = F bit: No bit errors or other syntax violations
        .11. .... = Nal_ref_idc (NRI): 3 #受到保护权重最高
        ...1 1100 = Type: Fragmentation unit A (FU-A) (28) #nal_unit_type为28
    FU Header
        1... .... = Start bit: the first packet of FU-A picture #首帧开始
        .0.. .... = End bit: Not the last packet of FU-A picture #结束位为0
        ..0. .... = Forbidden bit: 0
        ...0 0101 = Nal_unit_type: Coded slice of an IDR picture (5) #对照nal_unit_type是IDR帧
    H264 NAL Unit Payload
        1... .... = first_mb_in_slice: 0
        .000 1000 = slice_type: I (I slice) (7)
        1... .... = pic_parameter_set_id: 0
        [Not decoded yet]

结束 帧

bash 复制代码
H.264
    FU identifier
        0... .... = F bit: No bit errors or other syntax violations
        .11. .... = Nal_ref_idc (NRI): 3 #受到最高保护
        ...1 1100 = Type: Fragmentation unit A (FU-A) (28)#nal_unit_type 为自定义
    FU Header
        0... .... = Start bit: Not the first packet of FU-A picture #起始标志为0
        .1.. .... = End bit: the last packet of FU-A picture #结束标志
        ..0. .... = Forbidden bit: 0
        ...0 0101 = Nal_unit_type: Coded slice of an IDR picture (5) #nal_unit_type为IDR

2.SODB和RBSP

上面说过,VCL层对于数据进行编码,编码输出的数据是SODB(String Of Data Bits),然后经过映射到NAL为RBSP(Raw Byte Sequence Payload)。

想要了解映射过程可以看一下,不过也可以不用关注:

如果SODB的内容是空的,那么RBSP的内容也是空的。否则,RBSP的第一个字节取自SODB的第1到第8个比特,RBSP字节内部按照从左到右从高到低的顺序排列。以此类推,RBSP中的每个字节都直接取自SODP的相应比特。RBSP的最后一个字节包含SODB的最后几个比特,以及trailing bits。其中,trailing bits的第一个比特为1,其余的比特为0,保证字节对齐。所以RBSP就等于,SODB在它的最后一个字节的最后一个比特后,紧跟值为1的1个比特,然后增加若干比特的0,以补齐这个字节。

从网上找的图片以供参考


总结

这是一篇很长的文章,也是对于视频流媒体入门的最后一篇文章,我希望能够对于一些刚进去音视频领域的朋友能够进行更好的梳理,能够提供一些帮助。

那么还是那句话

如果对您有所帮助,请帮忙点个赞吧!

相关推荐
sakabu1 小时前
ESP32 外设驱动开发指南 (ESP-IDF框架)——GPIO篇:基础配置、外部中断与PWM(LEDC模块)应用
笔记·单片机·学习·esp32
代码哈士奇1 小时前
VitePress学习笔记
javascript·笔记·学习
小眼睛FPGA1 小时前
【盘古100Pro+开发板实验例程】FPGA学习 | 基于紫光 FPGA 的键控 LED 流水灯
科技·学习·ai·fpga开发·fpga
天才少女爱迪生1 小时前
pytorch的自定义 CUDA 扩展怎么学习
人工智能·pytorch·学习
不可描述的两脚兽2 小时前
学习笔记《区块链技术与应用》第4天 比特币脚本语言
笔记·学习·区块链
sukalot3 小时前
window显示驱动开发—Direct3D 11 视频播放改进
驱动开发·音视频
明长歌4 小时前
【javascript】Reflect学习笔记
javascript·笔记·学习
超浪的晨4 小时前
Maven 与单元测试:JavaWeb 项目质量保障的基石
java·开发语言·学习·单元测试·maven·个人开发
ls_qq_26708134704 小时前
cocos打包web - ios设备息屏及前后台切换音频播放问题
前端·ios·音视频·cocos-creator
MingYue_SSS6 小时前
【未解决】STM32无刷电机驱动电路问题记录
笔记·嵌入式硬件·学习