1. 程式人生 > 其它 >使用FFMPEG做d3d11va硬解,並實現360°全景視訊播放器的開發過程

使用FFMPEG做d3d11va硬解,並實現360°全景視訊播放器的開發過程

程式碼太多,就不發上來了,就聊聊實現過程。重點講趟雷過程,方便後來者避雷。

原本幹了4年多安防,跑了。新到一個公司做技術管理。公司有個360°全景的播放需求,因為VLC播放支援360°全景播放模式,所以直接拿了VLC的控制元件嵌入到程式裡來做播放器,播放360°全景視訊。
最近幾年一直做產品和專案管理,程式碼寫的少了,都是看為主。就去年做WCDMA的協議棧試了下身手。公司直接嵌VLC控制元件播放器出了不少問題,於是我又開始有點興致,想把360°全景播放顯示部分的程式碼直接剝出來用,順便學習下d3d11的3D渲染顯示(VLC用的d3d11做的全景渲染顯示,還不支援opengl,不過掌握了渲染方式自己也能移植過去)。
這部分程式碼完全能自己實現一個全景播放器,做個摩托車頭盔全景攝像頭產品,或者車載全景倒車這類應用。

開始逐步剝離VLC的DirectX顯示部分的程式碼,主流程程式碼直接照抄,接上自己寫的解碼部分,再接上視窗部分,就能工作了。
最先剝離的d3d9的程式碼,當作熟悉流程,順手整合到自己以前寫的的d3d9的視訊顯示程式碼中,因d3d9只有2d視訊顯示,程式碼不多,過程很順利。
然後逐步抽離d3d11寫的360°全景視訊繪製程式碼,才發現比想像中複雜。d3d11的繪製支援360°全景顯示的好幾種貼圖格式。一個帶狀視訊畫面在球型模型上貼圖,越接近南北極區域的影象損失越大,通常魚眼攝像頭拍攝的畫面用,主要看四周範圍。方盒形式的天空盒全景,6個方盒面貼圖,南北極影象無損失,但是需要6個攝像頭畫面,前後左右加上下。另還有兩種格式…裡面還有附帶色彩格式轉換的GPU運算,HDR亮度調整演算法,10bit的渲染程式碼等等各種特效…很多功能實際用不上,但我有強迫症,想把全部流程剝離,增加了不少程式碼閱讀量。並且,d3d11的三維渲染繪製流程和介面,跟d3d9時代差異太大了,又造成不少額外工作量。然後就是不停踩雷。

首先d3d11裡面的ID3D11Texture2D紋理是需要通過建立ID3D11ShaderResourceView才能繫結到渲染管線上。
我自己搭建的播放框架,是通過ffmpeg去解碼視訊檔案,呼叫硬解碼,取到AVFrame裡面的ID3D11Texture2D紋理資料,建立ID3D11ShaderResourceView,再送入到VLC的全景顯示流程程式碼裡進行顯示渲染。然而這裡建立ShaderResourceView失敗…
後來看VLC裡的全景顯示初始化程式碼,它在用紋理建立ID3D11ShaderResourceView前,加了一個判斷紋理是否具有D3D11_BIND_SHADER_RESOURCE屬性的斷言,查閱文件,確認ID3D11Texture2D紋理建立時需要指定D3D11_BIND_SHADER_RESOURCE屬性,才能執行繫結到渲染管線上。
回頭來看ffmpeg的硬解程式碼,在建立ID3D11Texture2D紋理的程式碼,確實沒有指定這個繫結屬性。耗費了好幾天,才確認了問題。
這下尷尬了,不能像d3d9的dxva2一樣,ffmpeg解碼出來的紋理資料直接就能拿去渲染顯示。囧rz…
我又比較抵制去改ffmpeg的程式碼加上繫結屬性,這樣以後升級更新都會帶來麻煩。

再看VLC裡面的程式碼流程,是在Pool入口函式裡面建立了一個ID3D11Texture2D陣列做緩衝區(建立時指定了D3D11_BIND_SHADER_RESOURCE),然後分別建立ID3D11ShaderResourceView繫結到渲染管線。
翻了半天文件,又發現d3d11提供了CopySubresourceRegion這個API用於兩種紋理之間進行拷貝。於是迂迴作戰,嘗試把ffmpeg AVFrame裡面沒有帶繫結屬性的ID3D11Texture2D的紋理,Copy到VLC建立的ID3D11Texture2D帶繫結屬性的紋理數組裡面,這樣就能建立ID3D11ShaderResourceView進行渲染了。

程式碼修改好後順利渲染成功,不過視訊畫面整個都是綠色,視角拉遠了就是一個綠色的球…YUV資料全0就是綠屏(還好有以前經驗… ー( ̄~ ̄)ξ),這裡肯定是懷疑紋理沒有拷貝成功,還是全0的初始資料。蛋疼的是d3d11裡面這個CopySubresourceRegion並沒有返回值,莫法確認函式執行成功與否。

繼續搞,先存個圖片看看做驗證。又發現以前dx9上挺好用的D3DXSaveTextureToFile到了d3d11的D3DX11SaveTextureToFile就失靈。翻文件,NV12格式的紋理麼有搞…
再繼續搞,套用強悍的ffmpeg裡面av_hwframe_transfer_data的程式碼,把渲染時的ID3D11Texture2D拷貝到stagin紋理上,再用map download拷貝出來,用sws_scale轉換成YUV420P,再用ffmpeg的mjpeg的編碼器,迂迴了一圈寫了個函式,把ID3D11Texture2D紋理download到記憶體中再編碼儲存為jpg圖片檔案。開啟一看,圖片全黑,確認CopySubresourceRegion確實是沒有拷貝出來資料。

仔細推敲,發現CopySubresourceRegion裡面有個Source subresource index索引的引數。ffmpeg的AVFrame裡data[0]是ID3D11Texture2D,data[1]也是一個索引index值。根據文件,這裡解碼的ID3D11Texture2D是一個數組序列,CopySubresourceRegion在也需要指定每個ID3D11Texture2D的序列索引才能正確拷貝。
於是加上序列索引引數,然而渲染畫面依然是綠屏,存出來的圖片依然確認紋理拷貝失敗。

這下完全沒頭緒了。乾脆一步一步的檢視VLC的整個d3d11的渲染顯示程式碼,程式碼量蠻大,仔細一句一句的跟蹤推敲了幾天的程式碼。

最終發現VLC的程式碼裡,建立Pool的紋理陣列時,D3D11_TEXTURE2D_DESC的
ArraySize裡面遞入了外界的pool_size的值。而我為了照顧VLC剝離程式碼的流程,完整保留了程式碼的引數,並且想當然的使用ffmpeg解碼的紋理裡面的ArraySize值遞進去指定這個紋理的ArraySize,這個紋理在渲染時就會被理解為一個紋理陣列序列的1幀。

原來是自己搞暈頭了,以前拷貝其實成功的,但是紋理並不是一個序列的一幀。這裡其實我只需要一個紋理做轉換而已。把ArraySize引數值改成固定的1,只建立1個帶繫結屬性的紋理,而不是建立一個紋理序列。ffmpeg解碼後不帶繫結屬性紋理,通過CopySubresourceRegion拷貝過來,然後套用VLC的顯示流程程式碼,建立ID3D11ShaderResourceView並進行三維渲染。這次,在球體三維模型上順利顯示出了視訊畫面。

自此,走了一大圈彎路,dx9的dxva2和dx11的d3d11va的硬解碼和d3d11的三維紋理渲染顯示都順利熟悉了一遍。

上面說了一大堆廢話,權做記錄下最近的工作而已。感謝你看到這裡,想必也是有需求。
原始碼參考摘抄自VLC和FFMPEG,如果有需要,全景播放器的程式碼可以直接找我要,只是無責任不講解哈。o( ̄▽ ̄)d