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