使用FFMPEG做d3d11va硬解,并实现360°全景视频播放器的开发过程

代码太多,就不发上来了,就聊聊实现过程。重点讲趟雷过程,方便后来者避雷。

原本干了4年多安防,跑了。新到一个公司做技术管理。公司有个360°全景的播放需求,因为VLC播放支持360°全景播放模式,所以直接拿了VLC的控件嵌入到程序里来做播放器,播放360°全景视频。
最近几年一直做产品和项目管理,代码写的少了,都是看为主。就去年做WCDMA的协议栈试了下身手。公司直接嵌VLC控件播放器出了不少问题,于是我又开始有点兴致,想把360°全景播放显示部分的代码直接剥出来用,顺便学习下d3d11的3D渲染显示(Win32环境下VLC用的d3d11,Linux下用的opengl)。
这部分代码完全能自己实现一个全景播放器,做个摩托车头盔全景摄像头产品,或者车载全景倒车这类应用。

开始逐步剥离VLC的DirectX显示部分的代码,主流程代码直接照抄,接上自己写的解码部分,再接上窗口部分,就能工作了。
最先是剥离的d3d9的代码,当作熟悉VLC的渲染框架。d3d9并没有支持360全景播放,只是想顺手整合到自己以前写的的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,C语言功底好,做事细致点,完全可以自己剥离。有问题可以给我站内信息讨论。有需要,全景播放器的整个代码也可以直接找我要,只是无责任不讲解哈。o( ̄▽ ̄)d

posted @ 2021-09-13 10:27  裤子多多  阅读(3751)  评论(11编辑  收藏  举报