视频技术基础02:视频编码基础

Posted 麦兜的学习笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了视频技术基础02:视频编码基础相关的知识,希望对你有一定的参考价值。

目录

1 概述

1.1 视频为什么要编码?

1.2 常用视频编码标准

1.3 视频编码对象

1.4 视频为什么可以编码压缩?

2 视频编码原理推理过程

2.1 熵编码需求

2.2 减少空间冗余

2.3 减少时间冗余

2.4 减少高频信息

2.5 推理过程小节

3 H264编码结构

3.1 帧类型

3.1.1 帧内编码帧和帧间编码帧

3.1.2 H264帧类型

3.1.3 IDR帧

3.2 GOP的概念

3.3 Slice的概念

4 H264码流结构

4.1 码流格式

4.1.1 Annexb格式(附录B格式)

4.1.2 MP4格式

4.2 码流结构

4.2.1 参数集

4.2.2 NALU的概念

4.2.3 码流观察实例

4.3 常见工程问题

4.3.1 如何判断哪些Slice属于同一帧?

4.3.2 如何从SPS中获取图像分辨率?

4.3.3 如何计算得到QP值?

5 帧内预测

5.1 理论基础

5.2 帧内预测规则

5.3 4*4亮度块帧内预测模式

5.3.1 Vertical模式

5.3.2 Horizontal模式

5.3.3 DC模式

5.3.4 Diagonal Down-Left模式

5.3.5 Diagonal Down-Right模式

5.3.6 Vertical-Right模式

5.3.7 Horizontal-Down模式

5.3.8 Vertical-Left模式

5.3.9 Horizontal-Up模式

5.4 16*16亮度块帧内预测模式

5.5 8*8色度块帧内预测模式

5.6 帧内预测模式的选择

6 帧间预测

6.1 理论基础

6.2 块大小

6.3 参考帧

6.4 运动矢量

6.5 运动搜索

6.5.1 运动搜索目标

6.5.2 全搜索算法

6.5.3 快速运动搜索算法

6.6 亚像素插值

6.6.1 问题引入

6.6.2 亚像素插值方法

6.6.3 亚像素精度运动搜索

6.7 运动矢量预测

6.7.1 运动矢量编码

6.7.2 运动矢量预测步骤

6.8 SKIP模式

6.9 帧间预测模式的选择

7 变换量化简介

7.1 DCT变换概述

7.2 量化概述

7.3 Hadamard变换概述

7.4 H264中的变换与量化


1 概述

1.1 视频为什么要编码?

1. 对视频进行编码的最主要目的为了压缩

2. 假设有一部YUV420格式的电影,分辨率为1080P,帧率为25fps,时长为2小时,如果不进行编码压缩,数据量约为521.4GB。如果不进行编码压缩,对于存储和传输的资源消耗都是不可接受的

1920 * 1080 * 1.5 * 25 * 2 * 3600 ≈ 521.4GB

说明:上述计算过程中的1.5B,是YUV420格式存储一个像素的Y、U、V像素值所需的空间,详情可参考视频技术基础01:图像基础和前处理 chapter 2.2.3.4

1.2 常用视频编码标准

目前市面上常见的编码标准有H264、H265、VP8、VP9和AV1,其中,

1. H264和VP8是最常用的编码标准,且二者的标准非常相似

2. H265和VP9分别是他们的下一代编码标准,这两个标准也非常相似

3. AV1是VP9的下一代编码标准

说明:需要注意的是,使用H264和H265需要专利费,而VP8和VP9则是完全免费的

1.3 视频编码对象

1. 视频是由一帧帧图像序列组成的,视频编码就是对一帧帧图像进行的

2. 进行编码的图像为YUV格式,并且是Y分量和UV分量分开编码

3. 对于每一帧图像,又是划分为一个个块进行编码,在不同编码标准中这种块的命名和大小有所不同

① 在H264中称为宏块,在VP9、AV1中称为超级块,下文使用宏块这个名字进行叙述

② 块的大小一般为16*16(H264、VP8)、32*32(H265、VP9)、64*64(H265、VP9、AV1)、128*128(AV1)

说明:关于为什么YUV格式更适用于视频编码,可参考视频技术基础01:图像基础和前处理 chapter 2.4

1.4 视频为什么可以编码压缩?

可以对视频进行编码压缩,是因为图像一般都有数据冗余,主要包括如下4种,

1. 空间冗余

将图像划分为一个个宏块之后,相邻的块很多时候都有比较明显的相似性,这就是空间冗余

2. 时间冗余

假设帧率为25fps,由于前后两帧图像相差只有40ms,因此前后两帧图像的变化是比较小的,相似性很高,这就是时间冗余

3. 视觉冗余

人眼对于图像中高频信息的敏感度小于低频信息,有时去除图像中的一些高频信息,人眼看不出差别,这就是视觉冗余

注意:去除图像中高频信息的操作属于有损编码

4. 信息熵冗余

人们用于表达某一信息所使用的比特数总比理论上表示该信息所需要的最少比特数大,他们之间的差距就是信息熵冗余(或称作编码冗余)

2 视频编码原理推理过程

2.1 熵编码需求

1. 熵编码(entropy encoding)是指利用数据的统计信息进行压缩的无损编码,常用的熵编码方法有香农-范诺编码(Shannon-Fano)、哈夫曼编码(Huffman)、算术编码(arithmetic)和行程编码(Run Length Encoding)

2. 行程编码示例

以行程编码为例,将字符串"aaaabbbccccc"编码压缩为字符串"4a3b5a",就可以将字符串由13B压缩到7B

3. 如何将行程编码方法应用在图像编码中?

① 以对YUV420格式的图像进行H264编码为例,首先将一帧图像划分成一个个16*16的宏块,则对应的Y、U、V分量分别是16*16、8*8、8*8。由于是对三个分量分别编码,以对Y分量的编码为例,可以从图像的左上角开始以之字形扫描每个像素值,就可以得到一个像素值串

② 将行程编码方法应用在图像编码中,就是对得到的像素值串进行行程编码

4. 如何提高行程编码的压缩率?

① 假设使用行程编码方法对字符串"abcdabcdabcd"进行编码,得到的字符串为"1a1b1c1d1a1b1c1d1a1b1c1d",字符串的长度反而从13B增加到25B

可见如果要达到压缩的目的,必须要使得编码前的字符串中出现比较多连续相同的字符

② 类推到图像编码上也是一样的,必须要使得编码前的像素值串也尽量出现连续相同的像素值,最好是一连串数字很小(最好是0)的像素值串

之所以像素值最好为0,是因为在有些编码算法中(e.g. 指数哥伦布算法、算数编码)可以做到只用一个bit或者0.几个bit就可以存储0值

因此在对图像进行熵编码之前的自然需求,就是先对要编码的像素值进行处理,尽可能去除其中的冗余信息,从而将要编码的像素值串变成有很多0的像素值串

说明:本文以行程编码为例进行说明只是为了便于理解,在实际的H264编码标准中,使用的熵编码方式为CAVLC和CABAC

2.2 减少空间冗余

1. 为了提高熵编码的压缩率,使用帧内预测的方法减少空间冗余

2. 帧内预测处理流程如下,

① 在当前编码图像内部已经完成编码的块中找到与当前编码块相邻的块,并通过帧内预测算法得到帧内预测块

② 将当前编码块减去帧内预测块得到残差块,从而减少空间冗余

说明1:相邻块的选择

一般选择即将编码块的左边块、上边块、左上角块和右上角块作为相邻块

说明2:预测块生成

使用不同的算法可以得到多个不同的预测块(上图示例中只是其中一种),因此将当前编码块与不同的预测块相减就会得到多个不同的残差块

② 最终选择这些残差块中像素值的绝对值之和最小的块作为最终的残差块,这个残差块的像素经过扫描生成的像素值串就比直接扫描当前编码块生成的像素值串更接近0

2.3 减少时间冗余

1. 同样是为了提高熵编码的压缩率,使用帧间预测的方法减少时间冗余

2. 帧间预测处理流程如下,

① 在已经编码完成的帧中,通过运动搜索算法得到帧间预测块

② 将当前编码块减去帧间预测块得到残差块,从而减少时间冗余

说明1:通过运动搜索算法得到的预测块也会有多个,因此也会得到多个不同的残差块,此时仍然选择像素值的绝对值之和最小的残差块

说明2:最终选择的预测块所在的已经编码的图像称为参考帧

2.4 减少高频信息

1. 通过帧内预测和帧间预测得到的残差块已经去除了大部分空间冗余和时间冗余,对该残差块进行熵编码可以得到更大的压缩率

但是我们的目标不只是将像素值变小,而是希望能出现连续的0像素,此时就需要通过变换和量化来去除一些高频信息

2. 为了分离图像块的高频和低频部分,需要将残差块变换到频域,通常使用离散余弦变换(DCT变换)。经过DCT变换之后,就可以从残差块得到变换块

变换块中的每个值称为系数,其中左上角的系数值是图像的低频信息,其余的是图像的高频信息

3. 高频信息与低频信息的特性与分布

① 低频信息表示一张图的总体样貌,一般低频系数的值也比较大

② 高频信息主要表示图像中人物或物体的轮廓边缘等变化剧烈的地方,因此高频系数的数量多,但是高频系数的值一般比较小

4. 通过量化减少高频信息

① 由于人眼对高频信息不太敏感,如果通过一种方法去除大部分高频信息,就可以实现在不太影响人眼观感的情况下将变换块中的大部分系数值变为0,也就达到了我们提高熵编码压缩率的目的

② 这种方法就是量化,我们将变换块中的系数都除以一个值(称为量化步长,QStep),得到的结果就是量化后的系数。由于高频系数相比低频系数值更小,量化后就更容易变成0,也就达到了去除高频信息的效果

说明1:量化步长(QStep)和量化参数(QP)

① QStep是编码器内部的概念,用户一般使用QP

② QP和QStep是一一对应的,二者的对应关系如下图所示,

说明2:QStep取值影响

① QStep越大,得到的量化后系数值就会越小,就会去除越多的高频信息,熵编码的压缩率就会更高

② 在解码时,会将QStep乘以量化后的系数得到变换系数,对于之前由于量化变为0的系数此时是无法恢复的,因此量化是一种有损编码

③ QStep值越大,损失就越大,从而解码恢复后的图像清晰度就会越低

说明3:在H264中,如果宏块的大小是16*16,一般会将其划分为16个4*4的块,然后对每个4*4的块做DCT变换

2.5 推理过程小节

1. 为了能够在熵编码时压缩率更高,我们希望进行熵编码的像素值串有尽可能多的连续0像素

2. 为了达到这个目标,先通过帧内预测和帧间预测去除空间冗余和时间冗余,从而得到一个像素值比编码块小很多的残差块

3. 再对残差块进行DCT变换得到变换块,分离出其中的低频信息和高频信息,并利用人眼对高频信息不敏感的特性对变换块的系数进行量化,从而得到有很多连续0像素的像素值串,并对该像素值串进行熵编码

说明1:以上就是视频编码原理的推理过程,视频编码的实际步骤是预测、DCT变换与量化,最后是熵编码

从上图中还可以看出帧内预测和帧间预测是同时进行的,实际上编码器会以宏块为单位遍历所有的预测模式,然后选择最优的预测方式

说明2:在对视频编码过程有了了解之后,下图给出不同编码标准的比较,从中可以看出标准越新,块划分的方式就越多,编码模式也就越多。因此压缩效率也会越大,但是带来的编码耗时也越大

说明3:目前H264和H265的硬件支持已经很好,AV1才刚开始,硬件支持较少

3 H264编码结构

3.1 帧类型

3.1.1 帧内编码帧和帧间编码帧

1. 帧内预测不需要参考已编码帧,可以自行完成编码和解码

2. 帧间预测需要参考已编码帧

① 从类型上,可以参考已编码好的帧内编码帧或帧间编码帧

② 从方向上,可以只参考前面已编码好的帧,也可以同时参考前后已编码好的帧

3.1.2 H264帧类型

1. H264将图像分为I帧(I-frame,Intra-coded picture)、P帧(P-frame,Predicte picture)和B帧(B-frame,Bidirectional predicted picture)

2. 下图是H264中各类帧参考关系的一个示例,其中箭头是从参考帧指向编码帧,其中

① I帧独自编码,不参考其他帧

② 第1个B帧同时参考第1个I帧和第1个P帧

③ 第1个P帧只参考第1个I帧

3.1.3 IDR

1. 如果编码或解码过程中有一个参考帧出现错误,那么参考他的P帧或B帧肯定也会出现错误,而这些出现问题的P帧和B帧又会作为其他帧的参考帧,从而导致错误不断被传递

2. 为了截断这种错误编码的传递,H264引入了一种特殊的I帧,称作IDR帧(Instantaneous Decoder Refresh,立即刷新帧)

H264编码标准中规定,IDR帧之后的帧不能参考IDR帧之前的帧。这样IDR帧之前有错误的帧,也不会被IDR帧之后的帧继续参考

说明1:相较于IDR帧,普通的I帧只是使用帧内预测编码,但是在他后面的P帧和B帧还是可以参考普通I帧之前的帧。但是在大多数情况下,会直接使用IDR帧,而不再使用普通的I帧(虽然标准上可以使用)

说明2:B帧虽然也可以作为参考帧,但实际上很少如此使用

3.2 GOP的概念

1. 在引入IDR帧之后,我们将从一个IDR帧开始到下一个IDR帧的前一帧为止的范围称作GOP(Group of Pictures,图像组)

2. GOP的大小由IDR帧之间的间隔决定,这个间隔被称作关键帧间隔

① GOP越大,编码的I帧就会越少。由于P帧和B帧的压缩率更高,因此整个视频的压缩率就越高

② 但是GOP太大,也会导致IDR帧距离太大,如果中间有参考帧错误,则会引起长时间的花屏和卡顿

③ 因此GOP不是越大越好,也不是越小越好,而是需要根据实际的场景来选择

3.3 Slice的概念

1. Slice(也称作"片")是为了并行编码而设计的

① 首先将一帧图像划分为多个相互独立、互不依赖的Slice

② 在机器性能比较高的情况下,可以多线程并行对多个Slice进行编码,从而提升编码速度

③ 但是也因为一帧图像内的Slice是相互独立的,所以如果进行帧内预测,就不能跨Slice进行,因此编码性能会差一些

2. 引入Slice的概念后,一帧图像内的层次结构如下图所示

① 一帧图像可以划分成一个或多个Slice

② 一个Slice包含多个宏块(Macro Block,MB)

③ 一个宏块又可以划分成多个不同尺寸的子块

4 H264码流结构

视频编码的码流结构是指视频经过编码之后得到的二进制数据是如何组织的

4.1 码流格式

H264码流有两种格式:Annexb格式和MP4格式

4.1.1 Annexb格式(附录B格式)

1. Annexb格式使用起始码来表示一个编码数据的开始

① 起始码本身不是图像编码的内容,只是用于分隔

② 起始码有两种,一种是4B的"00 00 00 01",一种是3B的"00 00 01"

2. 通过字节填充解决编码冲突

① 在图像编码数据中也可能出现"00 00 00 01"和"00 00 01"的模式,这样就会导致起始码和图像编码数据的混淆

② H264通过字节填充的方式解决这种冲突,H264会对图像编码数据中的如下模式进行修改,

  • 将"00 00 00"修改为"00 00 03 00"
  • 将"00 00 01"修改为"00 00 03 01"
  • 将"00 00 02"修改为"00 00 03 02"
  • 将"00 00 03"修改为"00 00 03 03"

 ③ 解码端在去掉起始码之后,需要将对应的模式转换回来

4.1.2 MP4格式

MP4格式没有起始码,而是在图像编码数据的开始使用4B作为长度标识,用来表示编码数据的长度

4.2 码流结构

4.2.1 参数集

1. 在H264码流中,除了图像数据,还有一些视频编码时的参数数据。为了能够将一些通用的编码参数提取出来,不在图像编码数据中重复,H264设计了两个重要的参数集

① SPS(Sequence Parameter Set,序列参数集),主要包括图像的高、宽、YUV格式和位深等基本信息

② PPS(Picture Paramter Set,图像参数集),主要包括熵编码类型、基础量化参数QP和最大参考帧数量等基本编码信息

2. H264码流中的SPS和PPS是至关重要的,如果没有SPS和PPS中的基础信息,之后的I帧、P帧和B帧都无法进行解码

3. 在引入了SPS和PPS之后,H264的码流结构如下,

由于图像帧可以划分为Slice,因此图像帧在码流中实际上是以Slice的形式呈现的

4.2.2 NALU的概念

1. 为了在码流中区分上述数据,H264设计了NALU(Network Abstraction Layer Units,网络抽象层单元)。具体结构如下图所示,其中,

① SPS是一个NALU、PPS是一个NALU、每个Slice也是一个NALU

② 每个NALU由1B的NALU Header和若干字节的NALU Data组成

③ 对于Slice NALU,其中的NALU Data又是由Slice Header和Slice Data组成,而Slice Data又是由一个个MB Data组成

2. NALU Header结构如下图所示,其中,

① F位:forbidden_zero_bit,禁止位,H264码流必须位0

② NRI字段:nal_ref_idc,表示当前NALU的重要性,取值范围为00 ~ 11。参考帧、SPS和PPS对应的NALU该字段必须大于0

③ Type字段:nal_unit_type,表示NALU的类型,其取值如下表所示,

可见对于图像Slice,NALU类型中只区分了IDR Slice(类型为5)和非IDR Slice(类型为1),至于非IDR Slice是普通I Slice、P Slice还是B Slice,需要继续解析Slice Header中的Slice Type字段得到

4.2.3 码流观察实例

1. 可以使用Elecard StreamEye工具分析H264码流

2. 查看H264码流的二进制数据,可以从中解析出各NALU,可见与预期是一致的

说明:分析H265码流可以使用Elecard HEVC工具

4.3 常见工程问题

4.3.1 如何判断哪些Slice属于同一帧?

1. 如上文所述,在H264码流中,图像帧在码流中是以Slice的形式呈现的,因此需要能够区分哪些Slice属于同一个帧

2. 这需要通过Slice的Slice Header进行判断,Slice Header中,有一个first_mb_in_slice字段,该字段表示当前Slice的第一个宏块在当前编码图像中的序号

① 如果first_mb_in_slice字段的值为0,表示当前Slice的第一个宏块是当前编码图像的第一个宏块,也就是说当前Slice是一帧图像的第一个Slice

② 如果first_mb_in_slice字段的值不为0,表示当前Slice不是一帧图像的第一个Slice

只要使用这种方法继续检查后续的Slice,直到找到下一个first_mb_in_slice字段值为0的Slice,就代表新的一帧开始,那么其前一个Slice就是前一帧图像的最后一个Slice

说明:first_mb_in_slice以无符号指数哥伦布编码方式存储

4.3.2 如何从SPS中获取图像分辨率?

1. 在编码端编码视频时,需要设置图像分辨率,但是在解码端不需要设置。这是因为解码端可以从H264码流的SPS中获取分辨率信息

2. 在SPS中使用如下几个字段来表示分辨率的大小,解码端在解码出这几个字段后,通过一定规则的计算就可以得到分辨率的大小

说明1:使用Elecard StreamEye工具查看H264码流的SPS信息

说明2:上述字段也是以无符号指数哥伦布编码方式存储

4.3.3 如何计算得到QP值?

H264码流提供了一种(全局 --> Slice --> 宏块)的QP值设置与调节方式

1. 在PPS中有一个全局基础QP值,字段是pic_init_qp_minus26。当前序列中所有依赖该PPS的Slice共用这个基础QP

2. 每个Slice可以在这个基础QP的基础上做调整,在Slice Header的slice_qp_delta字段中记录了调整偏移值

3. 更进一步地,H264允许在宏块级别对QP值做进一步的精细化调节,这个字段在宏块数据中,叫做mb_qp_delta

说明:设置QP值的字段汇总

① 如果需要计算Slice级别的QP值,则只需要考虑前2个字段

② 如果需要计算宏块级别的QP值,则需要考虑这3个字段

具体计算公式如下,

5 帧内预测

5.1 理论基础

1. 图像具有空间相关性

一帧图像中相邻像素的亮度和色度信息是比较接近的,并且亮度和色度信息也是逐渐变化的,不太会出现突变

2. 帧内编码利用空间相关性进行编码

帧内预测通过利用已经编码的相邻像素的值来预测待编码的像素值,最后达到减少空间冗余的目的

说明:如何利用已编码的像素值预测待编码像素值

① 已编码的像素值已经变成码流,不再是一个个像素

② 在进行编码时,已经编码的块会通过解码重建像素用来做参考像素

5.2 帧内预测规则

说明:以H264标准编码YUV420图像为例

1. 宏块大小为16*16,其中亮度块为16*16,色度块为8*8

2. 16*16的亮度块可以继续划分为16个4*4的子块

因为图像中有的地方细节很多,划分为更小的块来做预测会更精细

3. 帧内预测中亮度块和色度块是分开独立进行预测

亮度块参考已编码亮度块的像素,色度块参考已编码色度块的像素

说明:在实际帧内预测时会分为:4*4亮度块的预测、16*16亮度块的预测、8*8色度块的预测

其中4*4亮度块的预测模式最多,且基本包含了16*16亮度块和8*8色度块的预测模式,因此先对其进行说明

5.3 4*4亮度块帧内预测模式

4*4亮度块帧内预测模式共有9种,包含8种方向模式和一种DC模式,其中方向模式是指预测是有方向角度的

5.3.1 Vertical模式

1. Vertical模式中,当前编码亮度块的每一列的像素值,都是复制上边已经编码块的最下面一行的对应位置的像素值

2. Veritcal模式预测块像素值计算方法如下

3. Vertical模式只有在上边块存在时才可使用,如果不存在则该模式不可用

例如图像最上边的块就没有可参考的块存在

5.3.2 Horizontal模式

1. Horizontal模式中,当前编码亮度块的每一行的像素值,都是复制左边已经编码块的最右边一列的对应位置的像素值

2. Horizontal模式预测块像素值计算方法如下

3. Horizontal模式只有在左边块存在时才可使用,如果不存在则该模式不可用

5.3.3 DC模式

1. DC模式中,当前编码亮度块的每一个像素值,是上边已经编码块最下边一行和左边已编码块最右边一列的所有像素值的平均值

2. 根据上边块和左边块的存在情况,DC模式预测块像素值计算方法如下,可见DC模式预测块中的每个像素值是一样的

3. DC模式在上边块和左边块都不存在时仍可使用,此时会将预测块的像素值设置为1 << (位深 - 1),如果位深为8bit,则该值为128

5.3.4 Diagonal Down-Left模式

1. Diagonal Down-Left模式中,当前编码亮度块的每一个像素值,是上边块和右上块的像素通过插值得到

注意:上边块和右上块可能是同一个块,因为可能是一个16*16的亮度块

2. Diagonal Down-Left模式预测块像素值计算方法如下

3. Diagonal Down-Left模式只有在上边块和右上块都存在时才可使用,如果有一个不存在则该模式不可用

5.3.5 Diagonal Down-Right模式

1. Diagonal Down-Right模式中,当前编码亮度块的每一个像素值,是上边块、左边块和左上角对角的像素通过插值得到

2. Diagonal Down-Right模式预测块像素值计算方法如下

3. Diagonal Down-Right模式只有在上边块、左边块和左上角对角像素都存在时才可使用,如果有一个不存在则该模式不可用

5.3.6 Vertical-Right模式

1. Vertical-Right模式中,当前编码亮度块的每一个像素值,是上边块、左边块和左上角对角的像素通过插值得到

2. Vertical-Right模式预测块像素值计算方法如下

3. Vertical-Right模式只有在上边块、左边块和左上角对角像素都存在时才可使用,如果有一个不存在则该模式不可用

5.3.7 Horizontal-Down模式

1. Horizontal-Down模式中,当前编码亮度块的每一个像素值,是上边块、左

媒体基础视频重新编码产生音频流同步偏移

【中文标题】媒体基础视频重新编码产生音频流同步偏移【英文标题】:Media Foundation video re-encoding producing audio stream sync offset 【发布时间】:2019-03-07 23:35:18 【问题描述】:

我正在尝试编写一个简单的 Windows Media Foundation 命令行工具来使用 IMFSourceReaderIMFSyncWriter 加载视频,将视频和音频作为未压缩流读取并将它们重新编码为 H.246/具有一些特定硬编码设置的 AAC。

The simple program Gist is here

sample video 1

sample video 2

sample video 3

(注意:我一直在测试的视频都是立体声,48000k 采样率)

该程序可以运行,但是在某些情况下,在编辑程序中将新输出的视频与原始视频进行比较时,我看到复制的视频流匹配,但复制的音频流预先固定了一些静音并且音频是偏移的,这在我的情况下是不可接受的。

audio samples:
original - |[audio1] [audio2] [audio3] [audio4] [audio5] ... etc
copy     - |[silence] [silence] [silence] [audio1] [audio2] [audio3] ... etc

在这种情况下,进入的第一个视频帧具有非零时间戳,但第一个音频帧确实具有 0 时间戳。

我希望能够生成一个复制的视频,该视频和音频流中的第一帧为 0,因此我首先尝试从生成视频 i 的所有后续视频帧中减去该初始时间戳 (videoOffset)想要,但导致音频出现这种情况:

original - |[audio1] [audio2] [audio3] [audio4] [audio5] ... etc
copy     - |[audio4] [audio5] [audio6] [audio7] [audio8] ... etc

音轨现在向另一个方向移动了一小部分,但仍然没有对齐。有时,当视频流的起始时间戳为 0 但 WMF 仍会在开头截断一些音频样本时(参见示例视频 3),有时也会发生这种情况!

通过在将音频样本数据传递到IMFSinkWriter 时插入以下代码,我已经能够修复此同步对齐并将视频流偏移到从 0 开始:

//inside read sample while loop
...

// LONGLONG llDuration has the currently read sample duration
// DWORD audioOffset has the global audio offset, starts as 0
// LONGLONG audioFrameTimestamp has the currently read sample timestamp

//add some random amount of silence in intervals of 1024 samples
static bool runOnce false ;
if (!runOnce)

    size_t numberOfSilenceBlocks = 1; //how to derive how many I need!?  It's aribrary
    size_t samples = 1024 * numberOfSilenceBlocks; 
    audioOffset = samples * 10000000 / audioSamplesPerSecond;
    std::vector<uint8_t> silence(samples * audioChannels * bytesPerSample, 0);
    WriteAudioBuffer(silence.data(), silence.size(), audioFrameTimeStamp, audioOffset);

    runOnce= true;


LONGLONG audioTime = audioFrameTimeStamp + audioOffset;
WriteAudioBuffer(dataPtr, dataSize, audioTime, llDuration);

奇怪的是,这会创建一个与原始视频文件匹配的输出视频文件。

original - |[audio1] [audio2] [audio3] [audio4] [audio5] ... etc
copy     - |[audio1] [audio2] [audio3] [audio4] [audio5] ... etc

解决方案是在音频流的开头插入 额外的静音,块大小为 1024。 IMFSourceReader 提供的音频块大小无关紧要,填充是 1024 的倍数。

我的问题是静音偏移似乎没有可检测的原因。为什么我需要它?我怎么知道我需要多少?经过几天的努力,我偶然发现了 1024 样本静音块解决方案。

有些视频似乎只需要 1 个填充块,有些需要 2 个或更多,有些则根本不需要额外的填充!

我的问题是:

有人知道为什么会这样吗?

在这种情况下我是否错误地使用了 Media Foundation 导致了这种情况?

如果我是正确的,我如何使用视频元数据来确定是否需要填充音频流以及填充中需要多少 1024 块静音?

编辑:

对于上面的示例视频:

sample video 1 :视频流从 0 开始,不需要额外的块,原始数据的透传工作正常。

sample video 2:视频流从 834166 (hns) 开始,需要 1 1024 块静默才能同步

sample video 3 : 视频流从 0 开始,需要 2 1024 个静音块才能同步。

更新:

我尝试过的其他事情:

增加第一个视频帧的持续时间以解决偏移:不产生任何效果。

【问题讨论】:

源文件和结果文件中前两个音频和视频样本的时间戳是什么? ^^ 忽略你的人工静音样本 @AndriyTylychko 在这种情况下,第一个视频时间戳是834166,然后是1251249,然后是1668332。音频时间戳从0 开始,然后是213333426666 ^^ 用于羊驼测试视频(需要 1 1024 块静音才能同步) 如果偏移问题是特定于文件的,可能是媒体基金会或用于比较输出的软件中 MP4 文件中的编辑列表原子支持不佳/缺失/错误。也就是说,也许您应该查看原始文件中的 edts/elst 原子,并检查它们是否与您需要添加的偏移量相关。 【参考方案1】:

我编写了另一个版本的程序来正确处理 NV12 格式(你的不工作):

EncodeWithSourceReaderSinkWriter

我使用 Blender 作为视频编辑工具。这是我使用 Tuning_against_a_window.mov 的结果:

从下往上:

原始文件 编码文件 我通过将“elst”原子设置为数字条目的值为 0 来更改原始文件(我使用 Visual Studio hexa 编辑器)

就像 Roman R. 所说,MediaFoundation mp4 源不使用“edts/elst”原子。但是 Blender 和你的视频编辑工具可以。 mp4 源也会忽略“tmcd”音轨。

“edts/elst”:

Edits Atom ( 'edts' )

编辑列表可用于提示轨道...

MPEG-4 File Source

MPEG-4 文件源默默地忽略提示音轨。

所以其实编码是好的。与真实的音频/视频数据相比,我认为没有音频流同步偏移。例如,您可以将“edts/elst”添加到编码文件中,以获得相同的结果。

PS:在编码文件中,我为两个音频/视频轨道添加了“edts/elst”。我还增加了 trak 原子和 moov 原子的大小。我确认,Blender 对原始文件和编码文件显示相同的波形。

编辑

我试图了解 3 个视频样本中 mvhd/tkhd/mdhd/elst 原子之间的关系。 (是的,我知道,我应该阅读规范。但我很懒……)

您可以使用 mp4 浏览器工具来获取 atom 的值,或者使用我的 H264Dxva2Decoder 项目中的 mp4 解析器:

H264Dxva2Decoder

Tuning_against_a_window.mov

tkhd 视频中的 elst(媒体时间):20689 来自 tkhd 音频的 elst(媒体时间):1483

GREEN_SCREEN_ANIMALS__ALPACA.mp4

elst(媒体时间)来自 tkhd 视频:2002 年 来自 tkhd 音频的 elst(媒体时间):1024

GOPR6239_1.mov

tkhd 视频的 elst(媒体时间):0 来自 tkhd 音频的 elst(媒体时间):0

如您所见,对于 GOPR6239_1.mov,从 elst 开始的媒体时间为 0。这就是该文件不存在视频/音频同步问题的原因。

对于 Tuning_against_a_window.mov 和 GREEN_SCREEN_ANIMALS__ALPACA.mp4,我尝试计算视频/音频偏移量。 我修改了我的项目以考虑到这一点:

EncodeWithSourceReaderSinkWriter

目前,我没有找到适用于所有文件的通用计算。

我只是找到了正确编码两个文件所需的视频/音频偏移量。

对于 Tuning_against_a_window.mov,我在(电影时间 - 视频/音频 mdhd 时间)之后开始编码。 对于 GREEN_SCREEN_ANIMALS__ALPACA.mp4,我在视频/音频 elst 媒体时间之后开始编码。

没关系,但我需要为所有文件找到正确的唯一计算。

所以你有两个选择:

对文件进行编码并添加 elst atom 使用右偏移计算对文件进行编码

这取决于您的需求:

第一个选项允许您保留原始文件。但您必须添加第一个原子 使用第二个选项,您必须在编码之前从文件中读取 atom,编码后的文件会丢失一些原始帧

如果您选择第一个选项,我将解释如何添加第一个原子。

PS:我对这个问题很感兴趣,因为在我的 H264Dxva2Decoder 项目中,edts/elst atom 在我的待办事项列表中。 我解析它,但我不使用它......

PS2:这个链接听起来很有趣: Audio Priming - Handling Encoder Delay in AAC

【讨论】:

感谢您对这个问题的详细检查。这确实看起来像正在发生的事情。我正在尝试重新创建您的解决方案,但被困在复制视频的 edts/elst 原子中的内容。当您在 PS 中说您为两个音频/视频轨道添加了原子时,您能否扩展您添加回这些轨道上的 elst 字段的值的确切位置?谢谢!

以上是关于视频技术基础02:视频编码基础的主要内容,如果未能解决你的问题,请参考以下文章

视频编解码学习之一:理论基础

ffmpeg基础知识

视频当量分析和建模---题目分析

视频物联网智能编码,机器视觉编码新体系,AI Image Codec,走向实用的AI图像编解码...

二、视频编解码基础知识

OpenCV3.x视频教程出来啦!