原文作者 Ori Gold。本文已获得作者授权,发布于声网开发者社区。
-
提供 非常 简化的代码示例。我省去了返回代码检查、错误处理等步骤。我的观点是,代码样本就是: 样本。 (我本提供更多充实的示例,但你知道的,这涉及到知识产权及所有其他内容。) -
我将不介绍硬件加速视频解码/渲染的原理,因为这超出了本文的范围。此外,还有很多资源能解释得比我好。 -
FFmpeg 支持几乎所有协议和编码格式。RTSP 和 UDP 都可以使用这些样本,以及使用 H.264 和 H.265 编码的视频。我敢肯定,还有很多程序都可以对其进行使用。 -
我创建的项目基于 CMake,并且不依赖 Visual Studio 的构建系统(因为我们也需要支持非 DX 渲染器),这使事情变得有点困难,这就是为什么我认为我会提到它。
步骤一:设置流源和视频解码器。
示例:https://ffmpeg.org/doxygen/3.4/hw__decode_8c_source.html Moonlight:https://github.com/moonlight-stream/moonlight-qt/blob/master/app/streaming/video/ffmpeg.cpp
AVCodecContext 上提供硬件设备类型。我选择以与 FFmpeg 示例相同的方式执行此操作:基本字符串。
// initialize streamconst std::string hw_device_name = "d3d11va";AVHWDeviceType device_type = av_hwdevice_find_type_by_name(hw_device_name.c_str());// set up codec contextAVBufferRef* hw_device_ctx;av_hwdevice_ctx_create(&hw_device_ctx, device_type, nullptr, nullptr, 0);codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);// open stream
AVPacket* packet = av_packet_alloc();av_read_frame(format_ctx, packet);avcodec_send_packet(codec_ctx, packet);AVFrame* frame = av_frame_alloc();avcodec_receive_frame(codec_ctx, frame);
步骤二:将 NV12 转换为 RGBA。
AV_PIX_FMT_NV12 转换为 AV_PIX_FMT_RGBA 。
swscale:https://ffmpeg.org/doxygen/0.5/swscale-example_8c-source.html
SwsContext 的过程就像调用单个函数一样简单。SwsContext* conversion_ctx = sws_getContext(SRC_WIDTH, SRC_HEIGHT, AV_PIX_FMT_NV12,DST_WIDTH, DST_HEIGHT, AV_PIX_FMT_RGBA,SWS_BICUBLIN | SWS_BITEXACT, nullptr, nullptr, nullptr);
sws_scale() ,我们需要将帧从GPU传输到CPU。我是使用 av_hwframe_transfer_data() 中FFmpeg的内置功能做到这一点的。有很多这样的例子(https://ffmpeg.org/doxygen/3.4/hw__decode_8c_source.html)。
// decode frameAVFrame* sw_frame = av_frame_alloc();av_hwframe_transfer_data(sw_frame, frame, 0);sws_scale(conversion_ctx, sw_frame->data, sw_frame->linesize,0, sw_frame->height, dst_data, dst_linesize);sw_frame->data = dst_datasw_frame->linesize = dst_linesizesw_frame->pix_fmt = AV_PIX_FMT_RGBAsw_frame->width = DST_WIDTHsw_frame->height = DST_HEIGHT
-
我在 AVFrame中需要的是一个简单易懂的字节数组,使用“d3d11va”作为硬件设备名称为我们提供了简单的字节数组以外的东西,所以我将硬件设备名称更改为“dxva2”。现在,frame->data仅仅是uint8_t*形式上的位图。目前可以使用它,但是作为一种长期解决方案, 不 使用“d3d11va”是基本上错了的要点。 -
为了调用 sws_scale()并将帧转换为 RGBA 格式,我们需要将帧从 GPU 移到 CPU。目前还可以这样使用,但将来绝对是我们希望删除的内容。
步骤三:设置 DirectX 11 呈现。
示例:http://www.directxtutorial.com/Lesson.aspx?lessonid=11-4-5 三角形变正方形:https://stackoverflow.com/questions/20412807/directx-11-make-a-square
fxc.exe 可执行文件,并使用适当的选项(详细文档链接如下)执行命令以编译着色器。(我使用 /Fh 将它们编译为自动生成的标头。)
文档:https://docs.microsoft.com/en-us/windows/win32/direct3dtools/dx-graphics-tools-fxc-syntax
步骤四:交换颜色以获得纹理。
COLOR 切换成 TEXCOORD 即可。这意味着需要更改一些内容:
-
现在,顶点结构的纹理坐标是 XMFLOAT2( x,y ),替代了颜色XMFLOAT4坐标( r , g , b , a )。 -
像素着色器需要从纹理中采样颜色,而不仅仅是使用提供的颜色,这意味着需要一个采样器。 -
请记住,纹理坐标和位置坐标是不同的(http://www.rastertek.com/dx11s2tut05.html)。最初我并不知道,这给我带来很多麻烦。
步骤五:渲染实际帧。
ID3D11Texture2D 采用的是 DXGI_FORMAT_R8G8B8A8_UNORM 格式,因此简单 memcpy 就可以了。我们需要复制的数组长度仅是帧中字节的计算: width_in_pixels * height_in_pixels * bytes_per_pixel 。
Map() 以获取一个指针,这样我们就能访问纹理的基础数据。
// decode and convert framestatic constexpr int BYTES_IN_RGBA_PIXEL = 4;D3D11_MAPPED_SUBRESOURCE ms;device_context->Map(m_texture.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &ms);memcpy(ms.pData, frame->data[0], frame->width * frame->height * BYTES_IN_RGBA_PIXEL);device_context->Unmap(m_texture.Get(), 0);// clear the render target view, draw the indices, present the swapchain
步骤六:渲染实际帧……但是,这次正确了。
AVBufferRef* hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA);AVHWDeviceContext* device_ctx = reinterpret_cast<AVHWDeviceContext*>(hw_device_ctx->data);AVD3D11VADeviceContext* d3d11va_device_ctx = reinterpret_cast<AVD3D11VADeviceContext*>(device_ctx->hwctx);// m_device is our ComPtr<ID3D11Device>d3d11va_device_ctx->device = m_device.Get();// codec_ctx is a pointer to our FFmpeg AVCodecContextcodec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);av_hwdevice_ctx_init(codec_ctx->hw_device_ctx);
ComPtr<ID3D11Texture2D> texture = (ID3D11Texture2D*)frame->data[0];
文档:https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering?redirectedfrom=MSDN
// DXGI_FORMAT_R8_UNORM for NV12 luminance channelD3D11_SHADER_RESOURCE_VIEW_DESC luminance_desc = CD3D11_SHADER_RESOURCE_VIEW_DESC(m_texture, D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8_UNORM);m_device->CreateShaderResourceView(m_texture, &luminance_desc, &m_luminance_shader_resource_view);// DXGI_FORMAT_R8G8_UNORM for NV12 chrominance channelD3D11_SHADER_RESOURCE_VIEW_DESC chrominance_desc = CD3D11_SHADER_RESOURCE_VIEW_DESC(texture, D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8G8_UNORM);m_device->CreateShaderResourceView(m_texture, &chrominance_desc, &m_chrominance_shader_resource_view);
当然,我们还需要确保允许我们的像素着色器访问这些色度和亮度通道。
PSSetShaderResources(0, 1, m_luminance_shader_resource_view.GetAddressOf());m_device_context->PSSetShaderResources(1, 1, m_chrominance_shader_resource_view.GetAddressOf());
ID3D11Texture2D 对象是 FFmeg 框架和着色器资源视图之间真正的桥梁。我们将新框架复制到其中,并从中提取着色器资源视图。这是一种共享资源,我们需要这样做。
ComPtr<IDXGIResource> dxgi_resource;m_texture->QueryInterface(__uuidof(IDXGIResource), reinterpret_cast<void**>(dxgi_resource.GetAddressOf()));dxgi_resource->GetSharedHandle(&m_shared_handle);m_device->OpenSharedResource(m_shared_handle, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(m_texture.GetAddressOf()));
memcpy 不再可行,因为它不能让我们轻松访问纹理的基础数据。我认为将接收到的帧复制到纹理的正确方法是使用内置的 DirectX 函数,例如 CopySubresourceRegion() 。
ComPtr<ID3D11Texture2D> new_texture = (ID3D11Texture2D*)frame->data[0];const int texture_index = frame->data[1];m_device_context->CopySubresourceRegion(m_texture.Get(), 0, 0, 0, 0,new_texture.Get(), texture_index, nullptr);
做完这些更改之后,我就可以放心地使用 av_hwframe_transfer_data() 和 sws_scale() 功能,在最后的最后,向每一个完全集成 FFmpeg-DirectX11 的视频播放器问好。
获取更多教程、Demo、技术帮助,请点击「阅读原文」访问声网开发者社区


