本文主要介紹金山云Android推流耸峭、短視頻SDK設(shè)計(jì)中,為保證SDK的靈活性蝉稳、可擴(kuò)展性抒蚜,在SDK組件化方向上所做的一些探索。
成熟的PC端多媒體架構(gòu)簡(jiǎn)介
PC誕生之初耘戚,就有了強(qiáng)烈的多媒體處理需求嗡髓,在幾十年發(fā)展中,比較知名的幾個(gè)多媒體框架有:
- 微軟的DirectShow
- 開(kāi)源跨平臺(tái)的GStreamer
- FFMPEG
- VLC
其中收津,F(xiàn)FMPEG更偏重于提供muxer/demuxer, encoder/decoder等實(shí)用穩(wěn)定的多媒體組件饿这,VLC更偏重于提供ALL IN ONE的軟件產(chǎn)品,其框架更多的是為特定的應(yīng)用場(chǎng)景服務(wù)撞秋,靈活性及擴(kuò)展性均不及DirectShow和GStreamer.
DirectShow和GStreamer的組件化設(shè)計(jì)
DirectShow與GStreamer均為組件化設(shè)計(jì)的多媒體框架长捧,具體工作均交由各個(gè)組件來(lái)實(shí)現(xiàn)。
DirectShow的Filter Graph
微軟提供了可視化的組件編輯工具GraphEdit吻贿,借助該工具串结,我們可以通過(guò)直觀的方式將各個(gè)DirectShow的組件連接起來(lái),并對(duì)實(shí)際效果進(jìn)行預(yù)覽舅列。
比如下圖的連接方式就實(shí)現(xiàn)了一個(gè)基本的視頻文件播放器:
根據(jù)上圖奉芦,我們可以看到,一個(gè)典型的視頻播放器包含一個(gè)視頻源分離模塊(Demuxer), 一個(gè)視頻解碼模塊剧蹂,一個(gè)音頻解碼模塊声功,一個(gè)視頻渲染模塊,一個(gè)音頻渲染模塊宠叼。在DirectShow中先巴,這些模塊被稱為Filter其爵,連接起來(lái)的各個(gè)Filter組成了一個(gè)Filter Graph。
各個(gè)Filter包含不同類型與數(shù)目的引腳伸蚯,通過(guò)引腳間的連接摩渺,實(shí)現(xiàn)數(shù)據(jù)流在不同模塊間的傳遞。
這些引腳在DirectShow中稱為Pin, 其中產(chǎn)生數(shù)據(jù)的Pin被稱為Source Pin剂邮,接受數(shù)據(jù)的Pin稱為Sink Pin摇幻。
例如分離模塊中包含音視頻兩個(gè)Source Pin, 解碼模塊包含一個(gè)Sink Pin和一個(gè)Source Pin, 渲染模塊只有一個(gè)Sink Pin。
當(dāng)然我們也可以通過(guò)選擇組合不同的Filter組成新的Filter Graph來(lái)達(dá)成不同的功能挥萌,或者增添绰姻、更改當(dāng)前Filter Graph中的Filter來(lái)動(dòng)態(tài)調(diào)整Filter Graph的功能特性。
GStreamer的Pipeline
GStreamer中存在類似的組件化結(jié)構(gòu)引瀑,例如下圖的Pipeline實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的ogg音頻文件播放器:
如圖中所示的source, demuxer, decoder, output模塊狂芋,在GStreamer中被稱為Element, Element上的引腳被稱為Pad, 輸入輸出引腳分別被稱為Source Pad和Sink Pad,而連接起來(lái)的各個(gè)Element則組成了一個(gè)Pipeline憨栽。
GStreamer同樣支持使用不同的Element及連接方式來(lái)組成不同的Pipeline帜矾,以及對(duì)其中的Element進(jìn)行增添、改動(dòng)來(lái)調(diào)整Pipeline的功能特性屑柔。
背后的工作
前面我們看到了DirectShow和GStreamer直觀屡萤、靈活的組合方式,以及強(qiáng)大的擴(kuò)展性掸宛,但要實(shí)現(xiàn)這些特性是需要框架完成大量的配套工作的死陆。
模塊間連接時(shí)的協(xié)商過(guò)程。
在多媒體處理中旁涤,存在著多種數(shù)據(jù)類型翔曲,例如未解碼的視頻數(shù)據(jù)(其中又存在多種編碼格式HEVC/AVC/VP9等)迫像,解碼后的視頻數(shù)據(jù)(又包含RGB/I420/YV12/NV12等)劈愚,不同模塊能夠處理的數(shù)據(jù)類型是不同的,因此兩種框架均實(shí)現(xiàn)了完善的協(xié)商過(guò)程闻妓。
例如菌羽,對(duì)于不支持的連接方式,拋出錯(cuò)誤由缆,或者智能添加一個(gè)轉(zhuǎn)換模塊來(lái)完成連接注祖。模塊的動(dòng)態(tài)添加及移除處理。
例如在播放或者編輯視頻的過(guò)程中均唉,需要添加是晨、改變或者移除一種特效Filter,就需要對(duì)已連接的模塊進(jìn)行動(dòng)態(tài)重建舔箭。
兩種框架均實(shí)現(xiàn)了該功能罩缴,不過(guò)為此也做了大量的工作蚊逢,例如模塊在變動(dòng)過(guò)程中的狀態(tài)處理、數(shù)據(jù)流處理等箫章。模塊間的數(shù)據(jù)傳送烙荷。
一般存在兩種方式,一種為push模式檬寂,另一種為pull模式终抽。
兩種框架均對(duì)push及pull模式做了支持。例如上面GStremer框架下的ogg播放器桶至,ogg-demuxer的sink pad就以pull模式從file-source拉取數(shù)據(jù)昼伴,后繼模塊則均以push模式運(yùn)行,由上一級(jí)模塊的src pad將數(shù)據(jù)推送到后一級(jí)的sink pad塞茅。狀態(tài)控制和事件響應(yīng)亩码。
GStreamer中,要控制Pipeline的開(kāi)始野瘦、暫停描沟、停止?fàn)顟B(tài),只需控制Pipeline的狀態(tài)鞭光,GStreamer框架內(nèi)部會(huì)實(shí)現(xiàn)對(duì)各個(gè)子Element的狀態(tài)切換吏廉。
對(duì)Pipeline運(yùn)行過(guò)程中的seek操作也是類似,框架內(nèi)部會(huì)將SEEK事件發(fā)送到Pipeline的所有Sink Pad以完成seek操作惰许。錯(cuò)誤及消息處理席覆。
GStreamer中每個(gè)Pipeline均包含一個(gè)傳遞錯(cuò)誤及消息的Bus,每個(gè)Element會(huì)將其本身產(chǎn)生的錯(cuò)誤及事件消息放進(jìn)該Bus中汹买,上層應(yīng)用通過(guò)監(jiān)聽(tīng)Bus中的事件來(lái)進(jìn)行必要的錯(cuò)誤及事件消息的處理佩伤。音視頻同步。
兩種框架中晦毙,均提供了Clock選擇機(jī)制生巡,被選中的Clock可以被各個(gè)模塊作為參考,用來(lái)控制數(shù)據(jù)的發(fā)送節(jié)奏见妒,特別是音視頻Render模塊孤荣,可以使用相同的參考時(shí)鐘來(lái)控制渲染時(shí)機(jī),以達(dá)到音視頻同步播放的效果须揣。
金山云Android多媒體SDK的架構(gòu)設(shè)計(jì)
金山云Android多媒體SDK是以在保證性能前提下提供足夠靈活的擴(kuò)展性為目標(biāo)的盐股。為此,我們采用將SDK中的各個(gè)功能模塊組件化耻卡,然后根據(jù)應(yīng)用場(chǎng)景進(jìn)行組裝的方式來(lái)達(dá)成疯汁。
以下圖為例,展示了推流SDK中各個(gè)模塊的典型Pipeline結(jié)構(gòu):
圖中的各個(gè)模塊通過(guò)KSYStreamer類組合在一起卵酪,實(shí)現(xiàn)完整的直播推流功能幌蚊。而通過(guò)不同的組織方式秸谢,又可以組成一個(gè)短視頻合成SDK,如下圖所示:
框架中對(duì)模塊的形式霹肝,模塊間的組織方式的處理參考了DirectShow和GStreamer框架中的一些概念估蹄,不過(guò)框架最初只是為了推流功能所設(shè)計(jì),為兼顧實(shí)現(xiàn)難度及性能沫换,做了較大幅度的簡(jiǎn)化及限制臭蚁。
基于Pin的模塊間連接方式
在金山云Android多媒體SDK中,參照DirectShow及GStreamer的概念讯赏,以簡(jiǎn)化模塊間連接為目的垮兑,引入了Pin的概念,簡(jiǎn)要介紹如下:
在搭建推流Pipeline的時(shí)候漱挎,各個(gè)模塊之間的連接使用 SrcPin 和 SinkPin 來(lái)完成系枪。
- 一個(gè)Module包含若干個(gè)Pin, Module之間的連接由Pin來(lái)實(shí)現(xiàn)
- Pin包含SrcPin和SinkPin, 分別產(chǎn)生和消耗數(shù)據(jù)流
- SrcPin及SinkPin均是泛型類,創(chuàng)建時(shí)需要指定數(shù)據(jù)格式磕谅,相同數(shù)據(jù)格式的Pin才可以連接私爷,例如:
SrcPin<ImgTexFrame> -> SinkPin<ImgTexFrame>
SrcPin<ImgBufFrame> -> SinkPin<ImgBufFrame>
SrcPin<AudioBufFrame> -> SinkPin<AudioBufFrame>
- 一個(gè)SrcPin可以連接多個(gè)SinkPin, 一個(gè)SinkPin只能跟一個(gè)SrcPin連接;
- 所有連接或斷開(kāi)連接的操作均由SrcPin端操作膊夹;
Pin的相關(guān)操作
- 調(diào)用SrcPin的connect接口連接兩個(gè)模塊
public void connect(SinkPin<T> sinkPin)
- 調(diào)用SrcPin的disconnect接口斷開(kāi)連接
// 斷開(kāi)所有已連接的SinkPin, recursive為true時(shí)表示需要遞歸斷開(kāi)后面所有已連接的模塊
public void disconnect (boolean recursive)
// 斷開(kāi)指定的某個(gè)已連接的SinkPin衬浑,recursive為true時(shí)表示需要遞歸斷開(kāi)后面所有已連接的模塊
public void disconnect (SinkPin<T> sinkPin, boolean recursive)
SrcPin調(diào)用disconnect后,SinkPin端可以收到onDisconnect事件
// 源端已斷開(kāi)連接放刨,recursive為true時(shí)需要release當(dāng)前模塊工秩,并遞歸斷開(kāi)后面所有已連接的模塊
public abstract void onDisconnect (boolean recursive)
-
處理onFormatChanged
該接口表示數(shù)據(jù)格式的改變,源端數(shù)據(jù)初始化完成及發(fā)生改變時(shí)均需要觸發(fā)改事件进统,Sink端一般需要在該回調(diào)中進(jìn)行一些初始化的工作助币。- 包含SrcPin的模塊需要在合適的時(shí)機(jī)觸發(fā)onFormatChanged;
- 包含SinkPin的模塊需要根據(jù)需要處理SrcPin觸發(fā)的onFormatChanged事件。
-
處理onFrameAvailable
- 包含SrcPin的模塊需要在新的一幀數(shù)據(jù)ready時(shí)觸發(fā)onFrameAvailable;
- 包含SinkPin的模塊在onFrameAvailable中可以獲取新的一幀數(shù)據(jù)螟碎。
其他部分的處理方式
模塊間連接的兼容檢測(cè)眉菱。
參照上一節(jié)對(duì)Pin的介紹,模塊間連接的兼容檢測(cè)是通過(guò)Pin中所包含的數(shù)據(jù)類型來(lái)確定的抚芦,這個(gè)檢測(cè)在編譯階段就完成了倍谜。
不過(guò)迈螟,即便對(duì)于同一種數(shù)據(jù)類型叉抡,例如ImgBufFrame,也包含I420, RGBA, NV12等不同的色彩格式答毫,可以處理ImgBufFrame的模塊不一定支持所有的色彩格式褥民,這時(shí)就需要使用者在組織模塊的時(shí)候留意,或者在模塊間顯式加入一個(gè)通用的色彩空間轉(zhuǎn)換模塊洗搂。模塊的動(dòng)態(tài)添加及移除處理消返。
在已經(jīng)運(yùn)行的Pipline的A载弄、B模塊間加入模塊C時(shí),以直觀的方式撵颊,先斷開(kāi)A宇攻、B間的連接,然后使用A的SrcPin連接C的SinkPin倡勇,以C的SrcPin連接B的SinkPin逞刷。移除模塊的方式也是類似的處理。
模塊的動(dòng)態(tài)變動(dòng)一般發(fā)生在切換音視頻濾鏡妻熊,或者切換編解碼方式的時(shí)候夸浅,SDK針對(duì)這種通用場(chǎng)景,實(shí)現(xiàn)了濾鏡管理類扔役,以及Codec管理類以方便使用帆喇。模塊間的數(shù)據(jù)傳送。
參照上節(jié)Pin的相關(guān)接口亿胸,這里對(duì)數(shù)據(jù)流的傳遞僅實(shí)現(xiàn)了push模式坯钦,也就是數(shù)據(jù)一定是從上一級(jí)推到下一級(jí)。如果下一級(jí)模塊要實(shí)現(xiàn)媒體流的步進(jìn)控制侈玄,可以通過(guò)阻塞上一級(jí)輸入的方式來(lái)實(shí)現(xiàn)葫笼。狀態(tài)控制及消息處理等。
框架中并未對(duì)Pipeline及其中各個(gè)模塊的狀態(tài)拗馒、消息及錯(cuò)誤信息提供一個(gè)統(tǒng)一的處理方式路星,需要開(kāi)發(fā)者在組裝各個(gè)模塊時(shí),分別控制及監(jiān)聽(tīng)各個(gè)模塊的狀態(tài)诱桂、消息及錯(cuò)誤信息等洋丐。音視頻同步。
框架在構(gòu)建時(shí)僅針對(duì)直播推流場(chǎng)景挥等,本身并未實(shí)現(xiàn)音視頻同步的機(jī)制友绝,時(shí)鐘部分則直接使用System.nanoTime()調(diào)用獲取系統(tǒng)時(shí)間作為系統(tǒng)時(shí)鐘源。
在需要進(jìn)行音視頻同步的模塊中肝劲,可以通過(guò)阻塞輸入過(guò)快的媒體源來(lái)達(dá)成對(duì)上級(jí)模塊的節(jié)奏控制迁客。
總結(jié)與改進(jìn)
上述框架是在構(gòu)建推流端SDK時(shí)所設(shè)計(jì),為Android直播推流SDK提供了靈活強(qiáng)大的擴(kuò)展能力辞槐,不過(guò)依然存在很多可優(yōu)化部分掷漱。
需要完善對(duì)短視頻SDK場(chǎng)景的支持
數(shù)據(jù)流部分加入pull模式的支持。
短視頻應(yīng)用場(chǎng)景下榄檬,是以本地文件作為視頻源的卜范,其讀取文件以及demux過(guò)程不會(huì)成為整個(gè)處理過(guò)程的瓶頸,另外鹿榜,對(duì)解碼節(jié)奏的控制交由對(duì)解碼后數(shù)據(jù)進(jìn)行處理的模塊來(lái)進(jìn)行更為合理海雪,框架中加入pull模式支持對(duì)于短視頻應(yīng)用的構(gòu)建更為方便锦爵。加入全局的Clock機(jī)制來(lái)實(shí)現(xiàn)音視頻同步。
短視頻預(yù)覽奥裸、編輯险掀、轉(zhuǎn)場(chǎng)效果等場(chǎng)景下有音視頻同步的需求,在框架中加入全局的時(shí)鐘機(jī)制能夠簡(jiǎn)化應(yīng)用的復(fù)雜度湾宙。
簡(jiǎn)化模塊實(shí)現(xiàn)以及模塊組裝的工作
考慮引入模塊組裝管理類(Pipeline類)迷郑,連接、移除模塊時(shí)不再直接通過(guò)SrcPin進(jìn)行创倔,而是通過(guò)Pipeline類代理實(shí)現(xiàn)嗡害。通過(guò)這種方式,可以達(dá)到:
- 對(duì)于狀態(tài)切換及資源釋放畦攘,只需要操作Pipeline的相應(yīng)接口霸妹,不需要對(duì)逐個(gè)模塊進(jìn)行操作(特殊場(chǎng)景下依然可以逐模塊控制)。
- 可以即時(shí)獲取當(dāng)前Pipeline的鏈接結(jié)構(gòu)知押,方便debug叹螟。
- 可以將GLRender, Clock等可能全局需要的參數(shù)自動(dòng)設(shè)置到各個(gè)模塊,以簡(jiǎn)化模塊組裝的過(guò)程台盯。
- 能夠?qū)⒏鱾€(gè)模塊的異步事件罢绽、錯(cuò)誤消息等匯集到一處,應(yīng)用構(gòu)建者只需要監(jiān)聽(tīng)統(tǒng)一的接口静盅。