您好,登錄后才能下訂單哦!
搞音視頻開發(fā)好些年,分享過許多博客文章,比如:前幾年發(fā)布的《FFmpeg Tips》系列,《Android 音頻開發(fā)》系列,《直播疑難雜癥排查》系列等等。最近想把多年來開發(fā)和優(yōu)化播放器的經(jīng)驗也分享出來,同時也考慮把自己業(yè)余時間開發(fā)的基于 ffmpeg 的播放器內(nèi)核開源出來,希望能幫助到音視頻領域的初學者。第一期文章要推出的內(nèi)容主要涉及到播放器比較核心的幾個技術點,大概的目錄如下:
播放器技術分享(1): 架構(gòu)設計
播放器技術分享(2):緩沖區(qū)管理
播放器技術分享(3):音畫同步
播放器技術分享(4):首開時間
播放器技術分享(5):延時優(yōu)化
本篇是系列文章的第一篇,主要聊一聊播放器的架構(gòu)設計。
1 概述
首先,我們了解一下播放器的定義是什么 ?
“播放器,是指能播放以數(shù)字信號形式存儲的視頻或音頻文件的軟件,也指具有播放視頻或音頻文件功能的電子器件產(chǎn)品?!? —— 《百度百科》
我的解讀如下:“播放器,是指能讀取、解析、渲染存儲在本地或者服務器上的音視頻文件的軟件,或者電子產(chǎn)品?!?/p>
歸納起來,它主要有如下 3 個方面的功能特性:
讀?。↖O):“獲取” 內(nèi)容 -> 從 “本地” or “服務器” 上獲取
解析(Parser):“理解” 內(nèi)容 -> 參考 “格式&協(xié)議” 來 “理解” 內(nèi)容
渲染(Render):“展示” 內(nèi)容 -> 通過揚聲器/屏幕來 “展示” 內(nèi)容
把這 3 個方面的功能串起來,就構(gòu)成了整個播放器的數(shù)據(jù)流,如圖所示:
IO:負責數(shù)據(jù)的讀取。從數(shù)據(jù)源讀取數(shù)據(jù)有多種標準協(xié)議,比如常見的有:File,HTTP(s),RTMP,RTSP 等
Parser & Demuxer:負責數(shù)據(jù)的解析。音視頻數(shù)據(jù)的封裝格式,都有著各種業(yè)界標準,只需要參考這些行業(yè)標準文檔,即可解析各種封裝格式,比如常見的格式:mp4,flv,m3u8,avi 等
Decoder:其實也屬于數(shù)據(jù)解析的一種,只不過更多的是負責對壓縮的音視頻數(shù)據(jù)進行解碼,拿到原始的 YUV 和 PCM 數(shù)據(jù),常見的視頻壓縮格式如:H.264、MPEG4、VP8/VP9,音頻壓縮格式如 G.711、AAC、Speex 等
Render:負責視頻數(shù)據(jù)的繪制和渲染,是一個平臺相關的特性,不同的平臺有不同的渲染 API 和方法,比如:Windows 的 DDraw/DirectSound,Android 的 SurfaceView/AudioTrack,跨平臺的如:OpenGL 和 ALSA 等
下面我們逐一剖析一下播放器整個數(shù)據(jù)流的每一個模塊的輸入和輸出,并一起設計一下每一個模塊的接口 API。
2 模塊設計
2.1 IO 模塊
IO 模塊的輸入:數(shù)據(jù)源的地址(URL),這個 URL 可以是一個本地的文件路徑,也可以是一個網(wǎng)絡的流地址。
IO 模塊的輸出:二進制的數(shù)據(jù),即通過 IO 協(xié)議讀取的音視頻二進制數(shù)據(jù)。
視頻數(shù)據(jù)源的 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 模塊的接口設計如下所示:
Open/Close 方法主要是用于打開/關閉視頻流,播放器內(nèi)核可以通過 URL 的頭(Schemes)知道需要采用哪一種 IO 協(xié)議來拉流(如:FILE/RTMP/HTTP),然后通過繼承本接口的子類去完成實際的協(xié)議解析和數(shù)據(jù)讀取。
IO 模塊讀取數(shù)據(jù),則定義了 2 個方法,Read 方法用于順序讀取數(shù)據(jù),ReadAt 用于從指定的 Offset 偏移的位置讀取數(shù)據(jù),后者主要用于文件或者視頻點播,為播放器提供 Seek 能力。
對于網(wǎng)絡流,可能出現(xiàn)斷線的情況,因此獨立出一個 Reconnect 接口,用于提供重連的能力。
2.2 解析模塊
從 IO 模塊讀到的音視頻二進制數(shù)據(jù),其實都是用如 mp4、flv、avi 等格式封裝起來的,如果想分離出音頻包和視頻包,則需要通過一個 Parser & Demuxer 模塊進行解析。
解析模塊的輸入:由 IO 模塊讀取出來的 bytes 二進制數(shù)據(jù)
解析模塊的輸出:音視頻的媒體信息,未解碼的音頻數(shù)據(jù)包,未解碼的視頻數(shù)據(jù)包
音視頻的媒體信息主要包括如下內(nèi)容:
視頻時長、碼率、幀率等?
音頻的格式:編碼算法,采樣率,通道數(shù)等?
視頻的格式:編碼算法,寬高,長寬比等
綜上,解析模塊的接口設計如下圖所示:
創(chuàng)建好解析對象后,通過 Parse 函數(shù)輸入音視頻數(shù)據(jù)解析出基本的音視頻媒體信息,通過 Read 函數(shù)讀取分離的音視頻數(shù)據(jù)包,然后分別送入音頻和視頻×××,通過 Get 方法獲取各種音視頻參數(shù)信息。
2.3 解碼模塊
解析模塊分離好音頻和視頻包以后,就可以分配送入到音頻×××和視頻×××了
解碼模塊的輸入:未解壓的音頻/視頻包
解碼模塊的輸出:解壓好的音頻/圖像的原始數(shù)據(jù),即 PCM 和 YUV
由于音視頻的解碼,往往不是每送入×××一幀數(shù)據(jù)就一定能輸出一幀數(shù)據(jù),而是經(jīng)常需要緩存幾幀參考幀才能拿到輸出,所以編碼器的接口設計常常采用一種 “生產(chǎn)者-消費者” 模型,通過一個公共的 buffer 隊列來串聯(lián) “生產(chǎn)者-消費者”,如下圖所述(截取自 Android MediaCodec 編解碼庫的設計):
綜上,解碼模塊的接口設計如下所示:
解析模塊輸出的媒體信息,包含有該使用什么類型的音頻/視頻×××,可利用該信息完成×××的初始化。剩下的過程,就是通過 Queue 和 Dequeue 不斷跟×××交互,送入未解碼的數(shù)據(jù),拿到解碼后的數(shù)據(jù)了。
2.4 渲染模塊
×××輸出原始的圖像和音頻數(shù)據(jù)后,下一步就是送入到渲染模塊進行圖像的渲染和音頻的播放了。
一般視頻數(shù)據(jù)渲染是輸出到顯卡展示在窗口上,音頻數(shù)據(jù)則是送入聲卡利用揚聲器播放出來。雖然不同平臺的窗口繪制和揚聲器播放的系統(tǒng)層 API 都不太一樣,但是接口層面的流程也都差不多,如圖所示:
對于視頻渲染而言,流程則是:Init 初始化 -> SetView 設置窗口對象 -> SetParam 設置渲染參數(shù) -> Render 執(zhí)行渲染/繪制
對于音頻播放而言,流程則是:Init 初始化 -> SetParam 設置播放參數(shù) -> Render 執(zhí)行播放操作
2.5 把模塊串起來
如圖所示,把各個模塊這樣串起來后,就是播放器的整個數(shù)據(jù)流走向了,但這是一個單線程的結(jié)構(gòu),從 IO 讀到數(shù)據(jù)后,立馬送入解析 -> 解碼 -> 渲染,這樣的單線程結(jié)構(gòu)的播放器設計,會存在如下幾個問題:
音視頻分離后 -> 解碼 -> 播放,中間無法插入邏輯進行音畫同步
無數(shù)據(jù)緩沖區(qū),一旦網(wǎng)絡/解碼抖動 -> 導致頻繁的卡頓
單線程運行,沒有充分利用 CPU 多核
要想解決單線程結(jié)構(gòu)的問題,可以以數(shù)據(jù)的 “生產(chǎn)者 - 消費者” 為邊界,添加數(shù)據(jù)緩沖區(qū),將單線程模型,改造為多線程模型(IO 線程、解碼線程、渲染線程),如圖所示:
改造為多線程模型后,其優(yōu)勢如下:
幀隊列(Packet Queue):可抵抗網(wǎng)絡抖動
顯示隊列(Frame Queue):可抵抗解碼/渲染的抖動
渲染線程:添加 AV Sync 邏輯,可支持音畫同步的處理
并行工作,高效,充分利用多核 CPU
注:我們將在下一篇文章專門來聊一聊這 2 個新增的緩沖區(qū)該如何設計和管理。
3 播放器 SDK 接口設計
前面詳細介紹了播放器內(nèi)涵的關鍵架構(gòu)設計和數(shù)據(jù)流,如果期望以該播放器內(nèi)核作為 SDK 給 APP 提供底層能力的話,還需要設計一套易用的 API 接口,這套 API 接口,其實可抽象為如下 5 大部分:
創(chuàng)建/銷毀播放器
配置參數(shù)(如:窗口句柄、視頻 URL、循環(huán)播放等)
發(fā)送命令(如:初始化,開始播放,暫停播放,拖動,停止等)
音視頻數(shù)據(jù)回調(diào)(如:解碼后的音視頻數(shù)據(jù)回調(diào))
消息/狀態(tài)消息回調(diào)(如:緩沖開始/結(jié)束、播放完成等)
綜上,播放器常見接口列表如下:
Create/Release/Reset
SetDataSource/SetOptions/SetView/SetVolume
Prepare/Start/Pause/Stop/SeekTo
SetXXXListener/OnXXXCallback
4 播放器的狀態(tài)模型
總體來說,播放器其實是一個狀態(tài)機,被創(chuàng)建出來了以后,會根據(jù)應用層發(fā)送給它的命令以及自身產(chǎn)生的事件在各個狀態(tài)之間切換,可以用如下這張圖來展示:
播放器一共有 9 種狀態(tài),其中,Idle 是創(chuàng)建后/重置后的到達的初始狀態(tài),End 和 Error 分別是主動銷毀播放器和發(fā)生錯誤后進入的最終狀態(tài)(通過 reset 重置后可恢復 Idle 狀態(tài))
其他的狀態(tài)切換和達到方式,圖中已經(jīng)標注得比較清楚了,這里就不再贅述了。
5 總結(jié)
播放器的架構(gòu)設計,就分享到這里了,有些內(nèi)容沒有展開講,但比較關鍵的點應該都基本闡述清楚了,如有疑問的小伙伴歡迎來信 lujun.hust@gmail.com 交流。另外,也歡迎大家關注我的新浪微博 @盧_俊 或者 微信公眾號 @Jhuster 獲取最新的文章和資訊。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。