引言
调用系统 VideoToolbox 的 API 实现一个硬编很容易,仔细看看文档、了解 API 的使用实现一个基本功能相信难不倒大家。但实际工作中有许多细节,一不注意就会掉坑里,甚至有些系统性问题难以解决。本文一方面会介绍必备的基础知识,带大家对编码有一个基本的认识,另一方面也会分享直播 SDK 在 VT 硬编实现上遇到的问题和解决方案,希望能帮助到大家。
必备基础知识
帧概念
-
I 帧(帧内编码图像帧)即帧内(Intra)图像,采用帧内编码,不参考其它图像,但可作为其它类型图像的参考帧。
-
P 帧(预测编码图像帧)即预测(Predicted)图像,采用帧间编码,参考前一幅 I 或 P 图像,用作运动补偿。
-
B 帧(双向预测编码图像帧)即双向预测(Bi-predicted)图像,提供最高的压缩比,它既需要之前的图像帧( I 帧或 P 帧),也需要后来的图像帧( P 帧),采用运动预测的方式进行帧间双向预测编码。
时间戳
-
PTS:显示时间戳,主要用于视频的同步和输出,在渲染的时候使用,在没有 B frame 的情况下 DTS 和 PTS 的输出顺序是一样的。
-
DTS:解码时间戳,主要用于视频的解码,在解码阶段使用。
-
CTS = PTS - DTS。
示例:
gop | I | B | B | P | B | B | P |
---|---|---|---|---|---|---|---|
显示顺序 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
解码顺序 | 1 | 3 | 4 | 2 | 6 | 7 | 5 |
PTS | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
DTS | 1 | 3 | 4 | 2 | 6 | 7 | 5 |
GOP & Reference
GOP:一段时间内图像变化不大的图像集我们就可以称之为一个序列,gop 就是一组视频帧,其中第一个 I 帧我们称为是 IDR 帧。
Reference:参考周期,指两个 P 帧之间的距离,iOS 硬件编码器中无法指定。
IDR
一个 GOP 的第一个帧称 IDR 帧(立即刷新帧),IDR 帧的作用是立刻刷新,使错误不致传播。从 IDR 帧开始, 重新算一个新的序列开始编码。而 I 帧不具有随机访问的能力,这个功能是由 IDR 承担。IDR 帧会导致 DPB (DecodedPictureBuffer 参考帧列表——这是关键所在)清空,而 I 不会。
IDR 帧一定是 I 图像,但 I 帧不一定是 IDR 图像。一个序列中可以有很多的 I 帧图像,I 帧图像之后的图像可以引用 I 帧图像之间的图像做运动参考。
码率控制
-
ABR:平均目标码率,简单场景分配较低 bit,复杂场景分配足够 bit,使得有限的 bit 数能够在不同场景下合理分配,这类似 VBR。同时一定时间内,平均码率又接近设置的目标码率,这样可以控制输出文件的大小,这又类似 CBR。可以认为是 CBR 和 VBR 的折中方案,这是大多数人的选择。特别在对质量和视频带宽都有要求的情况下,可以优先选择该模式,一般速度是 VBR 的两倍到三倍,相同体积的视频文件质量却比 CBR 好很多。
- 适用场景:ABR 在直播和低延时系统用的比较多,因为只编码了一次,所以速度快,同时兼顾了视频质量和带宽,对于转码速度有要求的情况下也可以选择该模式。
- 特点:视频质量整体可控,同时兼顾了视频码率和速度,是一个折中方案,实际用的比较多;使用过程一般要让调用方设置,最低码率、最高码率和平均码率,这些值要尽可能设置合理点。
-
VBR:(Variable Bit Rate)可变码率,简单场景分配比较大的 QP,压缩率小,质量高。复杂场景分配较小 QP。得到基本稳定的视觉质量,因为人眼本来就对复杂场景不敏感,缺点在于输出码率大小不可控。
- 适用场景:VBR 适用于那些对带宽和编码速度不太限制,但是对质量有很高要求的场景。特别是在运动的复杂场景下也可以保持比较高的清晰度且输出质量比较稳定,适合对延时不敏感的点播,录播或者存储系统。
- 特点:码率不稳定,质量基本稳定且非常高;编码速度一般比较慢,点播、下载和存储系统可以优先使用,不适合低延时、直播系统;这种模型完全不考虑输出的视频带宽,为了质量,需要多少码率就占用多少,也不太考虑编码速度。
-
CBR:(Constant Bit Rate)恒定码率,一定时间范围内比特率基本保持的恒定,属于码率优先模型。
- 适用场景:一般也不建议使用这种方式,虽然输出的码率总是处于一个稳定值,但是质量不稳定,不能充分有效利用网络带宽,因为这种模型不考虑视频内容的复杂性,把所有视频帧的内容统一对待。但是有些编码软件只支持固定质量或者固定码率方式,有时不得不用。用的时候在允许的带宽范围内尽可能把带宽设置大点,以防止复杂运动场景下视频质量很低,如果设置的不合理,在运动场景下直接就糊了。
- 特点:码率稳定,但是质量不稳定,带宽有效利用率不高,特别当该值设置不合理,在复杂运动场景下,画面非常模糊,非常影响观看体验。
编码数据裸流
H264 的码流结构它主要有两种格式:Annex B 和 AVCC。Annex B 格式以 0x000001 或 0x00000001 开头,AVCC 格式以所在的 NALU 的长度开头,以 Annex B 为例。
但对于一个 H.264 裸流来说,就是一系列 NALU 的集合 ,每个 NALU 既可以表示图像数据,也可以表示处理图像所需要的参数数据。
NALU结构分为视频编码层(VCL)和网络适配层(NAL):
视频编码层( VCL 即 Video Coding Layer) :负责高效的视频内容表示,这是核心算法引擎,其中对宏块、片的处理都包含在这个层级上,它输出的数据是 SODB 。
网络适配层( NAL 即 Network Abstraction Layer) :以网络所要求的恰当方式对数据进行打包和发送,比较简单,先报 VCL 吐出来的数据 SODB 进行字节对齐,形成 RBSP ,最后把 RBSP 数据前面加上 NAL 头则组成一个 NALU 单元。
NALU = NALU Header + RBSP
但严格来讲 NALU = NALU Header + EBSP,而 EBSP = 防竞争的 RBSP,H.264 规范规定,编码器吐出来的数据需要在每个 NALU 添加起始码:0x00 00 01或者0x00 00 00 01, 用来指示一个 NALU 的起始 ,0x000000 时,也可以表示当前 NALU 的结束,如果 NALU 内部存在 0x00 00 01 or 0x000000 时,就要通过插入一个新的字节 0x03 防竞争。
NALU Header = forbidden_bit(1bit) + nal_reference_bit(2 bits )(优先级)+ nal_unit_type(5 bits )(类型)
NALU类型:
NALU 的类型即 RBSP 可以承载的数据类型。
Nalu_Type | NALU内容 | 备注 |
---|---|---|
0 | 未指定 | |
1 | 非 IDR 图像编码的 slice | 比如普通 I、P、B 帧 |
2 | 编码 slice 数据划分 A | 2 类型时,只传递片中最重要的信息,如片头,片中宏块的预测模式等;一般不会用到; |
3 | 编码 slice 数据划分 B | 3 类型是只传输残差;一般不会用到; |
4 | 编码 slice 数据划分C | 4 时则只可以传输残差中的AC系数;一般不会用到; |
5 | IDR 图像中的编码 slice | IDR 帧,IDR 一定是 I 帧但是 I 帧不一定是 IDR 帧。 |
6 | SEI 补充增强信息单元 | 可以存一些私有数据等; |
7 | SPS 序列参数集 | SPS 对如标识符、帧数以及参考帧数目、解码图像尺寸和帧场模式等解码参数进行标识记录 |
8 | PPS 图像参数集 | PPS 对如熵编码类型、有效参考图像的数目和初始化等解码参数进行标志记录。 |
9 | 单元定界符 | 视频图像的边界 |
10 | 序列结束 | 表明下一图像为 IDR 图像 |
11 | 码流结束 | 表示该码流中已经没有图像 |
12 | 填充数据 | 哑元数据,用于填充字节 |
13-23 | 保留 | |
24-31 | 未使用 |
VCL 输出的原始数据比特流 SODB 即 String Of Data Bits,其长度不一定是 8bit 的整数倍,为了凑成整数个字节,往往需要对 SODB 最后一个字节进行填充形成 RBSP, 最后一个不满 8bit 的字节第一 bit 位置 1 ,然后后面缺省的 bit 置 0 即可。
接着我们再从层次结构理解码率的构成
帧: 一副图像编码后的视频数据也叫做一帧,其中有 I 帧、B 帧、P 帧。
片: 一帧图像又可以划分为很多片,由一个片或者多个片组成。
宏块: 视频编码的最小处理单元,承载了视频的具体 YUV 信息,一片由一个或者多个宏块组成。
VideoToolbox
介绍一下 VideoToolBox 及关键接口的使用,如果对接口使用很清楚的同学可以直接跳过看提炼部分或后续章节。
使用说明
第一步:VTCompressionSessionCreate 创建视频编码器并设置编码器初始属性。
NSDictionary *pixelBufferOptions = @{
(NSString*) kCVPixelBufferPixelFormatTypeKey : @