搞音視頻開發好些年,分享過許多博客文章,好比:前幾年發佈的《FFmpeg Tips》系列,《Android 音頻開發》系列,《直播疑難雜症排查》系列等等。最近想把多年來開發和優化播放器的經驗也分享出來,同時也考慮把本身業餘時間開發的基於 ffmpeg 的播放器內核開源出來,但願能幫助到音視頻領域的初學者。第一期文章要推出的內容主要涉及到播放器比較核心的幾個技術點,大概的目錄以下:html
播放器技術分享(1): 架構設計算法
播放器技術分享(2):緩衝區管理vim
播放器技術分享(3):音畫同步緩存
播放器技術分享(4):首開時間服務器
播放器技術分享(5):延時優化微信
本篇是系列文章的第一篇,主要聊一聊播放器的架構設計。網絡
1 概述多線程
首先,咱們瞭解一下播放器的定義是什麼 ?架構
「播放器,是指能播放以數字信號形式存儲的視頻或音頻文件的軟件,也指具備播放視頻或音頻文件功能的電子器件產品。」 —— 《百度百科》app
個人解讀以下:「播放器,是指能讀取、解析、渲染存儲在本地或者服務器上的音視頻文件的軟件,或者電子產品。」
概括起來,它主要有以下 3 個方面的功能特性:
讀取(IO):「獲取」 內容 -> 從 「本地」 or 「服務器」 上獲取
解析(Parser):「理解」 內容 -> 參考 「格式&協議」 來 「理解」 內容
渲染(Render):「展現」 內容 -> 經過揚聲器/屏幕來 「展現」 內容
把這 3 個方面的功能串起來,就構成了整個播放器的數據流,如圖所示:
IO:負責數據的讀取。從數據源讀取數據有多種標準協議,好比常見的有:File,HTTP(s),RTMP,RTSP 等
Parser & Demuxer:負責數據的解析。音視頻數據的封裝格式,都有着各類業界標準,只須要參考這些行業標準文檔,便可解析各類封裝格式,好比常見的格式:mp4,flv,m3u8,avi 等
Decoder:其實也屬於數據解析的一種,只不過更多的是負責對壓縮的音視頻數據進行解碼,拿到原始的 YUV 和 PCM 數據,常見的視頻壓縮格式如:H.26四、MPEG四、VP8/VP9,音頻壓縮格式如 G.7十一、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 模塊讀到的音視頻二進制數據,其實都是用如 mp四、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 總結
播放器的架構設計,就分享到這裏了,有些內容沒有展開講,但比較關鍵的點應該都基本闡述清楚了,若有疑問的小夥伴歡迎來信 lujun.hust@gmail.com 交流。另外,也歡迎你們關注個人新浪微博 @盧_俊 或者 微信公衆號 @Jhuster 獲取最新的文章和資訊。