FFmpeg iOS 音频开发的小总结

704 查看

iOS 音频开发一般是 AudioFileStream 配合 Audio Queue 或者 Audio Unit实现的,而 FFmpeg 是与原生截然不同的软解码实现,比起原生更支持了诸如 flac、ape、acc、m4a、mp3、wav等格式(原生可通过第三方支持 flac 等格式)。因为工作项目的音乐播放器是原生那套实现的,所以就想换个思路,看看用 FFmpeg 开发音频是怎样的体验。

我对 FFmpeg 的探索起初是从简单 demo 入坑,发现大部分 demo 都需要 sdl,再去了解过 sdl,可谓踩了不少坑,直到我上手 kxmovie、ijkplayer,思路才渐渐清晰起来。经过对比,ijkplayer 是基于 c 实现的项目,实现了跨平台,而 kxmovie 是偏 iOS 实现的项目;kxmovie 在播放 m3u8 有点卡顿的bug,而 ijkplayer 则没有。可知 kxmovie 无论是代码兼容性还是播放视频的效果都没有 ijkplayer 好。ijkplayer 是 Bilibili 开源的一个项目,它基于 FFmpeg 开发了支持移动端音视频解码、视频渲染、播放控制、状态监控等功能。通过 ijkplayer 源码的学习,我对 FFmpeg 有了大致了解,而本文主要聊聊音频相关的。

FFmpeg 的导入

ijkplayer ReadMe 里有关如何编译 FFmpeg 的介绍,只要注意下是否支持更多解码格式即可,将生成出的 .a 文件拖进项目里,为项目添加 libbz.tbd,libbz2.tbd ,再在Build SettingsLibrary Search Paths添加 项目中 FFmpeg 的.a文件的目录,如 $(PROJECT_DIR)/your_project_name/ffmpeg/lib,FFmpeg 的导入完成了。

代码处理流程大致介绍

我只挑了几个有意思的来总结下,像 ffplay.c 一来就两三千行代码,虽然流程都是套路,但是不记录一下的话,时间久了,挺容易没什么头绪。

负责底层调用的 ffplay.c,首先注册解码器,初始化 FFPlayer 和 VideoState 开启一条线程调用 read_thread函数。在函数中调用avformat_open_input打开多媒体文件,打开文件后avformat_find_stream_info获取文件中的流信息填充进为ic->streams,获取流信息后使用av_find_best_stream获取文件的音频和视频流,并准备对音频和视频信息进行解码。接着调用stream_component_open函数,通过avcodec_find_decoder找到codec_id已注册的音视频解码器,再就是avcodec_open2打开解码器准备音视频的解码,再从audio_open开启sdl_audio_callback回调。此时在read_thread函数中会循环读取av_read_frame(ic, pkt)包数据,并将包数据存入包队列以供解码时使用。而对于音频解码会起新的线程调用audio_thread(视频则是video_thread),取出包数据后,使用avcodec_decode_audio4将解码后的 Frame 交给帧队列。sdl_audio_callback里的audio_decode_frame负责从帧队列中取出 Frame frame_queue_peek_readable(&is->sampq),完成重采样后,将 data 通过memcpy拷贝的方式回调给高层使用。

ijkplayer.c 是对 ffplay.c 的封装,包括播放暂停,获取文件时长,可播放时长,seek到特定时间点播放等,也实现了播放器的状态的监听。

ijksdl_aout_ios_audiounitijksdl_aout的设计挺有趣,它们共同串联了从高层到 FFmpeg 层的操作,通过指针函数,在ijksdl_aout_ios_audiounit注册了高层音频调用实现,ijksdl_aout负责供 FFmpeg 调用,从而达到解藕的效果。

实践

先来张效果图

基于边学习边动手的原则,我完成了一个仅支持音频播放的 demo,因为仅仅是支持音频,demo中对 ijkplayer 的几个文件做了点修改,比如剔除原来视频相关的代码,修改其仅从音频文件中取出封面

总结

在学习和使用中还是发现了一点小遗憾,FFmpeg 还没有提供对 io 层的缓存支持,这导致了在播放网络文件的时候拖拽进度条会重新进行缓冲,也无法实现边播边存的功能,我尝试过获取pkt的data并在av_read_frame的ret<0文件结束的情况下缓存起来,但这其实并没能保证帧顺序,拖拽过进度条之后的data也将会不完整,所以放弃了这种方案,官方的说法是在 libavformat/cache.c 中进行实现。

有个小问题是对于缓冲进度的计算有点小误差,playableDuration 无法与多媒体文件时长一致。我尝试在av_read_frame文件结束的地方

之后发送一个通知,直接将缓冲进度条置为100%

还有个小问题是 ijkplayer 播放本地文件的时候,假如是一首2、30m的无损歌曲,播放器不会一下子把整个文件都解码进内存中,这时当seek到文件未进入到内存的部分,播放器就直接停止播放了,我试过修改MAX_QUEUE_SIZE的值也是无效,最后 bbcallen 回复我说:That should be an ffmpeg issue, take a look at the comment of avformat_seek_file(),如此看来也是暂时解决无望。

在 ijkplayer 的 某个issue 中看到了 bbcallen 貌似说b站的客户端也用系统原生来播放视频,我觉得iOS播放视频的话使用MPMoviePlayerController就好了。

当然,如果是直播项目,无疑是 FFmpeg 的用武之地。