播放器技術分享(1):架構設計
搞音視訊開發好些年,分享過許多部落格文章,比如:前幾年釋出的《FFmpeg Tips》系列,《Android 音訊開發》系列,《直播疑難雜症排查》系列等等。最近想把多年來開發和優化播放器的經驗也分享出來,同時也考慮把自己業餘時間開發的基於 ffmpeg 的播放器核心開源出來,希望能幫助到音視訊領域的初學者。第一期文章要推出的內容主要涉及到播放器比較核心的幾個技術點,大概的目錄如下:
播放器技術分享(1): 架構設計
播放器技術分享(2):緩衝區管理
播放器技術分享(3):音畫同步
播放器技術分享(4):首開時間
播放器技術分享(5):延時優化
本篇是系列文章的第一篇,主要聊一聊播放器的架構設計。
1 概述
首先,我們瞭解一下播放器的定義是什麼 ?
“播放器,是指能播放以數字訊號形式儲存的視訊或音訊檔案的軟體,也指具有播放視訊或音訊檔案功能的電子器件產品。” —— 《百度百科》
我的解讀如下:“播放器,是指能讀取、解析、渲染儲存在本地或者伺服器上的音視訊檔案的軟體,或者電子產品。”
歸納起來,它主要有如下 3 個方面的功能特性:
讀取(IO):“獲取” 內容 -> 從 “本地” or “伺服器” 上獲取
解析(Parser):“理解” 內容 -> 參考 “格式&協議” 來 “理解” 內容
渲染(Render):“展示” 內容 -> 通過揚聲器/螢幕來 “展示” 內容
把這 3 個方面的功能串起來,就構成了整個播放器的資料流,如圖所示:
IO:負責資料的讀取。從資料來源讀取資料有多種標準協議,比如常見的有:File,HTTP(s),RTMP,RTSP 等
Parser & Demuxer:負責資料的解析。音視訊資料的封裝格式,都有著各種業界標準,只需要參考這些行業標準文件,即可解析各種封裝格式,比如常見的格式:mp4,flv,m3u8,avi 等
Decoder:其實也屬於資料解析的一種,只不過更多的是負責對壓縮的音視訊資料進行解碼,拿到原始的 YUV 和 PCM 資料,常見的視訊壓縮格式如:H.264、MPEG4、VP8/VP9,音訊壓縮格式如 G.711、AAC、Speex 等
Render:負責視訊資料的繪製和渲染,是一個平臺相關的特性,不同的平臺有不同的渲染 API 和方法,比如:Windows 的 DDraw/DirectSound,Android 的 SurfaceView/AudioTrack,跨平臺的如:OpenGL 和 ALSA 等
下面我們逐一剖析一下播放器整個資料流的每一個模組的輸入和輸出,並一起設計一下每一個模組的介面 API。
2 模組設計
2.1 IO 模組
IO 模組的輸入:資料來源的地址(URL),這個 URL 可以是一個本地的檔案路徑,也可以是一個網路的流地址。
IO 模組的輸出:二進位制的資料,即通過 IO 協議讀取的音視訊二進位制資料。
視訊資料來源的 URL 示例如下:
rtmp://live.hkstv.hk.lxdns.com/live/hks
http://www.w3school.com.cn/i/movie.mp4
http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8
綜上,播放器 IO 模組的介面設計如下所示:
Open/Close 方法主要是用於開啟/關閉視訊流,播放器核心可以通過 URL 的頭(Schemes)知道需要採用哪一種 IO 協議來拉流(如:FILE/RTMP/HTTP),然後通過繼承本介面的子類去完成實際的協議解析和資料讀取。
IO 模組讀取資料,則定義了 2 個方法,Read 方法用於順序讀取資料,ReadAt 用於從指定的 Offset 偏移的位置讀取資料,後者主要用於檔案或者視訊點播,為播放器提供 Seek 能力。
對於網路流,可能出現斷線的情況,因此獨立出一個 Reconnect 介面,用於提供重連的能力。
2.2 解析模組
從 IO 模組讀到的音視訊二進位制資料,其實都是用如 mp4、flv、avi 等格式封裝起來的,如果想分離出音訊包和視訊包,則需要通過一個 Parser & Demuxer 模組進行解析。
解析模組的輸入:由 IO 模組讀取出來的 bytes 二進位制資料
解析模組的輸出:音視訊的媒體資訊,未解碼的音訊資料包,未解碼的視訊資料包
音視訊的媒體資訊主要包括如下內容:
視訊時長、位元速率、幀率等
音訊的格式:編碼演算法,取樣率,通道數等
視訊的格式:編碼演算法,寬高,長寬比等
綜上,解析模組的介面設計如下圖所示:
建立好解析物件後,通過 Parse 函式輸入音視訊資料解析出基本的音視訊媒體資訊,通過 Read 函式讀取分離的音視訊資料包,然後分別送入音訊和視訊×××,通過 Get 方法獲取各種音視訊引數資訊。
2.3 解碼模組
解析模組分離好音訊和視訊包以後,就可以分配送入到音訊×××和視訊×××了
解碼模組的輸入:未解壓的音訊/視訊包
解碼模組的輸出:解壓好的音訊/影象的原始資料,即 PCM 和 YUV
由於音視訊的解碼,往往不是每送入×××一幀資料就一定能輸出一幀資料,而是經常需要快取幾幀參考幀才能拿到輸出,所以編碼器的介面設計常常採用一種 “生產者-消費者” 模型,通過一個公共的 buffer 佇列來串聯 “生產者-消費者”,如下圖所述(擷取自 Android MediaCodec 編解碼庫的設計):
綜上,解碼模組的介面設計如下所示:
解析模組輸出的媒體資訊,包含有該使用什麼型別的音訊/視訊×××,可利用該資訊完成×××的初始化。剩下的過程,就是通過 Queue 和 Dequeue 不斷跟×××互動,送入未解碼的資料,拿到解碼後的資料了。
2.4 渲染模組
×××輸出原始的影象和音訊資料後,下一步就是送入到渲染模組進行影象的渲染和音訊的播放了。
一般視訊資料渲染是輸出到顯示卡展示在視窗上,音訊資料則是送入音效卡利用揚聲器播放出來。雖然不同平臺的視窗繪製和揚聲器播放的系統層 API 都不太一樣,但是介面層面的流程也都差不多,如圖所示:
對於視訊渲染而言,流程則是:Init 初始化 -> SetView 設定視窗物件 -> SetParam 設定渲染引數 -> Render 執行渲染/繪製
對於音訊播放而言,流程則是:Init 初始化 -> SetParam 設定播放參數 -> Render 執行播放操作
2.5 把模組串起來
如圖所示,把各個模組這樣串起來後,就是播放器的整個資料流走向了,但這是一個單執行緒的結構,從 IO 讀到資料後,立馬送入解析 -> 解碼 -> 渲染,這樣的單執行緒結構的播放器設計,會存在如下幾個問題:
音視訊分離後 -> 解碼 -> 播放,中間無法插入邏輯進行音畫同步
無資料緩衝區,一旦網路/解碼抖動 -> 導致頻繁的卡頓
單執行緒執行,沒有充分利用 CPU 多核
要想解決單執行緒結構的問題,可以以資料的 “生產者 - 消費者” 為邊界,新增資料緩衝區,將單執行緒模型,改造為多執行緒模型(IO 執行緒、解碼執行緒、渲染執行緒),如圖所示:
改造為多執行緒模型後,其優勢如下:
幀佇列(Packet Queue):可抵抗網路抖動
顯示佇列(Frame Queue):可抵抗解碼/渲染的抖動
渲染執行緒:新增 AV Sync 邏輯,可支援音畫同步的處理
並行工作,高效,充分利用多核 CPU
注:我們將在下一篇文章專門來聊一聊這 2 個新增的緩衝區該如何設計和管理。
3 播放器 SDK 介面設計
前面詳細介紹了播放器內涵的關鍵架構設計和資料流,如果期望以該播放器核心作為 SDK 給 APP 提供底層能力的話,還需要設計一套易用的 API 介面,這套 API 介面,其實可抽象為如下 5 大部分:
建立/銷燬播放器
配置引數(如:視窗控制代碼、視訊 URL、迴圈播放等)
傳送命令(如:初始化,開始播放,暫停播放,拖動,停止等)
音視訊資料回撥(如:解碼後的音視訊資料回撥)
訊息/狀態訊息回撥(如:緩衝開始/結束、播放完成等)
綜上,播放器常見介面列表如下:
Create/Release/Reset
SetDataSource/SetOptions/SetView/SetVolume
Prepare/Start/Pause/Stop/SeekTo
SetXXXListener/OnXXXCallback
4 播放器的狀態模型
總體來說,播放器其實是一個狀態機,被創建出來了以後,會根據應用層傳送給它的命令以及自身產生的事件在各個狀態之間切換,可以用如下這張圖來展示:
播放器一共有 9 種狀態,其中,Idle 是建立後/重置後的到達的初始狀態,End 和 Error 分別是主動銷燬播放器和發生錯誤後進入的最終狀態(通過 reset 重置後可恢復 Idle 狀態)
其他的狀態切換和達到方式,圖中已經標註得比較清楚了,這裡就不再贅述了。
5 總結
播放器的架構設計,就分享到這裡了,有些內容沒有展開講,但比較關鍵的點應該都基本闡述清楚了,如有疑問的小夥伴歡迎來信 [email protected] 交流。另外,也歡迎大家關注我的新浪微博 @盧_俊 或者 微信公眾號 @Jhuster 獲取最新的文章和資訊。