大数跨境
0
0

社区分享 | 使用 FFmpeg 和 DirectX 11 流式传输视频

社区分享 | 使用 FFmpeg 和 DirectX 11 流式传输视频 RTE开发者社区
2021-04-29
1
导读:一份 FFmpeg 的开发经验分享

原文作者 Ori Gold。本文已获得作者授权,发布于声网开发者社区。

几个月前,我当时的工作任务是开发定制的低延迟视频播放器。在此之前,我仅简单用过 FFmpeg 并且没用过 DirectX 11,但我想应该并不难。FFmpeg 非常受欢迎,而 DirectX 11 已经存在了一段时间,这(目前)还不像能够实现我需要创建的那种清晰的3D图形或其他任何东西。
会有大量的示例说明如何做一些例如解码和渲染视频这类基本的事情吗?
没有。所以请看本文。
下一位没有 FFmpeg 或 DirectX 11 经验,却需要这样做的可怜人,就不必为了要将一些视频发到屏幕上而抓狂。
在我们获得诀窍前,只需做一些最基本的准备工作。
  • 提供 非常 简化的代码示例。我省去了返回代码检查、错误处理等步骤。我的观点是,代码样本就是: 样本。 (我本提供更多充实的示例,但你知道的,这涉及到知识产权及所有其他内容。)
  • 我将不介绍硬件加速视频解码/渲染的原理,因为这超出了本文的范围。此外,还有很多资源能解释得比我好。
  • FFmpeg 支持几乎所有协议和编码格式。RTSP 和 UDP 都可以使用这些样本,以及使用 H.264 和 H.265 编码的视频。我敢肯定,还有很多程序都可以对其进行使用。
  • 我创建的项目基于 CMake,并且不依赖 Visual Studio 的构建系统(因为我们也需要支持非 DX 渲染器),这使事情变得有点困难,这就是为什么我认为我会提到它。
事不宜迟,让我们开始吧!


步骤一:设置流源和视频解码器。

这几乎完全是 FFmpeg 的东西。只需设置格式上下文,编解码器上下文以及 FFmpeg 需要的所有其他结构即可。对于设置,我非常依赖一个示例(链接如下)以及另一个名为Moonlight的项目(链接如下)的源代码。
示例: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 context
AVBufferRef* 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
一旦设置完成,实际的解码就非常简单了。只需从流源中检索 AVPackets,然后使用编解码器将它们解码为 AVFrame。
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。

要创建位图(事实证明,是渲染为 DX11 交换链),我需要帧是RGBA格式。但是解码器以NV12格式吐出帧,所以我使用 FFmpeg 的 swscale(链接如下)将 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

暂时这样做还不错,但是作为一个长期解决方案,存在两个主要问题。
  1. 我在 AVFrame 中需要的是一个简单易懂的字节数组,使用 “d3d11va” 作为硬件设备名称为我们提供了简单的字节数组以外的东西,所以我将硬件设备名称更改为 “dxva2” 。现在, frame->data 仅仅是 uint8_t* 形式上的位图。目前可以使用它,但是作为一种长期解决方案,  使用 “d3d11va” 是基本上错了的要点。
  2. 为了调用 sws_scale() 并将帧转换为 RGBA 格式,我们需要将帧从 GPU 移到 CPU。目前还可以这样使用,但将来绝对是我们希望删除的内容。
虽然不是完美的,但至少我们现在已经解码了帧,可以将它们放到位图上并且自己能看到。
部分 FFmpeg 就是这样(目前),在 DirectX 11 中进行渲染。


步骤三:设置 DirectX 11 呈现。

如果你还不知道,这是给你的警告:DX11 与 DX9 完全不同,真的完全不同。
在多次尝试显示绿色或黑色屏幕以外的内容失败之后,我复制并粘贴了一个示例(链接如下),以便从工作代码开始。在那之后,将三角形变成正方形(方法如链接)的任务变得异常复杂。(我选择了4个顶点,6个索引的选项。)
示例:http://www.directxtutorial.com/Lesson.aspx?lessonid=11-4-5
三角形变正方形:https://stackoverflow.com/questions/20412807/directx-11-make-a-square
相比于编译运行时的着色器,我选择从编译 的编译 时间运行它们。有一秒,我以为我必须要有一个第三方库来执行此操作,但其实需要做的只是在 CMakeLists.txt 文件中的几行代码。查找 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)。最初我并不知道,这给我带来很多麻烦。
一旦能够渲染基本的静态JPEG图像,我就知道自己离成功不远了,剩下的就是将实际的位图从帧传输到共享纹理。


步骤五:渲染实际帧。

由于我们的帧仍然是 RGBA 格式的简单字节数组,而且 ID3D11Texture2D 采用的是 DXGI_FORMAT_R8G8B8A8_UNORM 格式,因此简单 memcpy 就可以了。我们需要复制的数组长度仅是帧中字节的计算: width_in_pixels * height_in_pixels * bytes_per_pixel 。
注意,我们还需要调用设备上下文的 Map() 以获取一个指针,这样我们就能访问纹理的基础数据。
// decode and convert frame
static 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
达到这一点,并能在屏幕上观看实况视频令人十分开心。说实话,我已经举起双手,并赞叹编码之神眷顾了我。
可惜。我的工作还远远没有结束。现在,该回头解决我在步骤2中遇到的两个问题了。


步骤六:渲染实际帧……但是,这次正确了。

从研究开始,我就知道向 FFmpeg 提供 “d3d11va” 硬件设备,DirectX 11 渲染器可以以轻松消化的方式输出 FFmpeg 。但是我怎样才能做到这一点呢?
我们需要正确地初始化 d3d11va 硬件设备的上下文,这 意味着 FFmpeg 解码器需要了解其正在使用的 D3D11 设备。
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);
看起来有很多设置,但是最终,我们要做的只是在解码器 AVCodecContext 中,将指针存储到渲染器的 ID3D11Device 。这就是使解码器将帧输出为 DX11 纹理的过程。
现在,当我们将解码后的帧发送到渲染器时,不需要将它们传输到 CPU,也不需要将它们转换为 RGBA,就可以简单地做到这一点:
ComPtr<ID3D11Texture2D> texture = (ID3D11Texture2D*)frame->data[0];
但是,我们离完成任务还差远了。
我们需要将像素格式转换移至 GPU。 开始时,我们的交换链无法渲染 NV12 帧,这意味着从 NV12 到 RGBA 的转换仍然必须发生在 某个地方 。现在,它将发生在 GPU 中,而不是发生在 CPU中——在像素着色器中具体来说。
这是合乎逻辑的;我们不能再对纹理中的某个位置进行采样了,因为我们的纹理不再包含在 RGBA 中。为了使我们的像素着色器为每个像素返回正确的 RGBA 值,需要从纹理的 YUV 值中进行 计算 。
这意味着我们需要升级像素着色器,以使用 NV12 并输出 RGBA。你可以自己派生这样的着色器 1(参考如下文档),也可以只使用已经编写的着色器 1。
文档:https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering?redirectedfrom=MSDN
添加另一个着色器资源视图。 尽管 RGBA 像素着色器将单个着色器资源视图作为输入,但 NV12 像素着色器实际上需要两个:色度和亮度。因此,我们需要将一个纹理拆分为两个着色器资源视图。(在此之前,我不明白为什么 DirectX 需要区分纹理和着色器资源视图,但我很高兴他们这么做。)
// DXGI_FORMAT_R8_UNORM for NV12 luminance channel
D3D11_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 channel
D3D11_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);

当然,我们还需要确保允许我们的像素着色器访问这些色度和亮度通道。

m_device_context->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、技术帮助,请点击「阅读原文」访问声网开发者社区

【声明】内容源于网络
0
0
RTE开发者社区
RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
内容 1122
粉丝 0
RTE开发者社区 RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
总阅读653
粉丝0
内容1.1k