音视频学习笔记三——FFmpeg播放器线程设计

301 阅读3分钟

题记:本节播放器整体设计开始,陆续展开播放的设计原理,也会包含FFmpeg的模块细节。

线程设计

在学习播放器时,一开始最大的困惑便是线程,所以本节从线程的设计开始。

播放器线程介绍

由前面的FFmpeg简易播放 可知,视频播放保护解封装解码展示等多个流程。为了流畅播放,播放器通常会有多线程去管控这些流程,示意图如下(图中箭头对应单一或多个线程): 音视频线程.jpg

  • 解协议,解封装
    • 从(文件或者网络)中读取数据,解析出音视频数据
    • 解协议是指将网络数据包解析为封装层的数据包
    • 解封装则是指将封装层的数据包解析为音视频原始数据包,信息存储在AVPacket中
  • 音频解码、视频解码、字幕解码
    • 将原始数据包(Packet)解码为Frame数据
    • AVCodecContext中可以设置thread_count,即解码流程可能多线程,这里可以理解成解码控制线程
  • 音频播放
    • 将解码后的Frame数据播放出来,实现音频的输出
  • 视频播放
    • 将解码后的视频数据渲染到屏幕上,实现视频的播放
    • 如果有字幕,通常也在这个线程中处理

线程控制关联

基于多线程的设计,整个播放控制会复杂起来。其中线程之间主要通过生产消费模型来关联。

  • 解封装线程生产Packet,解码线程消费Packet;
  • 解码线程生产Frame,播放线性消耗Frame,由此完成控制

生产消费模型原图.jpg 播放线程之间有两个层面的生产消费模型: 生产消费模型.jpg 如上图所示

  • 解封装线程解析出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 WeChatfd27ee2c3eaab52aaf9d1932f4aef24c.jpg

WeChat1394a6fe07eda768fd17413865fc972e.jpg

C++11后提供condition_variablewait_fornotify_one/notify_all 具体可以在Demo中看到 OPFrameQueue