安卓音視頻播放器

隨着短視頻的發展,短視頻的需求越來越複雜,比如添加濾鏡、特效、字幕、貼紙等越來越多的功能都將添加到短視頻編輯的功能裏面。
爲了能夠實時預覽我們想要的效果,我們一般都需要自研播放器。
有些資料/項目雖然講解了音視頻,但也只是單純地將數據解碼然後進行播放,並沒有做音視頻同步以及丟幀處理等操作,並不能算一個真正的播放器,只是把媒體播放出來而已。
有些資料/項目雖然做了音視頻同步等處理,但在定位(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設備的時候,播放器的邏輯基本不需要更改,只需要將音頻輸出設備、視頻輸出設備進行更換即可。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章