ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

UE5实时录屏:基于FFmpeg的RenderTarget采集与编码方案

2026/9/2 4:31:49 拓冰建站 浏览量
UE5实时录屏:基于FFmpeg的RenderTarget采集与编码方案 简介UE5实时录屏插件FFmpeg是一套基于FFmpeg库实现的UE5实时屏幕录制解决方案面向需要在Windows/Linux平台为项目增加录制能力的游戏开发者解决UE5没有内置录屏功能的问题。资源包为zip压缩包共570个文件约164.02MB包含346个h头文件、29个dll动态库、26个lib导入库、多个c源文件及14个pc配置文件与14个html文档其中头文件与库文件对应不同编译环境文档提供配置参考涵盖FFmpeg编译产物、接口封装与使用说明等。已有3005人学习下载得到开发者广泛关注。资源内置EasyFFMPEG-main封装源码、使用说明.docx以及一整套FFmpeg开发库能够帮助读者从库的引入、接口封装到逐帧编码调用完整落地理解avformat/avcodec等API的调用方式并结合SceneCaptureComponent2D实现逐帧数据转码、线程同步与性能优化等关键环节。适合希望深入掌握UE5与FFmpeg集成技巧、具备一定C基础的中高级开发者作为参考模板。1. 为什么UE5实时录屏要选FFmpeg1.1 引擎自带录屏方案的真实痛点做UE5项目时录屏需求从来不是能录就行这么简单。我最早偷懒直接用引擎自带的Movie Render Queue结果踩了一脚泥它走的是离线渲染管线录制一帧可能要几十毫秒甚至更久完全不适合边跑边录的场景。你要录一个运营Demo、录一个自动化测试结果、或者在游戏里加一个一键分享录像功能Movie Render Queue的延迟渲染会直接拖垮帧率而且中途切换关卡、玩家进出场都会让录像端掉链子。另一个常见思路是用Windows系统级录屏比如Game Bar、Windows Graphics CaptureWGC。Game Bar确实零成本但它不受引擎控制开始、停止、文件命名全部依赖用户手动操作没法嵌入到项目流程里WGC API能定制但它是COM组件风格工程结构繁琐而且抓取的是桌面/窗口画面不是UE5内部的RT视口多显示器、DWM合成、分辨率缩放等一堆边缘Case能把你磨到怀疑人生。这就说到了FFmpeg的价值它是跨平台的多媒体处理库封装了视频编码、复用、推流、滤镜在内的全套能力。把FFmpeg嵌进UE5直接截取引擎渲染出来的RTRender Target像素在内存里交给libx264编码成H.264再封装成MP4。整个过程完全在引擎内闭环渲染线程、游戏线程、编码线程互不干扰录制开始/停止时机、分辨率、码率、帧率全部由你决定。这正是做工具链、自动化测试、游戏内录像功能最需要的自由度。1.2 什么样的人适合这个方案我做了这几年UE5工具链开发见过太多人在录屏这里临时抱佛脚要么用外部软件录要么用引擎离线渲染录最后都无法和自动化流程打通。如果你属于下面任何一类FFmpeg方案值得认真投入做UE5编辑器工具链需要批量录屏做自动化截图/录像回归测试的。做数字孪生、看板、展览类项目需要长时间循环播放并实时记录画面的。做带录像回放功能的游戏或仿真应用想把这套能力集成进产品的。做像素流或者其他音视频服务本身就需要和FFmpeg打交道的。简而言之只要录制不是一次性手工操作而是产品的一个功能模块就必须在引擎内实现本地编码。这也是我想把这篇文章写完整的原因。2. 插件整体设计与方案选型2.1 不同录屏路线的对比不光是UE5本身有自带方案FFmpeg其实也有好几种接入路径。很多人一上来就纠结用FFmpeg的gdigrab不就行了吗如果你真试过就会知道gdigrab抓的是屏幕不是UE5的视口。某个项目里用户开了高分屏缩放录出来的分辨率乱七八糟字还发虚根本没法用。核心区别在于方案采集源实时性可定制性工程集成复杂度适合场景Movie Render Queue引擎离线渲染缓冲差中低影视级离线输出Windows Graphics Capture桌面/窗口好中高系统级窗口录制FFmpeg gdigrab桌面好低低临时录屏FFmpeg UE RenderTargetUE视口/RT好高中引擎内嵌录屏、录像回放我的结论很明确真正要在UE5产品里做实时录屏必须走引擎RT采集 FFmpeg编码的路线。这样录什么、录多清晰、录多大码率全在引擎里控制还能配合关卡流送、Camera切换做到游戏逻辑层面绝对同步。2.2 插件流水线架构在设计插件之前先想想数据从渲染完成到文件落盘经历了什么。按照从业者的思维习惯我习惯把它拆成四段采集段从UE5渲染管线拿到一帧BGRA8或者RGBA8像素数据。转换段H.264编码器要求YUV420空间实际是YUV4:2:0布局需要把BGRA转成YUV同时做RGB-YUV色彩空间变换和降采样。编码段libx264拿到YUV帧按预设的码率、GOP、preset进行编码输出H.264 Annex-B流。封装段把编码后的包写入MP4/FLV/MKV容器生成时间戳、音视频索引等元数据。这四段中任何一段慢了整个录像帧率就会掉。我在项目里遇到最典型的坑是把像素转换放在游戏线程做结果直接掉帧20多。原因很简单ReadPixels从GPU读回CPU本身就带一次同步卡顿再叠加sws_scale做颜色转换游戏线程根本扛不住。所以我后来把所有CPU密集操作全部挪到独立编码线程游戏线程只做两件事发一帧数据到队列然后立即返回。2.3 FFmpeg库的集成方式FFmpeg集成到UE5时很多人纠结静态库还是动态库。我的实践经验是开发阶段用动态库DLL。升级版本、排查问题都方便不用每次重新链接整个工程。正式分发阶段如果需要单包部署再考虑静态链接到exe里避免DLL缺失被用户投诉。不管哪种方式FFmpeg的头文件和库建议统一放到插件目录下的ThirdParty/FFmpeg方便后面打包。需要引入的头文件大部分集中在libavformat、libavcodec、libavutil、libswscale几个模块。如果编码器选择libx264那还需要确认FFmpeg构建时开启了这个模块很多纯Lite版没有x264录出来的东西格式兼容性会差很多。3. 核心模块实现细节3.1 像素采集RenderTarget还是BackBuffer这一块是很多自己写过录屏的人最容易翻车的地方。UE5里拿到渲染像素的数据源主要有三条路SceneViewport::OnBackBufferReadyToPresent回调发生在后备缓冲区即将被Present的时刻可以直接拿到FTexture2DRHIRef。数据非常新鲜但需要注意这个回调在RHI线程上触发且纹理生命周期很短不能在回调里直接做长时间操作。正确做法是立即发起异步拷贝把像素拷到StagingTexture再在另一个线程Map后读出CPU内存。SceneCapture2DRenderTarget可以指定相机和分辨率把渲染内容渲染到RT上然后通过FRenderTarget::ReadPixels读回CPU。好处是灵活可以录任意视角、任意分辨率坏处是多了一次场景渲染或场景拷贝开销。如果你的需求是录当前主视口这个方案会多出一份冗余开销。自定义BeginRenderCommand在RHI线程做CopyTexture到StagingBuffer。这是效率最高的路也是我在正式项目里用的方式。它绕过了ReadPixels的同步等待直接用GPU拷贝拷贝完成后异步映射回CPU编码线程拿数据时完全不用卡渲染。我给插件的默认实现选了第3种但在配置项里保留兼容模式让用户切换到SceneCapture2D方案。为什么因为StagingBuffer这套对多平台、多RHIDX11/DX12/Vulkan有一些细节差异新手排查起来太崩溃。兼容模式虽然慢一点但能保证先跑通流程。实际代码里核心就这几行// 在OnBackBufferReadyToPresent回调中发起异步拷贝 FRHICommandListImmediate RHICmdList FRHICommandListExecutor::GetImmediateCommandList(); void* RHITexture BackBuffer-GetNativeResource(); RHICmdList.Transition(FRHITransitionInfo(BackBuffer, ERHIAccess::Present, ERHIAccess::CopySrc)); RHICmdList.CopyTexture( FRHICopyTextureInfo{}, BackBuffer, StagingTexture );然后编码线程对StagingTexture调用RHICmdList.MapStagingSurface取出像素缓冲。如果你只是想快速验证直接ReadPixels也够用但要知道它会把渲染线程和游戏线程一起拖慢。3.2 FFmpeg编码流程的关键步骤FFmpeg是一套纯C接口的库调用顺序错了轻则崩溃重则文件打不开。我建议按照固定的生命周期模板来写别跳步// 1. 分配输出上下文 AVFormatContext* FormatCtx nullptr; avformat_alloc_output_context2(FormatCtx, nullptr, nullptr, OutputFilename); // 2. 创建视频流并绑定编码器 AVStream* Stream avformat_new_stream(FormatCtx, nullptr); AVCodec* Codec avcodec_find_encoder_by_name(libx264); AVCodecContext* CodecCtx avcodec_alloc_context3(Codec); // 3. 设置编码参数 CodecCtx-width Width; CodecCtx-height Height; CodecCtx-time_base {1, static_castint(FPS)}; CodecCtx-pix_fmt AV_PIX_FMT_YUV420P; CodecCtx-codec_type AVMEDIA_TYPE_VIDEO; CodecCtx-bit_rate Bitrate; // 例如 8000000 表示8Mbps CodecCtx-gop_size FPS * 2; // 关键帧间隔2秒 // 4. 打开编码器 if (avcodec_open2(CodecCtx, Codec, nullptr) 0) { /* 处理错误 */ } // 5. 打开输出文件并写入头部 avio_open(FormatCtx-pb, OutputFilename, AVIO_FLAG_WRITE); avformat_write_header(FormatCtx, nullptr); // 6. 每次编码一帧后调用 avcodec_send_frame(CodecCtx, Frame); AVPacket Pkt; av_new_packet(Pkt, Size); avcodec_receive_packet(CodecCtx, Pkt); av_interleaved_write_frame(FormatCtx, Pkt); // 7. 结束录制 avcodec_send_frame(CodecCtx, nullptr); while (avcodec_receive_packet(CodecCtx, Pkt) 0) { av_interleaved_write_frame(FormatCtx, Pkt); } av_write_trailer(FormatCtx); avio_closep(FormatCtx-pb); avformat_free_context(FormatCtx);这里面最容易忽略的是第6步只处理了单帧没有处理编码器内部缓存的多帧数据。你录完视频发现最后1-2秒缺失或者时长比实际短基本都是因为在结束时没有把编码器缓冲的帧刷出来。avcodec_send_frame(CodecCtx, nullptr)就是刷缓冲的关键动作一定不能漏。另一个容易错的是PTS。网上很多文章说PTS用帧序号i就行这在固定帧率下勉强能用但一旦丢帧或者帧率波动录像就会出现快进/卡住的现象。正确做法是用递减的PtsCounter然后写入时根据实际采集时间计算Frame-pts PtsCounter;配合CodecCtx-time_base和封装容器的time_baseFFmpeg会把播放速率控制在正确节奏上。3.3 多线程同步与帧率控制录屏插件一旦跑起来就是一条三线程流水线游戏线程产生帧、编码线程做转换和编码、IO线程写文件。最常见的错误是给编码线程挂一个无界队列结果录制5分钟后内存涨了2个G玩家开始骂娘。解决方法是设计一个带丢弃策略的环形缓冲队列固定3-5帧如果编码线程跟不上新帧直接覆盖队首最旧的那一帧。对录屏来说丢几帧远好过内存爆炸。你可以把采集帧率独立于游戏帧率比如游戏跑120帧录像只需要30帧那游戏线程每4帧才抓取1次大幅减轻编码负担。帧率控制我不建议直接凭空造轮子。用真实时间来驱动是更稳的double Accumulator 0.0; double LastTime FPlatformTime::Seconds(); double FrameInterval 1.0 / TargetFPS; // 每次从队列获取渲染帧时 double Now FPlatformTime::Seconds(); Accumulator Now - LastTime; LastTime Now; if (Accumulator FrameInterval) { Accumulator FMath::Fmod(Accumulator, FrameInterval); // 执行编码帧写入 }这样即使某帧因为加载地图、打开UI之类卡顿录像时间轴也不会凭空多出内容。3.4 音频要不要做这个问题一开始就要想清楚。我的经验是第一版建议只做视频轨先把画面链路跑通后面再加音频。原因很现实音频采集在UE5里要分两条路一条是抓WASAPI系统环回设备录系统的声音但用户可能没开环回另一条是从UE的Audio Engine拿PCM数据用FAudioDevice的混音回调两条路都要同步管理音视频时间戳复杂度翻倍。如果确实需要音频我记得后来项目里是用WASAPI的AUDCLNT_STREAMFLAGS_LOOPBACK抓环回把PCM喂给FFmpeg的AAC编码器并单独开一条音频线程维持PTS。但是同步问题至今都很烦人常出现视频先行、音频迟到半秒的怪现象。所以如果实在要音频建议直接参考FFmpeg官方muxing.c示例里的音视频双流写法不要自己凭感觉造。4. 从零搭建插件并跑通的实操记录4.1 准备FFmpeg库文件Windows环境下我比较推荐用预编译的release包节省宝贵时间。下载时要注意架构必须选x64版本选较新的stable比如4.4/5.1/6.1都行。解压后的目录结构整理成ThirdParty/FFmpeg/ ├─ include/ │ ├─ libavcodec/ │ ├─ libavformat/ │ ├─ libavutil/ │ └─ libswscale/ ├─ lib/ │ ├─ avcodec.lib │ ├─ avformat.lib │ ├─ avutil.lib │ └─ swscale.lib └─ bin/ ├─ avcodec-*.dll ├─ avformat-*.dll ├─ avutil-*.dll └─ swscale-*.dll注意发行DLL时FFmpeg 6.0以后的DLL文件名带-61这种版本后缀打包时要一起拷贝别只拷一个不拷另一个。不然用户本机缺少运行库录制功能直接失效。4.2 在UE5里创建插件模块用编辑器自带的New Plugin工具创建一个Blank插件然后在Build.cs里配置第三方库public class FFmpegRecorder : ModuleRules { public FFmpegRecorder(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; 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)); // 开发阶段使用delay load让DLL在运行时加载 PublicDelayLoadDLLs.AddRange(new string[] { avcodec-61.dll, avformat-61.dll, avutil-61.dll, swscale-7.dll }); RuntimeDependencies.Add($(BinaryOutputDir)/avcodec-61.dll); RuntimeDependencies.Add($(BinaryOutputDir)/avformat-61.dll); RuntimeDependencies.Add($(BinaryOutputDir)/avutil-61.dll); RuntimeDependencies.Add($(BinaryOutputDir)/swscale-7.dll); } }RuntimeDependencies是打包时必写的不然打包后的程序跑起来提示找不到DLL。我早期忽略过这个编辑器里能录打包后立刻报错。4.3 核心封装类我会把FFmpeg相关逻辑封装成一个原生C类FFFmpegVideoEncoder不直接塞进Actor方便随时在工具、Actor、插件后台中使用。核心接口大致这样class FFFmpegVideoEncoder { public: bool StartRecording(const FString FilePath, int32 Width, int32 Height, int32 FPS, int32 Bitrate); bool WriteFrame(const uint8* BGRA8Data, int32 Width, int32 Height); void StopRecording(); private: AVFormatContext* FormatCtx nullptr; AVCodecContext* CodecCtx nullptr; AVFrame* Frame nullptr; SwsContext* ScaleCtx nullptr; };WriteFrame内部做两件事用sws_scale把BGRA8的AVFrame转成YUV420然后avcodec_send_frame。注意sws_scale的输入参数要对应UE的BGRA存储方式一般指定AV_PIX_FMT_BGRA。这里有一个非常经典的坑UE5的FColor在内存里是BGRA顺序你如果按RGBA配置sws_scale录出来的画面会红蓝互换——我第一次遇到时排查了大半天。4.4 给蓝图暴露接口插件最终给谁用大概率是蓝图开发者或设计师你不可能要求他们去写C。所以我的习惯是把插件核心逻辑封装在UFunction(BlueprintCallable)里在UActorComponent上暴露三个接口StartRecording传入文件名、分辨率、FPS、码率。StopRecording停止编码并关文件。IsRecording查询状态。蓝图侧只需在关卡Bluprint里拖个UI按钮点击时调用StartRecording再次点击时调用StopRecording。像素采集部分的逻辑完全藏在组件内部不暴露给蓝图。这样维护起来边界清晰谁都不越界。5. 常见问题与排查技巧实录5.1 问题速查表现象直接原因解决方式录出来的视频绿屏/花屏像素格式配置错误BGRA/RGBA/YUV不对检查sws_scale的输入格式UE用AV_PIX_FMT_BGRA视频里红色和蓝色互换颜色通道顺序未对齐改用AV_PIX_FMT_BGRA或者手动交换R/B录到一半内存持续增长帧队列无界堆积改用固定大小环形缓冲并加丢帧策略录制结束文件打不开没有调用av_write_trailer正确flush编码器缓冲并写尾打包后提示DLL缺失没有配置RuntimeDependencies或不带DLL在Build.cs里配置RuntimeDependencies找不到libx264编码器FFmpeg版本未编译x264换带x264的full版或改用libsvtav1等其他编码器视频前几秒卡顿没有设置GOP大小或者B帧策略异常显式设置gop_size和max_b_frames0录制中游戏掉帧明显像素读取导致CPU/GPU同步阻塞改用CopyTextureStagingBuffer异步映射视频时长和实际录制时间不符PTS计算错误用真实的采集时间戳计算帧PTS音视频不同步音视频PTS基准不一致用同一条时间轴不要用各自的帧计数5.2 我最想单独拎出来讲的两个坑第一个是编码器缓存机制。很多人录到结尾发现视频突然缺最后一两秒这是老生常谈但依然频繁发生。本质上是H.264编码器为了压缩率会缓存几帧在未来帧之后输出如果你录制结束时没有把缓存中的帧冲洗出来它们就永远不写进文件。解决方式就是上面提到的avcodec_send_frame(CodecCtx, nullptr)然后循环avcodec_receive_packet直到返回AVERROR_EOF再把包全部写进文件。第二个是长时间录制的崩溃。项目里遇到过录制4小时后内存涨到14G的情况最终定位是每一帧在av_packet_unref上少调用了一次。AVPacket是个引用计数容器av_interleaved_write_frame不会自动释放你传入的packet内存每次用完后必须av_packet_unref(Pkt)否则就是妥妥的内存泄漏。类似的还有av_frame_unref。写编码循环时必须保证每个AVPacket、AVFrame都用一次unref一次顺序错一个就有隐患。5.3 性能调优心得录屏功能上线前我会先固定一个参数组合来跑基准测试1080p、30fps、8Mbps、presetveryfast。这几个参数对大部分游戏足够清晰体积也不会失控。如果录制过程平均掉帧小于3帧就会尝试把preset降为medium如果掉帧明显先检查是不是编码线程CPU占用已经到了95%以上是就把preset调回fast或faster而不是优先去砍码率。码率过低反而会导致画面模糊视觉体验更差。还有一个很少有人提的点FFmpeg的av_interleaved_write_frame可能会在IO速度慢时阻塞这意味着编码线程长时间卡在文件写入上。稳妥的做法是编码线程和文件IO线程分离编码线程写完包后挂到IO队列IO线程统一写磁盘。但一般项目直接在编码线程里写磁盘也够用除非录的是4K超高清才需要考虑拆IO线程。6. 最后再分享一点实际经验这套插件方案我在三个项目里落地过从最初的ReadPixels硬抗到后来改成StagingBuffer异步拷贝再到现在跑得顺滑稳定的版本中间踩过的坑不少。最深的体会是FFmpeg本身并不复杂复杂的是和UE5渲染管线的集成时机、线程模型和资源生命周期管理。只要这些理顺后面加音频轨、加推流直播、加分段录制都只是顺着Pipeline再扩展模块而已。另外提醒一句FFmpeg的许可证在某些使用场景下是GPL/LGPL的。如果你只是内部工具用问题不大如果发出去给商业用户提前走一遍授权检查别等到产品都上架了才想起这回事到时候换编码器重写一遍成本极高。技术选型做在前面永远比事后补窟窿省心。本文还有配套的精品资源点击获取