H264 视频流的解析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了H264 视频流的解析相关的知识,希望对你有一定的参考价值。
参考技术A 以最流行的H.264编码为例,对于视频流的解析要了解视频帧在RTP包中作为荷载是如何承载和存放的。先回顾一些基础的背景知识,再写一个小程序来分析RTP 视频流。视频就是一幅幅图片以每秒几十幅的速度播放,这些图片称帧(frame), 播放速度称为帧率(FPS - Frame Per Second)
准确地说,视频是由一系列图像组成的动作序列,并且该序列中的每个图像都将在要显示的动作序列的时间轴中接替前一个图像。 这些静止图像称为视频帧。每个视频帧之间的时间差越小,刷新率就越高,并且视频中的运动表现得越自然。
现代视频编码将这些帧分为三类
信息帧用帧内压缩,用作关键帧
预测帧 Predictive Frame 用帧间压缩,反映之前的 I-frame 的变化
双向预测帧 Bidirectional Predictive Frames 使得总体压缩更高, 它参考了之前的 I-frame 和之后的 P-frame
访问单元序列,按解码顺序由瞬时解码刷新 (IDR) 访问单元和零个或多个非 IDR 访问单元组成,包括所有后续访问单元,直到但不包括任何后续 IDR 访问单元。
主要编码图片为 IDR 图片的访问单元。
仅包含 I 或 SI 类型的slice 的编码图片,这会在解码过程中导致“重置”。 在对 IDR 图片进行解码之后,可以根据在 IDR 图片之前解码的任何图片,在没有帧间预测的情况下,对按照解码顺序的所有后续编码图片进行解码。
解码过程使用的图片的编码表示,用于符合 H.264 的比特流。
主要编码图片包含图片的所有宏块。
图片或其中一部分的编码表示。 对符合 H.264 的比特流的解码过程不应使用冗余编码图片的内容。 冗余编码图片的内容可由解码过程用于包含错误或损失的比特流。
用于指代编码切片(slice)和编码数据分区(data partition) 的 NAL 单元的统称。
基于以上的概念, H.264 将这些视频帧进行了分组,这些组称为 GOP(Group of Picture), 在这些组中的第一个视频帧通常都是 I-Frame
以一个视频通信的应用程序为例,视频编码程序会将采样的视频图片从 RGB 格式转为 YUV 格式,再将它们打包为 RTP packet ,如下图所示
让我们从外而内,看看视频 RTP 包的结构
针对 H264视频帧,RTP 头中的某些字段有如下设置
为 RTP 包中时间戳所指示的访问单元的最后一个数据包中设置 marker=1,这样可以用来进行有效的播放缓冲区处理。
在FU-A中的 marker 设定为只有最后一包才会设定 marker=1,其它则为 0
根据 RFC 3550 的定义来设置和使用,对于单NALU和非交错打包模式,序列号用于确定NALU的解码顺序。
RTP 时间戳设置为视频内容的采样时间戳, 必须使用 90 kHz 时钟频率。
例如 H264的采样率为 90khz, 帧率 frame rate =15 那么每个包的时间戳 Timestamp 的步长约为 90000/15 = 6000
RTP 包的荷载中包含 H.264 中的视频流内容,也就是 NAL 网络抽象层
NAL Unit 是 header 和 playload 组成的
NAL Unit Header 就是一个字节,格式如下:
forbidden_zero_bit: 一个比特, H.264 规范将值 1 声明为语法违反规范。
NRI 两个比特,即 nal_ref_idc 称 NAL 参考索引
值 00 表示 NAL 单元的内容不用于重建用于图片间预测的参考图片,此类 NAL 单元可以丢弃,而不会危及参考图片的完整性。
大于 00 的值表示需要对 NAL 单元进行解码以保持参考图片的完整性。
例如:
H.264 规定如下的 NAL unit type , 其 NRI 必须为 0
NAL 类型以 5 个比特来表示
为适合于通过 RTP 协议在网络上传输, H264 的视频包大体上分为三种:
在 RFC6184 中有如下规定
在视频会议中,一般打包模式选择为非交错模式,会使用下面三种单元,不使用 STAP-B, FU-B 和 MTAP 单元
如果某帧较大不能单独打包,但是该帧内部单独的 NALU 比较小,可以使用STAP 方式合并多个NALU打包发送,但是这些 NALU 的时间戳必须一致,打包后的 RTP 时间戳也必须一致,一个 STAP 单元包含多个子单元,每个子单元之前会有一个 NAL unit size 来指明这个子单元的长度, 这里只以 STAP-A 为例, STAP-B 也就是多了一个 DON(Decoding Order Number)
例如下面的例子
如果一个视频帧的 NALU 过大(超过MTU)需要拆分成多个包,可以使用 FU 方式来拆分并打到不同的RTP包里,那么这几个包的RTP时间戳是一样的;
以 FU-A 为例 (FU-B 的区别就是多了一个 DON), 如图所示
其中 FU indicator 字节的格式如下
它就是一个普通的 NAL header, 只是它的 type 等于 28(FU-A) 或 29 (FU-B)
其后的 FU header 字节的格式如下
含义如下
当设置为 1 时,Start 位指示分片 NAL 单元的开始。 当随后的 FU 有效载荷不是分片 NAL 单元有效载荷的开始时,起始位设置为零。
当设置为 1 时,End 位表示分片 NAL 单元的结束,即有效载荷的最后一个字节也是分片 NAL 单元的最后一个字节。 当随后的 FU 有效载荷不是分片 NAL 单元的最后一个片段时,结束位设置为零。
实际被分片的 NAL 单元的类型,参见 RFC6184 的 Table 7-1
在 https://github.com/cisco/openh264 中有相关结构和类型的详细定义, 至于聚合包和分片包,openh264 中本身并未定义
在 webrtc 的 video_coding 中有相关的定义
third_party/webrtc/modules/video_coding/codecs/h264/include/h264_globals.h
我自己也写了一个小例子,演示如下
其中用到的一个工具类 rtputil 源码就不贴了,参见
编译运行
其中用到的一个工具类 MediaUtil 源码就不贴了,参见
编译运行
运行结果如下,
H.264/AVC视频编解码技术详解二十二熵编码:语法元素的CABAC解析
《H.264/AVC视频编解码技术详解》视频教程已经在“CSDN学院”上线,视频中详述了H.264的背景、标准协议和实现,并通过一个实战工程的形式对H.264的标准进行解析和实现,欢迎观看!
“纸上得来终觉浅,绝知此事要躬行”,只有自己按照标准文档以代码的形式操作一遍,才能对视频压缩编码标准的思想和方法有足够深刻的理解和体会!
链接地址:H.264/AVC视频编解码技术详解
GitHub代码地址:点击这里
在本系列的博文18中,我们讨论了算术编码的基本原理,以及实现一个简单的算术编码器内核的方法:
而在博文19和21中我们根据H.264对于CABAC的规定,讨论了语法元素二值化以及上下文概率模型的相关算法:
通过前面的研究我们已经了解,由于视频数据特殊的统计特性和对编码性能的高指标要求,CABAC的算法远远比博文18中描述的基本模型更加复杂。尽管如此,CABAC在算术编码的最核心层面依然遵循了博文18中所讲的根本原理,这一点在本文对标准协议文档的解读中也可以获得明确的证实。
一、CABAC解析(解码)的总流程
CABAC的解码过程定义与标准协议文档的9.3.3.2节。CABAC解码过程所需要的输入数据包括前一章所推导出的上下文模型索引ctxIdx、旁路模式标志bypassFlag,以及解码器引擎的状态。下图表示了CABAC解码过程的概念框图:
在上图中可以看出,根据参数的不同,CABAC解码总共可能有三种运行流程:
- DecodeBypass: 当旁路模式标志bypassFlag为1时执行,表示旁路模式的解析过程;
- DecodeDecision: 当bypassFlag为0且ctxIdx不为276时执行,表示CABAC解析过程;
- DecodeTerminate: 当bypassFlag为0且ctxIdx==276时执行,表示解析终结码的过程;
在这三种执行流程中最重要的就是第二种——DecodeDecision。接下来本文将从DecodeDecision开始分析这三种解析过程。
二、CABAC算术编码的解码(解析)过程
本节描述的即是DecodeDecision部分的方法。这一部分也是CABAC算法中最为重要的部分,H.264的main、high profile中的多数语法元素都是通过该过程进行解析的。该部分在标准协议文档的9.3.3.2.1中定义。
从标准协议中的该节内容中可知,该过程所需要的数据有ctxIdx, codIRange 和 codIOffset,其中ctxIdx由前一章《CABAC的上下文概率模型》中的方法推导,codIRange 和 codIOffset的初始值由CABAC解码器引擎的初始化确定。输出的数据包括解析出来的语法元素比特位值binVal,以及更新过的codIRange 和 codIOffset。
该过程的整体流程如下图所示:
2.1 计算 codIRangeLPS
计算 codIRangeLPS 的过程遵循以下流程:
- 根据当前的codIRange值,计算qCodIRangeIdx的值:
- qCodIRangeIdx =( codIRange >> 6 ) & 3
- 通过 qCodIRangeIdx 和 pStateIdx,计算 codIRangeLPS 的值:
- 通过查表获取:codIRangeLPS = rangeTabLPS[ pStateIdx ][ qCodIRangeIdx ]
- rangeTabLPS为预定义的表,在表9-44中指定;
2.2 更新 codIRange
更新 codIRange 的方法为:
codIRange = codIRange - codIRangeLPS;
更新完成后,判断 codIOffset,如果 codIOffset >= codIRange,输出比特位值binVal赋值为1 - valMPS,并且codIOffset自减去codIRange的值,然后codIRange的值赋值为codIRangeLPS;否则,输出比特位值binVal赋值为valMPS。
2.3 状态转移过程
在CABAC编码或解码语法元素的某一个bit,编码/解码器将随之更新编解码器的状态。状态转移过程定义于标准的9.3.3.2.1.1节。该过程根据当前的上下文模型索引和解码的比特值,来更新上下文模型索引以及LPS/MPS的定义。
状态转移过程可以下式表示:
if( binVal = = valMPS )
pStateIdx = transIdxMPS( pStateIdx )
else
if( pStateIdx = = 0 )
valMPS = 1 − valMPS
pStateIdx = transIdxLPS( pStateIdx )
在编码过程中上下文模型索引的更新同样以查表的形式实现,该表格在9-45中规定。
2.4 归一化过程
在第18篇博文“算术编码的基本原理中”已经讨论过算术编码器的归一化。在H.264实际定义的CABAC算法中,归一化同样也是必备过程。CABAC的归一化过程定义于9.3.3.2.2节,流程图以下图表示:
进行归一化的本质含义是更新codIRange和codIOffset的值,所需要的数据包括当前CABAC的codIRange和codIOffset以及当前的二进制比特流数据。主要步骤为:
- 判断如果codIRange不小于256,那么不需要进行归一化;
- 如果codIRange小于256,则进行归一化过程,方法为:
- codIRange和codIOffset分别自乘以2;
- 在码流中读取 1 bit,并与codIOffset按位取或运算并更新到codIOffset;
三、CABAC的bypass解析方法
本节描述的是CABAC的DecodeBypass方法,即旁路解析模式,在bypass值设为1时执行。DecodeBypass相比DecodeDecision方法的特点是编码效率较低但运算远比DecodeDecision简单。与DecodeDecision过程相比,DecodeBypass不需要ctxIdx,只需要codIRange和codIOffset两个值。
DecodeBypass过程的流程图如下:
解析过程为:
- 首先codIOffset自增一倍,并且从码流中读取1 bit与之求按位或操作;
- codIOffset与codIRange对比:
- 若codIOffset小于codIRange,输出二进制位0;
- 否则,输出二进制位1,且codIOffset自减去codIRange的值;
四、CABAC的终止符解析
CABAC的终止符解析即DecodeTerminate过程,主要用于解析end_of_slice和ctxIdx值为276的元素。解析的方法类似于DecodeBypass过程,流程如下:
- 首先codIOffset自减2;
- codIOffset与codIRange对比:
- 若codIOffset小于codIRange,输出二进制位0,并执行解码器的归一化过程;
- 否则,输出二进制位1;
以上是关于H264 视频流的解析的主要内容,如果未能解决你的问题,请参考以下文章
Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜