ffmpeg現在封裝的很是so easy,使用上不用多講。
如何啟用硬件解碼,在ffmpeg源碼中(doc\example\hw_decode.c)中也有完整樣例。
enum AVHWDeviceType hwDeviceType;
hwDeviceType = av_hwdevice_find_type_by_name("dxva2");
// 嘗試硬解碼
if (hwDeviceType != AV_HWDEVICE_TYPE_NONE)
{
decodecCtx->get_format = get_hw_format;
if (hw_decoder_init(decodecCtx, hwDeviceType) < 0)
printf("hw_decoder_init failed.\n");
else
printf("User dxva2 decodec.\n");
}
樣例中提供了get_hw_format函數和hw_decoder_init,照抄過來,即可啟用硬解碼。
與軟解碼流程一樣,給解碼器avcodec_send_packet 設置數據后,調用avcodec_receive_frame 即可拿到解碼后的AVFrame數據。
dxva2解碼數據以IDirect3DSurface9 紋理表面接口提供,保存在(IDirect3DSurface9 *)AVFrame->data[3]中。
IDirect3DSurface9 在顯存GPU內部是NV12格式,這里ffmpeg提供了av_hwframe_transfer_data函數,將IDirect3DSurface9 紋理表面的顯存數據傳輸到NV12格式的CPU內存中。
// GPU->CPU->Scale->View
if (av_hwframe_transfer_data(hw_frame, av_frame, 0) >= 0)
{
av_frame_copy_props(hw_frame, av_frame);
av_frame_unref(av_frame);
p_frame = hw_frame;
}
到這里就可以繼續沿用軟件縮放和顯示流程了。都是奔硬解來的,這里又變回軟處理。
avcodec_send_packet 在ffmpeg內部是在給顯卡的GPU顯存送數據,編碼數據量並不大,耗時不多。
解碼后的原始圖像數據就大了,等av_hwframe_transfer_data再從GPU傳輸回內存中,這里耗費的CPU資源,基本可以比肩軟解碼的速度。
如果再使用DirectX9接口送回顯存去渲染顯示,一來一去,速度就比較搞笑了,也失去了硬解的意義。
還是要想辦法直接顯示IDirect3DSurface9。
但ffmpeg樣例並沒有提供內部DirectX3D的接口調用,相關的接口資源也沒有暴露,如果自己去創建IDirect3DDevice9接口,顯然是沒有辦法繪制ffmpeg內部Direct3D對象創建的IDirect3DSurface9。
直接改ffmpeg源碼工作量也並不合算。
還好我們可以拿到(IDirect3DSurface9 *)AVFrame->data[3],IDirect3DSurface9接口提供了GetDevice函數,可以獲取到ffmpeg內部的IDirect3DDevice9接口。
CComPtr<IDirect3DDevice9> pD3DDevice;
hr = pSurface->GetDevice(&pD3DDevice);
if (FAILED(hr))
return FALSE;
用它即可直接繪制位顯存中的紋理表面。
這里還沒完,ffmpeg內部創建的設備翻轉鏈表的BackBufferWidth/BackBufferHeight尺寸分別只有640/480,窗口句柄是桌面窗口,同樣ffmpeg沒有提供暴露接口參數。
還好IDirect3DDevice9提供多個窗口繪制能力,這里需要添加自己的窗口繪制翻轉鏈表。
這里的翻轉鏈表,類似雙緩沖翻轉顯示,GDI繪制自定義UI經常會用的技巧。
如果有同學還不清楚DirectX3D的繪制方式,這里建議先百度補習下相關用法。
D3DDISPLAYMODE d3ddm;
hr = m_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
if (FAILED(hr))
return FALSE;
if (nWidth > d3ddm.Width)
nWidth = d3ddm.Width;
if (nHeight > d3ddm.Height)
nHeight = d3ddm.Height;
CComPtr<IDirect3DSwapChain9> spSwapChain;
hr = m_pDevice->GetSwapChain(0, &spSwapChain);
if (FAILED(hr))
return FALSE;
hr = spSwapChain->GetPresentParameters(&m_Present);
if (FAILED(hr))
return FALSE;
m_Present.hDeviceWindow = m_hWnd;
m_Present.Windowed = TRUE;
m_Present.BackBufferWidth = nWidth;
m_Present.BackBufferHeight = nHeight;
m_pAddSwapChain.Release();
hr = m_pDevice->CreateAdditionalSwapChain(&m_Present, &m_pAddSwapChain);
if (FAILED(hr))
return FALSE;
我這里首先獲取了顯卡設備的桌面分辨率,設置給翻轉鏈表的BackBufferWidth/BackBufferHeight(創建大於桌面分辨率的BackBuffer有意義?),設置窗口句柄,CreateAdditionalSwapChain添加自己的窗口繪制翻轉鏈表。
有了自己的窗口繪制翻轉鏈表,就可以把IDirect3DSurface9渲染到自己的窗口上。
首先需要設置IDirect3DDevice9的Render渲染目標為我們自己添加的窗口繪制翻轉量表的BackSurface。
CComPtr<IDirect3DSurface9> spBackSurface;
hr = m_pAddSwapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &spBackSurface);
if (FAILED(hr))
return FALSE;
hr = m_pDevice->SetRenderTarget(0, spBackSurface);
if (FAILED(hr))
return FALSE;
接下來是顯示,這里直接將表面StretchRect到翻轉鏈的背面,然后翻轉即可顯示。
StretchRect時即可在顯卡GPU內部完成NV12到BackBuffer的FMT格式轉換,以及縮放,都是硬件實現。
因為上面設置了Render渲染目標為自創建的翻轉鏈,當然也可以使用三維的Render渲染方式,
因為不需要翻轉,或者旋轉之類的特效,我用的StretchRect后翻轉顯示。
hr = m_pDevice->StretchRect(pSrcSurface, pRectSrc, spBackSurface, pRectDec, D3DTEXF_LINEAR);
if (FAILED(hr))
return FALSE;
hr = m_pAddSwapChain->Present(pSrcRect, pDecRect, NULL, NULL, 0);
if (FAILED(hr))
return FALSE;
Render渲染的代碼太長就不貼出來了,有興趣可以看微軟的DX文檔,使用ID3DXSprite二維精靈繪制接口比較便捷。
至此,我們已經可以完全依賴GPU去解碼並顯示視頻。
這里額外提一句,DX9的文檔上關於IDirect3DDevice9::StretchRect有這么一句:
StretchRect cannot be called inside of a BeginScene/EndScene pair.
實際上我看網上很多代碼依然是這樣:
hr = m_pDevice->BeginScene();
hr = m_pDevice->StretchRect(pSrcSurface, pRectSrc, spBackSurface, pRectDec, D3DTEXF_LINEAR);
hr = m_pDevice->EndScene();
網絡上的代碼還是別簡單的copy來用…
額外提些要點,
能開啟dxva2的顯卡,應該都支持NV12->RGB的硬件轉換,至少我在幾個不同的PC,以及平板上,獲取設備表面格式轉換能力,都是返回OK。但若遇到硬件不支持,還是轉為軟件處理兼容性最佳。
工作之余筆記,難免有錯,歡迎拍磚。
若有幫助幸甚。