第5章 实现一款视频播放器

前3章讨论了许多音视频相关的知识,包括音视频的基本概念,如何搭建移动平台下的开发环境,并学习了ffmpeg以及使用ffmpeg解码的方法,第4章又学习了如何将音视频的裸数据渲染到硬件设备上。所以到现在其实我们完全可以做一个实际的大项目——视频播放器了,这个项目可以把之前学的知识串联起来,并且可以学习到多线程控制、音视频同步等一些知识,那么具体要实现一款视频播放器需要开发者做哪一些工作呢,笔者会利用本章的内容带领大家一步一步实现出来。

5.1架构设计

先来看一下播放器要提供给用户哪一些功能:要可以从零开始播放(当然要保证音画对齐);支持暂停和继续播放功能;支持seek功能(就是可以随意拖动到任意位置仍然可以继续播放),也有播放器支持快进、快退15s。那么下面先来实现最基本的功能,即播放器可以从头自己播放以及暂停和继续的功能。

首先来思考一下我们要实现的场景,播放器可以从零开始自己播放直到结束。如果直接抛出这样一个项目,我们很容易找不到任何头绪,但是作为一个开发人员,要做的事情就是把复杂的问题简单化,简单的问题条理化,最终按照拆分得非常细的模块来逐个实现。基于这个项目,我们需要问自己以下几个问题:

  • 输入是什么?
  • 输出是什么?
  • 可以划分为几个模块?
  • 每个模块的职责是什么?

下面来一个问题一个问题的梳理,首先要搞清楚输入是什么,可以是本地磁盘上的一个媒体文件(可能是flv、mp4、avi、mov等格式的文件),也可以是网络上的一个媒体文件(可能是http、rtmp、hls等协议的),这就是我们确定的输入;那输出是什么呢?输出就是可以把视频中的音频放到扬声器让用户的耳朵可以听到声音,把视频画面渲染到屏幕上让用户的眼睛可以看到画面,同时听到的声音和画面也必须是同步的(也就是说不能让用户听到了一个‘你好’的发音,却看到了‘吃了’的画面);然后我们根据输入和输出拆分模块,并给模块分配合理的职责。

对于输入部分分析如下,有可能是不同的协议,比如是file(本地磁盘的文件),或者是http、rtmp、hls协议等;也有可能是不同的封装格式,比如是mp4、flv、mov等封装格式;而对于这一些封装格式里面的内容,会有两路流,分别是音频流和视频流,我们需要将这两路流都解码为裸数据。待视频流和音频流都解码为裸数据之后,需要为音视频各自建立一个队列将裸数据存储起来,不过,如果在需要播放一帧的时候再去做解码,那这一帧视频就有可能产生卡顿或者延迟,所以这里引出了第一个线程,即为系统的后台解码做一个线程,这个线程用于解析协议,处理解封装以及解码,最终把裸数据放到我们音频和视频的队列中,这个模块称之为输入模块。

再看输出部分,输出部分其实由两部分组成,一部分是音频的输出,另一部分是视频的输出。不过可以确定的是,不论音频的输出还是视频的输出,它们都会用一个线程来管理,这两个模块应该先会去队列中拿出音视频的裸数据,然后分别进行音视频的渲染,最终发布到扬声器和屏幕上,让用户可以听得到、看得到,这两个模块称之为音频输出和视频输出模块。

再来思考一件事情,由于输出模块都在各自的一个线程中,音频和视频各自播放,这就导致了两个输出模块的播放频率以及线程控制没有任何关系,就不可以保证音画对齐的事情。我们规划的各个模块里,好像还没有一个模块的职责是负责音视频同步,所以需要再建立一个模块来负责相关工作,这个模块称之为音视频同步模块。

至此,已把模块都拆分完毕,具体的模块分布如图5-1所示。

图5-1

不过,我们应该还写一个调度器,将这几个模块组装起来。也就是说,先把输入模块、音频队列、视频队列都封装到音视频同步模块中,然后向外界暴露获取音频数据、视频数据的接口,这两个接口应该要保持同步,内部负责解码线程的运行与暂停的维护。然后把音视频同步模块、音频输出模块、视频输出模块封装到调度器中,调度器模块分别会向音频输出模块和视频输出模块注册回调函数,回调函数允许两个输出模块来获取音频数据和视频数据。这样就可以进一步整理一下类图设计了,如图5-2所示。