找回密码
 立即注册
查看: 297|回复: 1

UE4/5-使用AVEncoder编码H264视频流

[复制链接]
发表于 2022-5-26 10:02 | 显示全部楼层 |阅读模式
本文记录如何在UE4中,捕获渲染结果,使用 AVEncoder 编码成 H264
本文源码:
1. 准备

我使用 UE4.27.2 和 UE5.0.0 编写代码,旧版本没有测试。
首先要做好前期准备,需要依赖内置插件,并且在 Build.cs 中增加模块。
1.1 插件

依赖插件 HardwardEncoders。硬件编码器,如果不开启插件,4.27会在创建编码器的时候报错退出。


可选插件 Experimental WebSocket Networking Plugin。如果需要将编码结果通过 WebSocket 发送出来,需要启用此插件。此插件可以在UE4中使用 WebSocket Server。(UE4旧版本中比较容易使用 WS Client,详见 Ref2 Ref3。在近期的版本中也可以使用 WS Server,详见 Ref4 Ref5)
WS Server 的用法本文不再赘述,源码里有。


1.2 模块

在 Build.cs 中增加如下模块:
PublicDependencyModuleNames.AddRange(new string[]
{
    "Core", "CoreUObject", "Engine", "InputCore",
    "AVEncoder", // 新增:编码
    "RHI", // 新增:获取渲染结果
    "RenderCore", // 新增:获取渲染结果
    "Slate", // 新增:获取渲染结果
    "WebSocketNetworking" // 新增:使用 WS Server
});
2. 原理

提到视频推流,首先想到的是UE4已经实现,且可以无需任何编码直接拿来用的功能——像素流送 PixelStreaming。像素流送是使用 WebRTC 来推流的,那么在推流之前首先要进行编码,有了码流才能推。
在UE4.27.2中,像素流送内部使用的是 AVEncoder 来进行编码的。除了像素流送,还有一些其他类同样使用了 AVEncoder,例如 GameplayMedieEncoder 以及使用它的 HighlightRecorder。他们都是通过 AVEncoder::FVideoEncoderFactory 来创建编码器。
要完成捕获渲染结果,将结果进行编码,再对编码做点什么。需要进行以下过程:

  • 捕获:捕获渲染结果,回调 OnFrameBufferReady。将结果给到编码器处理。
  • 创建:创建编码器,Create。根据平台和图形引擎的不同,要生成不同的 Input。
  • 编码:将 OnFrameBufferReady 传来的图像进行处理(尺寸大小等),写入 InputFrame,编码 InputFrame。
  • 完成:编码完成后,回调 SetOnEncodedPacket。做点什么。
3. 实现

编码的业务代码,至少可以写在这些类:Actor、UObject、纯C++类。这里我写在 Actor 中,这个类叫 ActorVideoEncoder。
3.1 捕获

参考 Ref8。依赖模块 Renderer、RenderCore、RHI。
在 ActorVideoEncoder::BeginPlay() 中注册捕获渲染结果的回调:
FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddUObject(this, &AActorVideoEncoder::OnFrameBufferReady);
OnFrameBufferReady() 的内容是仅将渲染结果传给 VideoEncoder,让他使用 ProcessVideoFrame() 来处理,这个方法是我们自己写的,具体内容见下文。
void AActorVideoEncoder::OnFrameBufferReady(SWindow& SlateWindow, const FTexture2DRHIRef& FrameBuffer)
{
    if (VideoEncoder)
    {
        ProcessVideoFrame(FrameBuffer);
    }
}
3.2 创建

其实就是用工厂方法来创建 AVEncoder::FVideoEncoderFactory::Get().Create()。但是,有坑!
在UE4.27.2,在主线程中直接创建,程序会卡死(不是崩溃也没有报错,就是卡死。使用 GameplayerMediaEncoder 也是一样卡死,原因没找到(雾))。使用 FRunnable 创建,不会卡死,但后续图像 Resize 的方法 CopyTexture() 必须要在渲染线程才能工作,渲染结果的捕获也需要在渲染线程工作。
而UE5.0.0显然是解决了这一bug,在主线程中创建编码器不会卡死,非常流畅。
所以如果是在UE5使用视频编码器,则直接创建。在UE4中使用,请在异步方法中创建!
BeginPlay() 异步调用创建编码器的方法:
AsyncTask(ENamedThreads::AnyHiPriThreadHiPriTask, [this](){CreateEncoder(););
CreateEncoder(),设定视频的宽高比特率等配置 -> 根据RHI创建Input -> 使用配置和Input创建编码器,并注册编码完成后的回调。
void AActorVideoEncoder::CreateEncoder()
{
    if (!VideoEncoder) // need create video encoder.
    {
        UE_LOG(LogTemp, Warning, TEXT("%s start"), *FString(__FUNCTION__));
        StartTime = FTimespan::FromSeconds(FPlatformTime::Seconds());

        // Create VideoEncoder Config
        // TODO: can set?
        VideoConfig.Width = 1280;
        VideoConfig.Height = 720;
        VideoConfig.TargetBitrate = 5000000;
        VideoConfig.MaxBitrate = 20000000;
        VideoConfig.MaxFramerate = 60;
        // VideoConfig.H264Profile = AVEncoder::FVideoEncoder::H264Profile::BASELINE; // Default profile is ok.

        // Create VideoEncoderInput (Windows Only)
        if (GDynamicRHI)
        {
            FString RHIName = GDynamicRHI->GetName();
            UE_LOG(LogTemp, Warning, TEXT("RHIName: %s"), *RHIName);
            if (RHIName == TEXT("D3D11")) // UE4 Default
            {
                VideoEncoderInput = AVEncoder::FVideoEncoderInput::CreateForD3D11(
                    GDynamicRHI->RHIGetNativeDevice(), VideoConfig.Width, VideoConfig.Height, true, IsRHIDeviceAMD());
            }
            else if (RHIName == TEXT("D3D12")) // UE5 Default
            {
                VideoEncoderInput = AVEncoder::FVideoEncoderInput::CreateForD3D12(
                    GDynamicRHI->RHIGetNativeDevice(), VideoConfig.Width, VideoConfig.Height, true, IsRHIDeviceNVIDIA());
            }
        }

        // Create VideoEncoder
        const TArray<AVEncoder::FVideoEncoderInfo>& Available = AVEncoder::FVideoEncoderFactory::Get().GetAvailable();
        VideoEncoder = AVEncoder::FVideoEncoderFactory::Get().Create(Available[0].ID, VideoEncoderInput, VideoConfig);

        // Callback: OnEncodedPacket
        if (VideoEncoder)
        {
            UE_LOG(LogTemp, Warning, TEXT("%s success"), *FString(__FUNCTION__));

            // OnEncodedVideoFrame
            VideoEncoder->SetOnEncodedPacket(
            [this](uint32 LayerIndex, const AVEncoder::FVideoEncoderInputFrame* Frame, const AVEncoder::FCodecPacket& Packet)
            {
                OnEncodedVideoFrame(LayerIndex, Frame, Packet);
            });
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("%s fail"), *FString(__FUNCTION__));
        }
    }
}
3.3 编码

编码的过程其实是调用编码器的编码方法来做的 VideoEncoder->Encode(InputFrame, EncodeOptions)。我们要做的,其实是准备好编码的 InputFrame,也就是每帧的输入。
具体过程是:拿到捕获的渲染结果 -> 调用 ObtainInputFrame() 获取 InputFrame -> 调用 CopyTexture() 将渲染结果适配编码配置的尺寸,并存入 InputFrame -> 调用编码器进行编码。
void AActorVideoEncoder::ProcessVideoFrame(const FTexture2DRHIRef& FrameBuffer)
{
    FTimespan Now = GetMediaTimestamp();

    AVEncoder::FVideoEncoderInputFrame* InputFrame = ObtainInputFrame();
    const int32 FrameId = InputFrame->GetFrameID();
    InputFrame->SetTimestampUs(Now.GetTicks());
    UE_LOG(LogTemp, Log, TEXT("%s FrameID:%d"), *FString(__FUNCTION__), FrameId);

    CopyTexture(FrameBuffer, InputFrameTextureMap[InputFrame]);
    // InputFrameTextureMap[InputFrame] = FrameBuffer;

    // Encode
    AVEncoder::FVideoEncoder::FEncodeOptions EncodeOptions;
    VideoEncoder->Encode(InputFrame, EncodeOptions);
}
这里有两个方法是直接套用 GameplayMediaEncoder(或 PixelStreamingVideoEncoder)的实现,他们是 ObtainInputFrame() 和 CopyTexture()。内容请参考引擎源码,这里不再赘述。
3.4 完成

编码完成后,会调用创建编码器时注册好的回调 OnEncodedVideoFrame()。
这里我是通过 WebSocket,将编码二进制结果直接发出去。可以找到一些Web播放器,能接收WS发来的原始码流,直接看结果。
void AActorVideoEncoder::OnEncodedVideoFrame(uint32 LayerIndex, const AVEncoder::FVideoEncoderInputFrame* Frame, const AVEncoder::FCodecPacket& Packet)
{
    UE_LOG(LogTemp, Log, TEXT("%s FrameID:%d"), *FString(__FUNCTION__), Frame->GetFrameID());

    // TODO: Do Something! ASS U CAN.
    // For Send Msg
    WebSocketServer->SendBytes(TArray<uint8>(Packet.Data, Packet.DataSize));

    Frame->Release(); // important! memory overflow
}
这一步有两个需要注意的:

  • 编码结果是什么类型:编码结果 Packet.Data 是 uint8*,它可以通过 TArray() 转成字节数组。(此处有坑,不要将字节转成FString再发送,UE4会将FF转成"255")
  • 编码完成的回调中,一定要记得释放用过的 InputFrame(Frame->Release())。否则会内存泄漏,等着瞬间爆炸吧!
4. 讨论

最终的H264编码结果,是啥样的呢?参考 Ref9


UE4编码后得到的结果是 Annexb 的哪部分呢?我认为是最终的 H264 Frame,封装好的。(但像素流送中,为了适配 WebRTC 协议,做了额外的处理。PixelStreamingVideoEncoder.cpp 279行 CreateH264FragmentHeader())
以上每个部分的大致作用是:

  • SODB:原始编码数据
  • RBSP:SODB + 结尾比特来字节对齐
  • EBSP:RBSP + 仿校验字节
  • NALU:NALU-header + EBSP。在 EBSP 前面加上了一个头,来区分是什么帧(P帧、I帧等)以及重要级别。
  • H264 Frame:Start-code + NALU。在 NALU 前面再加上起始码,例如 0x00000001 或者 0x000001
把UE4编码结果存下来,用二进制方式打开,可以看到第一帧头部是4个字节的 Start-code,即 0x00000001。


999. Ref


  • 本文源码:https://github.com/tiax615/UE427_MyVideoEncoder
  • 天剑行风-UE4_使用WebSocket和Json(上):https://zhuanlan.zhihu.com/p/314440982
  • 天剑行风-UE4_使用WebSocket和Json(下):https://zhuanlan.zhihu.com/p/314507897
  • 懵懵爸爸-unreal ue4 虚幻 websocket Server websocket服务 插件使用及下载 非官方自己写的:https://blog.csdn.net/ljason1993/article/details/123031678
  • liason1993-WebSocketServer-unreal:https://github.com/ljason1993/WebSocketServer-unreal
  • Using AVEncoder interface to encode output frame to h264 stream in runtime:https://forums.unrealengine.com/t/using-avencoder-interface-to-encode-output-frame-to-h264-stream-in-runtime/158518
  • How to encode scene capture textures into h264 video?:https://forums.unrealengine.com/t/how-to-encode-scene-capture-textures-into-h264-video/482564
  • HW140701-UnrealEngine4 - 获取UE4最后的渲染缓存数据BackBuffer:https://blog.csdn.net/hw140701/article/details/109994535
  • luke-skyworker-H264帧格式解析:https://blog.csdn.net/zhaoyun_zzz/article/details/87302600

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-5-26 10:04 | 显示全部楼层
控制台 NVENC.KeyframeInterval 调整 GOP。默认300有点大
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-10 06:37 , Processed in 0.158176 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表