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 Settings
的Library 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_audiounit
與ijksdl_aout
的設計挺有趣,它們共同串聯了從高層到 FFmpeg 層的操作,通過指標函式,在ijksdl_aout_ios_audiounit
註冊了高層音訊呼叫實現,ijksdl_aout
負責供 FFmpeg 呼叫,從而達到解藕的效果。
實踐
先來張效果圖
基於邊學習邊動手的原則,我完成了一個僅支援音訊播放的 demo,因為僅僅是支援音訊,demo中對 ijkplayer 的幾個檔案做了點修改,比如剔除原來視訊相關的程式碼,修改其僅從音訊檔案中取出封面
1 2 3 4 5 6 7 8 9 |
.... switch (d->avctx->codec_type) { case AVMEDIA_TYPE_VIDEO: if (d->pkt_temp.data && d->pkt_temp.size) { ffp->artist_data = d->pkt_temp.data; ffp->artist_size = d->pkt_temp.size; ffp->cover_data(ffp); } break; .... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void audio_cover_data(uint8_t *data, int size) { UIImage *image = nil; NSData *imgData = [NSData dataWithBytes:data length:size]; CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)(imgData)); if (provider) { CGImageRef imageRef = CGImageCreateWithJPEGDataProvider(provider, NULL, YES, kCGRenderingIntentDefault); if (imageRef) { image = [UIImage imageWithCGImage:imageRef]; CGImageRelease(imageRef); } CGDataProviderRelease(provider); } } |
總結
在學習和使用中還是發現了一點小遺憾,FFmpeg 還沒有提供對 io 層的快取支援,這導致了在播放網路檔案的時候拖拽進度條會重新進行緩衝,也無法實現邊播邊存的功能,我嘗試過獲取pkt的data並在av_read_frame
的ret<0檔案結束的情況下快取起來,但這其實並沒能保證幀順序,拖拽過進度條之後的data也將會不完整,所以放棄了這種方案,官方的說法是在 libavformat/cache.c 中進行實現。
有個小問題是對於緩衝進度的計算有點小誤差,playableDuration 無法與多媒體檔案時長一致。我嘗試在av_read_frame
檔案結束的地方
1 2 3 4 |
SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); ffp_statistic_l(ffp); |
之後傳送一個通知,直接將緩衝進度條置為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 的用武之地。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!