一、H264数据结构
一个原始的H.264 NALU 由一个接一个的 NALU 组成的,而它的功能分为两层,VCL(视频编码层)和 NAL(网络提取层).
VCL:包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码。
NAL:负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别。
组成:NALU (Nal Unit) = NALU头 + RBSP 在 VCL 数据传输或存储之前,这些编码的 VCL 数据,先被映射或封装进 NAL 单元(以下简称 NALU,Nal Unit) 中。每个 NALU 包括一个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)、一组 对应于视频编码的 NALU 头部信息。RBSP 的基本结构是:在原始编码数据的后面填加了结尾 比特。一个 bit“1”若干比特“0”,以便字节对齐。
一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成
如下所示:
iOS的硬编码器输出的就是一个一个NALU + pts + dts
StartCode : Start Code 用于标示这是一个NALU 单元的开始,必须是”00 00 00 01” 或”00 00 01”
NALU Header 下表为 NAL Header Type
RBSP :NAL包将其负载数据存储在 RBSP(Raw Byte Sequence Payload) 中,RBSP 是一系列的 SODB(String Of Data Bits)。
一帧图片跟NALU的关联:
一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了。
注意:片(slice)的概念不同与帧(frame),帧(frame)是用作描述一张图片的,一帧(frame)对应一张图片,而片(slice),是 H.264 中提出的新概念,是通过编码图片后切分通过高效的方式整合出来的概念,一张图片至少有一个或多个片(slice)。
片的类型:
- I片:只包 I宏块,I 宏块利用从当前片中已解码的像素作为参考进行帧内预测(不能取其它片中的已解码像素作为参考进行帧内预测)。
- P片:可包 P和I宏块,P 宏块利用前面已编码图象作为参考图象进行帧内预测,一个帧内编码的宏块可进一步作宏块的分割:即 16×16、16×8、8×16 或 8×8 亮度像素块(以及附带的彩色像素);如果选了 8×8 的子宏块,则可再分成各种子宏块的分割,其尺寸为 8×8、8×4、4×8 或 4×4 亮度像素块(以及附带的彩色像素)。
- B片:可包 B和I宏块,B 宏块则利用双向的参考图象(当前和 来的已编码图象帧)进行帧内预测。
- SP片(切换P):用于不同编码流之间的切换,包含 P 和/或 I 宏块
- SI片:扩展档次中必须具有的切换,它包含了一种特殊类型的编码宏块,叫做 SI 宏块,SI 也是扩展档次中的必备功能。
以上为H264流的数据结构定义,下面阐述I帧、P帧、B帧的关系
二、I帧、P帧、B帧
- I帧:帧内编码帧(intra picture),I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一帧,经过适度地压缩,作为随机访问的参考点可以当成静态图像。I帧可以看做一个图像经过压缩后觉得产物,I帧压缩可以得6:1的压缩比而不会产生任何可觉察的模糊现象。I帧压缩可去掉视频的空间冗余信息,下面即将介绍P帧和B帧是为了去掉时间冗余信息。
- P帧:前向预测编码在帧(predictive-frame),通过将图像序列中前面已编码帧的时间冗余信息去充分去除压缩传输数据量的编码图像,也成为预测帧。
- B帧:双向预测内插编码帧(bi-directionalinterpolated prediction frame),既考虑源图像序列前面的已编码帧,又估计源图像序列后面的已编码帧之间的时间冗余信息,来压缩传输数据量的编码图像,也成为双向预测帧。
基于上面的定义,我们可以从解码的角度来理解IBP帧。
- I帧自身可以通过视频解压算法解压成一行单独的完善的完整视频画面,所以I帧去掉视频帧在空间维度上的冗余信息。
- P帧需要参考其前面一个I帧或者P帧来解码成一张完整的视频画面。
- B帧则需要参考前一个I帧或者P帧及其后面一个P帧来生成后面一张完整的视频画面,所以P帧与B帧去掉是视频在时间维度上的冗余信息。
I帧并不是IDR帧,H264中的IDR帧(instantaneous decoding refresh picture),I帧之后P帧有可能会参考之前的I帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR就是一个特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR帧,而不会参考前面的帧。在解码器中,一旦接收到一个IDR帧,就会立即清理参考帧缓存区,并将IDR帧作为被参考的帧。
三、GOP、pts、dts 、帧的排列顺序
一个Gop一般由一个IDR帧和多个B帧和P帧组成。DTS是数据准备解码的时间、PTS是这一帧图像呈现的时间。
因为B帧存在的关系,会导致B帧的pts和dts不一致的情况,也就是解码器为了解码出一个B帧,必须等待到下一个P帧或者I帧出现。
比如:
上面的图中,从左到右为时间序输入的图像,最终展示也应当按照这个顺序进行渲染,IBBBPBBBPBBBP...
因为中间的B帧依赖后续的P帧才能解码,所以第2个B帧必须等到第5个P帧出现才能解码,因此会导致解码的延时,这也是大部分直播中的H264流关闭了B帧的原因。
那如果RTMP中真实存在B帧会是什么情况呢?
首先,直播中不断按照时间序列向编码器输入单帧图像,如下图所示:
也就是,最终编码器输出的顺序是按照dts输出的,不仅如此,所有视频的文件存放顺序都是按照dts的顺序存放的。
实际解码之后,在输入IPBBP之后,第一个P帧输入之后,解码器不会输出,输入第二个P帧之后,会先输出中间两个B帧,然后再输出P帧
这也是某些编码器解码器编码解码API是异步的原因,ffmpeg是同步的API,因此会出现在合格问题:
四、RTMP中如何兼容B帧
因为RTMP发送的是H264的NALU单元,实际上RTMP的帧类型中,只是区分了IDR帧和非IDR帧,非IDR帧中包含P帧和B帧
那怎么区分P帧和B帧呢,需要进入到NALU中SLICE的header中进行区分,所以RTMP协议可以不用关系到底是否包含B帧
但是,B帧的存在影响到RTMP中的cts时间,因为dts在RTMP中单调递增,pts一定大于dts,因此RTMP中用一个字段表示pts(H264本身没有标记pts)
rtmp视频包在h264的包前边再添加了9个Byte.具体内容填写参考srs的代码 SrsRawH264Stream::mux_ipb_frame 和 SrsRawH264Stream::mux_avc2flv
这个是ipb贞的编码.
buf[0] = 贞类型和编码. ( (1|2)<<4 | 7 ) 1是关键帧 2是P/B贞 7代表h264
buf[1] = 数据类型 ( 1 = NALU )
buf[2-4] = cts = pts – dts;
buf[5-8] = NAL的长度,不包括贞分隔符
buf[9-] = H264的RAW数据. 不包含分隔符,00 00 01 或者 00 00 00 01
buf[9+NAL_length- +4byte] = 下一个NAL长度 这个很重要, 一个rtmp的视频包可能包括多个NAL的, 切记转换TS的时候把它分割并添加NAL的分隔符,例如{0×00,0×00,0×01}等。
buf[9+NAL_length+4 -] = 下一个H264的RAW数据.
……
这里需要注意的是cts这个值,如果视频流没有B侦的话,这个cts值=0 如果,这个cts值是一个动态数字,你需要根据这个值去计算pts。
五、参考
https://juejin.im/post/5a8fe66b6fb9a0633e51eadc
https://blog.csdn.net/wzw88486969/article/details/62229133