4.ffplayer 原理、架构及代码分析——架构设计

播放器的组成模块

通过前面的播放器实现原理的介绍,我们可以初步总结出,一个播放器的主要核心模块:

  • demux - 从输入文件分离出 audio packet 和 video packet 等等
  • audio decode - 解码 audio packet
  • video decode - 解码 video packet
  • render - 负责 audio 和 video 的渲染
  • adev - 抽象的 audio 输出设备
  • vdev - 抽象的 video 输出设备

一开始我就是这样设想的,最初的设计中,demux + audio decode + video decode 这三个功能,放入了 ffplayer.cpp 中实现,render 的功能由 ffrender.cpp 来实现,然后是 adev.cpp 和 vdev.cpp。

单线程方式是否可行

看起来这几大模块似乎就足以实现完整的播放器功能了,而且看起来似乎也不需要什么多线程。基于 KISS 原则,我是极力避免用多线程去解决问题的。所以最初的架构设计,基本是就是一个主线程:

这样的设计,看起来还算行,至少可以把音视频解码和输出,可以说是最简单的播放器。那么缺陷在哪里?

  • 很难保证音视频回放的连续性
  • audio 和 video 同步问题
  • 帧率稳定性和均匀性的问题

首先 demux 出来的 packet 可能就没法保证 audio 和 video 的同步,比如 demux 出的 packet 序列如下:

 

 

这样看来,demux 出来的 packet,很可能 audio 与 video 之间的 pts 差距就非常大。

demux 本质上是磁盘 IO 或者网络 IO,要么耗时要么会阻塞线程。解码是非常耗时的,而且时间是不固定的(视频解码关键帧时也许耗时更多,非关键帧相对耗时较少)。渲染也是一个耗时的操作,视频渲染需要做像素格式转换,需要往屏幕绘制,很耗时但时间相对固定;音频渲染操作,只需要把 buffer 送给音频设备,不耗时但是可能会导致线程阻塞(阻塞可以理解为广义上的耗时)...

因此我们看到的问题就是,demux、解码、渲染这些操作,都可能非常耗时。为什么耗时是问题呢?因为视频播放,对时间非常敏感,对实时性要求也非常高。我们想想,如果视频帧率是 30fps,那么每一帧的时间只有 33ms。也就是说在 33ms 内,我们必须完成一帧视频的 demux、decoding、rendering;或者说这个线程的 while 循环必须平均每 33ms 就执行一次。但是在这种单线程架构中,如果其中任意一个环节耗时超过了 33ms,线程就没法持续地满足视频回放的吞吐量,就是没法做到连续播放的,更不要提音视频同步了。

解决办法:对于耗时的操作,有必要建立队列进行缓冲,同时开专门的线程进行处理。这个原理说白了就是流水线原理。最初的设计,流水线上每个工序只有一个人处理,也没有缓冲(只有一个产品在线上跑),假如某个工序很耗时,那么这个工序就必然拖慢下游的时间,从而拖慢整个流水线的时间。在耗时长的工序上,增加更多的人,增加缓冲,就能提高流水线的效率。队列就是用于衔接不同工序的缓冲,线程就是工位上做事情的人。耗时的任务,cpu 就会让线程执行更多的时间,直到线程把自己的队列都填满了,就进入阻塞。

因此,要实现一个播放器,我们必须使用多线程,使用多线程的目的,就是为了保证视频播放的连续性、帧率稳定和音视频同步。

多线程的架构设计

基于这样的原理,我们可以做出新的架构设计:

  • ffplayer 负责 demux 和 decode
  • render 负责音视频的渲染工作
  • adev & vdev 是抽象的音视频设备
  • 多线程的架构总共五个线程(demux,adecode,vdecode,arender,vrender)
  • packet 队列用于 demux 和 decode
  • audio buffer 队列用于 audio decode 和 audio render
  • video buffer 队列用于 video decode 和 video render

总结下来,我们必须要有 5 个线程,分别用于 demux, audio decode, video decode, audio render, video render。具体实现过程中,render 的线程是 adev/vdev 的内部实现,跟平台相关。ffplayer 的 windows 版本实现中,vdev 内部是有线程的,而 adev 的实现没有用到线程,但这个线程实际上是运行在 windows 操作系统音频驱动的内部的。所以我们也可认为只有 3 个线程,demux、audio decode 和 video decode,而 adev 和 vdev 是带缓冲队列的渲染设备。

线程之间的数据传递方式为:

  • 每个线程就是一个独立的工作单元
  • 线程之间通过队列传递数据
  • 队列的上下游都是工作线程
  • 上游的工作线程是队列的生产者
  • 下游的工作线程是队列的消费者

总结来说,就是一个源头、五个线程、三个队列、两个设备。这就是 ffplayer 的最终架构。

参考文章:https://github.com/rockcarry/ffplayer/wiki/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值