在做服务外包比赛的项目,有这么一个需求,如题,需要虚幻引擎录制并串流,而且这个过程不是在视口上完成的(因为视口给用户显示界面或者压根没有视口)
我并没有找到什么合适的插件,所以呀,写咯~
他山之石
这些天我一直在研究虚幻的C++,但我的水平也是不足以让我写一个调用FFMPEG的程序的,这里当然要找点开源的东西啦。
UE5的一个录屏插件,只能能保存文件
UE4的一个录屏插件,能推流RTMP,提供了详细的代码说明
我们就把前者的依赖和依赖关系拼上后者的代码逻辑,再缝缝补补吧!
我本以为UE4的东西到了UE5会有很多问题,但是实际上只有两个地方有问题,FTicker 重命名为了FTSTicker ,GetAudioDevice的返回值从FAudioDevice*变成了FAudioDeviceHandle ,就这样,编译成功。
就这样,在得到了很多漂亮的错误报告之后,编辑器能启动了。
UE4的代码直接拿到UE5真的可以跑得起来吗?
事实证明,编译能通过,但不一定能跑。
在从GPU内存中读取窗口中渲染的画面时,看似人畜无害的LockTexture2D 函数却抛了错误,这个错误出在DirectX的Dll里面,去Github仓库,不仅我在往UE5移植时出现了这个问题,其他人也遇到了。
解决方法嘛——没办法,退而求其次,去项目设置里把RHI从DirectX12改成DirectX11。
如此一来,移植完成,能跑起来,能录制。
离屏
捕获图像的时间
里面有这样一些代码:
FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddUObject(this, &UFFmpegEncoder::OnBackBufferReady_RenderThread);
/*...*/
void UFFmpegEncoder::OnBackBufferReady_RenderThread(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
{
if (gameWindow == &SlateWindow)
{
if (ticktime >= Video_Tick_Time)
{
GameTexture = BackBuffer;
ticktime -= Video_Tick_Time;
GetScreenVideoData();
}
}
}第一行注册了一个事件,当窗口渲染完成的时候触发。
离屏了的话,窗口有没有都不知道了,这个事件肯定不会触发了,要换一个,用什么事件比较合适呢?
我选择把这个事情推迟,公开给蓝图,让蓝图去找时机运行。
数据源
离屏,原本以窗口作为数据源肯定就不行啦,原本是在OnBackBufferReady_RenderThread 函数触发时会携带一个FTexture2DRHIRef,然后从这个对象拷贝数据。
我需要搞一个虚拟的FTexture2DRHIRef,替代原来的窗口。
接下来用到了Unity、UE甚至Godot都有的,叫做RenderTarget的东西,渲染目标!
UTextureRenderTarget2D 是能够在蓝图里面创建和使用的最外层包装,可以通过它拿到FRenderTarget
FRenderTarget也是一个包装,可以通过它拿到FTexture2DRHIRef
FTexture2DRHIRef 是对底层纹理对象的引用,可以快速拷贝数据
事实上,除了在层3复制数据,层2有个函数叫做ReadPixels 也可以复制数据,但是据说效率比较低,对帧率影响较大。
这样,就可以将UTextureRenderTarget2D作为离屏渲染源,完成离屏渲染啦。
在我查API时,意外的发现FViewport继承于FRenderTarget ,通过UGameViewportClient 可以获得FViewport ,因此,很意外的支持了从屏幕视口上获取数据。
错误处理
我尝试输入了一个无效的rtmp地址,虚幻在卡了一会儿之后崩了。
一个路径或者网路地址是否可写,只有等这个参数传递到FFMpeg层上才知道,但是插件原本对错误的处理不是很友好,首先是,遇到问题时,直接check(false) ,这会直接崩掉虚幻引擎。
为了解决这个问题,我把所有可能出现错误的函数都改成了返回bool,同时出错时返回false。
但是这样并没有解决问题,还是会崩,错误出在一个叫avformat_write_header 的函数内部,理论上这应该是第一次尝试往目标里写数据,失败了,所以就崩了。
于是抱着试一试的心态,聪明的我给这一句话加上了try catch(这似乎是我第一次在C++里面用错误捕获),不出所料,捕捉不到,可能之后throw出来的能捕获。
一顿问AI无果后,我发现,在此之前,avio_open 这个函数的返回值没判断。
破案啦~
于是,就这样,屏幕录制做好了。
意外收获
我意外的发现,UE的C++集成C++库,似乎是一个很简单的事情。
C++并没有我想象的这么难。
于是,顺带往里集成了个rtmp服务器。别怪我写程序臃肿,直接往里装开源库太香了
动态链接库和静态链接库我很久很久以前学过怎么用,只要编译成静态链接库,C++模块就可以想怎么加就怎么加(~ ̄▽ ̄)~