|
本文记录如何在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(&#34;%s success&#34;), *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(&#34;%s fail&#34;), *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(&#34;%s FrameID:%d&#34;), *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(&#34;%s FrameID:%d&#34;), *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转成&#34;255&#34;)
- 编码完成的回调中,一定要记得释放用过的 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
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|