尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity移动端录屏拍照Gif生成:Natcorder插件实战指南

Unity移动端录屏拍照Gif生成:Natcorder插件实战指南 做移动端 Unity 项目录屏、拍照、存 Gif 这几件事几乎是绕不开的。游戏里要一键分享精彩时刻拍照生成分享卡片或者做个成就达成 Gif 发给队友炫耀——这种时候总不能每次都让玩家自己屏幕录制再剪半天。我在几个移动端上线项目里反复折腾过录屏方案最后固定下来的就是 Natcorder 这个插件。它解决的核心问题很直接不依赖平台原生 SDK用一套 C# 接口就能在 iOS 和 Android 上完成 H.264 视频录制、JPG 截图、GIF 动图生成。这篇就基于我的实际接入经历把完整流程、避坑点、以及上线后才会遇到的细节都写出来希望对正在选型或已经被录屏需求折磨的同学有帮助。这个插件适合谁用如果你正在做休闲游戏、社交类 App、或者工具型 Unity 应用需要在移动端把游戏画面/相机画面录下来、保存到相册并分享那么 Natcorder 是目前成本很低的一条路。它本质上不是一个“傻瓜式录屏按钮”而是一套具备扩展性的媒体编码管线你可以像拼接积木一样把“画面来源”“音频来源”“编码器”组合起来实现录视频、拍照、生成 Gif 甚至是连续帧处理。下面我会从选型思路开始一步步带你从零接入。1. 为什么移动端录屏、拍照、Gif生成我会选 Natcorder1.1 项目里常见的录屏/截图需求先说一个现象录屏需求在移动端 Unity 项目里表现形式往往不只是“录一个 mp4”。我接过的需求至少有这么几类玩家在游戏内点击“分享”按钮把最近 10 秒的操作过程录制成视频保存到相册或直接调起系统分享面板。玩家在妆容/捏脸/换装界面点击“拍照”生成一张当前画面的高清 JPG用于生成海报。玩法结束后生成一段 23 秒的角色高光时刻以 Gif 形式发到聊天群里。工具类 App 需要把实时相机画面录制下来作为教学视频素材。这些需求有一个共同特征它们不是“开发者自己录屏做宣传视频”而是“让最终用户在自己的手机上生成媒体文件”。这就决定了方案选型上不能依赖 Unity 编辑器里的 Recorder 窗口也不能用 PC 端的录屏方案必须考虑移动端内存、发热、平台权限、相册兼容性等一系列问题。1.2 几种主流方案的对比和取舍市面上做移动端 Unity 录屏大致有四条路Unity 自带的 ScreenCapture.CaptureScreenshot这是很多新手最先找到的 API。但它的定位是“截图”不是“录屏”。虽然可以通过每帧调用、再异步保存的方式去拼接视频但性能和效率都很差没法同时捕获音频生成的图片序列还会把内存瞬间打满。只适合做极简单的拍照。Unity Recorder官方录制窗口/包这个工具在编辑器里非常好用可以录制 Game 视图、生成序列帧、输出 Mp4/Gif。但它更多是面向“开发者制作宣传视频、演示视频”而不是运行时由玩家触发的移动端录制。在移动端运行时去动态创建 Recorder 实例来录屏桥接起来很别扭生成的视频文件也很难直接存入 Android/iOS 相册。自研 RenderTexture 编码器方案思路是把 Camera 输出到 RenderTexture再通过 GPU 读回像素用 MediaCodecAndroid/ AVAssetWriteriOS 编码成视频。这条路最灵活但相当于把 FFmpeg、平台编码器、音视频同步、颜色空间转换全部自己啃一遍。哪怕只做“无声录屏”也要处理平台生命周期、Surface 创建、C#/Java/OC 互调开发周期直接翻倍。Natcorder 插件它把上面第三种的复杂度封装成了统一的 C# API底层自动选择 Android 的 MediaCodec 或 iOS 的 AVAssetWriter并提供 MP4Recorder、GIFRecorder、JPGRecorder 等现成编码器。我选择它的原因主要有三个接口统一、移动端适配成熟、GIF 录制成本极低。用表格总结一下这几种方案方案运行时支持移动端适配音频支持Gif支持开发成本ScreenCapture支持弱权限/相册处理麻烦无无低Unity Recorder编辑器实用运行时别扭弱部分支持中自研 RT编码器支持需要各平台单独实现可扩展困难高Natcorder支持好支持支持低这里不是否定自研方案如果你的团队恰好有熟悉 MediaCodec 和 AVAssetWriter 的移动端原生开发且产品有非常特殊的编码需求那自研是可控的。但常规的中小团队、独立开发者用 Natcorder 可以省掉至少三周的原生开发时间我后面讲的所有流程都是基于它。2. 接入前准备环境、权限与基础配置2.1 下载与导入插件包Natcorder 在 Unity Asset Store 上可以搜索到作者是开发 NatSuite 系列插件的 Yusuf Olokoba。导入方式很简单下载并导入后会看到NatSuite或者NatCorder相关目录不同版本的目录名有差异我用的版本是 1.x命名空间统一是NatSuite.Recorders。导入完成后有几件事必须确认当前项目的最低 Android API Level 是否满足要求。建议设为 Android 6.0API 23以上因为 MediaCodec 的硬件编码能力在 5.0/6.0 之后才比较稳定。iOS 端的 Deployment Target 建议 11.0 以上太低的话 AVAssetWriter 某些硬件编码特性会受限。确认 IL2CPP 模式下不会有裁剪问题。如果遇到真机上报FileNotFoundException或编码器初始化失败先去 Player Settings 里检查 Managed Stripping Level尽量设为 Minimal 或 Low。2.2 移动端权限配置Natcorder 本身不是一个“录屏服务”而是输入画面数据交给硬件编码器。所以它需要的权限主要集中在“保存文件”和“录制音频”上Android 端如果只录制视频画面不录制声音可以不申请麦克风权限但需要申请存储权限。老版本 Android 需要WRITE_EXTERNAL_STORAGEAndroid 10 以上推荐直接写到应用专属目录或者用 MediaStore 保存到系统相册。如果同时录麦克风声音需要在AndroidManifest.xml中加入RECORD_AUDIO权限并在运行时申请否则 AudioInput 初始化时会失败。iOS 端保存到相册需要依赖Photos框架记得在 Info.plist 中加入NSPhotoLibraryAddUsageDescription否则调用相册写入时会直接崩溃或没有任何反应。如果录制麦克风声音需要NSMicrophoneUsageDescription。这里有一个我在项目里踩过的真实例子iOS 上不做相册权限描述点“保存到相册”之后日志里没有任何报错但相册里就是找不到文件。排查了半天才发现是 Info.plist 缺了 key一旦补上立刻恢复。提示Unity 6 / 2022 LTS 里可以通过AndroidManifest.xml统一配置也可以直接在 Player Settings 的Android Permissions勾选权限。为了精细控制我建议用前者。2.3 基础 API 概念扫盲Natcorder 的核心结构可以分成三层Recorder编码器负责把纹理帧/音频帧转成目标格式。常见有MP4Recorder、GIFRecorder、JPGRecorder。它们都继承自同一个抽象接口所以使用模式高度一致。Input输入源负责把 Unity 侧的RenderTexture、Texture2D、AudioBuffer等数据转换成 Recorder 能吃的帧数据。一般你不需要自己写直接用CameraInput、AudioInput这类组件。Container容器/文件写入负责管理最终生成的文件路径、文件写入和生命周期。理解这层关系很重要因为遇到黑屏、无声、崩溃等问题时你才能快速判断是“输入源抓帧失败”还是“编码器初始化失败”还是“文件写入失败”。举个例子录视频时我把CameraInput挂在主相机上每帧调用CameraInput.ManualUpdate()把当前画面推给MP4Recorder同时挂一个AudioInput监听AudioListener把麦克风数据同步交给同一个 Recorder。这样视频和音频是在同一个时间基线上编码的几乎不需要自己去做音视频同步。3. 录屏功能从画面抓取到 MP4 落盘3.1 核心录制流程梳理录制 MP4 的完整流程可以拆成四个阶段初始化 Recorder指定输出路径、分辨率、帧率、比特率、是否包含音频。创建输入源把 Camera 的画面喂给 Recorder。逐帧提交在Update()或OnRenderImage()阶段把新帧推给 Recorder。停止并释放调用StopRecording()等待回调拿到最终文件路径。代码大致长这样以常见 1.x 版本为例using NatSuite.Recorders; using NatSuite.Recorders.Clocks; using NatSuite.Recorders.Inputs; public class VideoRecorderController : MonoBehaviour { private MP4Recorder mp4Recorder; private CameraInput cameraInput; private AudioInput audioInput; private string videoPath; public void StartRecording(int width, int height, int fps, int bitrate) { // 1. 创建 MP4 编码器 mp4Recorder new MP4Recorder( width, height, fps, bitrate, audio: true ); // 2. 创建计数器时钟用于驱动帧时间戳 var clock new RealtimeClock(); // 3. 将相机画面输入 cameraInput new CameraInput(mp4Recorder, clock, Camera.main); // 4. 将游戏内 AudioListener 的音频输入 audioInput new AudioInput(mp4Recorder, clock, AudioListener3D, true); } public void StopRecording() { // 结束输入源 cameraInput.Dispose(); audioInput.Dispose(); // 完成编码拿到最终文件路径 mp4Recorder.FinishWriting(OnRecordingFinished); } private void OnRecordingFinished(string path) { videoPath path; Debug.Log($视频已保存: {path}); } }这段代码的思路是先确定录制尺寸和帧率然后创建 Recorder随后声明 RealtimeClock。这里RealtimeClock的作用非常关键它使用真实时间作为视频的时间基准而不是 Unity 的帧时间。如果项目里的Time.timeScale会被修改比如暂停、慢动作用 RealtimeClock 可以保证视频播放速度和真实时间一致。3.2 音频录制与不同帧率/分辨率的权衡很多人录屏时会忽略音频真到上线才发现玩家希望视频里带声音。Natcorder 的音频录制很简单就是把AudioInput挂在场景中并把需要录制的AudioListener传进去。这样的话游戏里的背景音乐、音效都能被录进视频。但有两个需要注意的点音频来源选择如果只想录游戏声音不录麦克风AudioInput会采集场景中AudioListener能听到的所有声音。如果还想要玩家说话的声音需要额外引入麦克风数据并混合在一起。Natcorder 层面不会帮你做混音需要你自己把两个音频源叠加。我的做法是让AudioListener输出游戏声音再用原生Microphone类采集麦克风自己写一个简单的 AudoMixInput 喂给 Recorder。这块逻辑建议放后置不要让基础版本被它阻塞。帧率/比特率设置移动端录制分辨率不要盲目追求 4K。常规竖屏游戏用 1080x1920 或者原始 Game 视图的 0.8 倍缩放帧率 30视频比特率 8-12 Mbps 就足够了。比特率太高会让视频文件快速变大低端机也会在编码时产生明显发热。如果发现部分 Android 机型录出来的视频快速移动时有马赛克优先提高比特率而不是分辨率。3.3 录制完成后如何保存到系统相册Natcorder 生成的文件默认在应用沙盒或临时目录里它只管“写入”不管“入库”。所以录制结束后必须自己把文件拷贝到系统相册。Android 的推荐方式Android Q 及以上private void SaveToGallery(string filePath) { string directory Application.persistentDataPath /Recordings; System.IO.Directory.CreateDirectory(directory); string newPath directory /share_ System.DateTime.Now.ToString(yyyyMMddHHmmss) .mp4; System.IO.File.Move(filePath, newPath); // 通过 AndroidJavaClass 调用 MediaScanner using (var classUnity new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var currentActivity classUnity.GetStaticAndroidJavaObject(currentActivity)) using (var mediaScanner new AndroidJavaObject(android.media.MediaScannerConnection)) { // 这里可以调用 MediaScannerConnection.scanFile 通知系统扫描新文件 // 具体实现需拼接原生参数 } }iOS 端通常是用PHPhotoLibrary保存也可以直接调用UnitySendMessage唤起已经写好的原生 Objective-C 方法。需要提到的是iOS 的相册写入是异步的用户很可能在写入过程中切后台所以要做好“应用退到后台后回调可能延迟”的心理准备。提示不要把生成的视频文件直接丢在临时目录里让玩家自己去找绝大部分玩家并不知道沙盒路径。必须处理“保存到相册”或“分享面板”这一步这才是体验的最后一公里。3.4 录屏期间的内存与发热控制移动端录屏最容易被骂的就是“手机发烫”“录到一半被系统杀掉”。我实测下来这类问题和录屏方案的关系不是决定性因素而是由“分辨率 帧率 比特率 设备发热策略”共同决定的。我给的常规参数组合是休闲游戏分辨率 720p帧率 30比特率 5-8 Mbps中度图形游戏分辨率 1080p帧率 30比特率 10 Mbps高性能设备才建议 60 帧录制而且 60 帧录制时编码器压力会翻倍另外录制期间如果做大量加载操作比如切换场景、动态加载资源会发现掉帧明显。原因很简单GPU 既要渲染游戏画面又要处理编码器的纹理拷贝。所以如果有切换场景的需求最好在切换前自动结束一段录制切换完成后再开启新录制。4. 拍照 JPG 与 GIF 生成原理与代码实现4.1 JPG 拍照的两种路径拍照和录屏并不同一个思路。Natcorder 的JPGRecorder可以做到“单帧编码”也可以直接用 Unity 的ScreenCapture.CaptureScreenshot来实现。两条路都试过之后我更推荐根据需求来选如果只是“截取当前屏幕保存 JPG”建议直接ScreenCapture.CaptureScreenshot简单可靠Unity 引擎原生处理了 Android/iOS 的像素读取问题。如果是“有多个相机画面需要指定某个相机的 RenderTexture”或者“需要把多个画面拼成一张图”建议用JPGRecorder或者直接用Texture2D.ReadPixelsEncodeToJPG。使用JPGRecorder时节奏跟录 MP4 完全不同。因为只拍一帧你不需要CameraInput持续推流只需要构造 Recorder提交一帧然后结束var jpgRecorder new JPGRecorder(width, height, quality); ... // 从 RenderTexture 读取像素构造成 Texture2D var texture2D new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false); RenderTexture.active rt; texture2D.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); texture2D.Apply(); RenderTexture.active null; // 提交纹理给 JPGRecorder jpgRecorder.CommitFrame(texture2D, timestamp); jpgRecorder.FinishWriting(OnJpgSaved);这里timestamp可以用一个固定的值例如0L因为单帧 JPEG 不依赖时间轴。最终回调拿到的就是 JPG 文件的绝对路径。质量参数我习惯设置在 85 左右既能控制体积画面细节也不会损失太多。4.2 GIF 动图的实现要点GIF 是 Natcorder 的一个亮点也是很多人选它的理由。GIFRecorder的 API 跟MP4Recorder极其相似只不过输出容器格式是 GIF。关键代码模板using NatSuite.Recorders; public class GifController : MonoBehaviour { public void StartGifRecord(int width, int height) { gifRecorder new GIFRecorder(width, height); // GIF 不需要音频所以只用 CameraInput cameraInput new CameraInput(gifRecorder, new RealtimeClock(), Camera.main); } public void StopGifRecord() { cameraInput.Dispose(); gifRecorder.FinishWriting(OnGifSaved); } private void OnGifSaved(string path) { Debug.Log($GIF 已保存: {path}); } }和视频录制最大的区别在于GIFRecorder 内部默认会把每一帧都丢进调色板定量化流程生成颜色表并压缩。因此 GIF 文件比 MP4 大很多是正常的同样 5 秒、720p 的画面MP4 可能只有 3MBGIF 很容易冲到 30MB 以上。为了控制 GIF 体积建议做好三件事降低分辨率GIF 通常用于聊天窗口和社区评论没必要 1080p。推荐宽度 480 或 512甚至 360。降低帧率GIF 设置 15fps 就够用了。移动端聊天群里刷图30fps 的 GIF 除了让文件体积翻倍观感提升并不明显。缩短时长GIF 适合 3~5 秒的高光片段不要录 30 秒。4.3 帧间隔、颜色变化对 Gif 画质的影响GIF 的另一个特性是颜色数量有限通常每帧 256 色所以如果你录制的是渐变色很丰富的画面比如天空、霓虹灯、滤镜颜色带和波纹会比较明显。这个不是 Natcorder 的问题而是 GIF 格式本身的物理限制。我在实际项目里用过两种思路规避对固定 UI 和角色表演类场景GIF 观感还能接受如果是大面积粒子特效、颜色渐变建议改用短视频而不是 GIF。如果必须输出 GIF可以在录制前降低画面的饱和度/对比度减少颜色采样跳跃感或者在后期处理中加一层轻微噪点来掩盖色带。这不是通用做法但在我做社交分享场景时效果不错。5. 封装为一个通用的 MediaManager 管理器5.1 单例与生命周期Natcorder 的编码器实例是不能随意跨场景存活的因为它依赖不少原生资源。如果启动录屏后切换场景原相机被销毁CameraInput会引用空对象导致崩溃或录不到画面。我的做法是写一个场景内挂载的MediaManager单例并在游戏启动场景中初始化。切场景时不要销毁它而是通过事件机制让目标场景告诉它“当前主相机是谁”。public class MediaManager : MonoBehaviour { public static MediaManager Instance { get; private set; } private MP4Recorder currentRecorder; private CameraInput currentCameraInput; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); } public void BindCamera(Camera cam) { currentCameraInput?.Dispose(); currentCameraInput new CameraInput(currentRecorder, new RealtimeClock(), cam); } }这样做的目的是把“录制的动作”和“录制的内容”解耦。比如拍摄角色展示视频主界面传入角色相机拍 UI 教程传入 UI 相机。切换时只需重新绑定输入源底层编码器可以不中断。但实际开发中切换相机输入源时会出现短暂黑帧或时间戳跳跃我一般建议在 UI 上做一个“录制中”的遮罩避免玩家看到这一瞬间的问题。5.2 对外的接口怎么设计才不坑我习惯把对外接口收敛成四个方法避免业务层直接碰 Recorder 细节public void StartVideoRecording(int width, int height, bool withAudio); public void StopVideoRecording(Actionstring onComplete); public void CaptureJPG(Actionstring onComplete); public void StartGifRecording(int maxWidth); public void StopGifRecording(Actionstring onComplete);这个收敛的意义在于业务层只需要关心“什么时候开始、什么时候结束、文件在哪”完全不需要了解MP4Recorder和GIFRecorder的实现差异。后期如果要替换底层录制框架只需要改这个管理器的内部实现。5.3 常见的内存与性能优化点录屏期间的内存峰值主要来自Texture2D拷贝和编码器的内部缓冲。我实测中最大的内存尖峰出现在“把 RenderTexture 读回 Texture2D”这一步尤其是 4K 分辨率的读回峰值可以达到几百 MB。优化办法有几个读回前先辨别是否支持硬件采样某些平台可以直接把 RenderTexture 传给编码器不需要显式ReadPixels。Natcorder 的CameraInput底层已经做了这层优化所以尽量用CameraInput而不是自己手动每帧 ReadPixels。避免在录制过程中频繁new Texture2D如果必须手动处理纹理创建一个复用池反复使用同一张 Texture2D。使用AsyncGPUReadbackUnity 2020 以上支持异步 GPU 读回可以避免主线程卡顿。需要在支持的平台做分支处理支持AsyncGPUReadback的走异步不支持的走同步。Natcorder 自己会处理大部分平台差异但如果是自己写截图逻辑一定要记住这个优化点。6. 常见问题与排查实录6.1 常见问题速查表现象可能原因解决方案iOS 录屏后相册找不到文件Info.plist 缺少相册权限描述加入NSPhotoLibraryAddUsageDescriptionAndroid 录屏没有声音未申请RECORD_AUDIO权限运行时动态申请权限确认AudioInput构造参数正确录到一半黑屏相机被销毁或目标 RenderTexture 为空在切换场景/销毁相机前停止录制或重新绑定相机GIF 文件过大分辨率/帧率偏高用 480x480、15fps、时长 3-5 秒录屏时 UI 闪烁摄像机 targetTexture 与 UI 渲染冲突检查 UI 是否使用了 Overlay 模式优先用 Screen Space - Camera文件保存到沙盒但分享面板打不开路径不合法或文件未完成写入确认FinishWriting回调之后再做分享操作FinishWriting 后没有回调未正确 Dispose 所有 Input确保所有 CameraInput/AudioInput 都先 Dispose6.2 黑屏、卡顿、相册不显示的真实排查思路先说黑屏。之前有个项目在 Android 中端机上录制视频前 3 秒画面正常第 4 秒开始黑屏。日志显示 Recorder 没有报错但画面全是黑色。最终定位到原因是Android 部分设备在长时间高分辨率编码时底层 MediaCodec 会产生INFO_OUTPUT_BUFFERS_CHANGED或重新配置 Surface BuffersNatcorder 在特定版本上对这类信号处理不够完善。解决方法很粗暴但有效把录制分辨率从 1080p 降到 720p问题频率立刻下降。这是“硬件驱动层差异”带来的问题很难 100% 消灭只能通过参数调整和灰度测试来规避。再说卡顿。录制过程中每隔几秒掉一帧通常不是编码器性能不够而是主线程被某些同步操作卡住了。比如保存文件、压缩纹理、AndroidJavaObject 调用。遇到这类问题优先把“文件保存”“相册写入”放到协程或子线程中并且不要把录屏相关操作和资源加载放在同一帧。最后说相册不显示。这里特别容易把 Android 的“写入文件系统”和“媒体数据库扫描”弄混。哪怕文件已经存在于相册目录如果没通知 MediaScanner图库可能不会刷新。Android Q 以上也可以直接通过 MediaStore 的insert方法把文件插入系统媒体库路径会立刻可见。iOS 端用PHPhotoLibrary保存时会自动刷新但权限描述缺失会导致看上去“没反应”。6.3 跨平台行为边界与规避方式Natcorder 跨平台统一了接口但真正上线时还是会发现很多细碎差异iOS 上硬编码视频的色彩空间可能和 Android 不同人物肤色略有偏差。可以接受就接受不能接受就用后期调色统一。Android 部分厂商比如个别国产系统对后台录制有严格限制应用切后台超过几秒后编码器可能被系统挂起或杀掉。录制时长不宜过长且一定要在切后台时做保存处理。GIF 在 iOS 的UIImageView和 Android 的GifImageView中播放效果不同。如果只是临时文件优先用 MP4 而非 GIF能避免很多平台兼容问题。真机上用Microphone类录音需要确认同时开启了相机和麦克风权限。部分旧 Android 机型录制过程中麦克风被系统 UID 锁定导致无法同时进行其他语音功能。我在项目里会做一套“录制能力检测”启动时检查平台是否支持硬编码、是否授权麦克风、是否有足够的磁盘空间。不支持就禁用“录制声音”按钮或直接降级为无声音录制。这样可以把不可控因素挡在业务逻辑外。结尾一些实操后的个人体会每次有人问我录屏用什么我第一反应还是推荐 Natcorder但我会补一句它的上限取决于你项目对“玩家生成媒体”的流程设计。不要把它当成万能录屏器而是要理解它只是一条媒体编码管线。真正决定玩家体验的是你如何处理开始录制的时机、结束录制的时机、文件保存路径、权限失败兜底以及低端机上的参数动态调整。我踩过的坑里最不值得的一次是在 Android 低端机上硬上 1080p60 录制结果机子发热降频画面直接卡成幻灯片。后来加了“根据机型调整分辨率”的逻辑才把问题解决。如果你接这个插件强烈建议先跑通 720p30 的录制链路稳定后再慢慢尝试高分辨率和高帧率。有一点我到现在依然在提醒自己任何录屏相关的代码都别写在 Update 里一帧一帧裸调要统一通过 Manager 的接口走。不然代码经手三个人之后就再也找不到是谁在滥用编码器了。希望这篇经验能让你少走一点弯路至少在移动端录屏、拍照、GIF 生成这条路上不用再从零开始踩一遍。
返回列表