在做服务外包比赛的项目,有这么一个需求,如题,需要虚幻引擎录制并串流,而且这个过程不是在视口上完成的(因为视口给用户显示界面或者压根没有视口)

我并没有找到什么合适的插件,所以呀,写咯~

他山之石

这些天我一直在研究虚幻的C++,但我的水平也是不足以让我写一个调用FFMPEG的程序的,这里当然要找点开源的东西啦。

我们就把前者的依赖和依赖关系拼上后者的代码逻辑,再缝缝补补吧!

我本以为UE4的东西到了UE5会有很多问题,但是实际上只有两个地方有问题,FTicker 重命名为了FTSTickerGetAudioDevice的返回值从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的东西,渲染目标!

  1. UTextureRenderTarget2D 是能够在蓝图里面创建和使用的最外层包装,可以通过它拿到FRenderTarget

  2. FRenderTarget也是一个包装,可以通过它拿到FTexture2DRHIRef

  3. 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++模块就可以想怎么加就怎么加(~ ̄▽ ̄)~

我能想到的,最大的成功就是无愧于自己的心。