题记:本节播放器整体设计开始,陆续展开播放的设计原理,也会包含FFmpeg的模块细节。
线程设计
在学习播放器时,一开始最大的困惑便是线程,所以本节从线程的设计开始。
播放器线程介绍
由前面的FFmpeg简易播放 可知,视频播放保护解封装解码展示等多个流程。为了流畅播放,播放器通常会有多线程去管控这些流程,示意图如下(图中箭头对应单一或多个线程):
- 解协议,解封装
- 从(文件或者网络)中读取数据,解析出音视频数据
- 解协议是指将网络数据包解析为封装层的数据包
- 解封装则是指将封装层的数据包解析为音视频原始数据包,信息存储在AVPacket中
- 音频解码、视频解码、字幕解码
- 将原始数据包(Packet)解码为Frame数据
AVCodecContext中可以设置thread_count,即解码流程可能多线程,这里可以理解成解码控制线程
- 音频播放
- 将解码后的Frame数据播放出来,实现音频的输出
- 视频播放
- 将解码后的视频数据渲染到屏幕上,实现视频的播放
- 如果有字幕,通常也在这个线程中处理
线程控制关联
基于多线程的设计,整个播放控制会复杂起来。其中线程之间主要通过生产消费模型来关联。
- 解封装线程生产Packet,解码线程消费Packet;
- 解码线程生产Frame,播放线性消耗Frame,由此完成控制
播放线程之间有两个层面的生产消费模型:
如上图所示:
- 解封装线程解析出Packet,解码线程使用Packet,形成第一个生产消费模型
- 维护和Stream数目对应的Packet队列
- 解码线程从相应队列中取数据包
- 当队列中有pakcet,发送非空signal,解除解码的waiting状态
- 当队列中数据足够时,如几个队列都有一定量的packet,并且足够播放时间大于设定时间,解码线程即可进入waiting阶段,避免浪费内存和流量
- 当解码线程消耗掉packet时,发送signal,通知解封装继续
- 当队列为空时,对应解码线程进入waiting状态
- 解码线程解码出Frame,播放线程使用Frame,形成第二个生产消费模型
- 与Packet对应,维护Frame队列
- Frame队列为空时,播放waiting,等待解码push过来的非空signal
- Frame队列满时,解码的push进入waiting,等待消耗的signal
模型实现:
在ffplay源码中会看到很多SDL_cond,就是为实现这个目的,如下图,
- 解码
Decoder中有empty_queue_cond, - 包含队列PacketQueue,也包含一个
cond SDL_CondWait实现block,SDL_CondSignal解除block
C++11后提供condition_variable,wait_for、notify_one/notify_all
具体可以在Demo中看到 OPFrameQueue