随着短视频的发展,短视频的需求越来越复杂,比如添加滤镜、特效、字幕、贴纸等越来越多的功能都将添加到短视频编辑的功能里面。
为了能够实时预览我们想要的效果,我们一般都需要自研播放器。
有些资料/项目虽然讲解了音视频,但也只是单纯地将数据解码然后进行播放,并没有做音视频同步以及丢帧处理等操作,并不能算一个真正的播放器,只是把媒体播放出来而已。
有些资料/项目虽然做了音视频同步等处理,但在定位(seek)等处理上毛病比较多,比如快速定位出现杂音、渲染的视频画面出现雪花、块状等问题。
下面这个例子是Android环境下基于FFmpeg播放器开发的比较好的例子。
https://github.com/CainKernel/CainPlayer
初始化流程
1 利用avformat_alloc_context()方法创建解复用上下文并设置解复用中断回调
2 利用 avformat_open_input()方法打开url,url可以是本地文件,也可以使网络媒体流
3 在成功打开文件之后,我们需要利用avformat_find_stream_info()方法查找媒体流信息
4 如果开始播放的位置不是AV_NOPTS_VALUE,即从文件开头开始的话,需要先利用avformat_seek_file方法定位到播放的起始位置
5 查找音频流、视频流索引,然后根据是否禁用音频流、视频流判断的设置,分别准备解码器对象
6 当我们准备好解码器之后,通过媒体播放器回调播放器已经准备。
7 判断音频解码器是否存在,通过openAudioDevice方法打开音频输出设备
8 判断视频解码器是否存在,打开视频同步输出设备
其中,第7、第8步骤,我们需要根据实际情况,重新设定同步类型。同步有三种类型,同步到音频时钟、同步到视频时钟、同步到外部时钟。默认是同步到音频时钟,如果音频解码器不存在,则根据需求同步到视频时钟还是外部时钟。
解复用流程
经过前面的初始化之后,我们就可以进入读数据包流程。读数据包流程如下:
1 判断是否退出播放器
2 判断暂停状态是否发生改变,设置解复用是否暂停还是播放 —— av_read_pause 和 av_read_play
3 处理定位状态。利用avformat_seek_file()方法定位到实际的位置,如果定位成功,我们需要清空音频解码器、视频解码器中待解码队列的数据。处理完前面的逻辑,我们需要更新外部时钟,并且更新视频同步刷新的时钟
4 根据是否设置attachmentRequest标志,从视频流取出attached_pic的数据包
5 判断解码器待解码队列的数据包如果大于某个数量,则等待解码器消耗
6 读数据包
7 判断数据包是否读取成功。如果没成功,则判断读取的结果是否AVERROR_EOF,即结尾标志。如果到了结尾,则入队一个空的数据包。如果读取出错,则直接退出读数据包流程。如果都不是,则判断解码器中的待解码数据包、待输出帧是否存在数据,如果都不存在数据,则判断是否跳转至起始位置还是判断是否自动退出,或者是继续下一轮读数据包流程。
8 根据取得的数据包判断是否在播放范围内的数据包。如果在播放范围内,则根据数据包的媒体流索引判断是否入队数据包舍弃。
准备解码器
准备解码器的流程一般如下:
1 创建解码上下文
2 从解复用上下文中复制参数到解码上下文
3 根据解码上下文的id查找解码器,如果在播放器指定了实际的解码器名称,则需要根据指定的解码器名称查找解码器
4 给解码上下文设置一些解码参数,比如lowres、refcounted_frames等解码参数
5 打开解码器
6 如果成功打开解码器,则根据类型创建解码器类,AudioDecoder或者是VideoDecoder。
7 如果不成功,则需要释放解码上下文
OpenSLES 音频输出
Android 环境下音频播放通常有两种方式—— AudioTrack 和 OpenSLES。AudioTrack 本身是Java实现,如果要使用的话,需要通过JNI Call的方式调用,实现起来也比较简单,这里就不做介绍了,有兴趣的可以自行实现。本项目采用OpenSLES 播放音频。
我们继承前面的AudioDevice基类,封装OpenSLES 音频输出设备。
OpenSLES 在open方法时初始化,并根据传进来的数据计算出需要的SL采样率、channel layout 等数据。并且在调用start方法时,启用一个音频输出处理线程。由于slBufferQueueItf设定了缓冲区大小,这里是设置了4个缓冲区,因此,线程不断从回调中取得PCM数据时,需要首先取得缓冲区填充的数量,如果队列中的4个缓冲区都填满了数据,则等待消耗,再从回调中取得PCM数据入队SL中进行播放。
MediaSync 媒体同步器
MediaSync 媒体同步器包含了音频时钟、视频时钟以及外部时钟,分别对应三种同步类型。同时媒体同步器另外开启了一条同步视频的线程。在线程中不断刷新并记录剩余时间,根据剩余时间来判断是否更新视频画面。
视频转码输出
视频输出的逻辑就是根据视频帧(AVFrame)的格式,判断是否需要进行转码,然后通知VideoDevice请求渲染画面。
VideoDevice 是一个视频设备的基类,主要包括结束方法(terminate)、初始化文理(onInitTexture)、更新YUV/ARGB数据(onUpdateYUV/onUpdateARGB),请求渲染画面(onRequestRender)几个方法组成,基类啥也不做处理。
这里我们采用OpenGLES来渲染视频。因此我们创建GLESDevice类,继承VideoDevice基类。
我们在代码里面根据传递进来的格式创建不同的Renderer进行渲染视频画面。视频帧的数据存放在Texture 结构体中,并传递给Renderer对象进行处理。Renderer又分成BGRARenderer 和 YUV420PRenderer对象。
GLESDevice由于使用了ANativeWindow来构建EGLSurface,对应Java层的Surface,此时会有surfaceCreated、surfaceChanged、surfaceDestroyed三个方法来改变状态的。
为了方便使用,我们打算将视频输出设备对象,从外面传进来 ,这样我们就可以根据不同的OS环境进行更换输出设备了,比如你想接入iOS设备的时候,播放器的逻辑基本不需要更改,只需要将音频输出设备、视频输出设备进行更换即可。