尧图精选

NatCorder插件实战:Unity移动端录屏、拍照与GIF制作全攻略

🕒 发布时间:2026/9/15 13:43:57 📁 来源:尧图网络
做移动端Unity开发的人迟早会遇到录屏、截图、导Gif这类需求。不管是做社交分享、游戏高光时刻回放还是AR试戴后保存一段小视频都属于“看起来很基础真做起来一堆坑”的活。如果你还停留在调系统原生API或者硬写RenderTexture去拼帧那效率确实有点低。我这边长期在移动端项目里做录制相关功能市面上常见的方案基本都摸过一遍从原生Android MediaRecorder/iOS ReplayKit到Unity自带的Recorder窗口再到RenderTexture自研方案最后稳定下来用的是NatCorder这套插件。这篇文章就围绕NatCorder从选型逻辑讲到三个核心功能——视频录制、JPG拍照、Gif动图生成完整拆解一遍里面有可直接抄作业的代码也有我在真机上踩出来的坑。1. 插件选型与整体设计为什么最终落在NatCorder上1.1 三个主流方案的真实对比先说说市面上能选的路子。第一类是用Unity自带的Recorder这个玩意儿编辑器里用起来确实爽录制Game视图一键出片。但它是为编辑器离线录制设计的打着包跑到手机上根本没接口调用。移动端玩家要录像这条路直接堵死唯一能做的是在编辑器里产出一些宣传素材。第二类是自己调用Android和iOS的原生接口。Android那边用MediaRecorderiOS用ReplayKit。优点是完全可控、性能好、代码不依赖任何第三方库。但代价极其惨烈你需要写Android的Java/Kotlin层、iOS的Objective-C/Swift层再通过UnitySendMessage或者传到C层回调C#两边权限管理、生命周期处理、前后台切换全得自己扛。最恶心的是一旦设备厂商在某个系统版本改了行为你的原生层就得跟着改维护成本非常高。第三类是封装插件。NatCorder就是这一类它把各家平台的底层录屏能力统一封装成一套C#接口。你在C#里写一套代码它自动帮你适配Android的MediaCodec、iOS的VideoToolbox甚至是WebGL。这样上层业务逻辑完全跨平台这是它最大的价值。从成本角度看原生方案表面上省了一笔插件钱但算上开发和维护时间反而更贵。尤其团队里没有人精通双端原生开发时用NatCorder换来的时间收益是实打实的。1.2 NatCorder的核心设计逻辑NatCorder的API设计很有意思核心概念就是三个东西Recorder、Clock、Input。Recorder负责编码和写入文件。你要MP4就用MP4Recorder要Gif就用GIFRecorder抽象得非常干净。Clock负责帧节奏内置了RealtimeClock实时时钟也有FixedIntervalClock给固定帧率录制用。Input负责把图像和音频数据送进RecorderCameraInput可以一条龙抓相机画面和麦克风音频也可以手动用Frame.FromCamera从相机抓单帧传给Recorder的CommitFrame方法。这套设计的好处是分层清晰。如果你想在录视频的时候把UI也录进去或者想要分屏效果只要自己拼好RenderTexture再通过Frame送入Recorder就行不需要改任何底层逻辑。1.3 适用场景与功能边界NatCorder适合的典型场景有这么几类游戏内精彩时刻回放、用户生成内容的录制分享、试穿试戴类App的结果保存、教育类App需要把操作过程录下来、以及简单的拍照和动图导出。需要注意它的边界。它负责的是“录制并编码”这一段不是“视频编辑”。剪辑、加滤镜、加特效这些不需要指望它。另外Gif录制虽然可以做但它本质上是对每一帧编码成Gif格式文件大小和帧率关联非常紧密后面我会讲怎么控体积。2. 环境搭建与基础配置一步步把插件跑起来2.1 导入插件与版本选择NatCorder在Unity Asset Store里可以直接拿到搜索NatCorder就能找到。下载后通过Package Manager导入或者直接把整个目录拖进项目的Assets文件夹都行。需要注意的是版本兼容性。Unity 2019.4以上基本没问题我用的Unity 2021.3 LTS跑起来非常稳。如果你项目还在用老版本Unity最好先看一眼插件文档里的最低版本要求。导入之后确认Scripting Runtime Version设置成.NET 4.x Equivalent。NatCorder内部大量使用了C#的async/await如果项目还在.NET 3.5会让部分API不可用。设置路径在Player Settings - Other Settings - Configuration - Scripting Runtime Version里面。这里有一个实战提示最好把NatCorder相关脚本放在Assets目录下专门的文件夹里同时开启Assembly Definition。这样编译的时候可以单独管理依赖后面如果要做热更新或者分包会省很多事。不过新手阶段可以不用搞这么复杂默认放Assets就行。2.2 移动端平台设置与权限处理Android平台需要注意Target API Level建议在Android 10以上。插件本身会在内部申请存储权限但实测下来如果项目里的主场景在Android 6.0以上跑最好还是弹一次运行时权限申请避免出现录制结束但写入失败的情况。iOS平台的部署目标建议设在iOS 11以上NatCorder底层用的是VideoToolbox旧版本系统上容易出兼容性问题。还有一个容易踩的坑是Audio Capture。如果你在录视频时需要同时录麦克风声音iOS上必须在Info.plist里添加NSMicrophoneUsageDescription否则系统会直接杀掉App。Android那边如果只是录游戏内部AudioSource的声音不需要额外权限如果要录麦克风也需要在Manifest里声明RECORD_AUDIO并动态申请。我实际项目里通常的做法是录制前统一检查一下权限状态缺失就弹出申请框用户同意之后再开始。下面这段代码可以直接放在按钮点击的处理里。#if UNITY_IOS // iOS上没有动态权限判断只要plist配置好了首次使用会弹窗 #elif UNITY_ANDROID if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Microphone)) { UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Microphone); } #endif3. 三大核心功能实操录制、拍照、Gif全流程3.1 场景搭建与UI按钮布局动手写代码之前先在场景里准备一个主角。随便创建一个Cube或者放一个实际要录制的模型再放一个用来录制画面的相机。界面我习惯用UGAI来做Canvas下放三个按钮分别绑上开始录制视频、停止录制视频、拍照三个事件如果需要Gif录制再加两个按钮。录制的过程中建议在屏幕上显示一个红点或者计时文本让用户知道当前处于录制状态。后面代码里会用Debug.Log输出录制状态真机上跑的时候可以挂一个Text组件来展示提示信息。3.2 视频录制完整实现视频录制是NatCorder最核心也是最常用的功能。完整实现分为三步创建MP4Recorder和CameraInput、开始录制、停止并保存文件。先看完整的脚本结构。我按“一个Manager类管理所有媒体操作”的方式来组织方便以后扩展。using UnityEngine; using UnityEngine.UI; using System.IO; using System.Threading.Tasks; using NatSuite.Recorders; using NatSuite.Recorders.Clocks; using NatSuite.Recorders.Inputs; public class MediaCaptureManager : MonoBehaviour { [Header(相机引用)] public Camera captureCamera; [Header(视频参数)] public int videoWidth 1280; public int videoHeight 720; public int frameRate 30; public int audioSampleRate 44100; public int audioChannelCount 2; [Header(Gif参数)] public int gifWidth 480; public int gifHeight 270; public float gifFrameDuration 0.1f; [Header(UI提示)] public Text statusText; private MP4Recorder mp4Recorder; private CameraInput cameraInput; private RealtimeClock realtimeClock; private GIFRecorder gifRecorder; private bool isRecordingVideo false; private bool isRecordingGif false; private void SetStatus(string message) { Debug.Log(message); if (statusText ! null) { statusText.text message; } } public void StartVideoRecording() { if (isRecordingVideo) { SetStatus(已经在录制视频了); return; } isRecordingVideo true; realtimeClock new RealtimeClock(); mp4Recorder new MP4Recorder( videoWidth, videoHeight, frameRate, audioSampleRate, audioChannelCount ); cameraInput new CameraInput(mp4Recorder, realtimeClock, captureCamera); cameraInput.StartRecording(); SetStatus(开始录制视频...); } public async void StopVideoRecording() { if (!isRecordingVideo) { SetStatus(当前没有在录制视频); return; } isRecordingVideo false; var videoPath await cameraInput.FinishRecording(); SetStatus($视频保存路径: {videoPath}); } }这里有一个关键点为什么不直接调用mp4Recorder.FinishWriting()而是用cameraInput.FinishRecording()因为CameraInput内部既管视频帧又管音频数据。直接调mp4Recorder.FinishWriting()会把录制的封装逻辑打断正确做法是让CameraInput通知Recorder结束编码。返回的videoPath是字符串里面是实际文件路径可以直接拿来播放、分享或者上传。关于分辨率选择我默认给了1280x720这个分辨率在移动端是性能和画质的最佳平衡点。你如果要录制完整的竖屏UI界面可能需要换成1080x1920这样的竖屏分辨率但那样会明显增加内存占用和编码压力待会儿会单独说明。3.3 拍照JPG功能的实现拍照这个需求其实分两种一种是直接把当前屏幕截下来另一种是只把某个相机画面截下来。NatCorder官方主打的是录视频但我们可以用它提供的Frame机制也可以用Unity原生RenderTexture方案。我比较推荐后者因为拍照本质上是一次性的操作不需要持续编码用RenderTexture更轻量、更好控制画质。public void CapturePhoto() { int width Screen.width; int height Screen.height; var rt RenderTexture.GetTemporary(width, height, 24); var oldTargetTexture captureCamera.targetTexture; captureCamera.targetTexture rt; captureCamera.Render(); var oldActiveTexture RenderTexture.active; RenderTexture.active rt; var texture new Texture2D(width, height, TextureFormat.RGB24, false); texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); texture.Apply(); RenderTexture.active oldActiveTexture; captureCamera.targetTexture oldTargetTexture; RenderTexture.ReleaseTemporary(rt); var jpgBytes texture.EncodeToJPG(90); var path Path.Combine(Application.persistentDataPath, $photo_{System.DateTime.Now:yyyyMMdd_HHmmss}.jpg); File.WriteAllBytes(path, jpgBytes); Destroy(texture); SetStatus($照片保存路径: {path}); }这段代码有三个注意点。第一RenderTexture.GetTemporary比new一个RenderTexture更高效系统会从池子里复用显存用完一定要ReleaseTemporary还回去否则内存只增不减。第二ReadPixels之前必须把RenderTexture.active设置成目标RT否则读到的永远是屏幕当前激活的缓冲区内容。读完之后要记得恢复原来的active目标我代码里已经写了保存和恢复的逻辑。第三EncodeToJPG的第二个参数是画质范围0到100。实测下来85到95之间视觉差异非常小但文件体积差距明显一般90比较好用。如果遇到场景里还有UI需要截取的情况可以把Canvas的RenderMode设置成ScreenSpaceCamera并把renderCamera指向captureCamera这样UI和3D内容会一起渲染到同一个RT里一张图就搞定了。3.4 GIF动图生成实现Gif录制是NatCorder里相对特殊的一个分支。它不需要音频只需要持续地把帧提交给GIFRecorder。先看完整实现。public void StartGifRecording() { if (isRecordingGif) { SetStatus(已经在录制Gif了); return; } isRecordingGif true; gifRecorder new GIFRecorder(gifWidth, gifHeight, gifFrameDuration); SetStatus(开始录制Gif...); } public void RecordGifFrame() { if (!isRecordingGif || gifRecorder null) return; var frame Frame.FromCamera(captureCamera, gifFrameDuration, null, gifWidth, gifHeight); frame.Upload(); gifRecorder.CommitFrame(frame); frame.Dispose(); } public async void StopGifRecording() { if (!isRecordingGif) { SetStatus(当前没有在录制Gif); return; } isRecordingGif false; var gifPath await gifRecorder.FinishWriting(); gifRecorder null; SetStatus($Gif保存路径: {gifPath}); }GIFRecorder的构造函数参数含义很明确第一个和第二个是输出图像的宽高第三个是每帧之间的间隔时间单位是秒。如果你要10fps的Gif那frameDuration就填0.1要15fps就填0.0667。RecordGifFrame必须在每一帧或按固定频率手动调用。通常情况下放在Update里每次调用都从相机抓一帧提交给Gif Recorder。但要注意如果Update本身跑得很快比如60fps而Gif目标只有10fps会产生很多重复帧文件明显变大。所以更合理的做法是控制调用频率我项目的做法是用一个计时器累加时间到了设定间隔才抓帧。private float gifTimer 0f; void Update() { if (isRecordingGif) { gifTimer Time.deltaTime; if (gifTimer gifFrameDuration) { gifTimer - gifFrameDuration; RecordGifFrame(); } } }用FixedIntervalClock理论上也可以但GifRecorder配合手动的提交节奏更直观录制时长也更容易控制。从实测结果看NatCorder的Gif输出质量相当不错颜色还原度够但体积确实会比MP4大不少后面会说怎么压低体积。4. 移动端性能优化与内存管理4.1 分辨率和帧率怎么选移动端录制最大的敌人是发热和卡顿。如果把录制分辨率和游戏渲染分辨率设成一样比如同时是2K那GPU的压力会翻倍。我自己用的策略是录屏分辨率走独立档位不跟Screen.width走。视频录制的话游戏如果跑60fps录制帧率不用跟着6030就非常够看了。Gif录制更夸张10到15fps足够因为Gif的动态效果本身就带有大量的时间冗余帧率太高纯粹是浪费体积。分辨率方面竖屏真人出镜类内容用720x1280横屏游戏画面用1280x720方形贴纸类动图用512x512。如果纯粹是论坛或者聊天工具里的分享甚至可以压到480x270因为小尺寸Gif在手机端放大看会糊但胜在文件小、生成快。4.2 内存与纹理释放要点NatCorder录制过程中每帧都会从GPU拷贝一份纹理数据交给编码器。如果录制分辨率很高这个拷贝的开销非常可观。我实测录1080P的时候,每帧拷贝和编码大概要消耗4到8毫秒如果主线程Update里有其他耗时逻辑很容易出现顿卡。所以优化手段就是降低录制分辨率尽量使用RenderTexture作为中间缓冲避免在C#层反复new Texture2D。还有一点是Frame的Dispose。每次Frame.FromCamera拿到的是一个句柄用完了不Dispose帧数据会堆积在内存里。轻则内存在录制过程中持续涨重则直接触发低内存崩溃。音频方面如果游戏不需要录麦克风建议把MP4Recorder构造参数里的采样率和声道数传低一些。44100和2声道是CD音质在移动端游戏录制场景里其实有点奢侈换成22050和1声道能明显降低音频处理的负担。视觉和听觉重点不同音频降采样后几乎听不出来差别。4.3 不同平台的细微差异Android上NatCorder使用的是MediaCodec播放器和系统编码器对H.264的支持都很好录出来的MP4可以直接在系统相册里播放。但有些国产ROM对写入公共目录的权限卡得非常严格我一般会直接把文件写到Application.persistentDataPath需要分享的时候通过系统分享面板传出去这样绕开了直接访问公共存储的问题。iOS上VideoToolbox的编码效率非常高录1080P 60帧的发热控制也比Android好很多。但iOS上的文件路径管理更严格不能用File.WriteAllBytes往相册里写正确做法是把文件写到沙盒再调用保存到相册的面板。WebGL平台我用过一两次NatCorder有对应的后端实现但因为浏览器环境限制文件只会在内存里生成然后通过URL.createObjectURL让用户手动下载体验跟移动端原生保存是两回事。5. 常见问题排查与避坑指南5.1 录制出来是黑屏这个问题我遇到过无数次大部分情况下不是插件的问题而是相机配置的问题。检查顺序是这样的先确认captureCamera在Inspector里确实有赋值不是空引用再确认相机没有开启遮挡剔除或者Culling Mask把主场景的层给剔除了最后检查Frame.FromCamera或者CameraInput内部的相机参数是否跟实际相机一致。有一个很隐蔽的坑如果场景里存在多个相机其中一个负责渲染UI且Depth值较高那么录制的相机有时候拍不到UI层。解决方案是把UI相机的Depth调低或者把UI相机也切成ScreenSpaceCamera模式并挂到录制的相机身上。5.2 音画不同步音画不同步是录制类接口的经典问题。NatCorder用了时钟机制来保证同步所以问题往往出在你手动提交帧的时候没有遵循统一的时间节奏。如果你用的是RealtimeClock那系统会根据真实时间自动管理视频和音频的对应关系。如果你自己在Update里固定间隔提交帧又没设置正确的timeScale那么录制时间轴就会和真实时间错位。解决办法是除非有特殊需求否则视频录制尽量用CameraInput配合RealtimeClock。Gif没有音轨自然不存在音画同步问题但你提交帧的时间间隔必须和你设置的frameDuration一致否则Gif播放速度会失真。5.3 Gif文件体积过大怎么办Gif格式本身就不适合承载大动态、高分辨率的视频内容一张480x270、10fps、录5秒的动图轻松能到5MB以上。压体积从三个方向入手降分辨率、降帧率、减少录制时长。我现在项目里的Gif分享就固定用480x270、8fps、最长5秒一张图控制在2MB以内。还有一个技巧是录制过程中尽量保持画面简单不要有大幅度的摄像机晃动和复杂噪点纹理。Gif压缩对大面积纯色非常友好纹理越乱体积越大这一点和MP4的编码特性不太一样。5.4 Android和iOS上偶发崩溃录制过程中崩溃最常见的原因是FinishWriting还没执行完游戏App就退到后台了。移动系统在后台会迅速回收资源编码过程直接被砍掉文件就坏了甚至崩溃。解决方法是在OnApplicationPause和OnApplicationFocus里检测录制状态一旦发现暂停就主动结束录制。private void OnApplicationPause(bool pauseStatus) { if (pauseStatus isRecordingVideo) { StopVideoRecording(); } if (pauseStatus isRecordingGif) { StopGifRecording(); } }还有一个容易忽略的问题某些低端Android机型在录制过程中同时加载场景资源会产生明显卡顿甚至ANR。项目里如果要做这个功能尽量在录制开始前把所有资源加载好录制过程中不要频繁Instantiate和Destroy大物体。5.5 文件路径记不住怎么办我用NatCorder这么多次最花时间的不是功能实现而是文件管理。录制结束之后返回一个路径这个路径可能很长而且不同平台格式不同。建议封装一个小工具在拿到路径后自动做三件事打印日志、把路径存到PlayerPrefs或内存列表、用系统分享面板把文件传出去或直接在相册里展示。下面是一段非常实用的保存与分享函数。private void SaveAndShare(string filePath) { #if UNITY_ANDROID using (var intent new AndroidJavaObject(android.content.Intent, android.intent.action.SEND)) { var uri new AndroidJavaObject(android.net.Uri); var file new AndroidJavaObject(java.io.File, filePath); intent.CallAndroidJavaObject(setType, image/*); intent.CallAndroidJavaObject(putExtra, android.intent.extra.STREAM, uri.CallStaticAndroidJavaObject(fromFile, file)); // 启动系统分享面板的逻辑略 } #elif UNITY_IOS // iOS相册保存逻辑略 #endif }原生层调起来比较繁琐按需封装就行。6. 扩展思路录制视频后的二次处理和分享链路功能做到这里已经满足日常录制需求了。但如果想让功能更实用还可以做一个分享按钮把录制完成的文件直接丢给微信、钉钉、系统相册等第三方App。NatCorder本身不含分享逻辑需要自己调原生分享接口。思路是录制结束后拿到文件路径Android上用Intent触发分享面板iOS上用UIActivityViewController。两个平台的代码都可以封装在Unity的Native层或直接用AndroidJavaObject调Java层。好在只需要传一个文件路径交互层不复杂。如果要更进一步的视频剪辑比如从一段录好的MP4里截取几秒再加个过渡效果那就需要接FFmpeg之类的方案了。NatCorder专注在录制这一段没必要硬扩展成剪辑工具。我目前的处理方式是录制完成直接分享用户需要的简单裁剪交给系统相册的能力去完成。另外如果你的项目里有热更新需求NatCorder因为是纯C#接口加少量原生桥接可以很容易地放到热更资源里动态加载。不过需要注意Android上部分原生库是编译进aar的热更新包只能更新C#层逻辑不能替换底层库文件。最后分享一点自己的习惯用过一段时间NatCorder之后我现在对移动端录制类功能的处理流程已经固定下来了进游戏先判断设备性能档位中低端机强制720P录制高端机可以上1080P录制时间超过30秒自动分段避免单文件过大导致编码时间太长每次录制结束立刻释放Recorder和Input绝不让多个录制实例同时存活。还有个小技巧录制视频时给mainCamera多加一个轻微的抗锯齿效果录出来的画面观感提升明显而且编码体积会稍微小一点。这是因为编码器对平滑的边缘处理效率更高边缘锯齿越多码率浪费越严重。NatCorder这套方案整体上是稳的感兴趣的可以拿我的代码先跑一遍Demo再根据你自己的业务场景去调参数。真正上线前建议至少准备3台不同品牌的真机做一轮验证把性能和兼容性问题提前揪出来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →