H.264视频流分析工具
(1)SpecialVH264
软件:
链接:https://pan.baidu.com/s/1O6UL_WDzp5zYukJvfhvixg
提取码:ooid
源代码:
链接:https://pan.baidu.com/s/1TsOwBwezLTXvEsYUfEzauA
提取码:49k1
(2)基于上面的一个软件,可以同时显示二进制,
软件:
链接:https://pan.baidu.com/s/1MeaMXl1qrDN83oOWH174yA
提取码:38vv
源代码:
链接:https://pan.baidu.com/s/19hhWwjejpP1_hRzicbs8dA
提取码:05hz
其他视音频编解码分析器:
H.264说明文档:https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC
一、H264的NAL单元详解
H.264 的功能分为两层,即视频编码层(VCL:Video Coding Layer)和网络提取层(NAL:Network Abstraction Layer)。
VCL 数据即编码处理的输出,它表示被压缩编码后的视频数据序列。在VCL 数据传输或存储之前,这些编码的VCL 数据,先被映射或封装进NAL 单元中。这样,高编码效率和网络友好性的任务分别由VCL和NAL来完成。
1、VCL只关心编码部分,重点在于编码算法以及在特定硬件平台的实现
(1)SODB 是VCL输出的是编码后的纯视频流信息,没有任何冗余头信息
2、NAL关心的是VCL的输出纯视频流如何被表达和封包以利于网络传输,
(1)RBSP 是通过SODB封装成nal_unit格式得到的,Nal_unit是一个通用封装格式,可以适用于有序字节流方式和IP包交换方式
具体封装流程:javascript:void(0)
(2)NALU 是针对不同的传送网络(电路交换|包交换),将RBSP 封装成针对不同网络的封装格式(加上NAL header)
3、之间关系:
SODB + RBSP trailing bits = RBSP
NAL header(1 byte) + RBSP = NALU
RTP封装格式(12个字节) + NALU = 最后sendto出去的完整包
RBSP就是H.264编码后出来的裸流文件,给文件加上后缀.h264,得到xxx.h264
2、RBSP的内容
如何判断帧类型(是图像参考帧还是I、P帧等)?
nal_unit_type | NAL类型 | C |
0 | 未使用 | |
1 | 不分区、非 IDR 图像的片 | 2, 3, 4 |
2 | 片分区 A | 2 |
3 | 片分区 B | 3 |
4 | 片分区 C | 4 |
5 | IDR 图像中的片 | 2, 3 |
6 | 补充增强信息单元(SEI) | 5 |
7 | 序列参数集 (SPS) | 0 |
8 | 图像参数集 (PPS) | 1 |
9 | 分界符 | 6 |
10 | 序列结束 | 7 |
11 | 码流结束 | 8 |
12 | 填充 | 9 |
13..23 | 保留 | |
24..31 | 未使用 | |
我们还是接着看最上面图的码流对应的数据来层层分析,以00 00 00 01分割之后的下一个字节就是NALU类型,将其转为二进制数据后,解读顺序为从左往右算,如下:
(1)第1位禁止位,值为1表示语法出错
(2)第2~3位为参考级别
(3)第4~8为是nal单元类型
例如上面00000001后有67,68以及65
其中0x67的二进制码为:0110 0111
4-8为00111,转为十进制7,参考第二幅图:7对应序列参数集SPS
其中0x68的二进制码为:0110 1000
4-8为01000,转为十进制8,参考第二幅图:8对应图像参数集PPS
其中0x65的二进制码为:0110 0101
4-8为00101,转为十进制5,参考第二幅图:5对应IDR图像中的片(I帧)
3、序列 sequence
(1) 一段h.264的码流其实就是多个sequence组成的
(2)一个sequence是一秒,如果FPS等于30,就有30帧图像(I/P/B帧)
(3)每个sequence均有固定结构单元:1sps+1pps+1sei+1I帧+若干p帧(加上B帧一共有6种单元情况)
分隔符
(1) H.264在编码的时候,生成一个序列时,序列中每个单元前面就会加上00 00 00 01作为分隔符
分隔符后第一个字节
(1)分隔符后面紧跟着的第一个字节就是用来判断是什么类型的单元
1、第1位禁止位,值为1表示语法出错
2、第2~3位为参考级别
3、第4~8为是nal单元类型nal_unit_type(比如是0x67(取5位00111表示sps)
播放器算法解码时,就知道这个字节开始到下一个分隔符之间的数据按照sps类型解析数据
单元之一:sps(序列参数集:固定14个字节)
(1)序列参数集。SPS中保存了一组编码视频序列(Coded video sequence)的全局参数,所谓的编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中
(2)里面包含宏块编码方式、图像大小尺寸、宏块个数,播放器通过这些参数,调用播放器里面的对应算法去解码
单元之二:pps(图像参数集:固定4个字节)
单元之三:SEI
(1)也是一些图像的额外信息,帮助播放器解析压缩图像
单元之四:I帧、P帧、B帧
(1)I帧:帧内编码帧,I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面),I帧大,说明本身压缩比不高,图像数据更完整,则P帧可以越小,反之I帧越小则P帧会越大
(2)P帧:前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
(3)B帧:双向预测内插编码帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别
(4)IDR帧:一个序列的第一个图像叫做 IDR 图像(立即刷新图像), IDR 图像都是 I图像。 H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果在前一个序列的传输中发生重大错误,如严重的丢包,或其他原因引起数据错位,在这里可以获得重新同步。 IDR 图像之后的图像永远不会引用 IDR 图像之前的图像的数据来解码。
要注意 IDR 图像和 I 图像的区别, IDR 图像一定是 I 图像, 但 I 图像不一定是 IDR 图像。一个序列中可以有很多的 I 图像, I 图像之后的图像可以引用 I 图像之间的图像做运动参考。
宏块、片: 一个编码图像通常划分成若干宏块组成,一个宏块由一个16×16亮度像素和附加的一个8×8 Cb和一个8×8 Cr彩色像素块组成。每个图象中,若干宏块被排列成片的形式。
CBR(固定码率)和VBR(可变码率)(压缩时候一个sequence参考的码率方式)
(1)一个sequence有30个帧,每个帧就是1/30秒,压缩后的字节数不一样,2000到10000+不等,主要看图像动作是否很大
(2)CBR就是牺牲图像清晰度,保证每帧图像字节数变化不会很大,这样码率就能均衡,网络传输也能稳定
(3)VBR就是牺牲码率,动作变化大的时候,参考VBR,帧数据会很大,保证清晰度,不管码率是否稳定,这样在网络传输的时候,网络带宽有限,带宽给的宽,资源限制,给的小,大的帧传输不完,比如I帧,网络就会不稳定
在雷神的H.264分析器中可以判断I,P,B帧
对应的代码为:
//在 debug_slice_header 函数中
printf("======= Slice Header =======\n");
printf(" first_mb_in_slice : %d \n", sh->first_mb_in_slice );
switch(sh->slice_type)
{
case SH_SLICE_TYPE_P : slice_type_name = "P slice"; break;
case SH_SLICE_TYPE_B : slice_type_name = "B slice"; break;
case SH_SLICE_TYPE_I : slice_type_name = "I slice"; break;
case SH_SLICE_TYPE_SP : slice_type_name = "SP slice"; break;
case SH_SLICE_TYPE_SI : slice_type_name = "SI slice"; break;
case SH_SLICE_TYPE_P_ONLY : slice_type_name = "P slice only"; break;
case SH_SLICE_TYPE_B_ONLY : slice_type_name = "B slice only"; break;
case SH_SLICE_TYPE_I_ONLY : slice_type_name = "I slice only"; break;
case SH_SLICE_TYPE_SP_ONLY : slice_type_name = "SP slice only"; break;
case SH_SLICE_TYPE_SI_ONLY : slice_type_name = "SI slice only"; break;
default : slice_type_name = "Unknown"; break;
}
printf(" slice_type : %d ( %s ) \n", sh->slice_type, slice_type_name );
slice_type 表示片的类型