1)RTMP 消息分优先级的设计有什么好处?

RTMP 的消息优先级是:控制消息 > 音频消息 > 视频消息。当网络传输能力受限时,优先传输高优先级消息的数据。

要使优先级能够有效执行,分块也很关键:将大消息切割成小块,可以避免大的低优先级的消息(如视频消息)堵塞了发送缓冲从而阻塞了小的高优先级的消息(如音频消息或控制消息)。

2)什么是 DTS 和 PTS?它们有什么区别?

DTS 是解码时间戳;PTS 是显示时间戳。

虽然 DTS、PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。

当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是一致的。但如果有 B 帧时,就回到了我们前面说的问题:解码顺序和播放顺序不一致了。DTS 告诉我们该按什么顺序解码这几帧图像,PTS 告诉我们该按什么顺序显示这几帧图像。

3)什么是 IDR 帧?它和 I 帧有什么区别?

IDR 帧全称叫做 Instantaneous Decoder Refresh,是 I 帧的一种。IDR 帧的作用是立刻刷新,重新算一个新的序列开始编码,使错误不致传播。

IDR 帧有如下特性:

IDR 帧一定是 I 帧,严格来说 I 帧不一定是 IDR 帧(但一般 I 帧就是 IDR 帧);

对于 IDR 帧来说,在 IDR 帧之后的所有帧都不能引用任何 IDR 帧之前的帧的内容。与此相反,对于普通的 I 帧来说,位于其之后的 B 和 P 帧可以引用位于普通 I 帧之前的 I 帧(普通 I 帧有被跨帧参考的可能);

播放器永远可以从一个 IDR 帧播放,因为在它之后没有任何帧引用之前的帧。因此,视频开头的 I 帧一定是 IDR 帧;一个封闭类 GOP 的开头的 I 帧也一定是 IDR 帧。

所以,在直播场景通常每个 I 帧都是 IDR 帧,这样服务端下发流数据的时候总是从一个 I 帧开始,播放器就可以立即开始播放。

本文福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs),有需要的可以进企鹅裙927239107领取哦~

4)什么是 SPS 和 PPS?它们有什么区别?

SPS,Sequence Paramater Set,保存了一组编码后的图像序列所依赖的全局参数。

PPS,Picture Paramater Set,保存了每一帧编码后的图像所依赖的参数。

SPS 中的信息至关重要,如果其中的数据丢失,解码过程就可能失败。SPS 和 PPS 通常作为解码器的初始化参数。一般情况,SPS 和 PPS 所在的 NAL 单元位于整个码流的起始位置,但是在某些场景下,在码率中间也可能出现这两种结构:

解码器要在码流中间开始解码。比如,直播流。

编码器在编码过程中改变了码率的参数。比如,图像的分辨率。

5)什么是 SEI?我们可以用它来做什么?

SEI 即补充增强信息(Supplemental Enhancement Information),属于码流范畴,它提供了向视频码流中加入额外信息的方法,是 H.264 标准的特性之一。

SEI的基本特征如下:

并非解码过程的必须选项;

可能对解码过程(容错、纠错)有帮助;

集成在视频码流中。

在直播场景,我们通常使用 SEI 来携带推流端的信息,一直随着直播流传输到播放端。由于 SEI 是绑定着视频帧,所以它可以支持诸如:

统计直播推流端到播放端延时。

支持和视频帧绑定的内容交互。比如,直播答题在播放端的弹窗等。

6)什么是 MP4 的 moov Box?我们在封装 MP4 时通常怎么处理它?为什么?

moov Box 即 Movie Box,MP4 中存储所有媒体数据的索引信息的 Box。moov Box 可以说是 MP4 文件中最重要的 Box,一般播放器的实现都需要读取到 moov 的数据才能开始播放流程。

对于通过网络播放 MP4 视频的场景,都建议将视频处理为 moov 前置。因为 moov 前置后,从网络读取和播放 MP4 文件时,就可以较快获取到 moov 的数据并开始播放。

7)如何根据 NALU 裸流数据来判断其是 H.264 编码还是 H.265 编码?

1)通常在处理音视频数据时,我们如何选择解码器?

通常我们不是根据 NALU 裸流数据中的信息来选择解码器,而是根据媒体封装层的信息来确定解码器。

媒体封装层是表示媒体数据是什么封装格式的,比如 MP4、FLV。在这层信息里,通常会携带码流编码格式的信息。

拿 MP4 来说,我们可以根据 Sample Description Box(moov/trak/mdia/minf/stbl/stsd) 中的信息来确定其封装的码流的编码格式。参考:《MP4 格式》。

对于 FLV,我们可以根据 VideoTagHeader 中的 CodecID 等信息来确定其封装的码流的编码格式。参考:《FLV 格式》。

这样的好处是效率比较高,解封装的时候就可以确定选择何种解码器了。

2)怎么识别 NALU 裸流数据的编码格式是 H.264 还是 H.265?

但是,如果出现题目中的情况,没有对码流进行封装,而是直接传输码流时,这时候 NALU 中有什么字段能标识自己的编码格式吗?答案是,没有这样明确的字段能标识码流的编码格式。

但是这个问题也不是不能解决,因为 H.264、H.265 码流本身也是遵循一定格式规范的,我们可以按照它的格式规范进行探测,如果能解析出来正确的信息,那也可以确定它的编码格式。

比如,拿 H.265 来说,FFmpeg 中 hevcdec.c 就有对其码流数据进行探测的函数 hevc_probe(...)。

所以,我们可以按照编码格式规范探测,比如 H.265 如果解析出了 pps、sps、vps 的各字段信息符合规范,就认为它是 H.265 的编码;如果不是,在你们的码流格式范围中就只剩 H.264 了;接下来将码流数据交给对应的解码器解码即可。

8)为什么视频会议用 UDP?如果用 TCP 实现音视频,需要建立几次连接?用 UDP 实现音视频,有什么方法可以保证通话质量?

1)为什么视频会议用 UDP?

视频会议场景最重要的体验指标一般是『通话延时』和『语音音质』两方面。

在传输层使用 UDP 的主要考虑是为了降低通话延时。因为 UDP 的不需要 TCP 那样的面向连接、可靠传输、拥塞控制等机制,这些机制(比如三次握手建连、丢包重传等)通常都会带来相对 UDP 更高的延时。

当然,另外一方面原因是人们对视频会议中图像信息的损失容忍度是比较高的,这样即使 UDP 无法保证可靠性,有时候还是可以接受的。

2)如果用 TCP 实现音视频,需要建立几次连接?

可以做到只建连一次,多路复用。

也可以音频和视频各使用一路连接。

3)用 UDP 实现音视频,有什么方法可以保证通话质量?

使用 UDP 享受了低延时,牺牲了可靠性。但可靠性牺牲太多导致不可用也是不可接受的,所以还需要做一些机制来保证一定的可靠性,比如我们可以参考 WebRTC 的机制:

NACK:通过丢包重传解决丢包问题,会增加延时。

FEC:通过冗余数据解决丢包问题,会增加带宽占用。

JitterBuffer:通过队列对接收到的数据进行缓冲,出队时将数据包均匀平滑的取出,解决视频的乱序与抖动。

NetEQ:类似 JitterBuffer,解决音频的乱序与抖动。

9)为什么会有 YUV 这种数据?它相比 RGB 数据有什么优点?

RGB 工业显示器要求一幅彩色图像由分开的 R、G、B 信号组成,而电视显示器则需要混合信号输入,为了实现对这两种标准的兼容,NTSC(美国国家电视系统委员会)制定了 YIQ 颜色模型,它的主要优点是可以实现对彩色电视和黑白电视的兼容,即可以用黑白电视收看彩色电视信号。YUV 颜色模型则是在 YIQ 的基础上发展而来。

YUV 颜色模型中用亮度、色度来表示颜色。它的亮度信息和色度信息是分离的,其中 Y 表示亮度通道,U 和 V 则表示色度通道。如果只有 Y 信息,没有 U、V 信息,那么表示的图像就是灰度图像。YUV 常用在各种影像处理场景中。YUV 在对照片或视频编码时,考虑到人眼对亮度信息的敏感度高于色度信息,允许降低色度的带宽。这样一来就可以对色度信息进行压缩,所以 YUV 可以相对 RGB 使用更少的数据带宽。比如常见的采样格式有:4:2:1、4:1:1、4:2:0 等,它们分别相对 RGB 压缩了 33.3%、50%、50% 的数据量。

10)OpenGL 是按照什么架构设计的?

OpenGL 的渲染架构是 Client/Server 模式:Client(客户端)指的是我们在 CPU 上运行的一些代码,比如我们会编写 OC/C++/Java 代码调用 OpenGL 的一些 API;而 Server(服务端)则对应的是图形渲染管线,会调用 GPU 芯片。我们开发的过程就是不断用 Client 通过 OpenGL 提供的通道去向 Server 端传输渲染指令,来间接的操作 GPU 芯片。

11)什么是渲染上下文(Context)?

OpenGL 自身是一个巨大的状态机(State Machine):一系列的变量描述 OpenGL 此刻应当如何运行。OpenGL 的状态通常被称为 OpenGL 上下文(Context)。我们通过改变上下文中的状态来改变接下来绘画的属性和操作的缓冲对象,然后 OpenGL 利用当前的上下文(Context)的状态去渲染。因此状态的改变要非常小心,因为是状态是全局,会影响接下来的所有渲染操作。

12)什么是离屏渲染?

GPU 渲染机制:CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后屏幕控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行。

离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

特殊的离屏渲染:如果将不在 GPU 的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的离屏渲染方式:CPU 渲染。

13)为什么离屏渲染会造成性能损耗?

当使用离屏渲染的时候会很容易造成性能消耗,因为离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。

由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

14)什么是 OpenGL 渲染管线(Pipeline)?

OpenGL 渲染管线就是 OpenGL 的工作流程,指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。

图形渲染管线可以大致被划分为两个主要部分:第一部分把你的 3D 坐标转换为 2D 坐标;第二部分是把 2D 坐标转变为实际的有颜色的像素。

15)OpenGL 渲染管线主要包含哪些部分?

OpenGL 的渲染管线其实也是类似的一个过程,它的工序包括:顶点着色器 → 图元装配 → 几何着色器 → 光栅化 → 片段着色器 → 测试与混合。

16)为什么说 OpenGL 渲染管线中的着色器(Shader)是可编程管线?

OpenGL 渲染管线中着色器允许开发者自己配置,这样我们就可以使用 GLSL(OpenGL Shading Language)来编写自己的着色器替换默认的着色器,从而更细致地控制图形渲染管线中的特定部分。

17)有哪些着色器可以由程序员进行编程?

可编程的着色器有:顶点着色器(Vertex Shader)、几何着色器(Geometry Shader)、片段着色器(Fragment Shader)。常用的是顶点着色器和片段着色器。

18)什么是 VBO、EBO 和 VAO?

可以认为它们是在 OpenGL 中处理数据的三大类缓冲内存对象。

VBO(Vertex Buffer Objects)顶点缓冲区对象,指的是在 GPU 显存里面存储的顶点数据(位置、颜色)。

EBO(Element Buffer Object)图元索引缓冲区对象,指的是为了更高效的利用数据,存储索引来达到减少重复数据的索引数据。

VAO(Vertex Array Object)顶点数组对象,主要作用是用于管理 VBO 或 EBO,减少 glBindBuffer、glEnableVertexAttribArray、glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。

19)Vertex Buffer Object 的布局格式是怎样的?

当我们的 Vertex Shader 如下:

#version 330 core
layout (location = 0) in vec3 position; // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 color;    // 颜色变量的属性位置值为 1

out vec3 ourColor; // 向片段着色器输出一个颜色

void main()
{
   gl_Position = vec4(position, 1.0);
   ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}

Fragment Shader 如下:

#version 330 core
in vec3 ourColor;
out vec4 color;

void main()
{
   color = vec4(ourColor, 1.0f);
}

创建 VBO 的代码如下:


GLfloat vertices[] = {
   // 位置              // 颜色
    0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
   -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
    0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};

GLuint VBO;
glGenBuffers(1, &VBO);  
glBindBuffer(GL_ARRAY_BUFFER, VBO);  
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

20)RTMP 和 RTSP 有什么区别?使用 RTSP 是基于 UDP 传输的话,我们怎样进行乱序重排?

RTMP 和 RTSP 的区别:

RTMP 使用 TCP 作为传输层协议,能保证不丢包和接收顺序,传输质量高。

RTSP 使用 RTP 格式协议和 RTCP 控制协议,命令与数据分离。传输层协议一般会选择 UDP,延迟比较低,传输效率高。

RTSP 中的 RTP 格式头中有 SequenceNumber 字段,可以通过这个序号实现排序。

21)假如给你一堆乱序的 RTP 包,应该怎样实现乱序重排?

可以利用接收 RTP 包缓冲队列使用包的序号进行排序。

在丢包情况下为保证传输质量会引入 NACK 和 FEC 机制。

NACK 表示接收端通知发送端一些包丢失,发送 NACK 包请求重传;FEC 前向纠错值的是每个包携带一些冗余信息可以在部分包丢失的时候利用其他包进行重建。

如果重传次数过多,包无法重建,或者丢的包过多,此时可以丢帧直接跳过丢失的部分。

22)对硬件解码有了解吗?

硬件解码指的是使用硬件的专门处理视频的硬件资源(GPU 和特殊芯片)来解码视频,与软解相比,硬解有速度快、能耗低的特点,但硬解依赖设备提供的能力,支持格式较少,解码前需要看设备是否支持当前的格式。

在 iOS 平台使用 Videotoolbox,Android 平台使用 Mediacodec 来使用硬解能力。硬解码可以直接解码出纹理进行渲染,相比于软解要做一层 CPU 数据转换到 GPU,渲染效率也更高。

FFmpeg 也集成了 Android 和 iOS 的硬解能力,如果在自己的项目中需要引入硬解能力,可以用 OpenMAX 来作为统一接口来集成各平台的硬解能力。

23)你在项目中使用过 SDL 进行渲染,能否讲一下 SDL 渲染?

SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,使用 C 语言写成。其主要用于游戏开发中的多媒体处理,如:视频渲染、音频播放、鼠标键盘控制等操作。它对外接供了一套统一的接口,但在内部,它会根据不同平台调用不同的底层 API 库。

SDL 的基本流程如下:

1、初始化 SDL

2、创建窗口

3、创建渲染器

4、清空缓冲区

5、绘制要显示的内容

6、最终将缓冲区内容渲染到 Window 窗口上

7、销毁渲染器

8、销毁窗口

9、退出 SDL

24)对 YUV 格式有了解吗?YUV 数据做转换是怎样实现的,比如说 YUV422 转为 YUV420?

YUV 格式是传输视频常用的格式,因为相对于 RGB 格式它可以节省更多空间。

YUV 的格式有很多,例如:YUV444、YUV422、YUV420,常用的 YUV 格式是 YUV420 格式。Y 表示亮度信息,是人眼最敏感的分量,UV 则表示色度信息。

YUV420 表示采样方式:UV 分量具有 2:1 的水平采样,2:1 的垂直采样,这里并不是指只有 U,没有 V,而是对于每一行,只有一个 U 或者 V 分量,如果第一行是 4:2:0,那么下一行就是 4:0:2。

可以用工具 YUVView 直接打开 YUV 格式的数据。

YUV 数据因为计算量大和数据量大可以都放到 GPU 存储和计算,YUV422 转 YUV420 可以利用 OpenGL 将 YUV422 的 UV 数据转换成 texture 纹理,编写 shader 做格式转换逻辑继而生成 YUV420 的 UV texture,再通过 readPixel 将显存的 UV 数据读取出来。转换逻辑即将纹理 UV 分量隔行采样。

25)你在项目中是怎么降低端到端的延时的?

下面是直播走 RTMP 推流、HTTP-FLV 播放方案降低端到端延迟的思路:

推流端的延迟包含编码延迟和发送缓存队列引入的延迟。可以通过调节编码参数(B 帧、码率、帧率)减小编码延迟但会影响画质。另外可以提高上传的传输性能来减小传输时长。

CDN 链路上的传输延迟。包括推流的链路和播放回源的链路,这部分延迟不是太大,但依旧会引入几百 ms 的延迟。

CDN 拉流边缘节点的吐流策略会直接影响延迟的大小。直播流编码的 GOP 的长度,CDN 在客户端拉流时吐几秒的数据、按照 GOP 分隔如何丢数据,这些策略都会影响延时。

播放端可以通过对当前已下载的 buffer 进行倍速播放和跳帧来降低缓存从而达到降低延迟。注意如果倍速过大,声音是会明显变调的,需要通过算法来调整。跳帧一定要注意视频跳到 I 帧,音频对齐视频进行丢弃。

26)你对视频倍速播放的时候,是否有改变音调?

倍速变大和变小都会带来变调的问题,目前流行的开源项目有 soundtouch 和 sonic 来达到变速不变调的效果,最经典的就是使用时域压扩(TSM)的算法。

27)点播的倍速播放要如何实现?

点播的倍速播放分为视频处理和音频处理部分。

1)视频处理

对应视频数据的处理,核心逻辑就是按照倍速重新计算各视频帧的 pts 时间戳。

比如,对一个视频做 2 倍速播放,假设原来各视频帧的 pts 依次是 0, 30, 60, 90 ...,倍速处理及将它们除以 2 变成 0, 15, 30, 45 ...。这样处理后,视频的帧率和总时长相应的也发生了变化,帧率变为原来的 2 倍,总时长变为原来的 1/2。

但是,如果对视频进行高倍速播放,比如 10 倍速,这时候如果只处理 pts,原视频的时间戳除以 10 变成 0, 3, 6, 9 ...,这时候 3ms 一帧,帧率达到了 333fps,已经超过了屏幕硬件的刷新率,根本渲染不过来。所以对于高倍速播放:第一步,我们像上面一样处理完 pts;第二步,我们还需要设定最大帧率的限制,并按照这个最大帧率来进行丢帧。

假如我们设定最大帧率是 60fps,这时候我们每 17ms 只需要一帧,上面的 0, 3, 6, 9, 12, 15, 18 ... 经过丢帧和帧率处理可能就变成了 0, 17(18→17), 34(33→34) ...。

高倍速播放还有另外的问题:解码性能是否跟得上、网络视频的下载速度是否跟得上等等。对于这些问题,我们可能还需要其他方案来解决,比如:在客户端在解码前就要丢弃非参考帧,对不需要解码的帧直接丢弃等等;在服务端对高倍速视频进行预处理,提前做好时间戳和丢帧处理,当用户切换高倍速时,帮用户切换资源即可。

2)音频处理

音频是每秒几 K 到几十 K 的采样点,对于音频数据的处理,如果只是跟视频一样,简单的处理 pts 时间戳会出现噪音、杂音,体验很差。音频一般需要进行重采样处理。比如,原来的音频是 48K 的采样率,播放设置了使用 48K 的采样率进行音频渲染,这时候要对音频做 2 倍速播放,可以将音频数据每秒 48K 个采样点重采样降低到 24K 个(把音频数据的采样率处理为 24K),当播放还是使用 48K 的采样率来播放时,每秒需要 48K 个采样点,这时候就需要 2s 的数据,这时候音频的播放速度就变成了原来的 2 倍。使用这样的方式来实现音频倍速,可以解决只简单处理 pts 带来的噪音、杂音问题,但是音频的播放会变调:快播时,声调会变高,听起来尖细;慢播时,声调会变低,听起来低沉。

28)视频编辑中如何实现视频倒放?

如果用最直接的思路去实现视频倒放,那就是把视频中的每一帧图像都解码出来逆排序一下,然后将原视频的 pts 时间戳一一对应的关联上逆排序后的每一帧,再重新编码就可以了。

这个思路在实际实现时会有几个问题:解码后的视频帧放在内存或磁盘可能都还挺大的;完整的解码、逆排序、编码一整个视频可能耗时较长。

对于这些问题我们可以采取分而治之、并行提效的思路。因为视频本身可以按照 GOP 单元独立编解码,所以我们可以把视频的每一个 GOP 单元取出来分别做解码、逆排序、编码,最后再把处理后的所有 GOP 重新 remux 封装起来即可。

其中逆排序过程中,对于一个 GOP 的各帧处理流程大致是这样的:比如一个 GOP 的各视频帧及对应的 pts 分别是 1(0), 2(30), 3(60), 4(90),那逆排序后就是 4(0), 3(30), 2(60), 1(90)。

29)播放器解码后的帧缓冲区一般设置多大合适?

对于播放器来说,在渲染之前一般会有一个解码后的帧缓冲区用于后续渲染。

使用 FFmpeg 软解码、Android MediaCodec 硬解码时,给编码器的数据是按照 dts 顺序输入的,解码器输出数据是按照 pts 输出的。但是使用 iOS VideoToolbox 硬解码时,解码器输出数据并没有按照 pts 顺序,而是解一帧出一帧,需要我们自己排序。

这样在使用 iOS VideoToolbox 硬解码时,还可以在这个缓冲区还可以用来对解码后的帧按 pts 做重排来保证正确的渲染顺序。

这个重排主要是针对 B 帧,因为 B 帧可能会依赖后面的帧才能完成解码,如果解码后的帧缓冲区太小,可能导致按照渲染顺序本该渲染的 B 帧由于依赖帧还未解码而出现播放卡顿。但是解码后的帧缓冲区也不是越大越好,因为解码后的视频帧数据是比较大的,会占用不少内存,缓冲区过大会造成播放器的内存占用过大。

那么这个缓冲区应该设置多大比较合适呢?这个其实取决于解码器需要的重排窗口大小,解码后的帧缓冲区大小只要不超过这个重排窗口尺寸即可。要计算重排窗口的大小通常可能会用到下面这几个参数:

  • max_ref_frames
  • max_num_reorder_frames

下面是一些开源项目里的实现方案:

1)IJKPlayer

IJKPlayer 计算重排窗口大小的实现方案是在保证最小为 2、最大为 5 的限制下来使用 sps.max_ref_frames 作为缓冲区大小。

fmt_desc->max_ref_frames = FFMAX(fmt_desc->max_ref_frames, 2);
fmt_desc->max_ref_frames = FFMIN(fmt_desc->max_ref_frames, 5);

2)VLC

VLC 计算重排窗口大小的实现方式如下:

  • 1)根据 AVC E.2.1 标准尝试取值 max_num_reorder_frames;
  • 2)判断如果是特定的 profile,就不需要排序,设置 max_num_reorder_frames 为 0 并返回,表示不用重排;
  • 3)如果 profile 都不符合,则根据 sps 中的 level 限制计算 max_dpb_frames 并返回,max_dpb_frames 默认为 16。

这里的 dpb 是 decoded picture buffer 的缩写。

static uint8_t h264_get_max_dpb_frames( const h264_sequence_parameter_set_t *p_sps )
{
    const h264_level_limits_t *limits = h264_get_level_limits( p_sps );
    if( limits )
    {
        unsigned i_frame_height_in_mbs = ( p_sps->pic_height_in_map_units_minus1 + 1 ) *
                                         ( 2 - p_sps->frame_mbs_only_flag );
        unsigned i_den = ( p_sps->pic_width_in_mbs_minus1 + 1 ) * i_frame_height_in_mbs;
        uint8_t i_max_dpb_frames = limits->i_max_dpb_mbs / i_den;
        if( i_max_dpb_frames < 16 )
            return i_max_dpb_frames;
    }
    return 16;
}

bool h264_get_dpb_values( const h264_sequence_parameter_set_t *p_sps,
                          uint8_t *pi_depth, unsigned *pi_delay )
{
    uint8_t i_max_num_reorder_frames = p_sps->vui.i_max_num_reorder_frames;
    if( !p_sps->vui.b_bitstream_restriction_flag )
    {
        switch( p_sps->i_profile ) /* E-2.1 */
        {
            case PROFILE_H264_BASELINE:
                i_max_num_reorder_frames = 0; /* only I & P */
                break;
            case PROFILE_H264_CAVLC_INTRA:
            case PROFILE_H264_SVC_HIGH:
            case PROFILE_H264_HIGH:
            case PROFILE_H264_HIGH_10:
            case PROFILE_H264_HIGH_422:
            case PROFILE_H264_HIGH_444_PREDICTIVE:
                if( p_sps->i_constraint_set_flags & H264_CONSTRAINT_SET_FLAG(3) )
                {
                    i_max_num_reorder_frames = 0; /* all IDR */
                    break;
                }
                /* fallthrough */
            default:
                i_max_num_reorder_frames = h264_get_max_dpb_frames( p_sps );
                break;
        }
    }

    *pi_depth = i_max_num_reorder_frames;
    *pi_delay = 0;

    return true;
}

3)Chrome

Chrome 计算重排窗口大小的实现方式如下:

  • 1)判断 sps 的 pic_order_cnt_type 是否为 2,如果是,则直接返回 0,表示不用重排;
  • 2)尝试取 max_dpb_frames,默认值为 16;
  • 3)根据 AVC E.2.1 标准尝试取值 max_num_reorder_frames 并返回;
  • 4)判断如果是特定的 profile,就直接返回 0,表示不用重排;
  • 5)如果上述流程都没返回,则直接返回 max_dpb_frames。
// TODO(sandersd): Share this computation with the VAAPI decoder.
int32_t ComputeH264ReorderWindow(const H264SPS* sps) {
  // When |pic_order_cnt_type| == 2, decode order always matches presentation
  // order.
  // TODO(sandersd): For |pic_order_cnt_type| == 1, analyze the delta cycle to
  // find the minimum required reorder window.
  if (sps->pic_order_cnt_type == 2)
    return 0;

  int max_dpb_mbs = H264LevelToMaxDpbMbs(sps->GetIndicatedLevel());
  int max_dpb_frames =
      max_dpb_mbs / ((sps->pic_width_in_mbs_minus1 + 1) *
                     (sps->pic_height_in_map_units_minus1 + 1));
  max_dpb_frames = std::clamp(max_dpb_frames, 0, 16);

  // See AVC spec section E.2.1 definition of |max_num_reorder_frames|.
  if (sps->vui_parameters_present_flag && sps->bitstream_restriction_flag) {
    return std::min(sps->max_num_reorder_frames, max_dpb_frames);
  } else if (sps->constraint_set3_flag) {
    if (sps->profile_idc == 44 || sps->profile_idc == 86 ||
        sps->profile_idc == 100 || sps->profile_idc == 110 ||
        sps->profile_idc == 122 || sps->profile_idc == 244) {
      return 0;
    }
  }
  return max_dpb_frames;
}

#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
int32_t ComputeHEVCReorderWindow(const H265VPS* vps) {
  int32_t vps_max_sub_layers_minus1 = vps->vps_max_sub_layers_minus1;
  return vps->vps_max_num_reorder_pics[vps_max_sub_layers_minus1];
}
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

30)如何监控视频播放黑屏、花屏、绿屏等异常?

视频播放时如果遇到黑屏、花屏、绿屏通常会伴有解码器的报错或异常信息,我们可以上报这些异常信息来实现对这些情况的监控。

但是也有一些情况,即使出现黑屏、花屏、绿屏的情况了,解码器也没有报错或异常,这时候就需要我们对解码后的画面进行检测来识别这些问题。一般可以这样:

  • 用传统的图像处理算法来识别。对于黑屏、绿屏可以用传统图像处理算法来进行识别,但这里也会有一些误识别的问题,比如视频本身就有些亮度较低、正常全黑帧、正常全绿帧的情况,可能也会被识别为异常。
  • 训练 AI 模型类识别。对于花屏,可以训练 AI 模型来进行识别。训练过程可以通过人工构造丢帧视频的方法来生成花屏样本,同时筛选出无花屏问题的正常样本,基于这两类样本来做二分类模型的训练。

31)PCM 音频数据是怎么组织的?

1)数据生成

模拟数据 → 采样 → 量化 → 编码 → 数字信号

2)数据组成

  • 采样频率
    • 单位时间内对模拟信号的采样次数,它用赫兹(Hz)来表示
    • 采样频率越高,声音的还原就越真实越自然,当然数据量就越大
    • 常见的采样频率有 22.05KHz、44.1KHz、48KHz
  • 采样位数
    • 又称为采样精度,量化级,也相当于每个采样点所能被表示的数据范围
    • 采样位数越大,所能记录声音的变化度就越细腻,相应的数据量就越大
    • 采样位数通常有 8bits、16bits、32bits,16bits 最为常见
    • 超过 8bits,存储时还会有 little endian 和 big endian 的问题
  • 声道数
    • LRLRLR.... 这是交错排列方式,比较常见
    • 也有 LLLLLL.......RRRRRR..... 这种是平铺方式,这种比较少见
    • 又称为通道数,指的是:能支持不同发声的音响个数,它是衡量音响设备的重要指标之一
    • 双声道通常排列方式:
  • 其它
    • 空间大小 = 时长 * 采样频率 * 声道数 * (采样位数 / 8);

32)说一下对信号时域、频域的理解?

1)时域

  • 定义:
    • 横轴时间,纵轴振幅(反应时间的能量),表示所有频率叠加的正弦波振幅的总大小随时间的变化规律,复合波形进行傅里叶变换,可拆解还原成每个频率上单一的正弦波构成
  • 在音频中的作用:
    • 检测音乐整体音量大小
    • 检测混音时的动态和响度问题
    • 辅助调节压缩器和限制器

2)频域

  • 定义:
    • 横轴频率,纵轴振幅,表示一个静态的时间点上各个频率正弦波的振幅的分布情况
  • 在音频中的作用:
    • 检测音乐细节在各频段上的分布
    • 辅助调节滤波器和均衡器

3)时域 vs. 频域

33)PCM 数据经过 AAC 编码器编码后,直接写 .aac 文件会怎么样?

正常播放器会没法识别播放(因为不知道声道数,采样率等信息)。一般要这样做:正常需要在编码每帧数据后,结合编码后的数据生成 ADTS 头,然后将 ADTS 头 + 编码后的数据 整体写入文件,循环往复,才能生成可正常播放的 .aac 文件(当然也可以是:1 个 ADTS + 多帧编码数据 这样的组合)。

34)AAC 封装到 MP4 中,是否需要为每个 AAC packet 添加 ADTS?

不需要,原因如下:

  • 1)我们所说的 AAC 文件实际是 AAC 封装格式,其实在 AAC 编码格式的基础上添加了 ADTS 头等信息,组装成 AAC 封装格式的;
  • 2)将 AAC 编码后的数据存放到 MP4 中,就需要按照 MP4 的封装格式来进行存储;
  • 3)MP4 中实际将类似 ADTS 这些信息存放到了 moov 中的音频通道对应的 box 中了。

35)拍短视频想把同时播放的音乐录制下来一般要经过怎样的处理流程?

有一种方案是把麦克风采集声音中的外放音乐声进行回声消除,然后再添加上音乐的原始音轨。一般在 iOS 设备上可以考虑这种方案。但是在 Android 设备上我们通常不这样做,原因有下面几点:

  • 对音乐进行回声消除的同时也会对麦克风采集到的其他声音有抑制效果,导致最后整体的声音效果不好;
  • 手机播放音乐到麦克风采集到声音之间有一定的延时,不同的 Android 设备的延时差异较大,这个延时估算不准确会影响回声消除的效果。

一般情况我们可以按照下面的流程来处理:

  • 1)音乐外放的情况,直接通过外放播放音乐,声音通过麦克风录制下来;
  • 2)戴耳机或手机静音的情况,音乐不会被麦克风录制下来;
  • 3)录制完成时,将录制得到的视频中的音轨(这里面可能包含已经和其他外音被采集下来的音乐)和音乐原始的音轨进行叠加增强。

36)如何代码实现 PSNR 来评估编码质量?

1)PSNR 定义

PSNR(Peak Signal-to-Noise Ratio,峰值信噪比)是一种衡量图像质量的指标,常用于评估压缩算法的效果。它通过比较原始图像与压缩/恢复后的图像之间的差异,来量化图像质量的损失程度。

2)PSNR 计算公式

PSNR = 10 * log10((MAX^2) / MSE)

其中,MAX 是图像像素值的最大可能取值(通常为255),MSE 是均方误差(Mean Squared Error),表示压缩/恢复图像与原始图像之间的平均像素差的平方。

3)PSNR 计算实现

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

double psnr(unsigned char* original_img, unsigned char* compressed_img, int width, int height) {
    double mse = 0.0;
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int diff = original_img[i * width + j] - compressed_img[i * width + j];
            mse += diff * diff;
        }
    }
    mse /= (double)(width * height);
    double max_value = 255.0;
    double psnr = 10.0 * log10((max_value * max_value) / mse);
    return psnr;
}

int main() {
    // 读取原始图像和压缩/恢复后的图像
    FILE* original_file = fopen("original_image.raw", "rb");
    FILE* compressed_file = fopen("compressed_image.raw", "rb");
    if (original_file == NULL || compressed_file == NULL) {
        printf("无法打开图像文件\n");
        return 1;
    }

    int width = 512;  // 图像宽度
    int height = 512; // 图像高度

    unsigned char* original_img = (unsigned char*)malloc(width * height * sizeof(unsigned char));
    unsigned char* compressed_img = (unsigned char*)malloc(width * height * sizeof(unsigned char));
    fread(original_img, sizeof(unsigned char), width * height, original_file);
    fread(compressed_img, sizeof(unsigned char), width * height, compressed_file);

    fclose(original_file);
    fclose(compressed_file);

    // 计算PSNR
    double psnr_value = psnr(original_img, compressed_img, width, height);
    printf("PSNR: %.2f\n", psnr_value);

    // 释放内存
    free(original_img);
    free(compressed_img);

    return 0;
}

上述代码中的 original_image.raw 和 compressed_image.raw 是示例图像的文件名,你需要根据实际情况修改为你自己的图像文件名。此外,代码中假设图像是灰度图像,如果你的图像是彩色图像,需要进行相应的修改。

通过计算 PSNR,我们可以得到一个数值来评估压缩/恢复图像与原始图像之间的质量损失程度。PSNR 的数值越高,表示图像质量的损失越小。

37)如何测试码率质量甜点?

在视频领域,质量甜点指的是在既定的码率和屏幕大小下通过设定合理的分辨率和帧率来得到最佳视频主观质量体验。因为编码复杂度和编解码质量亦不是线性关系,两者之间也存在一个质量甜点。在音频领域也有类似的情况,针对具体的情况,我们可以测试手机的编码质量来选择指定分辨率、帧率时对应的码率甜点。

在这种测试中我们一般需要分场景进行,比如:

  • 外景拍摄,低运动
  • 外景拍摄,中等运动
  • 外景拍摄,高运动
  • 人像拍摄,低运动
  • 人像拍摄,中等运动
  • 人像拍摄,高运动

测试指标我们可以采用 PSNR、SSIM、VMAF 进行综合考量。

比如,我们可以测试 iOS 硬编,使用 540P,15 帧推流时,设置不同的码率(800kbps-1300kbps)分别测试各场景下的各指标值,找出 R-D(码率-失真)曲线拐点出现的区间,这就是我们要找的码率甜点。

38)iOS 如何实现夜晚自动提示打开手电筒?

当夜晚使用共享单车扫码时,应该都见过提示“打开手电筒”,在 iOS 中我们如何实现呢?主要基于图像环境光参数,参考如下代码。

iOS 视频采集设置 AVCaptureVideoDataOutput AVCaptureVideoDataOutputSampleBufferDelegate,通过获取 CMSampleBufferRef 每一帧视频数据环境参数进行判断即可。

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
 if(!sampleBuffer){
        return;
    }
    
    CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer,  kCMAttachmentMode_ShouldPropagate);

 NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge  NSDictionary*)metadataDict];

 CFRelease(metadataDict);

 NSDictionary *exifMetadata = [[metadata objectForKey:(NSString  *)kCGImagePropertyExifDictionary] mutableCopy];

 float brightnessValue = [[exifMetadata objectForKey:(NSString  *)kCGImagePropertyExifBrightnessValue] floatValue];
 
 // 根据brightnessValue的值来打开和关闭手电筒
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    if ((brightnessValue < 0)) {
    // 打开手电筒
 else if(brightnessValue > 0) {
 // 关闭手电筒
 }
}

39)Android Camera 如何优化视频录制的卡顿?

1)视频录制流程

  • 打开 Camera。
  • 创建 SurfaceTextue ,将 Camera 输出的数据渲染到 SurfaceTextue。
  • SurfaceTexture 拿到的结果进行特效处理。
  • 特效处理的结果异步分发给 RenderView 预览与 MediaCodec 编码。
  • 编码后的结果进行 Muxer 合成 Mp4 视频。

2)视频录制流程优化

  • 相机、编码根据不同机型控制不同帧率、分辨率。
  • 实现丢帧模块,将采集后的帧进入丢帧模块进行控制帧率,降低渲染以及编码性能。
  • 采集、渲染、编码按照多个线程处理,每个模块发挥最优性能。
  • 预览视图优先采用 SurfaceView,少量使用 TextureView,因为 TextureView 占用主线程渲染。
  • 编码优先使用 Surface 异步编码。

40)Android Surface 解码如何支持带角度视频?

1)直接解码到 Surface

需要通过 MediaFormat 设置解码参,通过 MediaFormat.KEY_ROTATION 配置旋转角度,则可以正确显示。

2)解码到 SurfaceTexture

解码到 SurfaceTexture,通过 MediaFormat.KEY_ROTATION 配置旋转角度,但输出纹理提供接口获取旋转矩阵,mSurfaceTexture.getSurfaceTexture().getTransformMatrix,拿到旋转矩阵后通过 FBO 渲染调整为正确尺寸,这种模式好处可以将解码后数据经过自定义处理传递给编码层与渲染上屏。

41)iOS 如何实现 HDR 转 SDR?

HDR 与 SDR 的基础知识以及如何使用 FFmpeg 实现转换可以参考我们之前的文章:如何正确将 HDR 视频转换成 SDR 视频,下面我们主要介绍一下在 iOS 平台使用系统库来的完成 HDR 转 SDR 的具体方案。

1)iOS 如何判断是 HDR 视频

- (BOOL)isHDR:(CMVideoFormatDescriptionRef)description {
    CFDictionaryRef descriptionExtensions = CMFormatDescriptionGetExtensions(description);
    if(descriptionExtensions){
        NSDictionary * ocDescriptionExtensions = (__bridge  NSDictionary*) descriptionExtensions;
        NSString* bufferYCbCrMatrix = ocDescriptionExtensions[@"CVImageBufferYCbCrMatrix"];
        return [bufferYCbCrMatrix isEqualToString:(__bridge NSString*)kCVImageBufferYCbCrMatrix_ITU_R_2020];
    }
    return NO;
}

2)iOS 使用 AVVideoComposition 将 HDR 转换为 SDR

使用 AVVideoComposition 参数设置,可以支持系统 AVPlayer、AVAssetReader、AVAssetImageGenerator,将 AVVideoComposition 设置给 AVComposition 做为输入参数即可。

- (AVMutableVideoComposition*) buildVideoCompositon {
    AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
    videoComposition.frameDuration = CMTimeMake(1, 30);
    videoComposition.renderSize = CGSizeMake(720,1280);
    if(isHDR){
        videoComposition.colorPrimaries = AVVideoColorPrimaries_ITU_R_709_2;//默认转换为 BT709
        videoComposition.colorTransferFunction = AVVideoTransferFunction_ITU_R_709_2;
        videoComposition.colorYCbCrMatrix = AVVideoYCbCrMatrix_ITU_R_601_4;
    }
    return videoComposition;
}

3)iOS 使用 VideoToolBox HDR 转换为 SDR

使用 VideoToolBox 参数设置,可以支持系统硬件解码输出为 SDR 的 CVPixelBuffer。

- (void)vtbHDR2SDR {
    CFMutableDictionaryRef pixelTransferProperty = CFDictionaryCreateMutable(
           kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(pixelTransferProperty,
                                @"DestinationColorPrimaries",
                                kCVImageBufferColorPrimaries_ITU_R_709_2);
    CFDictionarySetValue(pixelTransferProperty,
                                @"DestinationTransferFunction",
                                kCVImageBufferTransferFunction_ITU_R_709_2);
    CFDictionarySetValue(pixelTransferProperty,
                                @"DestinationYCbCrMatrix",
                                kCVImageBufferYCbCrMatrix_ITU_R_709_2);
    VTSessionSetProperty(session, kVTDecompressionPropertyKey_PixelTransferProperties,
                         pixelTransferProperty);
    CFRelease(pixelTransferProperty);
}

42)iOS 如何支持封装 FMP4 格式?

1)FMP4 与 MP4 很相似,区别如下

  • FMP4 不需要一个 moov Box 来进行 Initialization,FMP4 的 moov Box 只包含了一些 Track 信息。
  • FMP4 的视频/音频 metadata 信息与数据都存在一个个 moof、mdat 中,它是一个流式的封装格式。

2)FMP4 应用场景如下

  • 流式转码与上传
  • 流式播放 (Dash 支持,内部数据为 FMP4)

3)iOS 封装 FMP4 如何实现

AVAssetWriter 设置以下参数即可,系统会按照设置的切片时长回调 didOutputSegmentData 返回数据,仅 iOS 14 系统以上才可以。

- (AVAssetWriter*)createFMP4Writer {
    AVAssetWriter *writer = [[AVAssetWriter alloc] initWithContentType:[UTType typeWithIdentifier:AVFileTypeMPEG4]];
    writer.outputFileTypeProfile = AVFileTypeProfileMPEG4AppleHLS;
    writer.preferredOutputSegmentInterval = CMTimeMake(10.0, 1);//10.0自定义切片时长
    writer.initialSegmentStartTime = kCMTimeZero;
    writer.delegate = self;
    return writer;
}

- (void)assetWriter:(AVAssetWriter *)writer didOutputSegmentData:(NSData *)segmentData segmentType:(AVAssetSegmentType)segmentType segmentReport:(nullable AVAssetSegmentReport *)segmentReport {
    //存储segmentData 即可
}

43)FFmpeg 如何支持封装 FMP4 格式?

FMP4 数据格式参考第 2 题,FFmpeg 实现 FMP4 需要通过 Muxer 配置参数,设置 avio_alloc_context 回调数据,具体分片通过控制 av_write_frame(formatContext, NULL) 调用时机执行相关策略。

void muxerInit {
    av_register_all();
    uint8_t databuf[5 * 1024 * 1024] = {};
    AVFormatContext *formatContext = avformat_alloc_context();
    formatContext->oformat = av_guess_format("mp4", NULL, NULL);
    formatContext->pb = avio_alloc_context(databuf, sizeof(databuf), AVIO_FLAG_WRITE, NULL, NULL, fmp4Write, NULL); // 设置输出回调
    formatContext->opaque = formatContext->pb->opaque = this;
    formatContext->flags |= AVFMT_FLAG_BITEXACT;
    
    AVDictionary*  dict = NULL;
    av_dict_set(&dict, "movflags", "frag_custom+empty_moov+dash+frag_discont", 0); // 开启FMP4
    avformat_write_header(formatContext, &dict);
}

static int fmp4Write(void* opaque, uint8_t* buf, int size) {
    // 存储数据
}

void processInputPacket(AVPacket *pkt){
    av_write_frame(formatContext, NULL); //通过关键帧以及到指定时长触发此方法进行切片
}

44)转码速度优化的几点建议?

1)解码

  • 优先使用系统硬件解码,软件解码仅兜底使用。
  • 解码方式优先使用异步。
  • 解码器可以创建复用池。
  • Android 解码优先考虑 Surface 解码方式。

2)多线程并发

  • 转码分为解码与编码,通常编码更加耗时,这样解码线程可保持几帧 Cache 数据供编码获取。
  • 同一个视频可以分片转码为 FMP4 ,最终拼接为一个 FMP4,并发数目设置参考 2-5。

3)其它

  • 码率不高的视频无需转码。
  • 对于帧率过高、码率过高视频可降低相关参数,提高转码速度。
  • 视频转码与发布可结合优化策略,例如一边分片转码一边上传。
  • 针对不同场景进行指定策略处理,例如添加了音乐仅转码音频,仅转换封装格式使用 Remuxer。

45)Seek 优化的几点建议?

1)解码

  • 优先使用系统硬件解码,软件解码仅兜底使用。
  • 解码方式优先使用异步。
  • 解码器可以创建复用池。
  • Android 解码优先考虑 Surface 。
  • 对于 Seek 过程中的帧可以不输出数据,例如 iOS kVTDecodeFrame_DoNotOutputFrame。

2)其它

  • 对于 Seek 过程中的帧可以丢弃非参考帧。
  • 优先判断 Seek 帧是否命中缓存。
  • 优先找到 Seek 帧最近的关键帧。
  • 对于向右侧 Seek 优先判断是否与当前解码帧同一个 GOP,同一个 GOP 无需 Flush,继续解码即可。
  • 对于 Seek 位置精准度可以给一点空隙,例如 100ms 内偏差用户无感,这样可以利用缓存优势以及最近关键帧。
  • 对于 Seek 超级频繁可以选择丢弃某些帧。

本文福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs),有需要的可以进企鹅裙927239107领取哦~

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐