全平台硬件解码渲染方法与优化实践
硬件解碼后不恰當?shù)厥褂肙penGL渲染會導致性能下降,甚至不如軟解。本文來自PPTV移動端研發(fā)經(jīng)理王斌在LiveVideoStackCon 2017大會上的分享,并由LiveVideoStack整理而成。分享中王斌詳細解析了Windows、Linux、macOS、Android、iOS等多種平臺下硬件解碼的渲染方法及優(yōu)化實踐。
文 / 王斌
整理 / LiveVideoStack
大家好,我是來自PPTV的王斌。接下來我將圍繞以下幾個話題,為大家分享有關(guān)全平臺硬件解碼的渲染與優(yōu)化的實踐經(jīng)驗。
解碼后的視頻數(shù)據(jù)需經(jīng)過紋理加載后才會進行下一步的OpenGL ES渲染操作,其關(guān)鍵在于如何將解碼后的數(shù)據(jù)填充到紋理中。不同的平臺對于此問題的解決方案也不盡相同,這也是我們今天討論的重點。
1、常規(guī)方法渲染硬解數(shù)據(jù)
1.1 常規(guī)的OpenGL渲染
1)軟解OpenGL渲染流程
常規(guī)的軟解OpenGL渲染流程主要分為兩部分:一是在渲染紋理前進行的準備紋理,二是渲染前更新紋理。準備紋理具體是指在第一次渲染第一幀前先創(chuàng)建一個設(shè)置好相應(yīng)參數(shù)的紋理,而后再使用Texlmage2D將GPU上一定大小的顯存空間分配給此紋理;進行渲染前首先需綁定此紋理,并借助TexSublmage2D技術(shù)將解碼數(shù)據(jù)填充進之前分配好的紋理存儲空間中,也就是所謂的“紋理上傳”。
2)軟解數(shù)據(jù)流
軟解OpenGL渲染的數(shù)據(jù)流為:首先,通過調(diào)用TexSublmage將解碼后放在主存上的數(shù)據(jù)拷貝到顯存上用于更新紋理,隨后的渲染過程也是基于顯存上的數(shù)據(jù)進行。
1.2 硬解OpenGL渲染
硬解OpenGL渲染的數(shù)據(jù)流原理與軟解略有不同,解碼過程中的數(shù)據(jù)存儲在顯存上。這里需要強調(diào)的是,即使對基于統(tǒng)一內(nèi)存模型的移動平臺而言不一定存在物理顯存,但移動平臺會通過將內(nèi)存映射給GPU與CPU來構(gòu)建邏輯顯存。解碼后的拷貝、更新紋理、渲染與軟解類似,數(shù)據(jù)流會分別經(jīng)過主存、顯存、顯存。這里的解碼在顯存上的數(shù)據(jù)其實是硬解提供的相應(yīng)解碼輸出而非各個平面的數(shù)據(jù)指針,因此系統(tǒng)需要將硬解出的數(shù)據(jù)拷貝至內(nèi)存上并借助TexImage2D技術(shù)上傳紋理。經(jīng)過實踐我們發(fā)現(xiàn)此方法的效率并不高,例如在實測中我們借助軟解流程可實現(xiàn)1080P全高清視頻的流暢播放,而若借助DXVA硬解流程處理同一個全高清視頻文件則會變得非??D,那么如何來優(yōu)化硬解流程呢?思路一是對顯存與內(nèi)存間的拷貝過程進行優(yōu)化,例如在Windows上較為出名的LAV Filters濾鏡就使用了如SSEV4.1加速、多線程拷貝等,提升顯著。但如果面對同時播放多個視頻等較為復雜的應(yīng)用場景,內(nèi)存之間的拷貝仍會影響整個處理流程的穩(wěn)定運行。
上圖表示GPU(CPU)內(nèi)存與顯存間數(shù)據(jù)的交換速度,其中虛線表示數(shù)據(jù)由顯存拷貝到內(nèi)存的速度,實線表示數(shù)據(jù)由內(nèi)存拷貝到顯存的速度。從中我們可以看到,數(shù)據(jù)由顯存拷貝到內(nèi)存的速度大約是內(nèi)存拷貝到顯存的1/5,這也是為什么使用DXVA硬解時會出現(xiàn)不如軟解流暢的原因。
我們期待將這個問題簡化,也就是實現(xiàn)從解碼開始到渲染結(jié)束視頻數(shù)據(jù)一直在顯存上進行處理。我猜想,是否存在一種數(shù)據(jù)共享方式也就是API間的數(shù)據(jù)共享從而避免數(shù)據(jù)在內(nèi)存與顯存之間不必要的來回拷貝?例如使用D3D則會生成D3D的Texture,如果D3D與OpenGL間存在允許數(shù)據(jù)共享的接口,那么就可以保證無論數(shù)據(jù)如何被傳輸都保留在顯存上或不需要傳輸就可直接進行下一流程的處理;如果上述猜想不成立,由于內(nèi)存與GPU間的數(shù)據(jù)傳輸速度和內(nèi)存與CPU間相比快很多,能否通過與GPU間的數(shù)據(jù)拷貝顯著提升性能?當然我們也可以針對GPU提供的接口,轉(zhuǎn)換GPU中的數(shù)據(jù),例如將OpenGL的紋理從原來的YUV轉(zhuǎn)換成RGB以獲得理想的硬解數(shù)據(jù)流,上述都是我們在考慮硬解優(yōu)化時想到的解決方案。
2、硬解紋理轉(zhuǎn)換一般思路
總結(jié)各個平臺的情況不難發(fā)現(xiàn),考慮硬解之前我們必須思考硬解的輸出,由于硬解的輸出不像軟解的輸出是一組到內(nèi)存指向各平面的指針,我們需要獲知硬解輸出的對象與格式。現(xiàn)在很多硬解都是以YUV作為輸出格式如NV12等,當然排除個別定制化產(chǎn)品通過參數(shù)配置調(diào)整輸出格式為RGB的情況,根據(jù)經(jīng)驗硬解一般選用YUV作為輸出格式。首先是因為RGB的輸出實際上是在GPU內(nèi)部進行的色彩空間轉(zhuǎn)換,會對性能產(chǎn)生一定影響;其次我們也面臨無法保證YUV轉(zhuǎn)換成RGB的精確性,矩陣系數(shù)是定值則無法適應(yīng)多樣場景的問題。
如果采取數(shù)據(jù)共享,該怎樣找到這些數(shù)據(jù)共享接口?首先我們應(yīng)當從平臺入手,了解像iOS、Android等不同平臺提供了什么共享接口。如iOS與一些硬解庫提供的數(shù)據(jù)拷貝接口,如英偉達的CUDA提供的轉(zhuǎn)換接口等。Linux中也集成了被稱為VA-API的硬解接口,針對GLX環(huán)境VA-API提供了一種可將硬解輸出轉(zhuǎn)換為RGB紋理的方法,開發(fā)者可直接調(diào)用此接口與其相應(yīng)功能。
但用GLX的方法已經(jīng)比較過時,而Linux平臺上出現(xiàn)的一些新解決方案可帶來明顯的硬解性能提升。如現(xiàn)在比較流行的EGL,我們可將其理解為一個連接渲染接口與窗口系統(tǒng)之間的橋梁。EGL的大多數(shù)功能通過集成擴展實現(xiàn),主要的共享方法為GELImage與GELStream。
被使用最多的EGLImage目前作為擴展形式存在,如OpenMarxAL等專門提供了一套可輸出到EGLImage的接口,而樹莓派的MMAL硬件解碼則提供了一套由MMAL輸出的Buffer轉(zhuǎn)換為GELImage的方法。EGLImage可與窗口系統(tǒng)無關(guān),同樣也可用于沒有窗口系統(tǒng)的服務(wù)器端。在實際應(yīng)用中我們會優(yōu)先考慮使用EGLImage,視頻數(shù)據(jù)經(jīng)過與EGLImage對應(yīng)的OpenGL擴展輸出為OpenGL紋理從而實現(xiàn)了接口之間的共享。而較新的EGLStream是英偉達一直推崇的方法,目前我所接觸到的應(yīng)用主要有兩個:一個是OpenMarxAL接口,其可直接作為EGLStream的輸入擴展并可輸出OpenGL紋理,另一個則應(yīng)用在D3D11的硬件解碼上。如果大家使用EGLStream則需要重點核對兩個擴展名:producer與consumer。producer是硬件解碼輸出的對象,consumer則是輸出的OpenGL紋理。除了這些擴展,我們還可利用其他OpenGL擴展。對于Windows平臺而言Windows使用DXVA與D3D11解碼,輸出結(jié)果為D3D紋理;在這里,英偉達提供了一個可將D3D資源直接轉(zhuǎn)換為OpenGL紋理的接口,但此接口受到GPU驅(qū)動的限制,存在一定的使用環(huán)境限制;對于Linux平臺而言如X11窗口系統(tǒng),Linux提供了一個將X11的pixmap轉(zhuǎn)換成GLX也就是OpenGL紋理的方法,此方法之前也用于VA-API現(xiàn)在已不被推薦使用。
3.、D3D11+EGLStream
接下來我將介紹D3D11硬解,D3D11硬解基于EGL提供的資源共享功能。而D3D可與OpenGL ES一直建立聯(lián)系的原因是最早的Windows平臺對OpenGL驅(qū)動的支持一直不佳,而火狐、Chromium等瀏覽器為了在各自環(huán)境下都能很好支持OpenGL,于是加入了一個由 Google發(fā)起的被稱為ANGLE的開源項目。ANGLE是指用D3D9與D3D11的一些指令和(著色器)實現(xiàn)OpenGL ES與EGL所有接口類似的功能。除了使用ANGEL實現(xiàn)對OpenGL ?ES的支持,這些廠商也通過ANGEL實現(xiàn)對WebGL的支持。除此之外,一些如QT還有微軟推出的Windows Bridge for iOS等開源項目都是基于ANGEL Project,這些項目都是通過ANGEL Project實現(xiàn)OpenGL ES的調(diào)用。
D3D11的硬解輸出結(jié)果為D3D11紋理,輸出格式為NV12。后續(xù)在轉(zhuǎn)換紋理時我們有兩個思路:思路一較為常見,這里就不再贅述。思路二是借助EGLStream擴展,在創(chuàng)建一個共享的D3D11紋理后再從此紋理創(chuàng)建一個EGLSurface,此Surface可綁定至OpenGL紋理;我們需要做的是將解碼出的紋理拷貝至共享的D3D11紋理上,拷貝方法是借助D3D11的Video Processor接口將YUV轉(zhuǎn)換成RGB。盡管此方法效率較高,但有些Chrome開發(fā)者仍然覺得需要盡可能減小其帶來的性能損失,也就是追求完全沒有任何數(shù)據(jù)轉(zhuǎn)換的最佳方案。因此在2016年時EGLStream擴展被推出,從而有效改善了性能損失帶來的影響。
通過上圖我們可以發(fā)現(xiàn)D3D11+EGLStream的軟解流程與常規(guī)的OpenGL軟解渲染流程有所不同,EGLStream首先需要創(chuàng)建EGLStream對象,而后再創(chuàng)建紋理對象;在紋理準備期間也需要利用此擴展并設(shè)置consumer的OpenGL ES紋理,更新、渲染紋理時EGLStream提供了PostD3D11的方法,此方法相當于直接將D3D紋理作為OpenGL ES紋理使用。在后期進行渲染時由于涉及到兩個API——D3D11與OpenGL,調(diào)用API時不能同時訪問二者,故需要進行Acquire過程用以鎖定D3D11資源使得只有OpenGL可訪問此資源。在此之后我們就可借助OpenGL渲染紋理,結(jié)束渲染后Release也就是解鎖資源。
4、?iOS & macOS
而macOS與iOS也是借助之前提到的平臺提供的紋理共享接口。
Apple的macOS使用VideoToolbox作為解碼器且輸出對象為CVPixelBufferRef也就是保存在內(nèi)存或顯存上的圖像數(shù)據(jù);VideoToolbox有多種輸出格式,如YUV420P、NV12、RGB、UYVY422等。剛接觸此平臺時我注意到了其他平臺沒有的UYVY422格式,由于老版本系統(tǒng)不提供NV12接口,故UYVY442格式普遍用于老系統(tǒng);而新系統(tǒng)上提供的NV12處理效率遠高于UVYV442。當時我將此發(fā)現(xiàn)反饋給FFmpeg社區(qū),隨后社區(qū)在FFmpeg中添加了用以選擇VideoToolbox輸出結(jié)果的接口:如果是支持性能不佳的老系統(tǒng)則使用UYVY442格式,而新系統(tǒng)則使用NV12格式。macOS的紋理準備過程與傳統(tǒng)軟解相似,而紋理更新過程則略有不同,在其紋理更新中的PixelBuffer之后會輸出并保存一個IOSurface,關(guān)于IOSurface的詳細內(nèi)容我會在后文提到。macOS通過OpenGL Framework中的一個CGL實現(xiàn)將IOSurface轉(zhuǎn)換為紋理,而輸出的結(jié)果較為獨特,如輸出的紋理并非2D類型而是一個矩形紋理。macOS也可通過TextureCache方法實現(xiàn)紋理轉(zhuǎn)換并輸出RGB型紋理,但性能較為低下,不在此贅述。
iOS僅提供TextureCache法,這意味著不需要生成紋理而僅需在準備紋理階段創(chuàng)建TextureCache類即可并從Cache中直接獲取紋理,此流程與絕大多數(shù)需要先生成一個紋理再進行轉(zhuǎn)換等操作的傳統(tǒng)硬解渲染方法有明顯不同。
即使iOS與macOS可實現(xiàn)沒有數(shù)據(jù)拷貝的紋理轉(zhuǎn)換,但一個平臺存在兩套處理流程,這也會對開發(fā)者帶來不便。而蘋果公司隨后公開的一個被稱為IOSurface的新框架為接下來的探索提供了思路,其中包括了從PixelBuffer獲取IOSurface的方法。IOSurface用以進程間進行GPU數(shù)據(jù)共享,硬件解碼輸出至GPU顯存并通過IOSurface實現(xiàn)進程間的數(shù)據(jù)共享。VideoToolbox作為一個服務(wù),只有在APP開始解碼時才會啟動解碼進程。而Get IOSurface的方法在macOS上早已存在,但在iOS11的SDK中第一次出現(xiàn)。除了需要GetIOSurface,我們還需要轉(zhuǎn)成紋理的函數(shù),同樣在macOS的OpenGL Framework中我們發(fā)現(xiàn)了TextureImageIOSurface。此函數(shù)的功能與macOS上的相似,這是不是意味著我們可以將iOS與macOS的處理流程進行整合?
事實證明這樣是可行的,最終我們可統(tǒng)一整個蘋果系統(tǒng)的解碼渲染流程,除了OpenGL接口與OpenGL ES接口的差異之外,其它的流程完全相同。
這就引起了進一步思考:既然可以將二者進行統(tǒng)一,那么之前老平臺上的Texturecache究竟起了什么作用?
上圖展示的是Texturecache由TexToolbox buffer轉(zhuǎn)到(Texture崩潰)的堆棧,仔細觀察不難發(fā)現(xiàn)原先的Texturecache法其實也是調(diào)用TexImageIOSurface,為何老平臺存在此接口卻沒有被啟用?最終我在iOS5中發(fā)現(xiàn)了TextureImageIOSSurface的存在,而iOS11相對于iOS5僅僅是參數(shù)的添加與接口的微調(diào),并且使用GPU分析工具檢查后可發(fā)現(xiàn)IOS11與老版本系統(tǒng)的Texturecache方法類似,都是通過調(diào)用一個從老版本iOS上就存在至今的接口來實現(xiàn)相關(guān)功能。
最終我們成功統(tǒng)一了macOS與iOS兩個平臺的處理流程,在此之后如果開發(fā)者想調(diào)用官方提供的接口,首先需要判斷iOS版本,如果是iOS11則使用新方法,老版本則需要使用添加參數(shù)的方法。?
5、Android硬解渲染及常見難題解決
Android平臺中集成了Java、MediaCodec、OMX AL(應(yīng)用層創(chuàng)建播放器)等可直接調(diào)用的接口。除此之外還有一種提供了如創(chuàng)建、解碼器組件等諸多更底層功能的OMX IL接口,但如果將此接口與OpenGL結(jié)合,由于EGLImage所需的擴展是非公開的,并且OMX IL并非一個NDK系統(tǒng)庫而Android7.0以后的版本不允許訪問非NDK系統(tǒng)庫,故而我們僅使用MediaCodec與OMX AL。
MediaCodec存在兩種輸出,其一是ByteBuffer也就是將結(jié)果輸出到內(nèi)存上,當然是不被我們采用的;其二是Surface也就是將結(jié)果輸出到顯存上,接下來我們需要討論如何構(gòu)造Surface。這里有兩種方法構(gòu)造Surface,方法一是由Surface View獲取Surface并直接輸出至View上,但這對我們而言意味著無法使用OpenGL,故排除。方法二是Surface Texture,在解碼線程的開始需要配置MediaCodec輸出,由紋理構(gòu)建Surface Texture,而后Surface Texture借助UpdateTexImage法實現(xiàn)渲染線程更新紋理。這里需要明確的是Surface Texture紋理的對象是什么樣的?由于Android沒有相關(guān)文檔,我們可假設(shè)此紋理是一個有效紋理,如何創(chuàng)建此紋理?
以XBMC為例,首先解碼線程會給渲染線程以創(chuàng)建好紋理的信息同時渲染線程會反饋信息給解碼線程。但由于此消息循環(huán)機制并未在所有APP上推行,這對設(shè)計適用所有APP框架下的播放器來說并不合理,針對此問題我們有兩套解決方案:第一套方案是可以在解碼線程創(chuàng)建共享上下文并在此上下文下創(chuàng)建一個可在渲染線程被訪問的紋理。
但創(chuàng)建共享上下文的方法對一些安卓開發(fā)者而言門檻較高。第二套方案是在流程開始時創(chuàng)建一個無效的紋理,由于Surface Texture可把紋理附加至Surface Texture上,這樣只需在第一次渲染時把這個在渲染線程創(chuàng)建的合適紋理附加上即可。
以上兩種方法基本解決了一些相對重要的MediaCodec問題,除此之外我們也會面臨APP后臺切換至前臺時UpdateTexImage()錯誤的情況,如果是由于上下文不對一般可通過重新初始化解碼器或使用TextureView等方法解決。但如果用戶想借助SurfaceView解決此問題,也可通過共享上下文的方法,為SurfaceView提供一個上下文并在每次渲染前激活。但此方法具有僅適用于自己創(chuàng)建的上下文的局限性,如果上下文由外部提供,那么我們還可以通過attach方法。
attach方法大致流程如下:每次渲染時生成紋理并attach至上下文,調(diào)用更新紋理的方法使得數(shù)據(jù)保留在紋理上,最后將此紋理Detach。
最后想介紹些關(guān)于Open MAX AL的內(nèi)容。Open MAX AL在安卓上并未提供EGLStream擴展,而創(chuàng)建OMXAL播放器時需要設(shè)置輸出參數(shù),對安卓而言輸出Native Display對象也就是ANative Window,其由Surface獲取并調(diào)用NDK接口,與OMX AL輸出的Surface一致,所以之后的與Surface相關(guān)的流程和MediaCodec完全相同。
總結(jié)
以上是生活随笔為你收集整理的全平台硬件解码渲染方法与优化实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Zoom的Web客户端与WebRTC有何
- 下一篇: 音视频技术开发周刊 72期