Table of Contents:

开篇词丨为什么说视频开发是程序员的一片蓝海?

视频技术难上手的另一个重要原因就是它涉及的技术太多了

image.png
image.png

01|基本概念:从参数的角度看视频图像

image.png

分辨率

像素就只是一个带有颜色的小块。
图像的分辨率越高,图像就越清晰。

视频行业常见的分辨率有 QCIF(176x144)、CIF(352x288)、D1(704x576 或 720x576),还有我们比较熟悉的 360P(640x360)、720P(1280x720)、1080P(1920x1080)、2K(2560x1440)、 4K(3840x2160)、8K(7680x4320)等。

“P”全拼为Progressive译为逐行扫描,几P则表示纵向有多少行像素,比如:720P表示纵向有720行像素、1080P表示纵向有1080行像素。
随着分辨率越来越大,我们就开始用“k”值来表示,比如:2160P就开始用4k来称呼,但还是有人会叫2160P。
“k”表示的是横向排列有多少像素,比如:2k就是视频横向大约有2000列像素、4k就是视频横向大约有4000列像素。

位深

通常 R、G、B 各占 8 个位,也就是一个字节。8 个位能表示 256 种颜色值,那 3 个通道的话就是 256 的 3 次方个颜色值,总共是 1677 万种颜色。我们称这种图像是 8bit 图像,而这个 8bit 就是位深
位深越大,我们能够表示的颜色值就越多
图像的位深越大,需要的存储空间就会越大,传输这张图像使用的流量就会越多
目前我们大多数情况下看到的图像以及视频还是 8bit 位深的。

Stride

Stride 也可以称之为跨距,是图像存储的时候有的一个概念。它指的是图像存储时内存中每行像素所占用的空间。你可能会问,一张图像的分辨率确定了,那一行的像素值不就确定了吗?为什么还需要跨距这个东西呢?其实,为了能够快速读取一行像素,我们一般会对内存中的图像实现内存对齐,比如 16 字节对齐。
举个例子,我们现在有一张 RGB 图像,分辨率是 1278x720。我们将它存储在内存当中,一行像素需要 1278x3=3834 个字节,3834 除以 16 无法整除。因此,没有 16 字节对齐。所以如果需要对齐的话,我们需要在 3834 个字节后面填充 6 个字节,也就是 3840 个字节做 16 字节对齐,这样这幅图像的 Stride 就是 3840 了。如下图所示:
image.png
这个地方你一定要注意,每读取一行数据的时候需要跳过这多余的 6 个字节。如果没有跳过的话,这 6 个字节的像素就会被我们误认为是下一行开始的 2 个像素(每个像素 R、G、B 各占 1 个字节,2 个像素共 6 个字节)。那这样得到的图像就完全错了,显示出来的就是“花屏”现象,屏幕会出现一条条的斜线。

所以,不管你去读取还是渲染一张图片,还是说你将这张图片存储下来,都需要设置正确的 Stride。很多时候,尤其是不规则分辨率的时候,它和图像的 Width(R、G、B 的话就是 Width x 3)是不一样的。

有的时候即便图像的 Width 是一个规则的值,比如说 1920 或者 1280 等能被 16 整除的宽度,图像存储在内存中有可能 Stride 和 Width(R、G、B 的话就是 Width x 3)也是不一样的,尤其是不同的视频解码器内部实现的不同,会导致输出的图像的 Stride 不一样。

所以,一定要在处理图片的时候注意这个 Stride 值。如果出现一条条斜线的花屏或者说解码后图像的颜色不对的情况,我们需要先确认一下这个 Stride 值对不对。

stride为什么是16字节对齐?
一般芯片读取效率在16字节对齐时最高,芯片能一次性读取16字节的数据,16字节对齐之后可以充分利用芯片的性能,速度更快。当然并不是stride一定是16字节对齐的,有的也32字节对齐,甚至64字节对齐。

帧率

1 秒钟内图像的数量就是帧率。据研究表明,一般帧率达到 10~12 帧每秒,人眼就会认为是流畅的了。
通常,我们在电影院看的电影帧率一般是 24fps(帧每秒),监控行业常用 25fps,而我们声网常用的帧率有 15fps、24fps 和 30fps。你可以根据自己的使用场景来具体设定你想使用的帧率值。
选择帧率的时候还需要考虑设备处理性能的问题,尤其是实时视频通话场景。帧率高,代表着每秒钟处理的图像数量会很高,从而需要的设备性能就比较高。
如果是含有多个图像处理过程,比如人脸识别、美颜等算法的时候,就更需要考虑帧率大小和设备性能的问题。同样,也要考虑带宽流量的问题。帧率越大,流量也会越多,对带宽的要求也会越高。

码率

我们已经知道,视频的帧率越高,1 秒钟内的图像数据量就会越大。通常我们存储视频的时候需要对图像进行压缩之后再存储,否则视频会非常大。
那么压缩之后的视频我们一般如何描述它的大小呢?一般对于一个视频文件,我们直接看视频的大小就可以了。但是在实时通信或者直播的时候,视频是视频流的形式,我们怎么衡量呢?

码率是指视频在单位时间内的数据量的大小,一般是 1 秒钟内的数据量,其单位一般是 Kb/s 或者 Mb/s。通常,我们用压缩工具压缩同一个原始视频的时候,码率越高,图像的失真就会越小,视频画面就会越清晰。但同时,码率越高,存储时占用的内存空间就会越大,传输时使用的流量就会越多。
并不是码率越高,清晰度就会越高。
事实上,视频压缩之后的清晰度还跟压缩时选用的压缩算法,以及压缩时使用的压缩速度有关。压缩算法越先进,压缩率就会越高,码率自然就会越小。压缩速度越慢,压缩的时候压缩算法就会越精细,最后压缩率也会有提高,相同的清晰度码率也会更小。

02|YUV & RGB:原来图像是这么丰富多彩的

我们的眼睛每天看到的颜色是千变万化的。为了能够更方便地表示和处理这些颜色,不同应用领域就建立了多种不同的颜色空间,主要包括 RGB 、YUV、CMYK 、 HSI 等

RGB

RGB 是我们平常遇到最多的一种图像颜色空间,比如摄像头采集的原始图像就是 RGB 图像,且显示器显示的图像也是 RGB 图像。
需要注意的是 RGB 图像像素中 R、G、B 三个值并不一定是按 R、G、B 顺序排列的,也有可能是 B、G、R 顺序排列。
比如 OpenCV 就经常使用 BGR 的排列方式来存储图像。所以在存储和读取 RGB 图像的时候需要稍微注意一下。

YUV

虽然 RGB 比较简单,同时在图像处理的时候也经常会用到。但是在视频领域,我们更多地是使用 YUV 颜色空间来表示图像的。这是因为 R、G、B 三个颜色是有相关性的,所以不太方便做图像压缩编码。
YUV 最早主要是用于电视系统与模拟视频领域。现在视频领域基本都是使用 YUV 颜色空间。

跟 RGB 图像中 R、G、B 三个通道都跟色彩信息相关这种特点不同,YUV 图像将亮度信息 Y 与色彩信息 U、V 分离开来。Y 表示亮度,是图像的总体轮廓,称之为 Y 分量。U、V 表示色度,主要描绘图像的色彩等信息,分别称为 U 分量和 V 分量。这样一张图像如果没有了色度信息 U、V,只剩下亮度 Y,则依旧是一张图像,只不过是一张黑白图像。这种特点有什么好处呢?
这是考虑到兼容老的黑白电视机,如果使用 RGB 表示图像,那么黑白电视机就没办法播放。这是因为 R、G、B 三个通道都是彩色的,而 Y、U、V 就可以。因为黑白电视机可以使用 Y 分量,Y 分量就是黑白图像,而且包含了图像的总体轮廓信息,只是没有色彩信息而已。

YUV 的类型和存储方式
YUV 主要分为 YUV 4:4:4、YUV 4:2:2、YUV 4:2:0 这几种常用的类型。其中最常用的又是 YUV 4:2:0。这三种类型的 YUV 主要的区别就是 U、V 分量像素点的个数和采集方式。
总的来说:
YUV 4:4:4,每一个 Y 对应一组 UV。
YUV 4:2:2,每两个 Y 共用一组 UV。
YUV 4:2:0,每四个 Y 共用一组 UV。

YUV 4:4:4 这种类型非常简单,所以存储的方式也非常简单。
那 YUV 4:2:2 和 YUV 4:2:0 这种共用 U、V 分量的情况,应该在内存中怎么存储呢?下面我就来为你介绍一下。

YUV 存储方式主要分为两大类:Planar 和 Packed 两种。
Planar 格式的 YUV 是先连续存储所有像素点的 Y,然后接着存储所有像素点的 U,之后再存储所有像素点的 V,也可以是先连续存储所有像素点的 Y,然后接着存储所有像素点的 V,之后再存储所有像素点的 U。
Packed 格式的 YUV 是先存储完所有像素的 Y,然后 U、V 连续的交错存储。
image.png

RGB 和 YUV 之间的转换

在讲转换之前,我们先了解 Color Range 这个东西。对于一个 8bit 的 RGB 图像,它的每一个 R、G、B 分量的取值按理说就是 0~255 的。但是真的是这样的吗?其实不是的。这里就涉及到 Color Range 这个概念。Color Range 分为两种,一种是 Full Range,一种是 Limited Range。Full Range 的 R、G、B 取值范围都是 0~255。而 Limited Range 的 R、G、B 取值范围是 16~235。

了解了 Color Range 之后,我们怎么规范 YUV 和 RGB 之间的互转呢?其实这也是有标准的,目前的标准主要是 BT601 和 BT709(其实还有 BT2020,我们这里不展开讲)。简单来讲,BT709 和 BT601 定义了一个 RGB 和 YUV 互转的标准规范。只有我们都按照标准来做事,那么不同厂家生产出来的产品才能对接上。BT601 是标清的标准,而 BT709 是高清的标准。
转换公式
在做 RGB 往 YUV 转换的时候我们需要知道是使用的哪个标准的哪种 Range 做的转换
image.png

03|缩放算法:如何高质量地缩放图像?

缩放算法场景:
- 情形 1:播放窗口与原始图像分辨率不匹配的时候需要缩放。如电影分辨率是 1080P,播放器的窗口大小是 720P。
- 情形 2:我们在线观看视频时会有多种分辨率可以选择,即需要在一个图像分辨率的基础上缩放出多种不同尺寸的图像出来做编码,并保存多个不同分辨率的视频文件。
- 情形 3:RTC 场景,有的时候我们需要根据网络状况实时调节视频通话的分辨率。这个也是需要缩放算法来完成的。

目前图像的缩放算法非常多,其中主要包括最常用的插值算法和目前比较火的 AI 超分算法。
插值算法有很多种,但是其基本原理都是差不多的。它们都是使用周围已有的像素值通过一定的加权运算得到“插值像素值”。插值算法主要包括:最近邻插值算法(Nearest)、双线性插值算法(Bilinear)、双三次插值算法(BiCubic)等。

缩放的基本原理

假设原图像的分辨率是 w0 x h0,我们需要缩放到 w1 x h1。那我们只需要将目标图像中的像素位置(x,y)映射到原图像的(x * w0 / w1,y * h0 / h1),再插值得到这个像素值就可以了,这个插值得到的像素值就是目标图像像素点(x,y)的像素值。注意,(x * w0 / w1,y * h0 / h1)绝大多数时候是小数
其实就是两步:
1. 根据缩放比例算出目标图像在原图像的位置
2. 在原图像的位置利用插值计算像素值

三种插值算法

最近邻插值
- 首先,将目标图像中的目标像素位置,映射到原图像的映射位置。
- 然后,找到原图像中映射位置周围的 4 个像素。
- 最后,取离映射位置最近的像素点的像素值作为目标像素。
缺点:放大图像大概率会出现块状效应,而缩小图像容易出现锯齿
优点:不需要太多的计算,速度非常的快

线性插值
线性插值是一种以距离作为权重的插值方式,距离越近权重越大,距离越远权重越小。

双线性插值
双线性插值本质上就是在两个方向上做线性插值。由于图像是两个方向的二维数据,正好适合使用双线性插值算法。
双线性插值其实就是三次线性插值的过程,我们先通过两次线性插值得到两个中间值,然后再通过对这两个中间值进行一次插值得到最终的结果。

双三次插值(也叫 BiCubic 插值)
双三次插值算法的基本原理同前两种插值算法差不多,不同的是:
第一,双三次插值选取的是周围的 16 个像素,比前两种插值算法多了 3 倍。
第二,双三次插值算法的周围像素的权重计算是使用一个特殊的 BiCubic 基函数来计算的。
双三次插值需要计算 16 个点的权重再乘以像素值求和,相较于前面的最近邻插值和双线性插值计算量较大,但插值后的图像效果最好。

04|编码原理:视频究竟是怎么编码压缩的?

视频编码的原理

视频编码是对一帧帧图像来进行的。一般我们所熟知的彩色图像的格式是 RGB 的,即用红绿蓝三个分量的组合来表示所有颜色。但是,RGB 三个颜色是有相关性的,为了去掉这个相关性,减少需要编码的信息量,我们通常会把 RGB 转换成 YUV,也就是 1 个亮度分量和 2 个色度分量。
另外,人眼对于亮度信息更加敏感,而对于色度信息稍弱,所以视频编码是将 Y 分量和 UV 分量分开来编码的。

图像一般都是有数据冗余的,主要包括以下 4 种:
- 空间冗余。比如说将一帧图像划分成一个个 16x16 的块之后,相邻的块很多时候都有比较明显的相似性,这种就叫空间冗余。
- 时间冗余。一个帧率为 25fps 的视频中前后两帧图像相差只有 40ms,两张图像的变化是比较小的,相似性很高,这种叫做时间冗余。
- 视觉冗余。我们的眼睛是有视觉灵敏度这个东西的。人的眼睛对于图像中高频信息的敏感度是小于低频信息的。有的时候去除图像中的一些高频信息,人眼看起来跟不去除高频信息差别不大,这种叫做视觉冗余。
- 信息熵冗余。我们一般会使用 Zip 等压缩工具去压缩文件,将文件大小减小,这个对于图像来说也是可以做的,这种冗余叫做信息熵冗余。

视频编码就是通过减少上述 4 种冗余来达到压缩视频的目的。接下来我们就一起来慢慢剥开视频编码这个“洋葱”吧。

而对于每一帧图像,又是划分成一个个块来进行编码的,这一个个块在 H264 中叫做宏块,宏块大小一般是 16x16(H264、VP8),32x32(H265、VP9),64x64(H265、VP9、AV1),128x128(AV1)这几种

视频编码主要分为熵编码、预测、DCT 变换和量化这几个步骤。
1. 熵编码(以行程编码为例):视频编码中真正实现“压缩”的步骤,主要去除信息熵冗余。在出现连续多个 0 像素的时候压缩率会更高。
2. 帧内预测:为了提高熵编码的压缩率,先将当前编码块的相邻块像素经过帧内预测算法得到帧内预测块,再用当前编码块减去帧内预测块得到残差块,从而去掉空间冗余。
3. 帧间预测:类似于帧内预测,在已经编码完成的帧中,先通过运动搜索得到帧间预测块,再与编码块相减得到残差块,从而去除时间冗余。
4. DCT 变换和量化:将残差块变换到频域,分离高频和低频信息。由于高频信息数量多但大小相对较小,又人眼对高频信息相对不敏感,我们利用这个特点,使用 QStep 对 DCT 系数进行量化,将大部分高频信息量化为 0,达到去除视觉冗余的目的。

这里你需要注意的是,视频编码实际的步骤是预测、DCT 变换和量化,最后是熵编码。经过这几步操作之后,视频中的冗余信息大部分被去除,达到了编码压缩的效果

编码器的对比及选择

现在市面上常见的编码标准有 H264、H265、VP8、VP9 和 AV1。目前 H264 和 VP8 是最常用的编码标准,且两者的标准非常相似。H265 和 VP9 分别是他们的下一代编码标准,这两个标准也非常相似。AV1 是 VP9 的下一代编码标准。
H264 和 H265 是需要专利费的,而 VP8 和 VP9 是完全免费的。由于 H265 需要付高额的版权费,以谷歌为首的互联网和芯片巨头公司组织了 AOM 联盟,开发了新一代压缩编码算法 AV1,并宣布完全免费,以此来对抗高额专利费的 H265。
目前普通产品还是使用 H264 最多,而 H265 因为专利费使用得比较少。VP8 是 WebRTC 默认的编码标准,且 WebRTC 使用 VP8 最多。同时,WebRTC 也支持 VP9 和 AV1。YouTube 使用了 VP9 和 AV1。Netflix 也使用了 AV1。

05|码流结构:原来你是这样的H264

视频编码的码流结构其实就是指视频经过编码之后得到的二进制数据是怎么组织的,换句话说,就是编码后的码流我们怎么将一帧帧编码后的图像数据分离出来,以及在二进制码流数据中,哪一块数据是一帧图像,哪一块数据是另外一帧图像。

H264 的编码结构

帧类型

在 H264 中,帧类型主要分为 3 大类,分别是 I 帧、P 帧和 B 帧。
我们知道帧内预测不需要参考已编码帧,对已编码帧是没有依赖的,并可以自行完成编码和解码。而帧间预测是需要参考已编码帧的,并对已编码帧具有依赖性。帧间预测需要参考已经编码好了的帧内编码帧或者帧间编码帧。并且,帧间编码帧又可以分为只参考前面帧的前向编码帧,和既可以参考前面帧又可以参考后面帧的双向编码帧。
image.png
由于 P 帧和 B 帧需要参考其它帧。如果编码或者解码的过程中有一个参考帧出现错误的话,那依赖它的 P 帧和 B 帧肯定也会出现错误,而这些有问题的 P 帧又会继续作为之后 P 帧或 B 帧的参考帧。因此,错误会不断的传递。为了避免错误的不断传递,就有了一种特殊的 I 帧叫 IDR 帧,也叫立即刷新帧,它可以截断编码错误的传递

GOP

在 H264 中,还有一个 GOP 的概念也经常会遇到,它是什么意思呢?从一个 IDR 帧开始到下一个 IDR 帧的前一帧为止,这里面包含的 IDR 帧、普通 I 帧、P 帧和 B 帧,我们称为一个 GOP(图像组)
GOP 的大小是由 IDR 帧之间的间隔来确定的,而这个间隔我们有一个重要的概念来表示,叫做关键帧间隔。关键帧间隔越大,两个 IDR 相隔就会越远,GOP 也就越大;关键帧间隔越小,IDR 相隔也就越近,GOP 就越小。
GOP 越大,编码的 I 帧就会越少。相比而言,P 帧、B 帧的压缩率更高,因此整个视频的编码效率就会越高。但是 GOP 太大,也会导致 IDR 帧距离太大,点播场景时进行视频的 seek 操作就会不方便。
并且,在 RTC 和直播场景中,可能会因为网络原因导致丢包而引起接收端的丢帧,大的 GOP 最终可能导致参考帧丢失而出现解码错误,从而引起长时间花屏和卡顿

Slice

前面我们讲的是视频图像序列的层次结构,那图像内的层次结构是怎样的呢?
Slice 其实是为了并行编码设计的。什么意思呢?就是说,我们可以将一帧图像划分成几个 Slice,并且 Slice 之间相互独立、互不依赖、独立编码。
那么在机器性能比较高的情况下,我们就可以多线程并行对多个 Slice 进行编码,从而提升速度。但也因为一帧内的几个 Slice 是相互独立的,所以如果帧内预测的话,就不能跨 Slice 进行,因此编码性能会差一些。
总结来说,图像内的层次结构就是一帧图像可以划分成一个或多个 Slice,而一个 Slice 包含多个宏块,且一个宏块又可以划分成多个不同尺寸的子块。
image.png

H264 的码流结构

码流格式

H264 码流有两种格式:一种是 Annexb 格式;一种是 MP4 格式。两种格式的区别是:
- Annexb 格式使用起始码来表示一个编码数据的开始。起始码本身不是图像编码的内容,只是用来分隔用的。
- MP4 格式没有起始码,而是在图像编码数据的开始使用了 4 个字节作为长度标识

NALU(网络抽象层单元)

H264 设计了两个重要的参数集:一个是 SPS(序列参数集);一个是 PPS(图像参数集)。
其中,SPS 主要包含的是图像的宽、高、YUV 格式和位深等基本信息;PPS 则主要包含熵编码类型、基础 QP 和最大参考帧数量等基本编码信息。如果没有 SPS、PPS 里面的基础信息,之后的 I 帧、P 帧、B 帧就都没办法进行解码。
H264 的码流主要是由 SPS、PPS、I Slice、P Slice和B Slice 组成的。如下图所示:
image.png
我们知道了 H264 码流主要由 SPS、PPS 和三种 Slice 组成,那我们如何在码流中区分这几种数据呢?
为了解决这个问题,H264 设计了 NALU(网络抽象层单元)。
每一个 NALU 又都是由一个 1 字节的 NALU Header 和若干字节的 NALU Data 组成的

image.png

常见工程问题

多 Slice 时如何判断哪几个 Slice 是同一帧的?
在 Slice Header 开始的地方有一个 first_mb_in_slice 的字段,表示当前 Slice 的第一个宏块 MB 在当前编码图像中的序号。

如何从 SPS 中获取图像的宽高?
在 SPS 中有几个字段用来表示分辨率的大小。我们可以解码出这几个字段并通过一定的规则计算得到分辨率的大小。

如何计算得到 QP 值?
在 PPS 中有一个全局基础 QP,字段是 pic_init_qp_minus26。

06|帧内预测:如何减少空间冗余?

我们知道在视频编码时主要需要减少 4 个冗余,包括:空间冗余、时间冗余、视觉冗余和信息熵冗余。其中空间冗余就是通过帧内预测的方式来实现的。
其实一般来说,一幅图像中相邻像素的亮度和色度信息是比较接近的,并且亮度和色度信息也是逐渐变化的,不太会出现突变。也就是说,图像具有空间相关性。帧内预测就是利用这个特点来进行的。即帧内预测通过利用已经编码的相邻像素的值来预测待编码的像素值,最后达到减少空间冗余的目的。

不同块大小的帧内预测模式

在 H264 标准里面,块分为宏块和子块。宏块的大小是 16 x 16(YUV 4:2:0 图像亮度块为 16 x 16,色度块为 8 x 8)。在帧内预测中,亮度宏块可以继续划分成 16 个 4 x 4 的子块。因为图像中有的地方细节很多,我们需要划分成更小的块来做预测会更精细,所以会将宏块再划分成 4 x 4 的子块。

帧内预测是根据块的大小分为不同的预测模式的。还有一个点就是亮度块和色度块的预测是分开进行的。这里我给你总结一下,主要有以下 3 点规则:

4 x 4 亮度块的帧内预测模式

Vertical 模式
Vertical 模式就是指,当前编码亮度块的每一列的像素值,都是复制上边已经编码块的最下面那一行的对应位置的像素值。
Horizontal 模式
Horizontal 模式就是指,当前编码亮度块的每一行的像素值,都是复制左边已经编码块的最右边那一列的对应位置的像素值
DC 模式
DC 模式就是指,当前编码亮度块的每一个像素值,是上边已经编码块的最下面那一行和左边已编码块右边最后一列的所有像素值的平均值。
Diagonal Down-Left 模式
Diagonal Down-Left 模式是上边块和右上块的像素通过插值得到。如果上边块和右上块不存在则该模式无效。
Diagonal Down-Right 模式
Diagonal Down-Right 模式需要通过上边块、左边块和左上角对角的像素通过插值得到
Vertical-Right 模式
Vertical-Right 模式是需要通过上边块、左边块以及左上角对角的像素插值得到的
Horizontal-Down 模式
Horizontal-Down 模式需要通过上边块、左边块以及左上角对角的像素插值得到。
Vertical-Left 模式
Vertical-Left 模式是需要通过上边块和右上块最下面一行的像素通过插值得到
Horizontal-Up 模式
Horizontal-Up 模式是需要通过左边块的像素通过插值得到的

16 x 16 亮度块的帧内预测模式

16 x 16 亮度块总共有4种预测模式。它们分别是Vertical模式,Horizontal模式、DC 模式和Plane 模式

8 x 8 色度块的帧内预测模式

8 x 8 色度块的帧内预测模式跟 16 x 16 亮度块的是一样的,也是总共有 4 种,分别为 DC 模式、Vertical 模式,Horizontal 模式、Plane 模式。与 16 x 16 亮度块不同的是,块大小不同,所以参考像素值数量会不同。但是基本是一致的。

07|帧间预测:如何减少时间冗余?

在自然状态下,人或者物体的运动速度在 1 秒钟之内引起的画面变化并不大,且自然运动是连续的。所以前后两帧图像往往变化比较小,这就是视频的时间相关性。帧间预测就是利用这个特点来进行的。通过在已经编码的帧里面找到一个块来预测待编码块的像素,从而达到减少时间冗余的目的。

参考帧和运动矢量

在帧间预测中,我们会在已经编码的帧里面找到一个块来作为预测块,这个已经编码的帧称之为参考帧。
我们用运动矢量来表示编码帧中编码块和参考帧中的预测块之间的位置的差值。
我们先把运动矢量编码到码流当中,这样解码端只要解码出运动矢量,使用运动矢量就可以在参考帧中找到预测块了,我们再解码出残差(如果有的话),残差块加上预测块就可以恢复出图像块了。

运动搜索

从前面的讨论我们知道,运动搜索的目标就是在参考帧中找到一个块,称之为预测块,且这个预测块与编码块的差距最小。从计算机的角度来说就是,编码块跟这个预测块的差值,也就是残差块的像素绝对值之和(下面我们用 SAD 表示残差块的像素绝对值之和)最小。
现在是不是目标就清晰很多了。比如说当前编码块大小是 16 x 16,那我们就先去参考帧中找到一个个 16 x 16 的块作为预测块,并用当前编码块减去预测块求得残差块,然后用我们经常做的绝对值求和操作得到两者之间的差距,最后选择差距最小的预测块作为最终的预测块。
所以,我们运动搜索的方法就很简单了,就是从参考帧中第一个像素开始,将一个个 16 x 16 大小的块都遍历一遍。我们总是可以找到差距最小的块。这种方法我们称之为全搜索算法

运动矢量是通过运动搜索得到的,而运动搜索是在参考帧中进行的。通常我们会使用钻石搜索和六边形搜索等快速运动搜索算法。一般不会使用全搜索算法。其中钻石搜索算法更简单,步骤更少,所以如果需要编码速度快,一般选择钻石搜索。六边形搜索步骤更多,更精细,要求编码质量高,同时对速度要求不高的时候,可以选择六边形搜索。

亚像素插值和亚像素精度搜索

光做整像素运动搜索不太能够准确的处理连续运动场景。为了能够处理好这种连续运动的问题,我们对参考帧进行亚像素插值得到半像素和 1/4 像素图像。然后在整像素搜索的基础上在亚像素图像上做亚像素精度的运动搜索。实验数据证明,半像素和 1/4 像素精度的运动搜索相比整像素精度的运动搜索可以明显地提高压缩效率。

08|变换量化:如何减少视觉冗余?

通过帧内编码可以去除空间冗余,通过帧间编码可以去除时间冗余,而为了分离图像块的高频和低频信息从而去除视觉冗余,我们需要做 DCT 变换和量化。

我们今天一开始主要讲解了 DCT 变换的基本原理。DCT 变换主要是将图像从空域转换到频域,并将图像的高频和低频信息分离开来。虽然高频信息数据多,但是幅值比较小。这样高频信息在量化的过程中能够比较容易被减少。这样可以比较有效地减少图像的视觉冗余,从而达到压缩的目的。

接着,我们简单地介绍了一下量化的原理。量化其实就是一个除法操作。通过除法操作就可以将幅值变小,而高频信息幅值比较小,就比较容易被量化成 0,这样就能够达到压缩的目的。

常规视频编码中的 DCT 变换和量化

DCT 变换

DCT 变换,就是离散余弦变换。它能够将空域的信号(对于图像来说,空域就是你平时看到的图像)转换到频域(对于图像来说,就是将图像做完 DCT 变换之后的数据)上表示,并能够比较好的去除相关性。其主要用于视频压缩领域。现在常用的视频压缩算法中基本上都有 DCT 变换。

在视频压缩中,DCT 变换是在帧内预测和帧间预测之后进行的。也就是说,DCT 变换其实是对残差块做的。
图片经过 DCT 变换之后,低频信息集中在左上角,而高频信息则分散在其它的位置。通常情况下,图片的高频信息多但是幅值比较小。高频信息主要描述图片的边缘信息。
DCT 变换的计算过程中涉及到了 cos 函数。那也就是说计算的过程中一定涉及到了浮点运算。而浮点运算计算速度比较慢。

Hadamard 变换

Hadamard 变换可以代替 DCT 变换将残差块快速转换到频域,以便用来估计一下当前块编码之后的大小。
Hadamard 变换是没有浮点运算的?因此其计算速度很快,并且也能够将图像块从空域变换到频域。因此,我们可以用它一定程度上粗略的代替 DCT 变换,从而用来简化运算。

量化

前面我们讲了,我们将图像块变换到频域之后,AC 系数比较多,但是一般幅值比较小。并且,我们可以去除一些 AC 系数,达到压缩图像的目的,同时人眼看起来差距不大。这个去除 AC 系数的操作是什么呢?很明显就是量化了。

在量化过程中,最重要的就是 QStep(用户一般接触到的是 QP,两者可以查表转换)。

通常 QStep 值越大,DC 系数和 AC 系数被量化成 0 的概率也就越大,从而压缩程度就越大,但是丢失的信息也就越多。这个值太大了会造成视频出现一个个块状的效应,且严重的时候看起来像马赛克一样;这个值比较小的话,压缩程度也会比较小,从而图像失真就会比较小,但是压缩之后的码流大小就会比较大。

09|RTP & RTCP:如何正确地将视频装进RTP中?

RTP 协议

RTP(Real-time Transport Protocol)协议,全称是实时传输协议。它主要用于音视频数据的传输。
在传输的时候我们通常不会直接将编码码流进行传输,而是先将码流打包成一个个 RTP 包再进行发送。
这是因为我们的接收端要能够正确地使用这些音视频编码数据,不仅仅需要原始的编码码流,还需要一些额外的信息。比如说:
- 当前视频码流是哪种视频编码标准,是 H264、H265、VP8、VP9 还是 AV1 呢?
- 当我们知道编码标准了,我们就可以正确地解析码流,并解码出图像了。但是我们又会遇到一个新的问题,那就是按照什么速度播放视频呢?这个也需要 RTP 协议告知接收端。

RTP 包包括两个部分:第一个部分是 RTP 头;另外一个部分是 RTP 有效载荷。
RTP 有效载荷,其实就是 RTP 包里面的实际数据。如果是 H264 编码打包成 RTP 包,那有效载荷就是经过 H264 编码的码流;如果是 VP8 编码呢,那就是 VP8 码流。
image.png
image.png
如果你只负责传输 RTP 包,而不需要管传输过程中有没有丢包,以及传输 RTP 包的时候有没有引起网络拥塞的话,那你只需要使用 RTP 协议就可以了。比如说,你选择使用 TCP 协议传输 RTP 包的话就可以不用管这些事情,因为 TCP 协议具有丢包重传、拥塞控制等功能。

但是通常情况下,我们在传输音视频数据的时候不会使用 TCP 协议作为传输层协议。这是因为 TCP 协议更适合传输文本和文件等数据,而不适合传输实时音频流和视频流数据,所以我们通常会使用 UDP 协议作为音视频数据的传输层协议。但 UDP 协议不具有丢包重传和拥塞控制的功能,需要我们自己实现。那怎么办呢?

RTCP 协议

RTCP(Real-time Transport Control Protocol)协议,全称是实时传输控制协议。它是辅助 RTP 协议使用的。RTCP 报文有很多种,分别负责不同的功能。常用的报文有发送端报告(SR)、接收端报告(RR)、RTP 反馈报告(RTPFB)等。而每一种报告的有效载荷都是不同的。我们就是通过这些报告在接收端和发送端传递当前统计的 RTP 包的传输情况的。我们使用这些统计信息来做丢包重传,以及预测带宽。

不过,我需要再次强调一下,RTCP 协议只是用来传递 RTP 包的传输统计信息,本身不具有丢包重传和带宽预测的功能,而这些功能需要我们自己来实现。

我们知道 RTP 是用来传输实际的视频数据的。它就像一个快递盒,先装好视频,然后填好运送的视频基本信息和收件人信息,最后将视频运送到收件人手上。

而 RTCP 协议则像是一个用来统计快递运送情况的记录表。其中的 NACK 报告就是快递丢件情况的记录表。它记录着哪些快递丢了。发件人收到了 NACK 之后,可以重新寄一个同样的快递给收件人,防止收件人没有收到快递。在这里也就是将丢失的视频 RTP 包重传一遍。

虽然我们只讲了一种 RTCP 报告,但是其它的报告也是类似的。大多数报告都是用来记录传输信息的。

H264 RTP 打包

H264 码流是放在 RTP 的有效载荷部分的。因此有效载荷前面的 RTP 头部跟码流本身是没有关系的,所以我们可以直接先将头部的字段填好就可以。接下来我们需要将 H264 码流填充到 RTP 有效载荷中去。

RTP H264 码流打包分为三种方式:分别是单 NALU 封包方式、组合封包方式、分片封包方式。顾名思义,单 NALU 封包方式是一个 NALU 打一个 RTP 包;而组合封包方式就是多个 NALU 打一个 RTP 包;分片封包方式则是一个 NALU 分开放在连续的多个 RTP 包中。

1、单 NALU 封包方式
这种打包方式适合于单个 RTP 包小于 1500 字节(MTU 大小)的时候。一般来说,一些 P 帧和 B 帧编码之后比较小,就可以使用这种打包方式。
image.png

2、组合封包方式

image.png
这种打包方式适合于单个 NALU 很小的时候。因此,我们将多个 NALU 打包到一起也小于 1500 字节的时候就可以使用。但是由于一般多个视频帧加到一起还小于 1500 的情况比较少,所以视频数据的 RTP 打包一般来说用组合封包方式的情况也很少。

3、分片封包方式
分片封包就更复杂一些了,但却是我们经常用到的打包方式。
这种打包方式主要用于将 NALU 数据打包成一个 RTP 包时大小大于 1500 字节的时候,这是经常使用的视频 RTP 打包方法。
image.png

我们怎么选择使用哪种方式打包呢?一般来说,我们在一个 H264 码流中会混合使用多种 RTP 打包方式。一般来说,对于小的 P 帧、B 帧还有 SPS、PPS 我们可以使用单个 NALU 封包方式。而对于大的 I 帧、P 帧或 B 帧,我们使用分片封包方式。

10|带宽预测:3大算法准确预估网络带宽

一般情况下,音视频场景中的拥塞控制和丢包重传等算法的基础就是 RTP 和 RTCP 协议。我们需要通过 RTP 包的信息和 RTCP 包中传输的统计信息来做拥塞控制和丢包重传等操作。
准确的预测带宽是实时视频通话技术里面的一个非常重要的环节。
带宽预测几乎是整个音视频传输和弱网对抗中最重要的环节,也是最难的一个环节。
如果不能够很好地预测出实际带宽,那有可能引起数据超发,导致发送数据量大于实际网络的承受能力,继而引起视频画面的延时和卡顿;也有可能预测的带宽太低,导致发送的数据量远低于实际网络的承受能力,不能很好地利用网络带宽,最终导致视频画面模糊和很明显的马赛克现象
image.png
WebRTC 中带宽预测主要分为基于延时的带宽预测算法、基于丢包的带宽预测算法以及最大带宽探测算法。

基于延时的带宽预测算法主要是解决网络中含有大缓冲网络设备场景的带宽预测。基于丢包的带宽预测算法主要是解决网络中有小缓冲或无缓冲网络设备场景的带宽预测。最终预估带宽等于这两者预测到的带宽值中的最小值。

同时,为了防止出现发送码率大幅低于实际网络带宽而导致网络带宽预估偏低的问题,我们还引入了最大带宽探测算法,可以周期性的探测网络的最大带宽。如果当前网络不是处于过载状态同时又探测到了最大带宽的话,就将预估带宽更新为探测到的最大带宽。

带宽预测

带宽预测,顾名思义,就是实时预测当前的网络带宽大小。预测出实际的带宽之后,我们就可以控制音视频数据的发送数据量。比如说,控制音视频数据的编码码率或者直接控制发送 RTP 包的速度,这都是可以的。控制住音视频发送的数据量是为了不会在网络带宽不够的时候,我们还发送超过网络带宽承受能力的数据量,最后导致网络出现长延时和高丢包等问题,继而引发接收端出现延时高或者卡顿的问题。因此,带宽预测是非常重要的。

而现在的网络中,大多存在两种类型的网络设备:一种是有较大缓存的;一种是没有缓存或者缓存很小的。
- 前者在网络中需要转发数据过多的时候,会把数据先缓存在自己的缓冲队列中,等待前面的数据发送完之后再发送当前数据。这种情况就会在网络带宽不够的时候,需要当前数据等一段时间才能发送,因此表现出来的现象就是网络不好时,延时会加大。
- 后者在网络中需要发送的数据过多的时候,会直接将超过带宽承受能力的数据丢弃掉。这种情况就会在网络带宽不够的时候,出现高丢包的现象。

基于延时的带宽预测算法

基于延时的带宽预测算法主要是通过计算一组 RTP 包它们的发送时长和接收时长,来判断当前延时的变化趋势,并根据当前的延时变化趋势来调整更新预测的带宽值。

基于延时的带宽预测算法,主要有 4 个步骤:
1. 计算一组 RTP 包的发送时长和接收时长,并计算延时;
2. 需要根据当前延时和历史延时的大小来计算延时变化的趋势;主要来平滑掉网络噪声引起的单个延时抖动。
3. 根据延时变化趋势判断网络状况;
4. 根据网络状况调整更新预测带宽值。

基于丢包的带宽预测算法

基于丢包的带宽预测算法相比基于延时的带宽预测算法简单很多,没有那么多步骤。其整体思路就是根据 Transport-CC 报文反馈的信息计算丢包率,然后再根据丢包率的多少直接进行带宽调整更新。

最大带宽探测算法

我们在网络变差的时候,预估带宽会快速的被下调,但是网络变好的时候预估带宽会比较缓慢的上升,同时如果当前发送码率比较小的话,预估带宽还会被限制,不能超过 1.5 倍的接收码率。

也就是说,如果我们当前视频处于静止画面的状态,发送的码率会很小。这样预估带宽就很难从一个比较小的带宽调整上去。如果此时画面突然动起来,即便实际网络带宽足够,还是会因为预估带宽不够而限制发送码率,从而导致画面出现模糊和马赛克等问题。

还有就是程序刚开始启动的时候,预估带宽的爬升也需要慢慢的加上去。但是在做音视频通信的时候,有可能一开始就会出现视频画面变化比较快的情况,这样可能刚开始的时候视频画面就会是模糊的或者有很多马赛克。

为了防止出现发送码率大幅低于实际网络带宽而导致网络带宽预估偏低的问题,我们还引入了最大带宽探测算法,可以周期性的探测网络的最大带宽。如果当前网络不是处于过载状态同时又探测到了最大带宽的话,就将预估带宽更新为探测到的最大带宽。

11|码控(视频码率控制)算法:如何控制视频的编码码率?

码控的原理

码控,顾名思义,就是码率控制,它是编码器的一个重要模块,主要的作用就是用算法来控制编码器输出码流的大小。
其实码控就是为每一帧编码图像选择一个合适的 QP 值的过程。
我们知道当一帧图像的画面确定了之后,画面的复杂度和 QP 值几乎决定了它编码之后的大小。由于编码器无法决定画面的复杂度,因此,码控的目标就是选择一个合适的 QP 值,以此来控制编码后码流的大小。

码控的类型

常用的码控算法主要有:VBR(动态码率)、CQP(恒定 QP)、CRF(恒定码率因子)和 CBR(恒定码率)这几种。

VBR(动态码率)
VBR 指的是编码器输出码率随着原始视频画面复杂度的变化不断的变化。通常当画面复杂或者说运动比较多的时候使用的码率会比较高;而当画面比较简单的时候使用的码率会比较低。VBR 主要的目标是保证视频画面质量,因此比较适合视频点播和短视频场景使用

CQP
CQP 很简单就是从头到尾每一个画面都是用同一个 QP 值去编码。
CQP 一般用来衡量编码算法的性能,在实际工程当中不会使用。

CRF
CRF 是 x264 默认的码控算法。它与 CQP 不同的是它的 QP 是会变化的。在画面运动大的时候,它会根据具体算法提高 QP 值;在画面运动小的时候,它会降低 QP 值。
它的思想是:运动很大的时候,人眼不太关注细节,因此 QP 可以稍微大一点;运动比较小的时候,人眼会将注意力放在细节上面,因此 QP 稍微小一点。

CBR
另外一种码控算法就是 CBR 了,它是恒定码率的。这种码控方式用户需要设置一个目标码率值给编码器。编码器在编码的时候不管图像画面复杂或简单、运动多或运动少的时候,都尽量使得输出的码率接近设置的目标码率。
这种方式非常适合 RTC 场景,因为 RTC 场景希望编码的码率跟实际预测的带宽值接近,不能超出目标码率太多,也希望能够尽量有效地利用可用带宽,不能太低于目标码率,从而尽量保证编码后图像画面清晰。

CBR 算法

其实,为了实现恒定码率,我们需要做很多个步骤,一步步的将输出码率逼近目标码率,而不是一步到位确定 QP 就可以实现恒定码率的目标的。所以,我们会分很多级做调整,分别是帧组级、帧级、宏块组 GOM(Group of MB)级。

具体的操作过程如下:
- 先确定帧组级(帧组就是将连续的几个帧组成一组,一般选择 8 个帧一组)的输出大小尽量接近目标码率。
- 然后,确定组内的每一帧具体应该分配多少的大小(称之为目标帧大小),才能保证帧组最后输出的大小可以达到要求。
- 接下来,我们再根据这个目标帧大小,确定一个帧级的 QP 值。
- 之后,我们再确定帧内的宏块组(宏块组就是连续的几行宏块组成的一组宏块,一般可以选 4 行宏块)应该分配多少大小,来保证当前帧最后的输出大小能接近于目标帧大小。
- 最后,我们再确定宏块的 QP 值。
- 还有一个很重要的事情,就是我们需要能够保证在不同的画面复杂度和不同的运动程度的情况下,并且输出码率都要尽量接近目标码率的话,我们还需要先计算得到当前帧的复杂度。

12|Jitter Buffer:拿什么拯救你,花屏卡顿问题

之前的两节课我们讲述了如何做好带宽预测和码率控制。好的带宽预测算法能够比较准确地预测出实际的网络带宽,而好的码率控制算法能够使得编码输出码率贴合实际网络带宽。这两个算法是视频流能够在各种网络状况下流畅播放的最基础的前提。

在讲述具体问题之前,先简单介绍一下 Jitter Buffer 这个模块。它是好几个卡顿和花屏问题的处理模块。Jitter Buffer 工作在接收端,主要功能就是在接收端收到包之后进行组帧,并判断帧的完整性、可解码性、发送丢包重传请求、发送关键帧请求以及估算网络抖动的。其中组帧、判断帧完整性、判断帧可解码性、丢包重传、关键帧请求都是这节课的重点。
Jitter Buffer 在接收端所处的位置如下图所示:
image.png

image.png

13|SVC:如何实现视频编码可伸缩?

什么是 SVC

SVC 是指一个码流当中,我们可以分成好几层。比如说分成三层:

第 0 层是最底层,可以独立进行编解码,不依赖第 1 层和第 2 层;
第 1 层编解码依赖于第 0 层,但是不依赖于第 2 层;
第 2 层的编解码需要依赖于第 0 层和第 1 层;

并且,第 0 层质量最低,第 0 层加第 1 层次之,三层加在一起的时候质量最高。注意这里的质量不是直接指的画面质量,而是帧率、分辨率的高低所代表的质量。

这样分层有什么好处呢?好处就是我们编码一个码流,可以组合出好几个不同的可解码码流出来。比如说上面三层 SVC 的例子:第 0 层就是一个可以独立解码的码流;第 0 层加上第 1 层也是一个可以独立解码的码流;第 0 层加上第 1 层和第 2 层也是一个可以解码的码流。

对于网络差的人,服务器给他转发第 0 层码流对应的 RTP 包;对于网络中等的人来说,服务器给他转发第 0 层加第 1 层码流对应的 RTP 包;对于网络很好的人,服务器给他直接转发所有层码流的 RTP 包。这样是不是就对大家都比较公平了。那具体怎么实现 SVC 分层编码呢?服务器又怎么转发呢?这里我给出我的思路,可供你参考。

SVC 的分类

根据是在帧率上做 SVC 还是在分辨率上做 SVC,我们可以将 SVC 分为时域 SVC 和空域 SVC 两种。

时域 SVC

空域 SVC

下面,我们介绍另一种 SVC 编码,空域 SVC。空域 SVC 是在分辨率上做分层。比如说,我们现在需要编码一个 720P 的视频。我们分成两层:第 0 层是 360P 的分辨率;第 0 层加第 1 层是 720P 的分辨率
但是我必须要说明一下,H264、H265、VP8 这些常用的编码标准(除了扩展)都是不支持空域 SVC 的

因此,在 WebRTC 中直接使用多个编码器编码多种分辨率的方式代替空域 SVC。

14|MP4 & FLV:不要再说AVI了

主要介绍了一下两种音视频封装格式,分别是 FLV 和 MP4。这两种封装格式是我们工作和生活中经常需要用到的。

FLV 在流媒体场景经常会用到,其实直播 RTMP 协议和 HTTP-FLV 协议里面也是用的 FLV 封装,所以还是很重要的。

MP4 封装就是平时视频文件最常用的封装了,它主要由一个个 box 组成,其中最重要的就是跟 sample 有关的 box,你需要重点掌握。当然你也不需要背下来,了解主要思想即可,等真正用到的时候查询一下就可以了。

15|音画同步:如何让声音和画面手拉手前进?

音视频同步的类型

音视频同步主要的类型有三种:视频同步到音频、音频同步到视频、音频和视频都做调整同步。

首先,视频同步到音频是指音频按照自己的节奏播放,不需要调节。如果视频相对音频快了的话,就延长当前播放视频帧的时间,以此来减慢视频帧的播放速度。如果视频相对音频慢了的话,就加快视频帧的播放速度,甚至通过丢帧的方式来快速赶上音频。
这种方式是最常用的音视频同步方式

其次,音频同步到视频是指视频按照自己的节奏播放,不需要调节。如果音频相对视频快了的话,就降低音频播放的速度,比如说重采样音频增加音频的采样点,延长音频的播放时间。如果音频相对视频慢了,就加快音频的播放速度,比如说重采样音频数据减少音频的采样点,缩短音频的播放时间。
这里需要格外注意的是,当音频的播放速度发生变化,音调也会改变,所以我们需要做到变速不变调,
一般来说这种方式是不常用的,因为人耳的敏感度很高,相对于视频来说,音频的调整更容易被人耳发现。因此对音频做调节,要做好的话,难度要高于调节视频的速度,所以我们一般不太会使用这种同步方法。

最后一种是音频和视频都做调整,具体是指音频和视频都需要为音视频同步做出调整。比如说 WebRTC 里面的音视频同步就是音频和视频都做调整,如果前一次调节的是视频的话,下一次就调节音频,相互交替进行,整体的思路还是跟前面两种方法差不多。音频快了就将音频的速度调低一些或者将视频的速度调高一些,视频快了就将视频的速度调低一些或者将音频的速度调高一些。这种一般在非 RTC 场景也不怎么使用。

视频同步到音频

我们主要是通过计算视频时钟和音频时间之间的差值 diff,来调节当前播放视频帧的播放时间 last_duration。如果 diff 大于 0,则加大 last_duration 的值,让视频速度慢下来,等等后面的音频;如果 diff 小于 0,则减小 last_duration 的值,让视频播放的速度快起来,赶上前面的音频。这就是音视频同步的原理。

加餐|基于纯浏览器的视频会议方案探究

结束语丨信心比黄金更重要