☰
UE5实时录屏插件:基于FFmpeg的架构设计与实现
2026/9/28 6:31:06 网站建设 项目流程

简介:针对UE5缺少内置录屏能力的痛点,该资源包提供一套基于FFmpeg的实时录屏插件及完整实现详解,适合具备C++基础、希望在Windows/Linux平台为项目加入录制功能的UE5开发者。包内包含346个头文件、23个C/CPP源码、26个lib/29个dll动态与静态库,以及pdb调试文件和docx使用说明,覆盖从FFmpeg库编译、接口封装到SceneCaptureComponent2D捕获屏幕数据的核心链路,同时也附带了FFmpeg文档手册与ffpreset预设文件,便于排查格式参数问题。资源包共570个文件,压缩后164.02MB,已吸引3005人学习。凭借可编译的工程源码与分平台库文件,使用者无需自行编译整套FFmpeg,能直接对照说明完成插件集成,并在理解线程同步、帧率控制和错误处理的基础上按需定制录制策略。 做UE5项目做久了,你会发现一套可用的实时录屏方案有多重要。不管是做测试视频、跑AI训练的图集采集,还是给策划演示玩法效果,都会遇到「我需要立刻把当前画面录下来」的场景。UE5自带的Movie Render Queue走的是离线渲染管线,出片质量没问题,但它是按帧烘焙的,实时性完全谈不上。编辑器下想快速录一段运行画面,要么用OBS抓窗口,要么就得自己想办法。

我最早也是用OBS凑合,但项目里要做自动化的批量录屏——比如无人值守的压测、一键录制回放、给算法组出训练素材——这种场景下OBS的手动操作就成了瓶颈。后来干脆在UE5里写了一个基于FFmpeg的实时录屏插件,把画面采集、编码、封装、落盘全链路打通。这篇文章就把这套方案的完整思路和实操细节整理出来,包括插件架构怎么设计、FFmpeg库怎么集成、踩过哪些坑,希望对正在做同样事情的朋友有帮助。

1. 为什么做这个插件:UE5录屏的痛点和选型思考

先说说为什么不用内置方案。UE5的Movie Render Queue确实强大,支持TAA、运动模糊、高采样率,渲染出来的画面是电影级的。但它的定位是离线渲染,每一帧都要重新走一遍渲染管线的稳定性优化,耗时和资源占用都很高。在编辑器里用Wireframe模式或者跑到Game Preview时想快速录一段,它并不合适。还一个问题,Movie Render Queue的输出是序列帧或者视频文件,它没法做到「按需启动编码、随时停止、实时推送流出去」,这对于很多自动化需求来说不够灵活。

内置的SceneCapture2D加RenderTarget2D倒是可以做实时采集,但官方并没有提供配套的编码推流方案。你需要自己把RT的像素数据取出来,然后交给某个编码器处理。这时候选型就出现了:自己裸写H.264编码器?不现实;调Windows Media Foundation?它能做,但API比较繁琐,而且跨平台能力基本没有;用FFmpeg?几乎是把「采集-编码-封装-推流」全流程一站式解决了,而且Linux、Windows、Mac都能编译过去。

1.1 内置方案解决不了实时问题

实际项目中我对录屏的需求很具体:第一,录制期间不能卡游戏主线程,录屏带来的性能损失必须可控;第二,编码要能异步进行,画面采集和写盘不能互相拖累;第三,最好能支持直接推RTMP流,因为团队有时候需要把游戏画面直播到内部服务器做远程评审。这三条,内置方案做到其中一条都要折腾很久,与其东拼西凑,不如直接在引擎层做一个封装好的插件。

性能这块是最关键的。录屏本质上是一个高频的数据搬运过程——每一帧都要把渲染结果从GPU端拷贝到CPU端,然后交给编码器处理。如果不做异步编码,录屏一开,游戏帧率直接掉一半,这在很多自动化测试场景下是不能接受的。所以插件架构从一开始就必须按「采集线程和编码线程分离」来设计,谁都不能阻塞谁。

1.2 为什么偏偏是FFmpeg

FFmpeg在这个领域基本就是事实标准。它提供的是完整的音视频处理库,不只是单个编码器。你想用H.264、H.265、VP9、AV1都行;你想封装成MP4、MKV、FLV也行;你想推RTMP、HLS、WebRTC也有对应的方案。换句话说,用FFmpeg做底层的录屏插件,相当于把「未来可能的输出需求」也一起覆盖了,以后想加一个推流功能或者多路录制,不需要换技术栈。

还有一个关键点:FFmpeg是纯C库,UE5的插件体系对C库的集成非常友好,只要把头文件和库文件路径配置好,用C++调用起来没有任何障碍。而且FFmpeg本身有大量现成的命令行工具,调试问题的时候可以直接用ffmpeg.exe跑一条命令,验证是采集端的问题还是编码端的问题,非常方便。

2. 插件整体架构与关键设计思路

录屏插件的架构决定了后续的稳定性和扩展性。我一开始的版本是把所有逻辑塞在一个类里,结果代码越来越难维护。后来拆成四个模块:采集、编码、封装、控制。每个模块只干一件事,模块之间通过明确的接口通信。

采集模块负责从UE5的RenderTarget读取像素数据;编码模块封装FFmpeg的avcodec相关操作,把原始RGB帧转成H.264帧;封装模块负责用avformat写MP4文件或者推RTMP流;控制模块对外暴露蓝图接口,让策划或者测试可以一键开始、停止、截取单帧。

2.1 模块设计与采集管线

先看采集。UE5里最常用的是SceneCaptureComponent2D,它可以捕获当前场景的画面并写入一个RenderTarget2D。这一步相当于给游戏画面拍了一张照片,存到了一块GPU显存里。要把它送给FFmpeg,得先把它读回CPU内存,也就是调用RenderTarget->ReadPixels()。

这里有个性能陷阱:ReadPixels是一个同步操作,它会堵塞渲染线程。如果你在游戏主线程直接调用,几千帧下来性能损耗会非常明显。我后来改成在渲染线程结束后的某个时机去读取,然后通过帧缓冲池(Frame Pool)把数据送到编码线程,这样就解耦了采集和编码的频率。说白了就是,游戏跑60帧的时候,我每帧都采,但编码线程可能只能处理30帧,那就让中间缓冲池去吸收这个速度差,而不是让游戏卡着等编码。

// 伪代码示意 void ARecorderActor::CaptureFrame() { // 从RenderTarget读取像素,注意分配足够大的缓冲 TArray<FColor> PixelData; RenderTarget->ReadPixels(PixelData); // 把像素数据扔进线程安全的队列,编码线程去取 FrameQueue.Enqueue(FEncodedFrame(PixelData, Timestamp)); }

2.2 帧数据格式与PTS同步

采集到的像素数据是什么格式?ReadPixels默认返回FColor,也就是RGBA8格式,每个像素4个字节。FFmpeg通常喜欢YUV420P或者RGB24之类的格式,需要做一个像素格式转换。这一步可以用FFmpeg的sws_scale完成,它专门负责图像缩放和像素格式转换,效率很高。

PTS(Presentation Timestamp)是另一个容易翻车的地方。H.264编码的单位是帧间距,比如你设置time_base为1/600,每帧PTS增加10,那就代表每秒60帧。很多人录出来的视频快进或者慢放,十有八九是PTS没算对。我后来在采集的时候直接用引擎的App::GetDeltaSeconds()累加一个frameIndex,然后pts = frameIndex * FPS,这样只要帧率稳定,时间戳就不会乱。

另外需要注意,ReadPixels拿到的像素数据是上到下排列的,而FFmpeg很多时候需要你在av_frame_set_linesize里告诉它每一行的字节数。UE5的FColor一个像素4字节,行大小就是图片宽度乘以4。如果这个值设置错了,画面会斜着撕裂或者颜色错乱。

2.3 异步编码与线程模型

编码线程要持续从队列里取数据,初始化AVCodecContext,然后把AVFrame送入avcodec_send_frame,再用avcodec_receive_packet取出编码后的数据包。这里最好用两个线程:一个负责把像素数据拷贝到AVFrame的缓冲中,另一个负责调用编码器。如果只有一个线程,每次待编码的数据堆积时,整个管线都会卡住。

线程池大小、队列长度这些参数需要结合实际项目调整。我的经验是,队列长度尽量不要超过5到10帧。假如游戏卡了一下,队列里的帧就会越来越多,如果你继续把所有帧都编码,出来的视频就会变长,和实际时间对不上。更合理的策略是:当队列超过阈值时,直接丢弃最旧的一帧,保证视频时长和录制时长尽可能一致。录屏嘛,最重要的是告诉别人「当时发生了什么」,丢几帧画面有时候比慢放更实用。

3. 核心功能落地:从采集到出片的完整实现

架构清晰之后,真正把代码跑起来还是有各种细节要处理。下面按顺序讲一下从工程配置到最终能录出MP4文件的完整步骤,每一步都给出我实际验证过的方案。

3.1 FFmpeg库的集成与链接

FFmpeg的库最好自己编译成共享库(DLL)或者静态库。UE5插件模式下,推荐用共享库,因为静态库在多个模块链接时容易产生符号冲突,而且FFmpeg依赖的三方库(如zlib、libx264)也会带来额外的链接负担。

Windows下最简单的办法是直接下载官方编译好的shared版本,里面会带bin目录下的DLL和lib目录下的导入库。把include目录和lib目录路径加到插件的Build.cs里就行。

// Recorder.Build.cs PublicDefinitions.Add("FFMPEG_SUPPORT"); PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ffmpeg", "include")); PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ffmpeg", "lib", "avcodec.lib")); PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ffmpeg", "lib", "avformat.lib")); PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ffmpeg", "lib", "avutil.lib")); PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ffmpeg", "lib", "swscale.lib")); // 注意把DLL拷到Binaries目录或者打包后的目标目录 RuntimeDependencies.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ffmpeg", "bin", "*.dll"));

如果你在Linux上开发,可以直接用系统包管理器装FFmpeg开发库(libavformat-dev、libavcodec-dev等),然后在Build.cs里填对应的系统路径。我的建议是Windows下用官方shared版本,Linux下用系统库,两者都省时省力。跨平台编译时,记得把PLATFORM_WINDOWS和PLATFORM_LINUX的条件判断加上,不然会链接失败。

一个容易踩的坑:FFmpeg 4.x和5.x的API有细微差别,最明显的是avcodec_open2和avcodec_parameters_from_context这类函数的调用顺序。最好锁定一个版本开发,不要混用。我的插件用的是FFmpeg 5.x分支,代码里也做了#if处理来兼容新旧API。

3.2 编码器初始化与参数选择

编码器选择上,H.264依然是目前兼容性最好的方案,软编推荐用libx264,性能足够且参数丰富。如果你想追求更高压缩率或者做4K录制,可以试试H.265/HEVC,但播放兼容性会差一些,尤其在Windows老版本上。如果你录制的画面里包含大量UI和细线条(比如CAD类工具),CRF值建议设置在18到23之间,太小的话文件体积会爆炸,太大又会糊掉。

下面是初始化AVCodecContext的一个核心片段,关键参数都标注了理由。

AVCodec* codec = avcodec_find_encoder_by_name("libx264"); if (!codec) { UE_LOG(LogTemp, Error, TEXT("libx264 not found")); return false; } AVCodecContext* ctx = avcodec_alloc_context3(codec); ctx->width = Width; ctx->height = Height; ctx->bit_rate = 4 * 1000 * 1000; // 4Mbps,可根据分辨率动态调整 ctx->time_base = (AVRational){1, FPS}; ctx->framerate = (AVRational){FPS, 1}; ctx->gop_size = FPS * 2; // 关键帧间隔2秒 ctx->max_b_frames = 2; // B帧能提升压缩率,但也增加延迟 ctx->pix_fmt = AV_PIX_FMT_YUV420P; // 输出像素格式 ctx->codec_type = AVMEDIA_TYPE_VIDEO; if (ctx->codec_id == AV_CODEC_ID_H264) { av_opt_set(ctx->priv_data, "preset", "fast", 0); av_opt_set(ctx->priv_data, "crf", "20", 0); // CRF模式,视觉无损 }

bit_rate和crf是两种不同的码率控制方式。前者是固定平均码率,后者是恒定质量模式,具体选哪个看你需要什么。如果录制后还要做二次剪辑,我建议用CRF;如果是要上传到内部视频平台做存档,限制码率更稳妥。实测下来,1080p画面crf设20,动态画面下码率大概在4-6Mbps之间,画质几乎无损。

3.3 每一帧如何送入编码器

从UE5那边拿到FColor数组之后,要写进AVFrame。这一步关键是把RGBA颜色顺序转换成FFmpeg要的YUV420P格式,可以用sws_scale。

SwsContext* swsCtx = sws_getContext( Width, Height, AV_PIX_FMT_RGBA, // 输入是UE5的RGBA Width, Height, AV_PIX_FMT_YUV420P, // 输出是编码器要的YUV420 SWS_FAST_BILINEAR, nullptr, nullptr, nullptr ); AVFrame* frame = av_frame_alloc(); frame->format = AV_PIX_FMT_YUV420P; frame->width = Width; frame->height = Height; frame->pts = FrameIndex++; // 用帧序号做PTS uint8_t* dstData[4] = { frame->data[0], frame->data[1], frame->data[2], nullptr }; int dstLinesize[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], 0 }; // 把FColor数组灌进去,这一步是RGBA -> YUV420P的转换 uint8_t* srcData[4] = { (uint8_t*)PixelData.GetData(), nullptr, nullptr, nullptr }; int srcLinesize[4] = { Width * 4, 0, 0, 0 }; sws_scale(swsCtx, srcData, srcLinesize, 0, Height, dstData, dstLinesize);

这里特别强调一下srcLinesize[0] = Width * 4,我记得第一次写的时候忘了乘以4,结果录出来的视频画面全部斜向撕裂,检查了很久才意识到是行字节数不对。其实在FFmpeg的文档里,这个行大小信息就是留给编码器搞清楚数据排列用的,填错就会乱套。

送入编码器是异步的,建议用单独线程循环处理。

int ret = avcodec_send_frame(codecCtx, frame); if (ret < 0) { UE_LOG(LogTemp, Warning, TEXT("avcodec_send_frame failed: %d"), ret); return; } while (true) { AVPacket packet; av_init_packet(&packet); ret = avcodec_receive_packet(codecCtx, &packet); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; // 拿到编码后的packet,写入文件或推流 WritePacket(&packet); av_packet_unref(&packet); }

这个while循环必须这样写,因为编码器内部有缓冲区,可能一帧输入产生多个packet,也可能一次要输入好几帧才输出一个packet。不能只调用一次avcodec_receive_packet就以为完了。

3.4 音视频同步的处理

如果你只是录画面,音视频同步可以绕开。但实际项目里没人想录哑巴视频。我最早尝试用UE5的USoundWaveProcedural采集音频,再通过FFmpeg的音频编码器合并进MP4,折腾了很久。

音频采集的方案是这样的:在插件里创建一个USoundSubmix,用FSoundSubmixCaptureListener监听混音数据,拿到PCM数据,然后送到FFmpeg的AAC编码器。AAC编码一次要接收1024个采样点,所以需要先攒够一帧再送,音频线程里需要做一个缓冲区累积。

音频同步时最重要的一点是:音视频的PTS起点要对齐。视频帧的PTS从0开始,音频帧的PTS也从0开始,但在封装进MP4的时候,需要给音频和视频分别设置不同的time_base,不然可能出现微小的偏移。我用的是视频time_base=1/FPS,音频time_base=1/44100或1/48000,封装时FFmpeg会自动转换。

如果你遇到音画不同步超过200ms的情况,先检查两个地方:第一,音频帧累积缓冲是否因为线程调度导致PTS跳跃;第二,视频采集帧率是否稳定,如果游戏经常掉帧,视频帧的PTS会不均匀,这时就需要把音频按实际帧率对齐,而不能简单广播成固定帧率。

3.5 推流与多路输出

当插件跑通落盘后,下一步很自然就想到推流。FFmpeg的AVOutputFormat支持rtmp、flv、hls等多种格式,只要把初始化时用的输出URL从test.mp4换成rtmp://your-server/live/stream_key,其余逻辑基本可以复用。

不过在推流模式下有几个差异需要注意:推流不能像落盘那样等到停止才写文件夹头和尾部索引,因为RTMP流是实时的,必须在启动时立刻发送flv头,结束时就地发送结尾标记。另外,推流模式一般建议把max_b_frames设成0或者1,减少解码延迟,因为RTMP播放器大都不太喜欢B帧带来的乱序,虽然播放器理论上能处理,但兼容性会打折扣。我实际测试过,B帧太多加上直播延迟高,播放端画面容易出现卡顿和花屏,为了稳定还是牺牲一点压缩率比较好。

多路输出的实现也不难:把编码后的AVPacket同时发送给多个AVFormatContext实例,一个写MP4,一个推RTMP,这样就能同时录像和直播。你甚至可以加一个分支直接调用avcodec_decode做实时预览,但要注意CPU开销。

4. 常见问题与排查经验

这个插件从原型到稳定运行,过程中踩了不少坑。我把遇到的高频问题整理成一个表格,方便大家直接对照排查。

4.1 moviewriter ffmpeg unavailable

搜索FFmpeg相关问题时,很多人会遇到UE5内置的MovieWriteQueue提示ffmpeg unavailable,这其实跟我上面说的插件是一回事也不是一回事。UE5自带的Movie Render Queue确实支持配置使用外部FFmpeg,但它需要在项目设置里指定FFmpeg可执行文件路径。如果提示unavailable,多半是路径没设置对,或者FFmpeg版本不兼容。

我这边自研的插件不会走这条路,但如果你遇到这个报错,建议先直接在命令行测试一下ffmpeg -version能不能跑,如果系统里根本没有FFmpeg,或者路径里有中文/空格导致解析失败,那就先把基础环境搞定。

4.2 录出来卡顿、丢帧

卡顿的排查思路要分三层:第一层是采集层,ReadPixels是否阻塞了游戏线程;第二层是传输层,队列是否满到丢弃帧;第三层是编码层,libx264的preset是否太慢。

我调成这样才解决:把编码preset从medium改成fast,同时让采集线程每帧都采,但只有在队列长度小于3时才真正入队。这样即使游戏帧率不稳定,视频帧率也能保持在目标值附近,不会有长时间停滞。另外,width和height设置成偶数很重要,因为H.264的宏块大小是16x16,如果宽高不是偶数,编码器会自动填充或者报错,画面边缘会有一条绿边。

4.3 音画不同步

音画不同步通常有两个来源:一是音频缓冲导致的延迟,二是视频PTS不均匀。音频缓冲的坑在于,每次从UE5拿到PCM数据后,不要立刻累计进FFmpeg的音频帧,因为这个数据本身有延迟。你可以在拿到第一帧音频数据时记录一个audioStartTime,然后在每个音频帧上减去这个偏移,保证从录制开始算起。

视频端如果PTS不连续,播放器会感知为跳帧或者快进。修这个问题的办法是:在编码前把帧索引按固定步长累加,放弃真实帧间隔,而是强制给每个入队帧分一个稳定的时间戳。这样即使游戏偶发掉帧,视频整体时长和你录制的真实时长偏差会很有限,看起来也自然。

4.4 崩溃与打包问题

FFmpeg库是纯C代码,和UE5的模块系统集成时,最常见的问题是运行时DLL找不到。开发时没问题,但打包后会挂,因为这些DLL不在EXE目录下。解决方案是在Builder.cs里用RuntimeDependencies显式声明DLL,或者干脆把FFmpeg编译成静态库,直接链接进插件模块。

另一个常见崩溃是像素缓冲释放时踩内存。AVFrame的数据要自己分配和对齐,不能直接指向UE5的TArray内存,因为FFmpeg内部可能要求16字节对齐。用av_frame_get_buffer分配内存,然后memcpy像素数据,这样最稳妥。

5. 实测效果与最终经验总结

这个插件上线到现在跑了几个月,我们主要用它做三件事:无人值守压测录制、玩法回放采集、自动化素材生成。1080p/60帧下,CPU占用大概在10%-15%之间,GPU基本无额外消耗(因为用的软编),游戏主线程性能损耗可以忽略不计。实测下来,录2小时的MP4文件,大小约3-4GB,画质肉眼很难看出和原始画面的区别。

如果要扩展方向,我目前想的是支持HDR视频录制。UE5本身支持HDR渲染,但FFmpeg的编码器要支持HDR需要设置颜色矩阵和传递函数,代码里需要额外处理色彩空间转换参数。另外还想接入NDI或者SDI硬件采集卡,做低延迟的直播推流,不过这个就依赖硬件兼容性了,不是纯软件能解决的问题。

最后再分享一个很有用的技巧:在开发调试时,不要直接用UE5跑整个插件系统。可以先用ffmpeg.exe -f gdigrab -i desktop output.mp4录一下系统桌面视频,对照这个录出来是否流畅,来判断是不是UE5侧采集的问题。这一步能帮你省下大量定位时间。FFmpeg这种工具,一旦用顺手了,你会想在UE5里处处用上它。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询