播放器技術分享(1):架構設計

搞音視頻開發好些年,分享過許多博客文章,比如:前幾年發佈的《FFmpeg Tips》系列,《Android 音頻開發》系列,《直播疑難雜症排查》系列等等。最近想把多年來開發和優化播放器的經驗也分享出來,同時也考慮把自己業餘時間開發的基於 ffmpeg 的播放器內核開源出來,希望能幫助到音視頻領域的初學者。第一期文章要推出的內容主要涉及到播放器比較核心的幾個技術點,大概的目錄如下:

  1. 播放器技術分享(1): 架構設計

  2. 播放器技術分享(2):緩衝區管理

  3. 播放器技術分享(3):音畫同步

  4. 播放器技術分享(4):首開時間

  5. 播放器技術分享(5):延時優化


本篇是系列文章的第一篇,主要聊一聊播放器的架構設計。


1 概述


首先,我們瞭解一下播放器的定義是什麼 ?

“播放器,是指能播放以數字信號形式存儲的視頻或音頻文件的軟件,也指具有播放視頻或音頻文件功能的電子器件產品。”  —— 《百度百科》


我的解讀如下:“播放器,是指能讀取、解析、渲染存儲在本地或者服務器上的音視頻文件的軟件,或者電子產品。”


歸納起來,它主要有如下 3 個方面的功能特性:

  1. 讀取(IO):“獲取” 內容  -> 從 “本地” or “服務器” 上獲取

  2. 解析(Parser):“理解” 內容  -> 參考 “格式&協議” 來 “理解” 內容

  3. 渲染(Render):“展示” 內容  -> 通過揚聲器/屏幕來 “展示” 內容


把這 3 個方面的功能串起來,就構成了整個播放器的數據流,如圖所示:image.png

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 模塊

image.png


IO 模塊的輸入:數據源的地址(URL),這個 URL 可以是一個本地的文件路徑,也可以是一個網絡的流地址。

IO 模塊的輸出:二進制的數據,即通過 IO 協議讀取的音視頻二進制數據。


視頻數據源的 URL 示例如下:

file:///c:/WINDOWS/clock.avi

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 模塊的接口設計如下所示:

image.png

Open/Close 方法主要是用於打開/關閉視頻流,播放器內核可以通過 URL 的頭(Schemes)知道需要採用哪一種 IO 協議來拉流(如:FILE/RTMP/HTTP),然後通過繼承本接口的子類去完成實際的協議解析和數據讀取。


IO 模塊讀取數據,則定義了 2 個方法,Read 方法用於順序讀取數據,ReadAt 用於從指定的 Offset 偏移的位置讀取數據,後者主要用於文件或者視頻點播,爲播放器提供 Seek 能力。


對於網絡流,可能出現斷線的情況,因此獨立出一個 Reconnect 接口,用於提供重連的能力。


2.2 解析模塊

image.png

從 IO 模塊讀到的音視頻二進制數據,其實都是用如 mp4、flv、avi 等格式封裝起來的,如果想分離出音頻包和視頻包,則需要通過一個 Parser & Demuxer 模塊進行解析。


解析模塊的輸入:由 IO 模塊讀取出來的 bytes 二進制數據

解析模塊的輸出:音視頻的媒體信息,未解碼的音頻數據包,未解碼的視頻數據包


音視頻的媒體信息主要包括如下內容:

  • 視頻時長、碼率、幀率等
    

  • 音頻的格式:編碼算法,採樣率,通道數等
    

  • 視頻的格式:編碼算法,寬高,長寬比等


綜上,解析模塊的接口設計如下圖所示:

image.png

創建好解析對象後,通過 Parse 函數輸入音視頻數據解析出基本的音視頻媒體信息,通過 Read 函數讀取分離的音視頻數據包,然後分別送入音頻和視頻×××,通過 Get 方法獲取各種音視頻參數信息。


2.3 解碼模塊

image.png


解析模塊分離好音頻和視頻包以後,就可以分配送入到音頻×××和視頻×××了


解碼模塊的輸入:未解壓的音頻/視頻包

解碼模塊的輸出:解壓好的音頻/圖像的原始數據,即 PCM 和 YUV


由於音視頻的解碼,往往不是每送入×××一幀數據就一定能輸出一幀數據,而是經常需要緩存幾幀參考幀才能拿到輸出,所以編碼器的接口設計常常採用一種 “生產者-消費者” 模型,通過一個公共的 buffer 隊列來串聯 “生產者-消費者”,如下圖所述(截取自 Android MediaCodec 編解碼庫的設計):

image.png

綜上,解碼模塊的接口設計如下所示:

image.png


解析模塊輸出的媒體信息,包含有該使用什麼類型的音頻/視頻×××,可利用該信息完成×××的初始化。剩下的過程,就是通過 Queue 和 Dequeue 不斷跟×××交互,送入未解碼的數據,拿到解碼後的數據了。


2.4 渲染模塊

image.png


×××輸出原始的圖像和音頻數據後,下一步就是送入到渲染模塊進行圖像的渲染和音頻的播放了。


一般視頻數據渲染是輸出到顯卡展示在窗口上,音頻數據則是送入聲卡利用揚聲器播放出來。雖然不同平臺的窗口繪製和揚聲器播放的系統層 API 都不太一樣,但是接口層面的流程也都差不多,如圖所示:

image.png

對於視頻渲染而言,流程則是:Init 初始化 -> SetView 設置窗口對象 -> SetParam 設置渲染參數 -> Render 執行渲染/繪製

image.png

對於音頻播放而言,流程則是:Init 初始化 -> SetParam 設置播放參數 -> Render 執行播放操作


2.5 把模塊串起來

image.png

如圖所示,把各個模塊這樣串起來後,就是播放器的整個數據流走向了,但這是一個單線程的結構,從 IO 讀到數據後,立馬送入解析 -> 解碼 -> 渲染,這樣的單線程結構的播放器設計,會存在如下幾個問題:

  1. 音視頻分離後 -> 解碼 -> 播放,中間無法插入邏輯進行音畫同步

  2. 無數據緩衝區,一旦網絡/解碼抖動 -> 導致頻繁的卡頓

  3. 單線程運行,沒有充分利用 CPU 多核


要想解決單線程結構的問題,可以以數據的 “生產者 - 消費者” 爲邊界,添加數據緩衝區,將單線程模型,改造爲多線程模型(IO 線程、解碼線程、渲染線程),如圖所示:image.png

改造爲多線程模型後,其優勢如下:

  1. 幀隊列(Packet Queue):可抵抗網絡抖動

  2. 顯示隊列(Frame Queue):可抵抗解碼/渲染的抖動

  3. 渲染線程:添加 AV Sync 邏輯,可支持音畫同步的處理

  4. 並行工作,高效,充分利用多核 CPU


注:我們將在下一篇文章專門來聊一聊這 2 個新增的緩衝區該如何設計和管理。


3 播放器 SDK 接口設計


前面詳細介紹了播放器內涵的關鍵架構設計和數據流,如果期望以該播放器內核作爲 SDK 給 APP 提供底層能力的話,還需要設計一套易用的 API 接口,這套 API 接口,其實可抽象爲如下 5 大部分:

  1. 創建/銷燬播放器

  2. 配置參數(如:窗口句柄、視頻 URL、循環播放等)

  3. 發送命令(如:初始化,開始播放,暫停播放,拖動,停止等)

  4. 音視頻數據回調(如:解碼後的音視頻數據回調)

  5. 消息/狀態消息回調(如:緩衝開始/結束、播放完成等)

綜上,播放器常見接口列表如下:

  1. Create/Release/Reset

  2. SetDataSource/SetOptions/SetView/SetVolume

  3. Prepare/Start/Pause/Stop/SeekTo

  4. SetXXXListener/OnXXXCallback


4 播放器的狀態模型


總體來說,播放器其實是一個狀態機,被創建出來了以後,會根據應用層發送給它的命令以及自身產生的事件在各個狀態之間切換,可以用如下這張圖來展示:

image.png

播放器一共有 9 種狀態,其中,Idle 是創建後/重置後的到達的初始狀態,End 和 Error 分別是主動銷燬播放器和發生錯誤後進入的最終狀態(通過 reset 重置後可恢復 Idle 狀態)

其他的狀態切換和達到方式,圖中已經標註得比較清楚了,這裏就不再贅述了。


5 總結


播放器的架構設計,就分享到這裏了,有些內容沒有展開講,但比較關鍵的點應該都基本闡述清楚了,如有疑問的小夥伴歡迎來信 [email protected] 交流。另外,也歡迎大家關注我的新浪微博 @盧_俊 或者 微信公衆號 @Jhuster 獲取最新的文章和資訊。

weixin_jhuster.jpg


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