第五章 實(shí)現(xiàn)一款視頻播放器

一 架構(gòu)設(shè)計(jì)
  • 輸入模塊

待視頻流和音頻流都解碼為裸數(shù)據(jù)之后,需要為音視頻各自建立一個(gè)隊(duì)列將裸數(shù)據(jù)存儲(chǔ)起來(lái),不過(guò),如果是在需要播放一幀的時(shí)候再去做解碼,那么這一幀的視頻就有可能產(chǎn)生卡頓或者延遲,所以這里引出了第一個(gè)線(xiàn)程,即為播放器的后臺(tái)解碼分配一個(gè)線(xiàn)程,該線(xiàn)程用于解析協(xié)議,處理解封裝以及解碼,并最終將裸數(shù)據(jù)放到音頻和視頻的隊(duì)列中,這個(gè)模塊稱(chēng)為輸入模塊。

  • 輸出模塊

輸出部分其實(shí)是由兩部分組成的,一部分是音頻的輸出,另一部分是視頻的輸出。

  • 音視頻同步模塊

所以需要再建立一個(gè)模塊來(lái)負(fù)責(zé)音視頻同步的工作,這個(gè)模塊稱(chēng)為音視頻同步模塊。

  • 調(diào)度器

先把輸入模塊音頻隊(duì)列、視頻隊(duì)列都封裝到音視頻同步模塊中,然后為外界提供獲取音頻數(shù)據(jù)、視頻數(shù)據(jù)的接口,這兩個(gè)接口必須保證音視頻的同步,內(nèi)部將負(fù)責(zé)解碼線(xiàn)程的運(yùn)行與暫停的維護(hù)。

然后把音視頻同步模塊音頻輸出模塊、視頻輸出模塊都封裝到調(diào)度器中,調(diào)度器模塊會(huì)分別向音頻輸出模塊和視頻輸出模塊注冊(cè)回調(diào)函數(shù),回調(diào)函數(shù)允許兩個(gè)輸出模塊獲取音頻數(shù)據(jù)和視頻數(shù)據(jù)。

1.1 詳細(xì)介紹
image.png
image.png
  • VideoPlayerController 調(diào)度器,內(nèi)部維護(hù)音視頻同步模塊、音頻輸出模塊、視頻輸出模塊,為客戶(hù)端代碼提供開(kāi)始播放、暫停、繼續(xù)播 放、停止播放接口;為音頻輸出模塊和視頻輸出模塊提供兩個(gè)獲取數(shù)據(jù)的接口。

  • AudioOutput 音頻輸出模塊,由于在不同平臺(tái)上有不同的實(shí)現(xiàn), 所以這里真正的聲音渲染API為Void類(lèi)型,但是音頻的渲染要放在一個(gè)單獨(dú)線(xiàn)程中進(jìn)行。

  • VideoOutput 視頻輸出模塊,雖然這里統(tǒng)一使用OpenGL ES來(lái)渲染視頻,必須 由我們主動(dòng)開(kāi)啟一個(gè)線(xiàn)程來(lái)作為OpenGL ES的渲染線(xiàn)程。

  • AVSynchronizer 音視頻同步模塊,會(huì)組合輸入模 塊、音頻隊(duì)列和視頻隊(duì)列,其主要為它的客戶(hù)端代碼VideoPlayerController調(diào)度器提供接口,包括:開(kāi)始、結(jié)束,以及最重要的獲取音頻數(shù)據(jù)和獲取對(duì)應(yīng)時(shí)間戳的視頻幀。此外,它還會(huì)維護(hù)一個(gè)解碼線(xiàn)程,并且根據(jù)音視頻隊(duì)列里面的元素?cái)?shù)目來(lái)繼續(xù)或者暫停該解碼線(xiàn)程的運(yùn)行。

  • AudioFrame 音頻幀,其中記錄了音頻的數(shù)據(jù)格式以及這一幀的具體數(shù)據(jù)、時(shí)間戳等信息。

  • AudioFrameQueue 音頻隊(duì)列,主要用于存儲(chǔ)音頻幀,為它的客戶(hù) 端代碼音視頻同步模塊提供壓入和彈出操作。由于解碼線(xiàn)程和聲音播放線(xiàn)程會(huì)作為生產(chǎn)者消費(fèi)者同時(shí)訪(fǎng)問(wèn)該隊(duì)列中的元素,所以該隊(duì)列要保證線(xiàn)程安全性。

  • VideoFrame 視頻幀,記錄了視頻的格式以及這一幀的具體的數(shù)據(jù)、寬、高以及時(shí)間戳等信息。

  • VideoFrameQueue 視頻隊(duì)列,主要用于存儲(chǔ)視頻幀,為它的客戶(hù)端代碼音視頻同步模塊提供壓入和彈出操作,由于解碼線(xiàn)程和視頻播放線(xiàn)程會(huì)作為生產(chǎn)者消費(fèi)者同時(shí)訪(fǎng)問(wèn)該隊(duì)列中的元素,所以該隊(duì)列要保證線(xiàn)程安全性。

  • VideoDecoder 輸入模塊,個(gè)是協(xié)議層解析器,一個(gè)是格式解封裝器,一個(gè)是解碼器,并且它主要向AVSynchronizer提供接口,打開(kāi)文件資源(網(wǎng)絡(luò)或者本 地)、關(guān)閉文件資源、解碼出一定時(shí)間長(zhǎng)度的音視頻幀。

1.2 具體實(shí)現(xiàn)
  • 輸入模塊
  1. 選擇FFmpeg開(kāi)源庫(kù)的libavformat模塊來(lái)處理各種不同的協(xié)議以及不同的封裝格式。
  2. 使用FFmpeglibavcodec模塊作為解碼器模塊的技術(shù)選型。
  • 音頻輸出模塊
  1. 對(duì)于iOS平臺(tái),其實(shí)也有很多種方式,比較常見(jiàn)的就是AudioQueueAudioUnit
  • 視頻輸出模塊

技術(shù)選型肯定是選擇OpenGL ES,在iOS平臺(tái)上使用EAGL來(lái)為OpenGL ES提供上下文環(huán)境,自己定義一個(gè)View繼承自UIView,使用EAGLLayer作為渲染對(duì)象,并最終渲染到這個(gè)自定義的View上。

  • 音視頻同步模塊
  1. 使用pthread維護(hù)解碼線(xiàn)程
  2. 對(duì)于音視頻隊(duì)列,我們可以自行編寫(xiě)一個(gè)保證線(xiàn)程安全的鏈表來(lái)實(shí)現(xiàn)。
  3. 采用視頻向音頻對(duì)齊的策略,即只需要把同步這塊邏輯放到獲取視頻幀的方法里面就好了。
  • 控制器

需要將上述的三個(gè)模塊合理地組裝起來(lái)。

二 解碼模塊的實(shí)現(xiàn)

直接使用FFmpeg開(kāi)源庫(kù)來(lái)負(fù)責(zé)輸入模塊的協(xié)議解析、封裝格式拆分、 解碼操作等行為,整體流程如圖所示。

image.png

整個(gè)運(yùn)行流程分為以下幾個(gè)階段:

  1. 建立連接、準(zhǔn)備資源階段。
  2. 不斷讀取數(shù)據(jù)進(jìn)行解封裝、解碼、處理數(shù)據(jù)階段。
  3. 釋放資源階段。

注意點(diǎn):

  1. 對(duì)于每個(gè)流都要分配一個(gè)AVFrame作為解碼之后數(shù)據(jù)存放的結(jié)構(gòu)體。
  2. 對(duì)于音頻流,需要額外分配一個(gè)重采樣的上下文,對(duì)解碼之后的音頻格式進(jìn)行重采樣, 使其成為我們需要的PCM格式。
  3. decodeFrames接口的實(shí)現(xiàn),該接口主要負(fù)責(zé)解碼音視頻壓縮數(shù)據(jù)成為原始格式,并且封裝成為自定義的結(jié)構(gòu)體,最終全部放到一個(gè)數(shù)組中,然后返回給調(diào)用端。
  4. 對(duì)應(yīng)于FFmpeg里面的AVPacket結(jié)構(gòu)體,對(duì)于視頻幀,一個(gè)AVPacket就是一幀視頻幀;對(duì)于音頻幀,一個(gè)AVPacket有可能包含多個(gè)音頻幀。
  5. 解碼之后,需要封裝成自定義的結(jié)構(gòu)體的AudioFrameVideoFrame。
  6. 對(duì)于音頻的格式轉(zhuǎn)換,FFmpeg提供了一個(gè)libswresample庫(kù)。
  7. 對(duì)于視頻幀的格式轉(zhuǎn)換,FFmpeg提供了一個(gè)libswscale的庫(kù),用于 轉(zhuǎn)換視頻的裸數(shù)據(jù)的表示格式。
三 音頻播放模塊的實(shí)現(xiàn)

在iOS平臺(tái),可使用AudioUnit(AUGraph封裝的實(shí)際上就是 AudioUnit)來(lái)渲染音頻。

構(gòu)造AUGraph,用來(lái)實(shí)現(xiàn)音頻播放,應(yīng)配置一個(gè)ConvertNode將客戶(hù)端代碼填充的SInt16格式的音頻數(shù)據(jù)轉(zhuǎn)換為RemoteIONode可以播放的Float32格式的音頻數(shù)據(jù)(采樣率、聲道數(shù)以及表示格式應(yīng)對(duì)應(yīng)上)。

image.png
四 畫(huà)面播放模塊的實(shí)現(xiàn)

無(wú)論是在哪一個(gè)平臺(tái)上使用OpenGL ES渲染視頻的畫(huà)面,都需要單獨(dú)開(kāi)辟一個(gè)線(xiàn)程,并且為該線(xiàn)程綁定一個(gè)OpenGL ES的上下文。

  1. 首先會(huì)書(shū)寫(xiě)一個(gè)VideoOutput類(lèi)繼承自UIView,然后重寫(xiě)父類(lèi)的layerClass方法,并且返回CAEAGLLayer類(lèi)型,重寫(xiě)該方法的目的是 該UIView可以被OpenGL ES進(jìn)行渲染;然后在初始化方法中,將 OpenGL ES綁定到Layer上。

  2. iOS平臺(tái)上的線(xiàn)程模型,采用NSOperationQueue來(lái)實(shí)現(xiàn)。

  3. iOS平臺(tái)有一個(gè)比較特殊的地方就是如果App進(jìn)入后臺(tái)之后,就不能再進(jìn)行OpenGL ES的渲染操作。

4.1 接下來(lái)看一下初始化方法的實(shí)現(xiàn),首先為layer設(shè)置屬性,然后初始化NSOperation-Queue,并且將OpenGL ES的上下文構(gòu)建以及OpenGL ES的渲染Program的構(gòu)建作為一個(gè)Block(可以理解為一個(gè)代碼塊)直接加入到該Queue中。

4.2 該Block中的具體行為如下:先分配一個(gè)EAGLContext,然后為該NSOperationQueue線(xiàn)程綁定OpenGL ES上下文,接著再創(chuàng)建FrameBufferRenderBuffer,

4.3 將RenderBufferstorage設(shè)置為UIViewlayer(就是前面提到的CAEAGLLayer),然后再將FrameBufferRenderBuffer綁定起來(lái),這樣繪制在FrameBuffer上的內(nèi)容就相當(dāng)于繪制到了RenderBuffer上,最后使用前面提到的VertexShaderFragmentShader構(gòu)造出實(shí)際的渲染Program,至此,初始化就完成了。

5.1 然后是關(guān)鍵的渲染方法,這里先判斷當(dāng)前OperationQueueoperationCount的值,如果其數(shù)目大于我們規(guī)定的閾值(一般設(shè)置為2或者3),則說(shuō)明每一次繪制所花費(fèi)的時(shí)間都比較多,這將導(dǎo)致很多繪制的延遲,所以可以刪除掉最久的繪制操作,僅僅保留等于閾值個(gè)數(shù)的繪制操作。

5.2 首先判定布爾型變量enableOpenGLRendererFlag的值,如果是YES,就綁定FrameBuffer,然 后使用Program進(jìn)行繪制,最后綁定RenderBuffer并且調(diào)用EAGLContextPresentRenderBuffer將剛剛繪制的內(nèi)容顯示到layer上去,因?yàn)?code>layer就是UIViewlayer,所以能夠在UIView中看到我們剛剛繪制的內(nèi)容了。

  1. 至于銷(xiāo)毀方法,也要保證這步操作是放在OperationQueue中執(zhí)行 的,因?yàn)樯婕?code>OpenGL ES的所有操作都要放到綁定了上下文環(huán)境的線(xiàn)程中去操作。

  2. 對(duì)于UIViewdealloc方法,其功能主要是負(fù)責(zé)回收所有的資源,首先移除所有的監(jiān)聽(tīng)事件,然后清空OperationQueue中未執(zhí)行的操作,最后釋放掉所有的資源。

五 AVSync模塊的實(shí)現(xiàn)

AVSynchronizer類(lèi)的實(shí)現(xiàn),第一部分是維護(hù)解碼線(xiàn)程,第二部分就是音視頻同步。

5.1 維護(hù)解碼線(xiàn)程

AVSync模塊開(kāi)辟的解碼線(xiàn)程扮演了生產(chǎn)者的角色,其生產(chǎn)出來(lái)的 數(shù)據(jù)所存放的位置就是音頻隊(duì)列視頻隊(duì)列,而AVSync模塊對(duì)外提供的填充音頻數(shù)據(jù)和獲取視頻的方法則扮演了消費(fèi)者的角色,從音視頻隊(duì)列中獲取數(shù)據(jù),其實(shí)這就是標(biāo)準(zhǔn)的生產(chǎn)者消費(fèi)者模型

在最后銷(xiāo)毀該模塊的時(shí)候,需要先將isOnDecoding變量設(shè)置為false,然后還需要額外發(fā)送一次signal指令,讓解碼線(xiàn)程有機(jī)會(huì)結(jié)束,如果不發(fā)送該signal指令,那么解碼線(xiàn)程就有可能一直wait在這里,成為一個(gè)僵尸線(xiàn)程。

5.2 音視頻同步
  • 音頻向視頻同步

音頻向視頻同步,顧名思義,就是視頻會(huì)維持一定的刷新頻率,或者根據(jù)渲染視頻幀的時(shí)長(zhǎng)來(lái)決定當(dāng)前視頻幀的渲染時(shí)長(zhǎng),或者說(shuō)視頻的每一幀肯定可以全都渲染出來(lái)。

AudioOutput模塊填充音頻數(shù)據(jù)的時(shí)候,會(huì)與當(dāng)前渲染的視頻幀的時(shí)間戳進(jìn)行比較。

  1. 在閾值范圍內(nèi),直接填充數(shù)據(jù)播放
  2. 音頻幀比視頻幀小,跳幀(加快音頻播放速度,或丟棄音頻幀)
  3. 音頻幀比視頻幀大,等待(放慢音頻播放速度或填充空數(shù)據(jù)靜音幀)

優(yōu)點(diǎn) 畫(huà)面看上去是最流暢的
缺點(diǎn) 音頻有可能會(huì)加速 (或者跳變)也有可能會(huì)有靜音數(shù)據(jù)(或者慢速播放),發(fā)生丟幀或者插入空數(shù)據(jù)的時(shí)候,用戶(hù)的耳朵 是可以明顯感覺(jué)到的。

  • 視頻向音頻同步

不論是哪一個(gè)平臺(tái)播放音頻的引擎,都可以保證播放音頻的時(shí)間長(zhǎng)度與實(shí)際這段音頻所代表的時(shí)間長(zhǎng)度是一致的。

  1. 視頻幀比音頻幀小,跳幀
  2. 視頻幀比音頻幀大,等待(重復(fù)渲染上一幀或者不進(jìn)行渲染)

優(yōu)點(diǎn) 音頻可以連續(xù)播放
缺點(diǎn) 視頻畫(huà)面有可能會(huì)有跳幀的操作,但是對(duì)于視頻畫(huà)面的丟幀和跳幀,用戶(hù)的眼睛是不太容易分辨得出來(lái)的。

  • 統(tǒng)一向外部時(shí)鐘同步

在外部單獨(dú)維護(hù)一軌外部時(shí)鐘,當(dāng)我們獲取音頻數(shù)據(jù)視頻幀的時(shí)候,都需要與這個(gè)外部時(shí)鐘進(jìn)行對(duì)齊,如果沒(méi)有超過(guò)閾值,那么就直接返回本幀音頻幀或者視頻幀,如果超過(guò)了閾值就要進(jìn)行對(duì)齊操作。

得出了一個(gè)理論,那就是人的耳朵比人的眼睛要敏感得多,我們所實(shí)現(xiàn)的播放器將采用音視頻對(duì)齊策略的第二種方式,即視頻音頻對(duì)齊的方式。

六 中控系統(tǒng)串聯(lián)各個(gè)模塊
6.1 初始化階段

調(diào)用AVSync模塊放在一個(gè)異步線(xiàn)程中來(lái)打開(kāi)連接會(huì)更加合理,所以這里使用GCD線(xiàn)程模型,將初始化的操作放在一個(gè)DispatchQueue中。首先也是調(diào)用AVSync模塊的openFile方法,如果可以打開(kāi)媒體資源連接,則繼續(xù)初始化VideoOutput對(duì)象。

6.2 運(yùn)行階段

就是為AudioOutput模塊填充數(shù)據(jù),并且通知VideoOutput模塊來(lái)更新畫(huà)面。

6.3 銷(xiāo)毀階段
  1. 由于音視頻對(duì)齊策略的影響,整個(gè)播放過(guò)程其實(shí)是由音頻來(lái)驅(qū)動(dòng)的,所以在銷(xiāo)毀階段肯定需要首先停止音頻。
  2. 然后停止AVSync模塊。
  3. 最后一步應(yīng)該是停止VideoOutput模塊。
  4. 最終再將VideoOutput這個(gè)自定義的viewViewController中移除,至此銷(xiāo)毀階段就實(shí)現(xiàn)完畢了。
七 總結(jié)
  • 輸入模塊(或者稱(chēng)為解碼模塊),輸出音頻幀是AudioFrame,其中的主要數(shù)據(jù)是PCM裸數(shù)據(jù);輸出視頻幀是VideoFrame,其中的主要數(shù)據(jù)是YUV420P的裸數(shù)據(jù)。

  • 音頻播放模塊,輸入是解碼出來(lái)的AudioFrame,直接就是SInt16表示的sample格式的數(shù)據(jù),輸出則是輸出到Speaker用戶(hù)能夠直接聽(tīng)到聲音。

  • 視頻播放模塊,輸入是解碼出來(lái)的VideoFrame,其中存放的是YUV420P格式的數(shù)據(jù),在渲染過(guò)程中可使用OpenGL ESProgramYUV格式的數(shù)據(jù)轉(zhuǎn)換為RGBA格式的數(shù)據(jù),并最終顯示到物理屏幕上。

  • 音視頻同步模塊,它的工作主要由兩部分組成;第一部分是負(fù)責(zé)維護(hù)解碼線(xiàn)程,即負(fù)責(zé)輸入模塊的管理;另外一部分是音視頻同步,可向外部提供填充音頻數(shù)據(jù)的接口和獲取視頻幀的接口,以保證所提供的數(shù)據(jù)是同步的。

  • 中控系統(tǒng),負(fù)責(zé)將AVSync模塊、AudioOutput模塊、 VideoOutput模塊組織起來(lái),最重要的就是維護(hù)這幾個(gè)模塊的生命周 期,由于其中存在多線(xiàn)程的問(wèn)題,所以需要重點(diǎn)注意的是,應(yīng)在初始 化、運(yùn)行、銷(xiāo)毀各個(gè)階段保證這幾個(gè)模塊可以協(xié)同有序地運(yùn)行,同時(shí)中 控系統(tǒng)應(yīng)對(duì)外提供用戶(hù)可以操作的接口,比如開(kāi)始播放、暫停、繼續(xù)、 停止等接口。


本文參考音視頻開(kāi)發(fā)進(jìn)階指南


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容