1 播放器技术分享:架构设计( 二 )


解析模块的输入:由 IO 模块读取出来的 bytes 二进制数据
解析模块的输出:音视频的媒体信息,未解码的音频数据包,未解码的视频数据包
音视频的媒体信息主要包括如下内容:
综上,解析模块的接口设计如下图所示:
创建好解析对象后,通过 Parse 函数输入音视频数据解析出基本的音视频媒体信息,通过 Read 函数读取分离的音视频数据包,然后分别送入音频和视频×××,通过 Get 方法获取各种音视频参数信息 。
2.3 解码模块
解析模块分离好音频和视频包以后,就可以分配送入到音频×××和视频×××了
解码模块的输入:未解压的音频/视频包
解码模块的输出:解压好的音频/图像的原始数据,即 PCM 和 YUV
【1播放器技术分享:架构设计】由于音视频的解码,往往不是每送入×××一帧数据就一定能输出一帧数据,而是经常需要缓存几帧参考帧才能拿到输出,所以编码器的接口设计常常采用一种 “生产者-消费者” 模型,通过一个公共的队列来串联 “生产者-消费者”,如下图所述(截取自编解码库的设计):
综上,解码模块的接口设计如下所示:
解析模块输出的媒体信息,包含有该使用什么类型的音频/视频×××,可利用该信息完成×××的初始化 。剩下的过程,就是通过 Queue 和不断跟×××交互,送入未解码的数据,拿到解码后的数据了 。
2.4 渲染模块
×××输出原始的图像和音频数据后,下一步就是送入到渲染模块进行图像的渲染和音频的播放了 。

1  播放器技术分享:架构设计

文章插图
一般视频数据渲染是输出到显卡展示在窗口上,音频数据则是送入声卡利用扬声器播放出来 。虽然不同平台的窗口绘制和扬声器播放的系统层 API 都不太一样,但是接口层面的流程也都差不多,如图所示:
对于视频渲染而言,流程则是:Init 初始化 ->设置窗口对象 ->设置渲染参数 ->执行渲染/绘制
对于音频播放而言,流程则是:Init 初始化 ->设置播放参数 ->执行播放操作
2.5 把模块串起来
如图所示,把各个模块这样串起来后,就是播放器的整个数据流走向了,但这是一个单线程的结构,从 IO 读到数据后,立马送入解析 -> 解码 -> 渲染,这样的单线程结构的播放器设计,会存在如下几个问题:
1. 音视频分离后 -> 解码 -> 播放,中间无法插入逻辑进行音画同步2. 无数据缓冲区,一旦网络/解码抖动 -> 导致频繁的卡顿3. 单线程运行,没有充分利用 CPU 多核
要想解决单线程结构的问题,可以以数据的 “生产者 - 消费者” 为边界,添加数据缓冲区,将单线程模型,改造为多线程模型(IO 线程、解码线程、渲染线程),如图所示:
改造为多线程模型后,其优势如下:
4. 帧队列(Packet Queue):可抵抗网络抖动5. 显示队列(Frame Queue):可抵抗解码/渲染的抖动6. 渲染线程:添加 AV Sync 逻辑,可支持音画同步的处理7. 并行工作,高效,充分利用多核 CPU
注:我们将在下一篇文章专门来聊一聊这 2 个新增的缓冲区该如何设计和管理 。
3 播放器 SDK 接口设计
前面详细介绍了播放器内涵的关键架构设计和数据流,如果期望以该播放器内核作为 SDK 给 APP 提供底层能力的话,还需要设计一套易用的 API 接口,这套 API 接口,其实可抽象为如下 5 大部分:
1. 创建/销毁播放器2. 配置参数(如:窗口句柄、视频 URL、循环播放等)3. 发送命令(如:初始化,开始播放,暂停播放,拖动,停止等)4. 音视频数据回调(如:解码后的音视频数据回调)5. 消息/状态消息回调(如:缓冲开始/结束、播放完成等)