视频编解码

视频编码

为什么要编码

假设一个 1 小时的未压缩的电影(1920 * 1080),像素数据格式为 RGB(3 Byte),按照每秒 25 帧来计算:

1
3600 * 25 * 1920 * 1080 * 3 = 521.42GB

500 多 G 的电影显然太大了,下载耗时也占地方,在线看也很慢,毕竟我们平时看的一般也就几个 G 而已。所以我们要对视频进行 压缩,而 编码 就是 压缩 的过程。

视频编码的作用: 将视频像素数据(RGB,YUV 等)压缩成对应标准的视频码流,从而降低视频的数据量。

视频编码首先要转换下色彩空间, 从 RGB 转化为 YUV:

YUV 色彩空间转换

YUV 和 RGB 是不同的色彩空间, RGB 是采用三种基本色为基础进行叠加, 从而产生不同的颜色; 而 YUV 是通过亮度和色差来描述颜色的颜色空间, Y表示明亮度(Luma), 也就是灰阶值, U 和 V 表示的则是色度(Chrominance)和浓度(Chroma), 作用是描述影像色彩及饱和度, 用于指定像素的颜色。因此对于黑白显示设备, 只需要去除色度分量, 只显示亮度分量即可。YUV细分的话有Y’UV / YUV / YCbCr / YPbPr等类型, 其中YCbCr主要用于数字信号。在流媒体领域中, YUV 其实就是指 YCbCr。

YCbCr的Y与YUV中的Y含义一致, Cb和Cr与UV同样都指色彩, Cb指蓝色色度, Cr指红色色度

RGB和YUV的换算公式如下:

1
2
3
Y =  0.299 R' + 0.587 G' + 0.114 B'
U = -0.147 R' - 0.289 G' + 0.436 B' = 0.492(B' - Y)
V = 0.615 R' - 0.289 G' + 0.436 B' = 0.877(R' - Y)

对于不同明亮度, UV 的表现如下:

YUV采样方式(即YCbCr)

YUV 码流的存储格式其实与其采样的方式密切相关, 主流的采样方式有三种, YUV4:4:4, YUV4:2:2, YUV4:2:0,
4:1:1含义就是:在2x2的单元中, 本应分别有4个Y, 4个U, 4个V值, 用12个字节进行存储。经过4:1:1采样处理后, 每个单元中的值分别有4个Y、1个U、1个V, 只要用6个字节就可以存储了

  • YUV 4:4:4采样, 每1个Y对应一组UV分量, 消耗 3*4 = 12byte
  • YUV 4:2:2采样, 每2个Y共用一组UV分量, 邻近4个像素点的亮度分量是被完整保留, Cb/Cr分量分别进行了下采样只保留了1/2, 而人眼看起来几乎不会察觉到变化. Y: 4byte, U: 2byte, V: 2byte, 共 8byte, 就这样简单粗暴的操作我们就节省出了1/3的存储空间
  • YUV 4:2:0采样, 每4个Y共用一组UV分量, Y: 4byte, U: 1byte, V: 1bit, 共 6byte, 对应地可以节省出 1/2 的空间.

视频编码方案

视频编码标准历史

  • MPEG:国际标准化组织及国际电工委员会ISO/IEC旗下的动态图像专家组MPEG(Moving Picture Experts Group)
  • VCEG:国际电联电信标准化部门ITU-T旗下的视频编码专家组VCEG(Video Coding Experts Group)

AVC/H.264:集大成者一统江湖

2001 年,ISO 的MPEG 组织认识到H.26L 潜在的优势,随后ISO 与ITU 开始组建包括来自ISO/IEC MPEG与ITU-T VCEG 的联合视频组(JVT),JVT 的主要任务就是将H.26L 草案发展为一个国际性标准。于是,在ISO/IEC中该标准命名为AVC(Advanced Video Coding),作为MPEG-4 标准的第10 个选项;在ITU-T 中正式命名为H.264标准。该标准在2003 年3 月正式获得批准。

他的特点是高压缩比、高图像质量、良好的网络适应性,在较低带宽上提供高质量的图像传输。 其采用了更灵活的宏块划分方法、数量更多的参考帧、更先进的帧内预测和压缩比更高的数据压缩算法。

HEVC/H.265/MPEG-H Part 2:视频编码王位继任者

H.264很强大,但是它在超清时代有点不够用了。随着视频分辨率的跨越式提升,H.264表现出了疲态,它在应对4K视频时已经没有办法提供很好的压缩比了。很明显,人们需要新的编码来继承它的位置,而它的直接继承者——HEVC,在经过多年研究之后,终于在2013年被通过了。

HEVC,全称高效视频编码(High Efficiency Video Coding),同样的,它也是由MPEG和ITU-T联合制定的国际标准编码。被包含在MPEG-H规范中,是为第二部分(Part 2),在ITU-T那儿,它是H.26x家族的新成员,为H.265。

HEVC主要是针对高清及超清分辨率视频而开发的,相比起前代AVC,它在低码率时拥有更好的画质表现,同时在面对高分辨率视频时,也能提供超高的压缩比,帮助4K视频塞入蓝光光盘。

相较于AVC,HEVC在高分辨率下的编码效率又有非常大的提升,举个实例,同样一段4K视频,使用H.264编码的大小可能会比使用HEVC大出个一倍。这种巨大的进步幅度也使得Blu-ray直接用它作为标准编码,推出了UHD BD,而它在单帧图像压缩上面的改进也让它拥有胜过JPEG的能力,于是我们看到在移动端,越来越多的设备选择将其作为默认的视频、照片输出编码。

但是相比起AVC,HEVC的推广速度慢了很多,

  1. 一个是它的编解码难度比H.264高了太多,但这点通过各路硬件编码器和软件优化逐渐化解掉了,目前常见的设备基本上支持HEVC的硬件编解码;
  2. 第二个就是HEVC高昂的专利费用问题,它并不是一个免费的编码格式,虽然个人使用它完全没有问题,但对于想要兼容它的厂商来说,这笔高昂的专利费用足以让他们却步,尤其是崇尚自由开放的互联网市场。于是,我们看到众多厂商选择了免费开放的VPx系列编码,以及系列的后继者——AV1。

Apple 生态中的 HEVC 支持

Apple 现在支持的 HEVC Codec(编解码器)类型为 kCMVideoCodecType_HEVC ,即 hvc1 ,对应的 Profiles 为 Main, Main Still Picture 及 Main 10 (即对应 HEVC Version 1 Profiles 中所有的内容),相应的文件封装格式(容器)还是我们熟悉 QuickTime Movie (.mov) 及 ISO MPEG-4 (.mp4) 。你可以在新的 iOS 11, tvOS 11 及 macOS 10.13 中使用它。

HEVC 的硬件要求

软编码 /软解码 是指使用 CPU 来完成编解码运算,硬编码/硬解码 是指使用非 CPU 进行解码运算,常见的就是使用 GPU。相对而言,硬编码/硬解码 能够省电、减少设备发热,对于移动设备延长续航有很大的作用。当前如果要在 iOS 10 或者更早设备上解码 HEVC 资源,一般是使用 FFmpeg 这一类的软解码方案(但请注意它的 License 是 LGPL )。

解码 Decode
新系统下所有 iOS / tvOS / macOS 设备均可执行 8-bit / 10-bit 的软解;在 iOS (tvOS) 上至少需要 A9 芯片来执行 8-bit / 10-bit 硬解码(也就是 iPhone 6s 之后的机型,猜测 9 月也会发布带 A9 / 4K 的 tvOS 新机型),而在 macOS 上至少需要酷睿 6 代来硬解码 8-bit 的资源,或者酷睿 7 代来硬解码 10-bit 的资源。

编码 Encode
新系统下,iOS 需要至少 A10 芯片来完成 8-bit 资源的硬编码( iPhone 的相机现在仅支持 8-bit 视频的拍摄,有兴趣可以研究一下使用 AVCaptureVideoDataOutput 来接收相机输出时的 videoSettings);在 macOS 上,至少需要酷睿 6 代来硬编码 8-bit 的资源,而 10-bit 的软编码则可以在所有的 Mac 上完成。

如果你想要查询当前设备是否支持硬件编码,可以使用下面这个新增的方法来查询。但是当前的测试版似乎还是存在 Bug,笔者使用了安装 iOS 11 beta 2 的 iPhone 7 Plus 及 iPad Pro 10.5 进行测试,均返回了 false ,而按照 Apple 给出的资料,应当是可以支持硬解的 🤷‍♂️。

1
2
// 测试设备 iPhone 7 Plus / iOS 11 beta 2 / Xcode 9 beta 2 / Swift 4
let isSupport = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) // false

HEIF 基本介绍

HEIF(High Efficiency Image File) 是一种图像文件封装格式, HEIF 相比 JEPG 能够提高 2 倍的压缩率,而且支持 HEVC 作为其压缩的编解码器;支持透明通道与深度;支持动画(比如动态 GIF , Live Photo);支持图像序列(比如照片的长曝光)。

HEVC 文件加载

与加载一张 JPEG 图像仅有扩展名的不同:

1
2
3
4
5
6
7
// Read a heic image from file
let inputURL = URL(fileURLWithPath: "/tmp/image.heic")
let source = CGImageSourceCreateWithURL(inputURL as CFURL, nil)
let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
let image = CGImageSourceCreateImageAtIndex(source, 0, nil)
let options = [kCGImageSourceCreateThumbnailFromImageIfAbsent as String: true, kCGImageSourceThumbnailMaxPixelSize as String: 320] as [String: Any]
let thumb = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
HEIF 文件写入

与写入一张 JPEG 图像也仅有扩展名的区别:

1
2
3
4
5
6
7
8
9
10
// Writing a CGImage to a HEIC file
let url = URL(fileURLWithPath: "/tmp/output.heic")
guard let destination = CGImageDestinationCreateWithURL(url as CFURL,
AVFileType.heic as CFString, 1, nil)
else {
fatalError("unable to create CGImageDestination")
}

CGImageDestinationAddImage(imageDestination, image, nil)
CGImageDestinationFinalize(imageDestination)
编辑 HEIF 照片,并存为 JPEG 格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Editing a HEIF photo -- save as JPEG
func applyPhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: () -> ()) {

guard let inputImage = CIImage(contentsOf: input.fullSizeImageURL!)
else { fatalError("can't load input image") }
let outputImage = inputImage
.applyingOrientation(input.fullSizeImageOrientation)
.applyingFilter(filterName, withInputParameters: nil)

// Write the edited image as a JPEG.
do {
try self.ciContext.writeJPEGRepresentation(of: outputImage,
to: output.renderedContentURL, colorSpace: inputImage.colorSpace!, options: [:])
} catch let error { fatalError("can't apply filter to image: \(error)") }
completion()
}
编辑 HEVC 视频,并使用 H.264 编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Editing an HEVC video -- save as H.264
func applyVideoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completionHandler: @escaping () -> ()) {

guard let avAsset = input.audiovisualAsset
else { fatalError("can't get AV asset") }
let composition = AVVideoComposition(asset: avAsset, applyingCIFiltersWithHandler: { request in
let img = request.sourceImage.applyingFilter(filterName, withInputParameters: nil)
request.finish(with: img, context: nil)
})
// Export the video composition to the output URL.
guard let export = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetHighestQuality)
else { fatalError("can't set up AV export session") }
export.outputFileType = AVFileType.mov
export.outputURL = output.renderedContentURL
export.videoComposition = composition
export.exportAsynchronously(completionHandler: completionHandler)
}

VPx系列与AV1:以免费为卖点

VPx系列编码实际上已经有很长的历史了。它的前身是On2 Technologies公司的TrueMotion系列视频编码,在开发TrueMotion VP8编码时,公司被Google收购了。在Google的介入下,VP8从原本的专有技术变成了开放技术,在BSD许可证下面进行开源。

从技术角度来说,VP8采用的技术是类似于H.264的。虽然在我们看到的宣传中,VP8拥有比H.264更佳的压缩效率,但在实际应用中,由于它在设计上有一定的瑕疵,表现并不如H.264,最终它虽然进入了Web标准,但也没见有人用它,反而是由它的帧内压缩技术提取而成的WebP受到了欢迎。

VP8的表现并不理想,Google很快就推出了它的继任者——VP9。这次,他们参考的是HEVC,设计目标同样是高分辨率下的高效编码。VP9中的一些设计是受到了HEVC的影响的,比如说同样最大为64x64的超级块(Super Block)。最终VP9达成的结果是提供了比VP8高达50%的效率提升。看起来它能够和HEVC比肩了,但是它也遇到了和VP8相似的问题,推广不开。VP9的应用范围实际也局限在Google自家的Youtube中,只能说是缺少实际应用场景。

但很快,一些厂商认识到HEVC高昂专利费用带来的弊端,他们决定创立一个开放联盟,推广开放、免费的媒体编码标准。这个联盟就是开放媒体联盟(Alliance for Open Media),创始成员有Amazon、Cisco、Google、Intel、Microsoft、Mozilla和Netflix这些我们熟悉的大公司,而后加入的还有苹果、ARM、三星、NVIDIA、AMD这些同样耳熟能详的公司。

Google将他们还在开发中的VP10贡献了出来作为联盟新编码的基础,很快,名为AV1的编码诞生了。在Facebook的测试中,它分别比VP9和H.264强上34%、46.2%,这次看上去是真的达到HEVC的级别了。

各种编码标准对比

编码标准 压缩效率 计算复杂度 专利许可费用 广泛支持情况
H.264 (AVC) 中等 较低 有专利费用 广泛支持
H.265 (HEVC) 较高 较高的专利费用 逐渐普及
VP9 较高 无专利费用(Google许可) 有限支持
VP10 更高 更高 无专利费用(Google许可) 尚未广泛支持
AV1 更高 更高 无专利费用(AOMedia成员 正在普及
AVS3 较高 收费 中国市场支持

H.264

简介

国际上制定视频编解码技术的组织有两个,一个是“国际电联(ITU-T)”,它制定的标准有 H.261、H.263、H.263+ 等,另一个是“国际标准化组织(ISO)”它制定的标准有 MPEG-1、MPEG-2、MPEG-4 等。而 H.264 则是由两个组织联合组建的联合视频组(JVT)共同制定的新数字视频编码标准,所以它既是 ITU-T 的 H.264,又是 ISO/IEC 的 MPEG-4 高级视频编码(Advanced Video Coding,AVC)的第 10 部分。

H.264/AVC标准在当前视频应用场景中仍然是应用最广、兼容性最高的视频编码标准,因此任何视频产品如果希望在支持最大范围用户流畅使用的同时保障视频质量,H.264/AVC软件编解码必不可缺。

编码过程和原理

H.264 是最常见的编码标准, 编码首先要把视频帧划分帧不同的类型

划分帧类型

  1. 将一串连续的相似的帧归到一个图像群组(Group Of Pictures,GOP)
  2. GOP中的帧可以分为3种类型
    • I帧(I Picture、I Frame、Intra Coded Picture),译为:帧内编码图像,也叫做关键帧(Keyframe)
      • 是视频的第一帧,也是GOP的第一帧,一个GOP只有一个I帧
      • 编码- 对整帧图像数据进行编码
      • 解码- 仅用当前I帧的编码数据就可以解码出完整的图像
      • 是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态图像
    • P帧(P Picture、P Frame、Predictive Coded Picture),译为:预测编码图像
      • 编码- 并不会对整帧图像数据进行编码
      • 以前面的I帧或P帧作为参考帧,只编码当前P帧与参考帧的差异数据
      • 解码- 需要先解码出前面的参考帧,再结合差异数据解码出当前P帧完整的图像
    • B帧(B Picture、B Frame、Bipredictive Coded Picture),译为:前后预测编码图像
      • 编码- 并不会对整帧图像数据进行编码
        • 同时以前面、后面的I帧或P帧作为参考帧,只编码当前B帧与前后参考帧的差异数据
        • 因为可参考的帧变多了,所以只需要存储更少的差异数据
      • 解码- 需要先解码出前后的参考帧,再结合差异数据解码出当前B帧完整的图像
    • 显示和编码顺序: I 帧-> P 帧 -> 中间的 B 帧 -> 下一个 P 帧 -> 中间的 B 帧 -> …

    • 帧类型的划分方式主要取决于:
  3. 编码设置:确定 GOP 的大小、P 帧与 B 帧之间的间隔。
  4. 视频内容:基于内容特征动态调整 GOP 结构和 P 帧、B 帧的间隔。例如,当存在高速运动或画面的剧烈变化时,可能会选择更多的 I 帧,以保持较好的图像质量。

GOP设置注意点

  1. GOP的长度表示GOP的帧数。GOP的长度需要控制在合理范围,以平衡视频质量、视频大小(网络带宽)和seek效果(拖动、快进的响应速度)等。
  2. 加大GOP长度有利于减小视频文件大小,但也不宜设置过大,太大则会导致GOP后部帧的画面失真,影响视频质量
  3. 由于P、B帧的复杂度大于I帧,GOP值过大,过多的P、B帧会影响编码效率,使编码效率降低
  4. 如果设置过小的GOP值,视频文件会比较大,则需要提高视频的输出码率,以确保画面质量不会降低,故会增加网络带宽
  5. GOP长度也是影响视频seek响应速度的关键因素,seek时播放器需要定位到离指定位置最近的前一个I帧,如果GOP太大意味着距离指定位置可能越远(需要解码的参考帧就越多)、seek响应的时间(缓冲时间)也越长

帧内/帧间预测

I帧采用的是帧内(Intra Frame)编码,处理的是空间冗余
P帧、B帧采用的是帧间(Inter Frame)编码,处理的是时间冗余

宏块划分

在进行编码之前,首先要将一张完整的帧切割成多个宏块(Macroblock),H.264中的宏块大小通常是16x16。

  1. 计算的简化与并行处理:将图像分解成较小的宏块,可以简化编码和解码过程中的预测、变换和量化等计算。此外,采用宏块处理也支持并行化处理,不同宏块间的数据处理可在多个处理单元上同时进行,从而能显著提高编码和解码的速度。
  2. 更精确的运动估计与补偿:视频编码中的帧间编码需要进行运动估计与补偿,以减少时间冗余。将图像划分为宏块可以针对每个局部区域进行更精细的运动估计,从而获得更准确的预测结果。
  3. 适应不同区域的编码优化:图像中不同区域的特征和纹理差异较大,例如局部光滑和局部纹理复杂的区域。将图像分解成较小的宏块有利于根据每个区域的特点进行优化编码。
  4. 降低空间冗余:局部区域(宏块)的像素之间的相关性较高。在该尺度上进行预测编码能有效降低空间冗余,达到更高的压缩效率。

帧内预测 (空间冗余)

一共有九种预测模式,通过选择模式从相邻已编码的像素计算预测值。

以4 * 4的宏块为例

1
2
3
4
150  140  130  120
160 150 145 130
145 159 161 128
170 160 140 110

水平预测(以左方相邻已编码像素为准)

1
2
3
4
150  150  150  150
160 160 160 160
145 145 145 145
170 170 170 170

垂直预测(以上方相邻已编码像素为准)

1
2
3
4
150  140  130  120
150 140 130 120
150 140 130 120
150 140 130 120

DC 模式(左边和上方相邻已编码像素的平均值)

1
2
3
4
150  150  150  150
150 150 150 150
150 150 150 150
150 150 150 150

接下来,将预测值与实际值进行比较,计算差值(残差)矩阵,例如用实际值减去水平预测值:

1
2
3
4
0  -10  -20  -30
0 -10 -15 -30
0 14 16 -42
0 -10 -30 -60

帧间预测(时间冗余)

用到了运动补偿和运动估计技术

1
2
3
4
5
6
7
参考帧				  当前帧
■ ■ - - - - - - - - - -
■ ■ - - - - - - - - - -
- - - - - - ---> - - ■ □ - -
- - - - - - - - □ ■ - -
- - - - - - - - - - - -
- - - - - - - - - - - -
  • 运动估计:我们在参考帧中寻找与当前帧中的正方形相似的区域。在这个例子中,我们可以看到正方形向下和向右移动了两个位置,但其形状发生了轻微变化。
  • 运动补偿:根据参考帧和当前帧之间的匹配结果,我们得到运动向量 (2, 2)。这意味着正方形沿着水平和垂直方向移动了两个单位距离。

生成预测块

1
2
3
4
5
6
- - - - - -
- - - - - -
- - ■ ■ - -
- - ■ ■ - -
- - - - - -
- - - - - -

计算当前帧和预测帧的残差

1
2
3
4
5
6
7
预测帧					  当前帧					残差矩阵
- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - -
- - ■ □ - - - - - ■ ■ - - = - - - □ - -
- - □ ■ - - - - ■ ■ - - - - □ - - -
- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - -

变换/量化

  1. 变换:通过预测并计算残差信息后,我们需要应用整数变换(例如离散余弦变换,DCT)将残差矩阵转换为频域。这有助于更有效地压缩数据,因为相关性较低的高频部分信息将在量化阶段被降低或丢弃。
  2. 量化:对已变换的残差系数进行量化。量化过程会根据量化参数(QP)压缩或丢弃较小的系数。降低数据精度有助于减小编码带宽并降低解码计算负担。

带来的问题:在变换和量化过程中,高频信息可能因为压缩而失真或被丢弃。这会导致视频中出现块效应,即边缘处的宏块边界变得明显可见。俗称马赛克
主要原因:DCT变换后的量化造成误差

数学好的可以看这个 Discrete cosine transform

滤波

为了减轻上述问题,许多编码算法(如 H.264)都引入了环路滤波器(in-loop filter)。环路滤波主要针对宏块边缘的块效应,降低边缘的可见性。滤波算法会检测边缘像素的强度和平滑程度,然后通过软化过渡,降低边缘明显的垂直和水平线条。这样可以改善视频画质,尤其是在低码率下。

还有一些编码器会对解码后的图像进行去噪,以减轻图像中的伪影、颜色变化和噪点。这使得解码后的图像在质量上更接近于原始图像。

滤波作为编码流程的补充,有助于在高压缩效率的情况下保持较好的视觉体验。

滤波前后对比

熵编码

熵编码模式的作用主要是利用数据的统计特性来进行无损压缩,降低视频编码的数据量,提高压缩效率。在不牺牲视频质量的前提下,这一阶段的压缩出色地减轻了带宽和存储的需求。在选择熵编码模式时,需权衡压缩效果与计算复杂度之间的关系,以便根据实际应用和设备性能找到合适的平衡点。

  1. CAVLC(Context-Adaptive Variable-Length Coding):CAVLC 是一种赫夫曼编码的扩展, 用短码来记录高频数据.用长码记录低频数据.
  2. CABAC(Context-Adaptive Binary Arithmetic Coding):CABAC 是一种基于二元算术编码的上下文自适应编码方法。CABAC 利用概率模型考虑相邻符号的依赖关系,并在算术编码的基础上进行优化。相比 CAVLC,CABAC 的压缩效果更好,但计算复杂度也更高,但是对于实时编码和解码是一个挑战。

码流结构

H.264 原始码流(裸流)是由一个接一个 NALU 组成,它的功能分为两层,VCL(视频编码层)和 NAL(网络提取层)。

VCL和NAL层

  • VCL: 视像编码层(Video Coding Layer, 简称VCL),包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码,对视频原始数据进行压缩。
  • NAL: 网络抽象层(Network Abstraction Layer,简称NAL)。- 该层的作用是将视频编码数据根据内容的不同划分成不同类型的NALU,以适配到各种各样的网络和多元环境中。对VCL输出的SODB数据后添加结尾比特,一个比特 1 和若干个比特 0,用于字节对齐,称为RBAP,然后再在 RBSP 头部加上 NAL Header 来组成一个一个的NAL单元(unit)即 NALU 。

VCL 数据传输或者存储之前,会被映射到一个 NALU 中,H264 数据包含一个个 NALU。如下图:

一个NALU = 一组对应于视频编码的NALU头部信息 + 一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)

上图中的 NALU头 + RBSP 就相当与一个 NALU (Nal Unit), 每个单元都按独立的 NALU 传送。 其实说白了,H.264 中的结构全部都是以 NALU 为主的,理解了 NALU,就理解 H.264 的结构了。

码流分析

参数概念

H.264 码流第一个 NALU 是 SPS,第二个 NALU 是 PPS,第三个 NALU 是 IDR(即时解码器刷新)

  • IDR:一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。
  • SPS:序列参数集 SPS(Sequence Parameter Sets),存储的是一个序列的信息,包括有多少帧等
  • PPS:图像参数集 PPS(Picture Parameter Sets ),存储的一帧的信息。解码的时候必须获取到 SPS 和 PPS 的信息,才能对后面的数据进行解码。

组成单元

一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成

  • StartCode : Start Code 用于标示这是一个NALU 单元的开始,必须是”00 00 00 01” 或”00 00 01”
  • NALU Header 下表为 NAL Header Type
    • H.264NAL Header结构:
    • 我们假定一个头信息字节为0x67作为例子,根据查表得知提取到SPS类型
    • 从表中我们可以获知,NALU类型1-5为视频帧,其余则为非视频帧。在解码过程中,我们只需要取出NALU头字节的后5位,即将NALU头字节和0x1F进行与计算即可得知NALU类型,即:NALU类型 = NALU头字节 & 0x1F
  • Payload: 具体码流信息示意图

注意: 可以将start code理解为不同nalu的分隔符,header是某种类型的key,payload是该key的value.

码流格式

因为NALU长度不一,要写到一个文件中需要标识符来分割码流以区分独立的NALU,解决这一问题的两种方案,产生了两种不同的码流格式:

  • Annex-B:在每个NALU前加上0 0 0 1或者0 0 1,称作start code(起始码),如果原始码流中含有起始码,则起用防竞争字节:如将0 0 0 1处理为0 0 0 3 1。
  • AVCC:在NALU前面加上几个字节,用于表示整个NALU的长度(大端序,读取时调用CFSwapInt32BigToHost()转为小端),在读取的时候先将长度读取出来,再读取整个NALU。

除此之外,Annex-B和AVCC/HVCC对参数集的不同处理方式:

  • Annex-B:参数集当成普通的NALU处理,每个I帧前都需要添加SPS/PPS。
  • AVCC:参数集特殊处理,放在头部被称为extradata的数据中。

为什么不统一为一种格式?
我们知道视频分为本地视频文件和网络直播流,对于直播流,AVCC 格式只在头部添加了参数集,如果是中途进入观看会获取不到参数集,也就无法初始化解码器进行解码,而 Annex-B 在每个I帧前都添加了参数集,可以从最近的I帧初始化解码器解码观看。而 AVCC 只在头部添加参数集很适合用于本地文件,解码本地文件只需要获取一次参数集进行解码就能播放,所以不需要像Annex-B一样重复地存储多份参数集。

为什么要了解这两种格式?
因为Video Toolbox编码和解码只支持 AVCC/HVCC 的码流格式,而Android的 MediaCodec 只支持 Annex-B 的码流格式。因此在流媒体场景下,对于iOS开发而言,需要在采集编码之后转为Annex-B格式再进行推流,拉流解码时则需要转为AVCC/HVCC格式才能用Video Toolbox进行解码播放。

H.264编码库

编码库 开源与否 专利费用 平台支持 速度 效率 码率模式(CBR、VBR、CRF) 编码类型(软编/硬编)
x264 开源 需要支付专利费用 Windows, macOS, Linux CBR, VBR, CRF 软编
OpenH264 开源 由思科支付专利费用 Windows, macOS, Linux, Android, iOS CBR, VBR 软编
Intel Media SDK (Quick Sync Video) 无须支付专利费用 只支持带有 Intel 集成显卡的设备 极高 CBR, VBR, ICQ 硬编(基于 Intel GPU)
NVIDIA NVENC 无须支付专利费用 只支持特定的 NVIDIA 显卡设备 极高 CBR, VBR, CQP 硬编(基于 NVIDIA GPU)
AMD VCE 无须支付专利费用 只支持特定的 AMD 显卡设备 极高 CBR, VBR, CQP 硬编(基于 AMD GPU)
Apple VideoToolbox 无须支付专利费用 仅支持 macOS 和 iOS 设备 极高 CBR, VBR, CVBR, CRF 硬编(基于 Apple GPU)
编解码类型 编解码硬件 优点 缺点
软编解码 CPU 兼容性好,升级方便,支持所有视频格式,画质清晰。 性能较差的机型会发热或卡顿。
硬编解码 非CPU:GPU或专用的DSP、FPGA、ASIC芯片等 对CPU占用率低,不会出现手机发热等现象。 某些设备硬件不支持,兼容性不好。

目前大部分业务场景的编解码策略是:手机端采用硬编码生成视频文件发送给服务器,服务器进行软编转码为支持更多的格式或码率的视频,再分发给观看端。考虑到有些设备不支持硬编解码,通常需要软编解码做兜底。

  1. CBR(恒定比特率):以固定码率进行编码。这意味着视频中每个部分都具有相同的码率。CBR 适用于对输出文件大小有严格要求、需要稳定网络带宽的场景,例如实时视频传输、在线直播等。
  2. VBR(可变比特率):根据编码内容的复杂度自适应地调整码率。较复杂的部分会分配更高的码率,而较简单的部分会分配较低的码率。VBR 适用于需要平衡文件大小与视频质量的场景,如在线短视频、非实时视频存储等。
  3. CRF(恒定速率因子):通过设置一个速率因子,调整压缩效率以实现恒定的输出质量。CRF 侧重于尽可能保持视频品质,可能会导致文件大小不固定,适用于对压缩效果和质量有高要求的非实时场景,例如高清电影编码、视频编辑等。
  4. ICQ(固定品质量):Intel 编码器特有,它在指定的最大和最小品质限制范围内,根据编码内容的复杂度调整速率。这使得视频在保持较高质量的同时,也不会使码率过高。适用于有明确质量要求的场景,例如录制高清游戏视频等。
  5. CQP(恒定量化参数):位于 NVENC 与 AMD VCE 编码器,手动设置量化参数,实现固定质量的编码。适用于具有一定编码经验和对质量有明确要求的场景,例如录制游戏或教育视频等。
  6. CVBR(约束可变比特率):Apple 编码器特有,与 VBR 类似,但需要指定最大和最小码率范围,使得码率保持在这个范围内进行调整。适用于需要限制码率范围的应用场景,例如对输出文件大小有限制要求同时尽可能保证视频质量的场合。

参考资料

  1. 69 篇文章带你系统性的学习音视频开发
  2. 【WWDC21 10158】VideoToolbox 视频编码基础及其低延时新特性
  3. 音视频开发入门:音频基础
  4. 音视频开发入门:视频基础
  5. 音视频学习基础概念
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道