像素
1.像素是图像的基本单元,一个个像素就组成了图像
分辨率
1.图像(或视频)的分辨率是指图像的大小或尺寸
2.一张 1920x1080 的图像,前者 1920 指的是该图像的宽度方向上有 1920 个像素点,而后者 1080 指的是图像的高度方向上有 1080 个像素点
3.常见的分辨率有QCIF(176x144)、CIF(352x288)、360P(640x360)、720P(1280x720)、1080P(1920x1080)、4K(3840x2160)
同样一张图像用不同的分辨率表示

第 2 句话不够严谨。原始图像的话,分辨率越高确实会越清
晰,但是我们看到的图像往往是经过后期处理的,比如放大缩小,或者磨皮美颜。经过处理过后的图像,尤其是放大之后的图像,分辨率很高,但是它并没有很清晰,放大的图像是通过“插值”处理得到的,而插值的像素是使用邻近像素经过插值算法计算得到的,跟实际相机拍摄的像素是不一样的,相当于“脑补”出来的像素值
位深
1.像素是由 R、G、B 三个值组成的(有的时候还会有 Alpha
值,代表透明度
2.通常 R、G、B 各占 8 个位,也就是一个字节。8 个位能表示 256 种颜色值,那 3 个通道的话就是 256 的 3 次方个颜色值,总共是 1677 万种颜色。我们称这种图像是 8bit 图像,而这个 8bit 就是位深。
3.,位深越大,我们能够表示的颜色值就越多,位深越大,我们能够表示的颜色值就越多
Stride(跨距)
1.图像存储时内存中每行像素所占用的空间

一张 RGB 图像,分辨率是 1278x720。我们将它存储在内存当中, 一行像素需要 1278x3=3834 个字节,3834 除以 16
无法整除。因此,没有 16 字节对齐。所以如果需要对齐的话,我们需要在 3834 个字节后面填充 6 个字节,也就是 3840个字节做 16
字节对齐,这样这幅图像的 Stride 就是 3840 了 每读取一行数据的时候需要跳过这多余的 6 个字节。如果没有跳 过的话,这 6
个字节的像素就会被我们误认为是下一行开始的 2 个像素(每个像素 R、G、B 各占 1 个字节,2 个像素共 6
个字节)。那这样得到的图像就完全错了,显示出来的就是“花屏”现象,屏幕会出现一条条的斜线
2.不同的视频解码器内部实现的不同,会导致输出的图像的 Stride 不一样。
帧率
1.1 秒钟内图像的数量就是帧率
2.帧率高,代表着每秒钟处理的图像数量会很高,从而需要的设备性能就比较高
码率
1.视频在单位时间内的数据量的大小,一般是 1 秒钟内的数据量,其单位一般是 Kb/s 或者 Mb/s
2.用压缩工具压缩同一个原始视频的时候,码率越高,图像的失真就会越小,视频画面就会越清晰。但同时,码率越高,存储时占用的内存空间就会越大,传输时使用的流量就会越多
并不是码率越高,清晰度就会越高
视频压缩之后的清晰度还跟压缩时选用的压缩算法,以
及压缩时使用的压缩速度有关。压缩算法越先进,压缩率就会越高,码率自然就会越小。压缩速度越慢,压缩的时候压缩算法就会越精细,最后压缩率也会有提高,相同的清晰度,码率也会更小
小结

码率可以是固定的,也可以是变化的。
如果是固定码率,
固定码率:
当然不同的码率其视频效果,文件大小等都是不一样的。在流式播放方案中使用固定码率最为有效。使用固定码率时,比特率在流的进行过程中基本保持恒定并且接近目标比特率,始终处于由缓冲区大小确定的时间窗内。固定码率的缺点在于编码内容的质量不稳定。因为内容的某些片段要比其他片段更难压缩,所以图像的某些部分质量就比其他部分差。此外,固定码率会导致相邻流的质量不同。通常在较低比特率下,质量的变化会更加明显。
动态码率:
动态码率近年来在视频编码处理中应用是比较多的。当编码内容中混有简单数据和复杂数据(例如,在快动作和慢动作间切换的视频)时,动态码率是很有优势的。使用动态码率时,系统将自动为内容的简单部分分配较少的比特,从而留出足量的比特用于生成高质量的复杂部分。这意味着复杂性恒定的内容(例如新闻播音)不会受益于动态码率。对混合内容使用动态码率时,在文件大小相同的条件下,动态码率的输出结果要比固定码率的输出结果质量好得多。在某些情况下,与固定码率文件质量相同的动态码率文件,其大小可能只有前者的一半
一般来说除非要求绝对固定,不然不会填充数据。毕竟浪费带宽。
你的回答考虑地很全面。
RGB

YUV
1.YUV 图像将亮度信息Y 与色彩信息 U、V 分离开来。Y 表示亮度,是图像的总体轮廓,称之为 Y 分量。U、V表示色度,主要描绘图像的色彩等信息,分别称为 U 分量和 V 分量
2.YUV 主要分为 YUV 4:4:4、YUV 4:2:2、YUV 4:2:0 这几种常用的类型

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

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。

在做 RGB往 YUV 转换的时候我们需要知道是使用的哪个标准的哪种 Range 做的转换
系统采集出来给到用户的图像就是 YUV 的话,你也需要获取这个 YUV 的存储格式、转换标准和 Color Range。这样才能保证正确地处理 YUV 和 RGB 之间的转换
情形 1:播放窗口与原始图像分辨率不匹配的时候需要缩放。
情形 2:我们在线观看视频时会有多种分辨率可以选择,即需要在一个图像分辨率的基础上缩放出多种不同尺寸的图像出来做编码,并保存多个不同分辨率的视频文件。
情形 3:RTC 场景,有的时候我们需要根据网络状况实时调节视频通话的分辨率。这个也是需要缩放算法来完成的
缩放的基本原理
1.先将目标图像的像素位置映射到原图像的对应位置上,然后把通过插值计算得到的原图像对应位置的像素值作为目标图像相应位置的像素值
2.我们只需要将目标图像中的像素位置(x,y)映射到原图像的(x * w0 / w1,y * h0 / h1),再插值得到这个像素值就可以了,这个插值得到的像素值就是目标图像像素点(x,y)的像素值


三种插值算法
1.最近邻插算法
首先,将目标图像中的目标像素位置,映射到原图像的映射位置。
然后,找到原图像中映射位置周围的 4 个像素。
最后,取离映射位置最近的像素点的像素值作为目标像素。

2.双线性插值算法
取待插值像素周围的 4 个像素,将这 4 个像素值通过一定的运算得到最后的插值像素
线性插值是在两个点中间的某一个位置插值得到一个新的值。线性插值认为,这个需要插值得到的点跟这两个已知点都有一定的关系,并且,待插值点与离它近的那个点更相似。
因此,线性插值是一种以距离作为权重的插值方式,距离越近权重越大,距离越远权重越小。
双线性插值本质上就是在两个方向上做线性插值
双线性插值其实就是三次线性插值的过程

假设我们要插值求的点是 p 点,其坐标为 (x,y)。已知周围 4 个像素分别是 a、b、c、d。我们先通过 a 和 b 水平线性插值求得 m,再通过 c、d 水平插值求得 n。有了 m 和 n之后,再通过 m、n 垂直插值求得 p 点的像素值

举例
720P 放大到 1080P 为例,那么 1080P 图像中的目标像素点(2,2)的双线性插值过程
首先,将目标像素点(2,2)映射到原图像的(1.33,1.33)位置,对应下面图中的点p。找到(1.33,1.33)周围的 4 个像素(1,1)、(2,1)、(1,2)和(2,2),分别对应图中的点a、b、c和d。

先通过这 4 个像素插值得到中间像素 m 和 n 的像素值。m 和 n 的坐标分别为(1.33,1)和(1.33,2)。通过上面的公式可以求得点 p(1.33,1.33)的像素值是:

插值求得(1.33,1.33)的值之后,将其赋值给 1080P 目标图像的(2,2)位置的像素点就可以了。这就是双线性插值的过程
双三次插值算法
基本原理同前两种插值算法差不多,不同的是:
第一,双三次插值选取的是周围的 16 个像素,比前两种插值算法多了 3 倍。
第二,双三次插值算法的周围像素的权重计算是使用一个特殊的 BiCubic 基函数来计算
的。
总结

双三次插值需要周围 16 个像素,对于左上角的点,比如(0.5,0.5),它周围不够 16 个点怎么办呢?
一般将第一行和第一列复制填充一下。
视频编码的原理
通过帧内预测或者帧间预测去除空间冗余和时间冗余,从而得
到一个像素值相比编码块小很多的残差块。之后我们再通过 DCT 变换将低频和高频信息分离开来得到变换块,然后再对变换块的系数做量化。由于高频系数通常比较小,很容易量化为 0,同时人眼对高频信息不太敏感,这样我们就得到了一串含有很多个 0,大多数情况下是一串含有连续 0 的“像素串”,并且人的观感还不会太明显。这样,最后熵编码就能
把图像压缩成比较小的数据,以此达到视频压缩的目的
宏块
每一帧图像,划分成一个个块来进行编码的,这一个个块在 H264 中叫做宏块,宏块大小一般是16x16(H264、VP8),32x32(H265、VP9),64x64(H265、VP9、AV1),128x128(AV1)这几种。
空间冗余。
比如说将一帧图像划分成一个个 16x16 的块之后,相邻的块很多时候都有比 较明显的相似性,这种就叫空间冗余。
时间冗余。
一个帧率为 25fps 的视频中前后两帧图像相差只有 40ms,两张图像的变化 是比较小的,相似性很高,这种叫做时间冗余。
视觉冗余。
我们的眼睛是有视觉灵敏度这个东西的。人的眼睛对于图像中高频信息的敏感度是小于低频信息的。有的时候去除图像中的一些高频信息,人眼看起来跟不去除高 频信息差别不大,这种叫做视觉冗余。
信息熵冗余。
我们一般会使用Zip 等压缩工具去压缩文件,将文件大小减小,这个对于 图像来说也是可以做的,这种冗余叫做信息熵冗余。
对于一个 YUV 图像,我们把它划分成一个个 16x16 的宏块(以 H264 为例),Y、U、V分量的大小分别是 16x16、8x8、8x8。这里我们只对 Y 分量进行分析(U、V 分量同理)。假设 Y 分量这 16x16 个像素就是一个个数字,我们从左上角开始之字形扫描每一个像素值,则可以得到一个“像素串”。

行程编码
“aaaabbbccccc” 压缩成 “4a3b5c”,字符串由 13 个字节压缩到 7 个字节,这个叫做行程编码 对图像宏块扫描出来的这个“像素串”做同样的行程编码操作
如果想要达到压缩的目的,我们必须要使得编码前的字符串中出现比较多连续相同的字符。这对于图像块也是一样的。我们需要使得扫描出来的“像素串”,也尽量出现连续相同的像素值,最好是一连串数字很小(比如 0)的“像素串”,因为 0 在二进制中只占 1 个位就可以了(有的编码算法是可以做到的,比如指数哥伦布编码,它就可以做到 0 只占用一个位。事实上,算术编码可以做到一个符号只占用 0 点几个位)
帧内预测
是在当前编码图像内部已经编码完成的块中找到与将要编码的块相邻的块。一般就是即将编码块的左边块、上边块、左上角块和右上角块,通过将这些块与编码块相邻的像素经过多种不同的算法得到多个不同的预测块。然后我们再用编码块减去每一个预测块得到一个个残差块。最后,我们取这些算法得到的残差块中像素的绝对值加起来最小的块为预测块。而得到这个预测块的算法为帧内预测模式。

帧间预测
在前面已经编码完成的图像中,循环遍历每一个块,将它作为预测块,用当前的编码块与这个块做差值,得到残差块,取残差块中像素值的绝对
值加起来最小的块为预测块,预测块所在的已经编码的图像称为参考帧。预测块在参考帧中的坐标值 (x0, y0) 与编码块在编码帧中的坐标值 (x1, y1) 的差值 (x0 - x1, y0 - y1) 称之为运动矢量。而在参考帧中去寻找预测块的过程称之为运动搜索。事实上编码过程中真正的运动搜索不是一个个块去遍历寻找的,而是有快速的运动搜索算法的
如何做到将这串像素值变成有很多 0 的“像素串”
人眼对高频信息不太敏感,因为人眼看到的效果可能差别不大,所以我们可以去除一些高频信息
DCT 变换和量化
为了分离图像块的高频和低频信息,我们需要将图像块变换到频域。常用的变换是 DCT 变换

低频信息在左上角,其余的都是高频信息。那么如果我们对变换块的像素值进行“之字形”扫描,这样得到的像素串,前面的就是数值比较大的低频系数,后面就是数值比较小的高频部分
量化
我们让变换块的系数都同时除以一个值,这个值我们称之为量化步长,也就是QStep(QStep 是编码器内部的概念,用户一般使用量化参数 QP 这个值,QP 和 QStep一一对应)QStep越大,得到量化后的系数就会越小。同时,相同的 QStep 值,高频系数值相比低频系数值更小,量化后就更容易变成 0

QStep越大,得到量化后的系数就会越小。同时,相同的 QStep 值,高频系数值相比低频系数值更小,量化后就更容易变成 0。这样一来,我们就可以将大部分高频系数变成 0
有损编码
QStep 越大,损失就越大。QStep 跟 QP 一一对应,也就是说确定
了一个 QP 值,就确定了一个 QStep。所以从编码器应用角度来看,QP 值越大,损失就越大,从而画面的清晰度就会越低。同时,QP 值越大系数被量化成 0 的概率就越大,这样编码之后码流大小就会越小,压缩就会越高
编码器

从上面表格中可以看到,标准越新,最大编码块就越大,块划分的方式也越多,编码模式也就越多。因此压缩效率也会越高,但是带来的编码耗时也越大。所以在选择编码器的时候需要根据自己的实际应用场景来选择,同时还需要考虑专利费的问题。还有一个就是考虑有没有硬件支持的问题。目前 H264 和 H265 的硬件支持已经很好了,AV1 才刚开始,
硬件支持较少,之后会有更多硬编硬件支持。

如果是在性能比较差的机器上编码,最好使用 H264 和 VP8 等速度快的编码器。如果是在比较新的机器上,可以考虑 H265 编码。中等机器如果支持 H265 硬编也是可以考虑的。但有一个问题就是 H265 需要考虑专利费的问题,同时浏览器原生不支持 H265 编码,所以有这方面需求的,最好不要使用 H265,可以考虑使用 VP9,甚至可以考虑AV1。另外,由于 AV1 原生标准就支持屏幕编码的优化,所以屏幕编码场景下可以考虑使
用 AV1 编码
小结
视频编码主要分为熵编码、预测、DCT 变换和量化这几个步骤。
这里你需要注意的是,视频编码实际的步骤是预测、DCT 变换和量化,最后是熵编码。
1.熵编码(以行程编码为例):视频编码中真正实现“压缩”的步骤,主要去除信息熵冗余。在出现连续多个 0 像素的时候压缩率会更高。
2.帧内预测:为了提高熵编码的压缩率,先将当前编码块的相邻块像素经过帧内预测算法得到帧内预测块,再用当前编码块减去帧内预测块得到残差块,从而去掉空间冗余。
3.帧间预测:类似于帧内预测,在已经编码完成的帧中,先通过运动搜索得到帧间预测块,再与编码块相减得到残差块,从而去除时间冗余。
4.DCT 变换和量化:将残差块变换到频域,分离高频和低频信息。由于高频信息数量多但大小相对较小,又人眼对高频信息相对不敏感,我们利用这个特点,使用 QStep 对DCT 系数进行量化,将大部分高频信息量化为 0,达到去除视觉冗余的目的
视频编码过程中,一帧图像能同时进行帧内预测和帧间预测吗?
1.一帧图像即存在空间冗余又存在时间冗余,所以是帧间预测和帧内预测都是可以同时可以在一帧上应用的。这样一个编码的宏块,都会即由本帧内的前面的宏块又由相关帧的预测块影响。
但是I帧是不能进行帧间预测的。因为帧间预测是需要依赖于
参考帧的,这样肯定需要一开始有一个帧是可以独立的编解码的。不然大家都相互依赖了。
2.帧内预测和帧间预测都是以块为基本单元的,而一帧包含多个块,所以,可以将帧间预测与帧内预测施加到同一帧的不同块上
3.I帧只能进行帧内预测,因为I帧需要能够自己独立编解码,如果使用帧间预测就有依赖了。P帧既可以帧间预测又可以帧内预测。
帧类型


1.IDR 帧之后的帧不能再参考 IDR 帧之前的帧,,如果某一帧编码错误,之后的帧参考了这个错误帧,则也会出错。此时编码一个 IDR 帧,由于它不参考其它帧,所以只要它自己编码是正确的就不会有问题。之前有错误的帧也不会再被用作参考帧,这样就截断了编码错误的传递,且之后的帧就可以正常编 / 解码了
2.有 IDR 这种特殊的 I 帧,也就有普通的 I 帧。普通的 I 帧就是指当前帧只使用帧内预测编码,但是后面的 P 帧和 B 帧还是可以参考普通 I 帧之前的帧。但是这里我要说明一下,一般来说我们不太会使用这种普通 I 帧,大多数情况下还是直接使用 IDR 帧,尤其是在流媒体场景,比如 RTC 场景。只是说如果你非要用这种普通 I 帧,标准也是支持的
GOP
1.从一个 IDR 帧开始到下一个 IDR 帧的前一帧为止,这里面包含的 IDR 帧、普通 I 帧、P 帧和 B 帧,我们称为一个 GOP(图像组)

2.GOP 越大,编码的 I 帧就会越少。相比而言,P 帧、B 帧的压缩率更高,因此整个视频的编码效率就会越高。但是 GOP 太大,也会导致 IDR 帧距离太大,点播场景时进行视频的seek 操作就会不方便
3.在 RTC 和直播场景中,可能会因为网络原因导致丢包而引起接收端的丢帧,大的GOP 最终可能导致参考帧丢失而出现解码错误,从而引起长时间花屏和卡顿
4.GOP 不是越大越好,也不是越小越好,需要根据实际的场景来选择
slice

1.图像内的层次结构就是一帧图像可以划分成一个或多个 Slice,而一个 Slice 包含多个宏块,且一个宏块又可以划分成多个不同尺寸的子块
码流格式
H264 码流有两种格式:一种是 Annexb 格式;一种是 MP4 格式

1.Annexb 格式使用起始码来表示一个编码数据的开始。起始码本身不是图像编码的内容,只是用来分隔用的。起始码有两种,一种是 4 字节的“00 00 00 01”,一种是 3字节的“00 00 01”
(1)“00 00 00”修改为“00 00 03 00”;
(2)“00 00 01”修改为“00 00 03 01”;
(3)“00 00 02”修改为“00 00 03 02”;
(4)“00 00 03”修改为“00 00 03 03”。
同样地在解码端,我们在去掉起始码之后,也需要将对应的字节串转换回来

2.MP4 格式没有起始码,而是在图像编码数据的开始使用了 4 个字节作为长度标识,用来表示编码数据的长度,这样我们每次读取 4 个字节,计算出编码数据长度,然后取出编码数据,再继续读取 4 个字节得到长度,一直继续下去就可以取出所有的编码数据了。
NALU
1.为了能够将一些通用的编码参数提取出来,不在图像编码数据中重复,H264 设计了两个重要的参数集:一个是 SPS(序列参数集);一个是 PPS(图像参数集)
2.SPS 主要包含的是图像的宽、高、YUV 格式和位深等基本信息;
3.PPS 则主要包含熵编码类型、基础 QP 和最大参考帧数量等基本编码信息。
4.如果没有 SPS、PPS 里面的基础信息,之后的 I 帧、P 帧、B 帧就都没办法进行解码。因此 SPS 和 PPS 是至关重要的
** NALU(网络抽象层单元)**
SPS 是一个 NALU、PPS是一个 NALU、每一个 Slice 也是一个 NALU。每一个 NALU 又都是由一个 1 字节的NALU Header 和若干字节的 NALU Data 组成的。而对于每一个 Slice NALU,其 NALU Data 又是由 Slice Header 和 Slice Data 组成

NALU Header
占一个字节 8位

F:forbidden_zero_bit,占 1bit,禁止位,H264 码流必须为 0;
NRI: nal_ref_idc,占 2bits,可以取 00~11,表示当前 NALU 的重要性。参考帧、SPS 和 PPS 对应的 NALU 必须要大于 0;
Type: nal_unit_type,占 5bits,表示 NALU 类型。其取值如下表所示

注意:NALU 类型只区分了 IDR Slice 和非 IDR Slice,至于非 IDR Slice 是普通 I Slice、P Slice 还是 B Slice,则需要继续解析 Slice Header 中的 Slice Type 字段得到

用二进制查看工具打开实际编码后的码流数据

多 Slice 时如何判断哪几个 Slice 是同一帧的?
在 H264 码流中,帧是以 Slice 的方式呈现的,或者可以说在 H264 码流
里是没有“帧“这种数据的,只有 Slice。但是有个问题是,一帧有几个 Slice 是不会告诉你的。也就是说码流中没有字段表示一帧包含几个 Slice。

Slice NALU 由 NALU Header 和 NALU Data 组成,其中 NALU Data 里面就是Slice 数据,而 Slice 数据又是由 Slice Header 和 Slice Data 组成。在 Slice Header 开始的地方有一个 first_mb_in_slice 的字段,表示当前 Slice 的第一个宏块 MB 在当前编码图像中的序号。我们只要解析出这个宏块的序号出来
如果 first_mb_in_slice 的值等于 0,就代表了当前 Slice 的第一个宏块是一帧的第一个宏块,也就是说当前 Slice 就是一帧的第一个 Slice
如果 first_mb_in_slice 的值不等于 0,就代表了当前 Slice 不是一帧的第一个 Slice。
直到找到下一个 first_mb_in_slice 为 0 的 Slice,就代表新的一帧的开始,那么其前一个 Slice 就是前一帧的最后一个 Slice 了

其中,first_mb_in_slice 是以无符号指数哥伦布编码的,需要使用对应的解码方式才能解码出来。但是有一个小技巧,如果只是需要判断 first_mb_in_slice 是不是等于 0,不需要计算出实际值的话,只需要通过下面的方式计算就可以了。

如何从 SPS 中获取图像的宽高?
在编码端编码一个视频的时候,我们是需要设置分辨率告诉编码器图像的实际宽高的。但是解码器是不需要设置分辨率的,那我们在解码端或者说接收端如何知道视频的分辨率大小呢
在编码器编码的时候会将分辨率信息编码到 SPS 中。在 SPS 中有几个字段用来表示分辨率的大小。我们可以解码出这几个字段并通过一定的规则计算得到分辨率的大小。这几个字段分别是

这几个字段都是通过无符号指数哥伦布编码的,需要先解码出来。解码得到具体值之后,通过以下方法就可以得到分辨率了

如何计算得到 QP 值?
量化过程是引入失真最主要的环节。而量化最主要的参数就是 QP 值,并且 QP 值的大小严重影响到编码画面的清晰度。因此 QP 值非常重要
在 PPS 中有一个全局基础 QP,字段是 pic_init_qp_minus26。当前序列中所有依赖该PPS 的 Slice 共用这个基础 QP,且每一个 Slice 在这个基础 QP 的基础上做调整。在Slice Header 中有一个 slice_qp_delta 字段来描述这个调整偏移值。更进一步,H264 允许在宏块级别对 QP 做更进一步的精细化调节。这个字段在宏块数据里面,叫做mb_qp_delta。

如果需要得到 Slice 级别的 QP 则只需要考虑前两个 QP 相关字段。如果需要计算宏块
QP,则需要三个都考虑。但是宏块 QP 需要解析整个 Slice 数据,计算量大。一般我们直
接计算到 Slice QP 就可以了。计算方法如下:

小结
在一个视频图像序列中,我们将其划分成一个个 GOP。GOP 包含一个 IDR 帧到下一个 IDR 帧的前一帧中的所有帧。GOP 的大小选择需要根据实际应用场景来选择,一般 RTC 和直播场景可以稍微大一些,而点播场景一般小一些
在 H264 中,每一帧图像又可以分为 I 帧、P 帧和 B 帧,而 I 帧又包含了普通 I 帧和 IDR帧。帧可以划分为一个或者多个 Slice,并且最后帧都是以 Slice 的方式在码流中呈现。同时 H264 码流中除了 Slice 数据之外,还有 SPS 和 PPS 两个参数集,分别用来存放基础图像信息和基础编码参数。SPS 和 PPS 非常重要,如果丢失了,将无法进行解码。
每一个 Slice 和 SPS、PPS 都是通过 NALU 来封装的,且 NALU 含有一个 1 字节的NALU Header。我们可以通过 NALU Header 中的 NALU Type 来判断 NALU 的类型。同时,每一个 NALU 的分隔有两种方式:一种是 Annexb 格式,通过使用起始码分隔;一种是 MP4 格式,通过一个 4 字节的长度来表示 NALU 的大小,从而起到分隔的作用。

为什么有B帧的时候延迟会高
1.B帧需要双向参考,pts 和 dts 不一致。因此需要等待后面的 p 帧解码后才能继续,从而引入了延时。编码的时候也是一样的,需要先等后面的P帧先编码才能编码B帧
一幅图像中相邻像素的亮度和色度信息是比较接近的,并且亮度和色度信息也是逐渐变化的,不太会出现突变。也就是说,图像具有空间相关性
帧内预测就是利用这个特点,帧内预测通过利用已经编码的相邻像素的值来预测待编码的像素值,最后达到减少空间冗余的目的
我们是通过已经编码了的像素值去预测待编码的像素值。你可能会问,已经编码了的像素值变成码流了,不再是一个个像素了,怎么去预测待编码的像素呢?其实已经编码了的像素是会重建成重建像素,用来做之后待编码块的参考像素的。你可以认为是已经编码的块会解码成像素用来做参考像素
不同块大小的帧内预测模式
视频编码是以块为单位进行的。在 H264 标准里面,块分为宏块和子块。宏块的大小是 16 x 16(YUV 4:2:0 图像亮度块为 16 x 16,色
度块为 8 x 8)。在帧内预测中,亮度宏块可以继续划分成 16 个 4 x 4 的子块。因为图像中有的地方细节很多,我们需要划分成更小的块来做预测会更精细,所以会将宏块再划分成 4 x 4 的子块。

帧内预测是根据块的大小分为不同的预测模式的。还有一个点就是亮度块和色度块的预测是分开进行的
所以,我们在实际帧内预测的时候就会分为:4 x 4 亮度块的预测、16 x 16 亮度块的预测、8 x 8 色度块的预测(注意亮度 8 x 8 模式和 I_PCM 模式很少使用,我们这里不做讨论)。
4 x 4 的块帧内预测模式,基本包含亮度 16 x 16 和色度 8 x 8的模式,4 x 4 亮度块的帧内预测模式总共有 9 个。其中有 8 种方向模式和一种 DC 模式,且方向模式指的是预测是有方向角度的。
1.Vertical 模式
当前编码亮度块的每一列的像素值,都是复制上边已经编码块的最
下面那一行的对应位置的像素值
Vertical 模式得到的预测块同一列中的像素值都是一样的。该模式得到的块就叫做Vertical 预测块。注意,该模式只有在上边块存在的时候才可用,如果不存在则该模式不可用。比如图像最上边的块就没有可参考的块存在。
2.Horizontal 模式
当前编码亮度块的每一行的像素值,都是复制左边已经编码块的
最右边那一列的对应位置的像素值Horizontal 模式得到的预测块同一行的像素值都是一样的,该模式得到的块就叫做 Horizontal 预测块。注意,该模式只有在左边块存在的时候才可用,如果不存在则该模式不可用。比如图像最左边的块就没有可参考的块存在
3.DC 模式
当前编码亮度块的每一个像素值,是上边已经编码块的最下面那一行和左
边已编码块右边最后一列的所有像素值的平均值。注意,DC 模式预测得到的块中每一个像素值都是一样的。DC 模式得到的块就叫做 DC 预测块。
4.Diagonal Down-Left 模式
Diagonal Down-Left 模式是上边块和右上块(上边块和右上块有可能是一个块,因为可能是一个 16 x 16 的亮度块,意思理解就可以)的像素通过插值得到。如果上边块和右上块不存在则该模式无效
5.Diagonal Down-Right 模式
Diagonal Down-Right 模式需要通过上边块、左边块和左上角对角的像素通过插值得到。如果这三个有一个不存在则该模式无效。
6.Vertical-Right 模式
Vertical-Right 模式是需要通过上边块、左边块以及左上角对角的像素插值得到的。必须要这三个都有效才能使用,否则该模式无效。
8.Vertical-Left 模式
Vertical-Left 模式是需要通过上边块和右上块(上边块和右上块有可能是一个块,因为可能是一个 16 x 16 的亮度块,意思理解就可以)最下面一行的像素通过插值得到
9.Horizontal-Up 模式
Horizontal-Up 模式是需要通过左边块的像素通过插值得到的。如果左边块不存在,则该模式不可用。
16 x 16 亮度块的帧内预测模式
16 x 16 亮度块总共有4种预测模式。它们分别是Vertical模式,Horizontal模式、DC 模式和Plane 模式
Plane 预测块的每一个像素值,都是将上边已编码块的最下面那一行,和左边已编码块右边最后一列的像素值经过下面公式计算得到的
8 x 8 色度块的帧内预测模式跟 16 x 16 亮度块的是一样的 与 16 x 16 亮度块不同的是,块大小不同,所以参考像素值数量会不同。
帧内预测模式的选择
学习了这么多的模式,而每一个块却只能有一种帧内预测模式。那我们怎么确定一个块到底使用哪种模式呢?编码基础弄明白了之后可以阅读一下 x264 的代码
学习了这么多的模式,而每一个块却只能有一种帧内预测模式。那我们怎么确定一个块到底使用哪种模式呢?我们这边先把思路讲一讲,具体细节不展开。等到你把编码基础弄明白了之后可以阅读一下 x264 的代码,里面有关于具体如何去选择模式的方法
对于每一个块或者子块,我们可以得到预测块,再用实际待编码的块减去预测块就可以得到残差块。主要有下面 3 种方案来得到最优预测模式:
第一种方案,先对每一种预测模式的残差块的像素值求绝对值再求和,称之为 cost,然后取其中残差块绝对值之和也就是 cost 最小的预测模式为最优预测模式。
第二种方案,对残差块先进行 Hadamard 变换(在 DCT 变换和量化那节课中会介绍),变换到频域之后再求绝对值求和,同样称为 cost,然后取 cost 最小的预测模式为最优预测模式。
第三种方案,也可以对残差块直接进行 DCT 变换量化熵编码,计算得到失真大小和编码后的码流大小,然后通过率失真优化(作为课外内容自行学习,这里不展开讨论)的方法来选择最优预测模式
率失真优化的思想
我们知道预测之后经过 DCT 变换再量化会丢失高频信息。一般来说 QP 越大,丢失的信息越多,失真就越大,但是码流大小也越小;反之,QP 越小,丢失的信息越少,但是码流大小就越大。这是一个跷跷板。我们一般会在失真和码流大小之间平衡,尽量找到在一定码率下,失真最小的模式作为最优的预测模式,这就是率失真优化的思想。
其实还有很多不同的方案,比如有的为了加速模式选择的过程,率失真计算的时候,只会进行 DCT 变换和量化,不会进行熵编码。码流大小直接通过 QP 值估算或者使用预测模式的大小来代替。这些方案都可以,具体看编码器的实现。一般来说,选择过程越精细效果越好,但是速度会越慢。
通过上面讲的这些方法我们找到了每一个 4 x 4 块的最优模式之后,将这 16 个 4 x 4 块的cost 加起来,与 16 x 16 块的最小 cost 对比,选择 cost 最小的块划分方式和预测模式作为帧内预测模式。
小结

我们还简单介绍了一下预测模式的选择方法,主要有计算残差块绝对值之和、将残差块做 Hadamard 变换之后再求和、率失真优化等几种方案来得到 cost,然后我们cost 最小的模式作为帧内预测模式
1.在帧内预测中,我们是在当前编码的图像内寻找已编码块的像素作为参考像素计算预测块。而帧间预测是在其他已经编码的图像中去寻找参考像素块的。这正是帧内预测和帧间预测的区别先过。。。。
2.帧间预测是可以在多个已经编码的图像里面去寻找参考像素块的,我们称之为多参考。多参考和单参考(只在一帧图像里面寻找参考像素块)其实底层的原理是一样的,只是多参考需要多搜索几个参考图像去寻找参考块而已,所以我们讲解的时候就使用单参考讲解
3.帧间预测既可以参考前面的图像也可以参考后面的图像(如果参考后面的图像,后面的图像需要提前先编码,然后再编码当前图像)。只参考前面图像的帧我们称为前向参考帧,也叫 P 帧;参考后面的图像或者前面后面图像都参考的帧,我们称之为双向参考帧,也叫做 B 帧。B 帧相比 P 帧主要是需要先编码后面的帧,并且 B 帧一个编码块可以有两个预测块,这两个预测块分别由两个参考帧预测得到,最后加权平均得到最终的预测
块。P 帧和 B 帧的底层逻辑基本是一样的
帧间编码
以 H264 标准为基础来聊聊 P 帧的帧间编码过程
块大小
帧内预测有亮度 16 x 16、亮度 4 x 4 和色度 8 x 8 这几种块。类似
地,在帧间预测也一样有不同的块和子块大小。相比帧内预测,帧间预测的块划分类型要多很多。宏块大小 16 x 16,可以划分为 16 x 8,8 x 16, 8 x 8 三种,其中 8 x 8 可以继续划分成 8 x 4,4 x 8 和 4 x 4,这是亮度块的划分。在 YUV 4:2:0 中,色度块宽高大小都是亮度块的一半。亮度宏块的划分方式如下图所示

参考帧和运动矢量
在已经编码的帧里面找到一个块来作为预测块,这个已经编码的帧
称之为参考帧。在 H264 标准中,P 帧最多支持从 16 个参考帧中选出一个作为编码块的参考帧,但是同一个帧中的不同块可以选择不同的参考帧,这就是多参考
通常在 RTC 场景中,比如 WebRTC 中,P 帧中的所有块都参考同一个参考帧。并且一般会选择当前编码帧的前一帧来作为参考帧
这是因为自然界的运动一般是连续的,同时在短时间之内的变化相对比较小,所以前面的帧通常是最接近当前编码帧的,并且两者的差距比较小。因此,我们比较容易从前一帧中找到一个跟当前编码块差距很小的块作为预测块,这样编码块减去预测块得到的残差块的像素值很多都是 0,压缩效率是不是就很高了
虽然运动变化比较小,但是还是有变化啊,比如说下图中的场景。

图中的小车在往前开,树是不动的。我们可以看到车相对于树的距离是变化的。那我们怎么来表示这个变化呢?
比如说上面两幅图像中,小车从前一幅图像中的(32,80)的坐标位置,变化到当前图像(80,80)的位置,向前行驶了 48 个像素。很明显,如果我们选用(32,80)这个块作为当前(80,80)这个编码块的预测块的话,是不是就可以得到全为 0 像素的残差块了?这是因为小车本身是没有变化的,变化的只是小车的位置。这个位置变化我们怎么表示呢?我们用运动矢量来表示。我们称(32 - 80, 80 - 80)也就是(-48, 0)为运动矢量。
我们先把运动矢量编码到码流当中,这样解码端只要解码出运动矢量,使用运动矢量就可以在参考帧中找到预测块了,我们再解码出残差(如果有的话),残差块加上预测块就可以恢复出图像块了用运动矢量来表示编码帧中编码块和参考帧中的预测块之间的位置的差值
运动搜索
运动搜索的目标就是在参考帧中找到一个块,称之为预测块,且这个预测块与编码块的差距最小。从计算机的角度来说就是,编码块跟这个预测块的差值,也就是残差块的像素绝对值之和(下面我们用 SAD 表示残差块的像素绝对值之和)最小。全搜索算法一定可以搜索到最相似的预测块,但费时
从参考帧中第一个像素开始,将一个个 16 x16 大小的块都遍历一遍。我们总是可以找到差距最小的块。这种方法我们称之为全搜索算法
搜索算法中每一个搜索的点都是搜索块的左上角像素点


小车的运动是连续的,如果小车向前
行驶了 48.5 个像素点呢?又或者是向前行驶了 48.25 个像素点呢?运动矢量选择(-48.5, 0)或者(-48.25,0)吗?可是 0.5 个像素点是什么样的,0.25 个像素点又是什么样的?图像上都没有这种像素点啊,怎么办呢?
其实没关系的,我们还是可以使用(-48,0)作为运动矢量,只是预测块中的小车位置与我们编码块中的小车位置会相差个 0.5 或者 0.25 个像素,得到的残差会大一些,压缩效率稍微低一些,问题也不大

为了能够解决这种半个像素或者 1/4 个像素的运动带来的压缩效率下降的问题,我们通过对参考帧进行半像素和 1/4 像素插值(统称为亚像素插值)的方式来解决用插值的方式将半像素和 1/4 像素算出来,也当作一个像素
插值得到的小车跟原始的小车的对应像素点的像素值并不是完全一样的,毕竟插值得到的像素点是利用滤波算法加权平均得到的
因此,半像素插值得到的预测块并不一定就比整像素预测块的残差小。只是我们多了很多个半像素预测块和 1/4 像素预测块的选择,所以我们可以在整像素预测块、半像素预测块和 1/4 像素预测块里面选择一个最好的
亚像素精度运动搜索
1.先通过快速搜索算法进行整像素运动搜索算法得到整像素的运动矢(就是我们在运动搜索小节中讲述的内容)。
2. 对参考帧进行半像素和 1/4 像素插值。
3.以整像素运动矢量指向的整像素为起点,进行钻石搜索算法,分别求得中心点以及上、下、左、右四个半像素点对应预测块的残差块,得到 SAD 值。取 SAD 值最小的点为最佳匹配点。
4.以半像素运动搜索的最佳匹配点为起点,分别求得中心点以及上、下、左、右四个 1/4像素点对应预测块的残差块,得到 SAD 值,并取最小的点为最佳匹配点。通过上面亚像素搜索算法得到的最佳匹配点就可以得到最后的运动矢量了。假设整像素运动矢量为 (a0, b0),半像素最佳匹配点相对于整像素最佳匹配点的运动矢量为 (a1,b1),1/4 像素最佳匹配点相对于半像素最佳匹配点的运动矢量为 (a2, b2),则最后运动矢量(a,b)的值的计算方法如下:

相当于原先的运动矢量乘以了 4,即原先 1/4 像素的 0.25 变成了 1,0.5 像素变成了 2,1个像素则变成了 4。这主要是因为我们不用小数形式来表示运动矢量。因为浮点型数据会有精度误差,所以我们通过乘以 4 把它变成整数。
通过上面的整像素运动搜索和亚像素精度运动搜索,我们就得到了最终的运动矢量了。有了运动矢量之后,我们需要将运动矢量的信息也编码到码流中,并且解码的时候直接取出来用就可以在参考帧中把预测块找出来了。那运动矢量是直接编码到码流中的吗?其实不是的
运动矢量预测
运动矢量跟我们的编码块一样不是直接编码进去的,而是先用周围相邻块的运动矢量预测一个预测运动矢量,称为 MVP。将当前运动矢量与 MVP 的残差称之为 MVD,然后编码到码流中去的。解码端使用同样的运动矢量预测算法得到 MVP,并从码流中解码出运动矢量残差 MVD,MVP+MVD 就是运动矢量了
以 16 x 16 宏块为例:

1.取当前编码宏块的左边块 A、上边块 B、右上块 C。如果右上块不存在或者参考帧与当前编码宏块不同(多参考的时候会存在),则使用左上块 D 替换 C,即 C = D。
2.求得 A、B、C 块的参考帧有多少个与当前编码块的参考帧相同,记为 count。
3.如果 count > 1,则取 A、B、C 块的运动矢量的中值(就是 A、B、C 块运动矢量的 3个 x 和 3 个 y 分别取中间值作为 MVP 的 x 和 y)
4.如果 count = 1,则直接将这个块的运动矢量作为 MVP。
5.如果 count = 0,并且 B、C 都不存在,A 存在的话,则直接将 A 的运动矢量作为MVP
6.如果上述条件都不满足,则取 A、B、C 块运动矢量的中值
SKIP 模式
如果运动矢量就是 MVP,也就是说 MVD 为 (0,0),同时,残差块经过变换量化后系数也都是等于 0,那么当前编码块的模式就是 SKIP
相比于 SKIP 模式,其它模式要不就是 MVD 不为 0,要不就是量化后的残差系数不为 0,或者两者都不为 0。所以说 SKIP 模式是一种特例,由于 MVD 和残差块都是等于 0,因此压缩效率特别高。
比如说 P 帧中的静止部分,前后两帧不会变化,运动矢量直接为 0,而且残差块像素值本身因为几乎没有变化基本为 0,只有少部分噪声引起的比较小的值,量化后更是全部变成了 0。这种图像中的静止部分或者是图像中的背景部分大多数时候都是 SKIP 模式。这种模式非常省码率,且压缩效率非常高。因为需要编码的信息非常少,所以单独在这里跟你讨论一下
帧间模式的选择
编码块帧间模式的选择其实就是参考帧的选择、运动矢量的确定,以及块大小(也就是块划分的方式)的选择,如果 SKIP 单独拿出来算的话就再加上一个判断是不是 SKIP 模式。我们主要是确定这 4 个东西
之前的讨论当中我们都是以当前编码帧的前一帧作为参考帧的,也就是说是单参考的,不涉及到参考帧的选择。其实,如果是多参考的话,编码块在选择参考帧的时候只需要遍历每一个参考帧进行块划分,然后再对每一个块进行运动搜索得到运动矢量就可以了。跟单参考相比就是多了一个参考帧遍历的操作。所以我们这里还是以单参考帧的方式来讲讲帧间模式的选择过程
注意,帧间模式的选择大多数是看编码器的实现的,并且不同编码器实现都会不一样,所以我们只是讲讲其中一种模式选择的思路,具体的细节各个编码器都各不相同。具体选择过程如下:
1.首先判断当前宏块是不是可以作为 SKIP 块(通过相邻已经编码的块是不是存在 SKIP块,和当前块使用 MVP 做运动矢量之后,残差块变换量化后是不是都为 0 等算法来判断),如果可以作为 SKIP 块则模式选择结束,不再进行下面的划分了。
2.宏块大小为 16 x 16。首先不划分宏块,直接使用 16 x 16 大小的块,在参考帧中进行运动搜索,得到运动矢量和预测块,通过 MVP 求得 MVD,通过预测块求得残差块,并求得残差块的 SATD 值(残差块经过 Hadamard 变换之后求绝对值再求和),估计MVD 的编码后占用的字节数,将两个值加起来作为 cost16x16。
3.将 16 x 16 块划分成 4 个 8 x 8 的子块,分别进行运动搜索,并求得每一个 8 x 8 子块的 MVD 和残差块,最后分别得到 4 个子块的 cost8x8
(1)如果 4 个 8 x 8 子块的 cost8x8 之和小于 16 x 16 块的 cost16x16 的话,我们再分别对每一个 8 x 8 子块划分成 4 个 4 x 4 子块,同样分别进行运动搜索,得到每一个 4 x 4子块的 cost4x4。
如果 4 个 cost4x4 之和小于 cost8x8,则将 8 x 8 块划分成 4 x 8 和 8 x 4 两种子块分别求得 cost4x8 和 cost8x4,再根据 4 个 cost4x4、2 个 cost4x8 和 2 个 cost8x4 的大小,选择最终的 8x8 划分的方式,并将对应的 cost 值更新到 cost8x8。否则不划分 8 x 8 子块。
(2)如果 4 个 8 x 8 子块的最新的 cost8x8 之和还是小于 cost16x16 的话,则再将 16 x16 划分成两个 8 x 16 和 16 x 8 子块,并分别求得 cost8x16 和 cost16x8,对比 8x8、16x8、8x16 的 cost 值,并决定最终 16 x 16 块的划分方式。
(3)否则的话,不划分 16 x 16 的块。
4.得到了编码宏块的帧间模式之后,我们还需要对编码宏块进行帧内模式的选择。是的,没错。在 P 帧和 B 帧中的宏块也是可以使用帧内模式的,所以我们需要看是帧间模式cost 更小还是帧内模式 cost 更小。这也回答了我们在第 4 节课里留的思考题。如果帧内模式更小则使用帧内模式;如果是帧间模式更小则使用帧间模式。但是一般来说 P 帧和 B 帧宏块决策出来绝大多数还是帧间模式的。
小结
1.宏块的划分。为了能够更准确的找到预测块,我们可以将 16 x 16 的宏块继续划分成更小的子块来做运动搜索。因为图像有的地方静止的背景画面或者平坦的区域可以直接选用最大的块来搜索预测块;而有的地方细节很多,图像中的物体运动方向也各不相同,可能就需要划分成更小的块来做运动搜索。这样每一个块都拥有自己独立的运动矢量,
并且得到的预测块更接近于编码块,从而有利于提高压缩效率。
2.参考帧和运动矢量。在 RTC 场景中我们一般选择单参考,并且一般选择当前编码图像的前一帧作为参考帧。运动矢量是用来表示参考帧中预测块与编码帧中编码块位置的相对距离的。
3.运动搜索。运动矢量是通过运动搜索得到的,而运动搜索是在参考帧中进行的。通常我们会使用钻石搜索和六边形搜索等快速运动搜索算法。一般不会使用全搜索算法。其中钻石搜索算法更简单,步骤更少,所以如果需要编码速度快,一般选择钻石搜索。六边形搜索步骤更多,更精细,要求编码质量高,同时对速度要求不高的时候,可以选择六边形搜索。
4.亚像素插值和亚像素精度搜索。光做整像素运动搜索不太能够准确的处理连续运动场景。为了能够处理好这种连续运动的问题,我们对参考帧进行亚像素插值得到半像素和1/4 像素图像。然后在整像素搜索的基础上在亚像素图像上做亚像素精度的运动搜索。实验数据证明,半像素和 1/4 像素精度的运动搜索相比整像素精度的运动搜索可以明显地提高压缩效率。
5.在最后我们大体讲了一下编码块帧间预测模式的具体选择过程,并单独讲解了一下 SKIP模式。SKIP 模式是一种比较特殊的模式,由于 MVD 和残差块都是等于 0,因此其压缩效率特别高。
通过帧内编码可以去除空间冗余,通过帧间编码可以去除时间冗余,而为
了分离图像块的高频和低频信息从而去除视觉冗余,我们需要做 DCT 变换和量化

DCT 变换
DCT 变换,就是离散余弦变换。它能够将空域的信号(对于图像来说,空域就是你平时到的图像)转换到频域(对于图像来说,就是将图像做完 DCT 变换之后的数据)上表示,并能够比较好的去除相关性。
图片经过 DCT 变换之后,低频信息集中在左上角,而高频信息则分散在其它的位置。通常情况下,图片的高频信息多但是幅值比较小。高频信息主要描述图片的边缘信息。
由于人眼的视觉敏感度是有限的,有的时候我们去除了一部分高频信息之后,人眼看上去感觉区别并不大。因此,我们可以先将图片 DCT 变换到频域,然后再去除一些高频信息。这样我们就可以减少信息量,从而达到压缩的目的。
DCT 变换本身是无损的,同时也是可逆的。我们可以通过 DCT 变换将图片从空域转换到频域,也可以通过 DCT 反变换将图片从频域转回到空域。
通常情况下 DCT 变换是在 4x4 的子块上进行的
左上角的系数为 DC 系数,而其它系数为 AC 系数

残差公式


未完待结
RTP 协议
RTP(Real-time Transport Protocol)协议,全称是实时传输协议
先将原始数据经过编码压缩之后,再将编码码流传输到接收端。在传输的时候我们通常不会直接将编码码流进行传输,而是先将码流打包成一个个 RTP 包再进行发送
那为什么需要打包成 RTP 包呢?这是因为我们的接收端要能够正确地使用这些音视频编码数据,不仅仅需要原始的编码码流,还需要一些额外的信息
当前视频码流是哪种视频编码标准,是 H264、H265、VP8、VP9 还是 AV1 呢?我们知道每种不同的编码标准,其码流解析的方式肯定也不一样。这个就需要通过 RTP 协议告知接收端。当我们知道编码标准了,我们就可以正确地解析码流,并解码出图像了。但是我们又会遇到一个新的问题,那就是按照什么速度播放视频呢?这个也需要 RTP 协议告知接收端
当我们知道编码标准了,我们就可以正确地解析码流,并解码出图像了。但是我们又会遇到一个新的问题,那就是按照什么速度播放视频呢?这个也需要 RTP 协议告知接收端。
这就是 RTP 协议的一个重要的作用,即告知接收端一些必要的信息。当然 RTP 协议的作用不止这些,它其实在网络带宽预测和拥塞控制的时候也发挥出了至关重要的作用。
RTP 包包括两个部分:第一个部分是 RTP 头;另外一个部分是 RTP 有效载荷
RTP头


RTP 包头有一个扩展头标志位X,当扩展头标志位 X 为 1 的时候,说明有 RTP 扩展头。RTP 扩展头由于平时大家很少用看似不怎么重要,但是在 RTC 场景中,尤其是 WebRTC 中经常会用到。另外,RTP 扩展头我们在带宽预测的时候也会用到
有了 RTP 协议,我们就能够将码流打包成 RTP包发给接收端了。如果你只负责传输 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 包的传输统计信息,本身不具有丢包重传和带宽预测的功能,而这些功能需要我们自己来实现
RTCP 协议有很多种报告,而每种报告其实定义的具体内容都是不一样
的。我们这里以 RTPFB 报告中的 NACK 报告(丢包提示报告)作为一个例子来看看 RTCP协议大概是什么样子的。(RTPFB 报告包含了多种子报告,NACK 报告只是其中的一种,因为我们后面还会用到这个报告,所以这里我们就先以这个报告为例子。)


RTP 是用来传输实际的视频数据的。它就像一个快递盒,先装好视频,然后填好运送的视频基本信息和收件人信息,最后将视频运送到收件人手上。
RTCP 协议则像是一个用来统计快递运送情况的记录表。其中的 NACK 报告就是快递丢件情况的记录表。它记录着哪些快递丢了。发件人收到了 NACK 之后,可以重新寄一个同样的快递给收件人,防止收件人没有收到快递。在这里也就是将丢失的视频 RTP 包重传一遍
RTP H264 码流打包分为三种方式:分别是单 NALU 封包方式、组合封包方式、分片封包方式。顾名思义,单 NALU 封包方式是一个 NALU 打一个 RTP 包;而组合封包方式就是多个 NALU 打一个 RTP 包;分片封包方式则是一个 NALU 分开放在连续的多个 RTP 包中。下面我们来分别看一下各种打包方式是怎么样的。
1.单 NALU 封包方式
单 NALU 封包方式非常简单。我们在 RTP 头部的后面,直接放置 NALU 数据即可。注意,根据 RTP 的规定,这里需要将 NALU 数据前面的起始码去除,不要将起始码也带入RTP 包中。其格式如下:


这种打包方式适合于单个 RTP 包小于 1500 字节(MTU 大小)的时候。一般来说,一些P 帧和 B 帧编码之后比较小,就可以使用这种打包方式。
2、组合封包方式
是将多个 NALU 放置在一个 RTP 包中。在 RTP 头部之
后,且放置 NALU 数据之前,我们需要放置一个 1 字节的 STAP-A 的头部。其中,STAP-A Header 跟 NALU Header 的格式是一样的,只是 Type 字段的值不一样。

Type 的取值如下表所示。表中的 24 和 25 类型就是STAP 组合封包方式

放置完 STAP-A Header 之后,在每一个 NALU 的前面我们需要放置一个 2 字节的 size字段,用于表示后面的 NALU 的大小。之后才是 NALU 的数据。记住同样需要去掉起始码


这种打包方式适合于单个 NALU 很小的时候。因此,我们将多个 NALU 打包到一起也小于
1500 字节的时候就可以使用。但是由于一般多个视频帧加到一起还小于 1500 的情况比较
少,所以视频数据的 RTP 打包一般来说用组合封包方式的情况也很少
3.分片封包方式
分片封包就更复杂一些了,但却是我们经常用到的打包方式。
它是将一个 NALU 分开打包在连续的多个 RTP 包中。因此,我们首先需要一个 1 字节的FU indicator 来表示当前 RTP 包是不是分片封包方式,再用一个 1 字节的 FU Header 来表示当前这个 RTP 包是不是 NALU 的第一个包,是不是 NALU 的最后一个包,以及NALU 的类型。
为什么需要表示是不是第一个包以及是不是最后一个包呢?这是因为一个 NALU 被分开放在多个 RTP 包中,我们需要知道哪个是第一个 NALU 分片,哪个是最后一个 NALU 分片,以及哪些是中间分片。这样我们才能组成一个完整的 NALU。
NALU 不是已经在 NALU Header 中有了 NALU Type 字段吗?为什么 FU
Header 中还要有 NALU Type 呢?这是因为分片封包时需要去掉 NALU Header。因此,我们需要通过 FU Header 中的 NALU Type 得到 NALU 的类型。
分片封装中的 FU indicator 跟 NALU Header 的格式也是一样的,也只是 Type 字段的值不同,所以我们可以参考组合封包小节中的表格。因为我们一般只使用 FU-A,所以接下来讲述的将是 FU-A 的分片封包方式

S:起始位,占 1bit,为 1 则表示是 NALU 的第一个 RTP 包。
E:结束位,占 1bit,为 1 则表示是 NALU 的最后一个 RTP 包。
R:预留位,占 1bit。
Type:占 5bits,表示 NALU 类型。
分片打包的格式如下:


这种打包方式主要用于将 NALU 数据打包成一个 RTP 包时大小大于 1500 字节的时候,这是经常使用的视频 RTP 打包方法。
一般来说,我们在一个 H264 码流中会混合使用多种 RTP 打包方式。一般来说,对于小的 P 帧、B 帧还有SPS、PPS 我们可以使用单个 NALU 封包方式。而对于大的 I 帧、P 帧或 B 帧,我们使用分片封包方式。
小结
RTP 协议用来封装音视频数据,并且将音视频数据和一些基本信息打包到 RTP 包中传输到接收端。而 RTCP 协议则辅助 RTP 协议使用,其中一个主要的功能就是用来统计 RTP 包的发送情况,比如说丢包率和具体哪些 RTP 包在网络发送的过程中丢失了。RTCP 包将这些信息收集起来发送给 RTP包的发送端
RTP 和 RTCP 协议是带宽预测和拥塞控制的基础,并且重点强调了
RTCP 协议本身只统计信息,而带宽预测和拥塞控制算法是需要我们自己实现的,RTCP 协议本身并没有这个功能。
单 NALU 封包方式,一般适合 NALU 大小比较小,且打包出来的 RTP 大小小于 1500字节的时候使用。
组合封包方式,适合多个 NALU 都很小,且合并在一起打包的 RTP 包小于 1500 字节的时候使用。
分片打包,则适合 NALU 比较大的情况,且打包成一个 RTP 包其大小会大于 1500 字节的时候使用。
这几种打包方式不是说只能选择一种,在一个 RTP 流中是可以存在多种打包方式的,即可以混合使用。
为什么我们在选择 RTP 打包方式的时候,需要根据 NALU 大小是不是大于 1500 字节(MTU)来选择?
不超过1500主要是因为Udp协议的MTU为1500,超过了会导致Udp分片传输,而分片的缺点是丢了一个片,整包数据就废弃了
一般情况下,音视频场景中的拥塞控制和丢包重传等算法的基础就是 RTP 和 RTCP 协议。我们需要通过 RTP 包的信息和 RTCP 包中传输的统计信息来做拥塞控制和丢包重传等操作
带宽预测
预测出实际的带宽之后,我们就可以控制音视频数据的发送数据量
控制音视频数据的编码码率或者直接控制,发送 RTP 包的速度,这都是可以的。控制住音视频发送的数据量是为了不会在网络带宽不够的时候,我们还发送超过网络带宽承受能力的数据量,最后导致网络出现长延时和高丢包等问题,继而引发接收端出现延时高或者卡顿的问题。因此,带宽预测是非常重要的
未完待结。。。
未完待结。。。

简单介绍一下 Jitter Buffer 这个模块。它是好几个卡顿和花屏问
题的处理模块。Jitter Buffer 工作在接收端,主要功能就是在接收端收到包之后进行组帧,并判断帧的完整性、可解码性、发送丢包重传请求、发送关键帧请求以及估算网络抖动的

未完待续
为什么需要 SVC
两个人进行视频通话,我是发送端,网络非常好,你是接收端,网络比较
差

由于服务器到接收端的网络比较差,那么最后会引起:
一组视频 RTP 包的接收时长很长,而一组视频 RTP 包的发送时长比较小;或者发送端的视频 RTP 包发送给接收端之后,网络中丢包率很高。
如果不做带宽预测和码控的话最终接收端看到发送端的画面会非常卡。
未完待结。。。
FLV
FLV 是一种非常常见的音视频封装,尤其是在流媒体场景中经常用到。FLV 封装也是比较简单的封装格式,它是由一个个 Tag 组成的。Tag 又分为视频 Tag、音频 Tag 和 ScriptTag,分别用来存放视频数据、音频数据和 MetaData 数据


最重要的是时间戳,因为播放的速度还有音视频同步都需要依赖这个时间戳的值时间戳的单位是 ms。RTP 的时间戳单位是1/90000 秒,MP4 的时间戳是可以自定义的。这个时间戳的单位也是至关重要
Tag Data 有 Script、音频和视频。首先来看一下 ScriptTag 的 Data。这个 Tag 存放的是 MetaData 数据,主要包括宽、高、时长、采样率等基础信息
Script Data 使用 2 个 AMF 包来存放信息。第一个 AMF 包是 onMetaData 包。第 1 个字节表示的是 AMF 包的类型,一般是字符串类型,值是 0x02,之后是 2 字节的长度,一般长度总是 10,值是 0x000A。之后就是 10 字节长度字符串了,值是 onMetaData
第二个 AMF 包的第一个字节是数组类型,值是 0x08,紧接着 4 个字节为数组元素的个数。后面即为各数组元素的封装,数组元素为元素名称和值组成的对。常见的数组元素如
下表所示:

音频 Tag Data 的第一个字节表示音频的编码方式、采样率和位宽等信息,如下图所示。之后就是音频数据了。

视频 Tag 的第 1 个字节包含了这个 Tag 的视频帧类型和视频编码方式,格式如下图:
stts box 中放置的是每一个 sample 的时长,这个值是 DTS

ctts box 放置着 CTS,也就是每一个 sample 的 PTS 和 DTS 的差值

stss box 中放置的是哪些 sample 是关键帧。

stsc box 中放置的是 sample 到 chunk 的映射表,也就是哪些 sample 属于哪个chunk。

stco box 或 co64 box 中放置着每个 chunk 在文件中的偏移地址

stsz box 中放置着每一个 sample 的大小。

工程实践
接下来我们结合一个工程问题来实践一下。我们如何计算每一个 sample 在文件中的具体位置,判断它是不是关键帧,并计算它的具体时间。
计算 sample 的具体位置需要使用 stco(或 co64)、stsc 和 stsz。我们首先通过 stsc 将每一个 sample 属于哪一个 chunk 计算出来。这样每一个 chunk 的第一个 sample 就知道是哪个了。然后我们通过 stco 和 co64 就可以知道对应序号的 chunk 的第一个 sample在文件中的地址了。我们再通过 stsz 查询每个 sample 的大小,从 chunk 的第一个sample 的地址将中间的 sample 的大小一个个地加上去就可以得到每一个 sample 的地址
了。

而 sample 是不是关键帧,我们只需要通过 stss 对应每一个 sample 序号查询就可以得到。

计算 sample 的时间我们需要用到 stts 和 ctts。我们先通过 stts 得到每一个 sample 的时长,第 n 个 sample 的结束时间就是第 n-1 个 sample 的结束时间加上第 n 个 sample 的时长。但是需要注意一下,这个是 DTS,我们还需要通过 ctts box 得到每一个 sample 的PTS 和 DTS 的差值。最后每一个 sample 的 PTS 就是等于 DTS 加上 CTS。

PTS 和 DTS
PTS 表示的是视频帧的显示时间,DTS 表示的是视频帧的解码时间对于同一帧来说,DTS 和 PTS 可能是不一样的。
为什么呢?主要的原因是 B 帧,因为 B 帧可以双向参考,可以参考后面的 P 帧,那么就需要将后面用作参考的 P 帧先编码或解码,然后才能进行 B 帧的编码和解码。所以就会导致一个现象,后面显示的帧需要先编码或解码,这样就有解码时间和显示时间不同的问题了。如果说没有 B 帧的话,只有 I 帧和 P 帧就不会有 PTS 和 DTS 不同的问题了。

时间基
时间的单位。比如说,编程的时候我们经常使用 ms(毫
秒)这个时间单位,毫秒是 1/1000 秒,如果你用毫秒表示时间的话,时间基就是1/1000。再比如说 RTP 的时间戳,它的单位是 1/90000 秒,也就是说 RTP 时间戳的时间基是 1/90000。意思是 RTP 的时间戳每增加 1,就是指时间增加了 1/90000 秒。
FLV 封装,时间基是 1/1000,意思是 FLV 里面的 DTS 和 PTS 的单位都是 ms。MP4 的话,时间基就是在 box 中的 time_scale,是需要从 box 中读取解析出来的,不是固定的
音视频同步的类型
三种:视频同步到音频、音频同步到视频、音频和视频都做调整同步。
首先,视频同步到音频是指音频按照自己的节奏播放,不需要调节。如果视频相对音频快了的话,就延长当前播放视频帧的时间,以此来减慢视频帧的播放速度。如果视频相对音频慢了的话,就加快视频帧的播放速度,甚至通过丢帧的方式来快速赶上音频。
其次,音频同步到视频是指视频按照自己的节奏播放,不需要调节。如果音频相对视频快了的话,就降低音频播放的速度,比如说重采样音频增加音频的采样点,延长音频的播放时间。如果音频相对视频慢了,就加快音频的播放速度,比如说重采样音频数据减少音频的采样点,缩短音频的播放时间
一般来说这种方式是不常用的,因为人耳的敏感度很高,相对于视频来说,音频的调整更容易被人耳发现。因此对音频做调节,要做好的话,难度要高于调节视频的速度,所以我们一般不太会使用这种同步方法。
最后一种是音频和视频都做调整,具体是指音频和视频都需要为音视频同步做出调整。比如说 WebRTC 里面的音视频同步就是音频和视频都做调整,如果前一次调节的是视频的话,下一次就调节音频,相互交替进行,整体的思路还是跟前面两种方法差不多。音频快了就将音频的速度调低一些或者将视频的速度调高一些,视频快了就将视频的速度调低一些或者将音频的速度调高一些。这种一般在非 RTC 场景也不怎么使用。
视频同步到音频
首先,我们使用的时间戳是 PTS,因为播放视频的时间我们应该使用显示时间。而且我们
需要先通过时间基将对应的时间戳转换到常用的时间单位,一般是秒或者毫秒。
然后,我们有一个视频时钟和一个音频时钟来记录当前视频播放到的 PTS 和音频播放到的PTS。注意这里的 PTS 还不是实际视频帧的 PTS 或者音频帧的 PTS,稍微有点区别。
区别是什么呢?比如说一帧视频的 PTS 的 100s,这一帧视频已经在渲染到屏幕上了,并且播放了 0.02s 的时间,那么当前的视频时钟是 100.02s。也就是说视频时钟和音频时钟
不仅仅需要考虑当前正在播放的帧的 PTS,还要考虑当前正在播放的这一帧播放了多长时间,这个值才是最准确的时钟。
而视频时钟和音频时钟的差值就是不同步的时间差。这个时间差我们记为 diff,表示了当前音频和视频的不同步程度。我们需要做的就是尽量调节来减小这个时间差的绝对值。
那怎么调节呢?我们知道,我们可以通过计算得到当前正在播放的视频帧理论上应该播放多长时间(不考虑音视频同步的话)。计算方法就是用还没有播放但是紧接着要播放的帧的 PTS 减去正在播放的帧的 PTS,我们记为 last_duration。
如果说当前视频时钟相比音频时钟要大,也就是 diff 大于 0,说明视频快了。这个时候我们就可以延长正在播放的视频帧的播放时间,也就是增加 last_duration 的值,是不是视频的播放画面就会慢下来了?因为后面的待播放帧需要等更长的时间才会播放,而音频的播放速度不变,是不是就相当于待播放的视频帧在等音频了?
反之,如果说当前的视频时钟相比音频时钟要小,也就是 diff 小于 0,说明视频慢了。这个时候我们就缩短正在播放的视频帧的播放时间,也就是减小 last_duration 的值,是不是视频的播放画面就会加快速度渲染,就相当于待播放的视频帧在加快脚步赶上前面的音频了?
ffplay 源码 compute_target_delay
音视频同步并不是完完全全同步的,而是通过调整正在播放的视
频帧的播放时间来尽量达到一个动态的同步状态,这个状态里面的视频时钟和音频时钟并不是完全相等的,只是相差得比较少,人眼的敏感度看不出来而已。这就是音视频同步的原理。
未完待续。。