FFmpeg iOS 音訊開發的小總結

Hawk0620發表於2016-10-11

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 呼叫,從而達到解藕的效果。

實踐

先來張效果圖

FFmpeg iOS 音訊開發的小總結

基於邊學習邊動手的原則,我完成了一個僅支援音訊播放的 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 的用武之地。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

FFmpeg iOS 音訊開發的小總結

相關文章