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

资讯详情

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

Unity3D实现摄像头画面采集与RTMP推流直播完整指南

Unity3D实现摄像头画面采集与RTMP推流直播完整指南 先说结论这套功能在 Unity 里完全能做而且不一定要依赖付费插件。核心链路就一句话把场景里指定 Camera 渲染出来的画面从 GPU 上读回来变成字节流交给底层编码器压成 H.264再封装成 FLV 格式推到 RTMP 服务器。录制则是同一条链路的“分支”编码后的数据同时写一份到本地文件就行。这篇文章是我把整套“Unity3D C# 采集摄像头画面 本地录制 RTMP 推流直播”从零跑到能用的完整记录包括方案取舍、关键代码、性能调优和踩坑实录。适合正在做虚拟直播、游戏录播、数字孪生远程展示、工业现场可视化这类项目的 Unity 开发者也适合第一次接触音视频方向的客户端同学。我会把每一步为什么这么做讲清楚你照着做至少能跑通一个 720p / 30fps 的稳定推流 Demo。1. 整体方案设计与链路梳理1.1 需求拆解采集、录制、推流其实是同一件事标题里说了三个需求采集摄像头画面、录制、推流 RTMP 直播。很多团队一开始会把它们拆成三个独立模块来搞结果做着做着发现到处是重复劳动。实际上这三个功能在数据流上是高度重合的。先看采集Unity 场景里的 Camera 每帧渲染结果本质是一张纹理我们要拿到这张纹理对应的像素数据。这是所有后续操作的源头。再看推流拿到像素数据后不能直接往上扔因为原始像素数据量太大。1080p 的 BGRA 原始画面一帧大约 8MB30 帧就是 240MB/s这带宽根本不是普通网络扛得住的。所以必须压缩编码常见选 H.264。编码出来的数据按 FLV/RTMP 格式封装通过 TCP 推给服务器。最后看录制录制本质就是把编码后的数据写进一个容器文件比如 MP4。和推流唯一的区别是“出口”不同一个写到网络 socket一个写到本地文件。所以录制完全可以复用编码模块编码器输出的 packet 同时投递给两个 muxer 就行了。我把链路整理成一句话你照着理解就行Camera 渲染 → RenderTexture → GPU 异步读取像素 → 内存中的 Byte[] → 交给 FFmpeg 编码器压成 H.264 → FLV/RTMP 封装 → 推到服务器同一份编码数据再走 MP4 muxer → 写入本地文件。1.2 为什么选 RTMP 而不是 WebRTC、SRTRTMP 这个协议本身有点“老”Adobe 时代的东西但直播行业对它的支持太完善了。国内云厂商的直播 CDN 基本都支持 RTMP 推流自建服务器也有 nginx-rtmp、SRS、MediaMTX 一堆开源方案而且它走 TCP在复杂网络下比 UDP 系协议更容易穿透和调优。对于 Unity 客户端来说把 RTMP 推流做到稳定是性价比最高的路线。有人会问WebRTC 不是延迟更低吗确实WebRTC 端到端延迟能做到几百毫秒但它需要信令服务器、ICE 穿透、SRTP 加密等一整套基础设施Unity 端最好直接用成熟的 WebRTC 原生库。如果你只是做普通直播没必要一上来就上 WebRTC。SRT 则更偏广电级传输场景服务器生态没有 RTMP 那么好找。所以我最终的选型是Unity 客户端跑 RTMP 推流播放端用 HTTP-FLV / HLS 拉流播放兼顾延迟和兼容性。1.3 从零到一的最小闭环长什么样在做任何代码之前建议先搭一个最小可跑的链路。我的做法是本地搭建一个 RTMP 服务器推荐 SRS 或 nginx-rtmp后面讲怎么起。用 OBS 或 ffmpeg 命令先手动推一路流到服务器确认服务器没问题。然后在 Unity 里从“采集画面”开始一点一点打通到“推流”。这步很重要否则你会陷入“到底是自己代码问题还是服务器问题”的泥潭。我用一条 ffmpeg 命令做“对照组”只要这条命令能推流成功就说明服务器正常剩下的坑都在 Unity 端。命令长这样ffmpeg -re -i test.mp4 -c copy -f flv rtmp://127.0.0.1/live/test播放端直接用 ffplay 拉流验证ffplay -fflags nobuffer rtmp://127.0.0.1/live/test如果能正常播放服务器这块就过了我们专心折腾 Unity。2. 核心模块拆分与关键技术选型2.1 画面采集RenderTexture AsyncGPUReadback 是首选Unity 里把 Camera 画面截下来的方案有好几种我把它们放一起对比过各有各的坑。方案原理优点缺点Camera.targetTexture ReadPixels先把渲染结果打到 RenderTexture再用 Texture2D.ReadPixels 读回 CPU实现最简单老项目可用ReadPixels 是同步操作1080p 下能把主线程卡出明显掉帧AsyncGPUReadbackGPU 异步把 RenderTexture 拷贝到 CPU不阻塞渲染线程性能最好URP/HDRP 都能用多一次异步回调处理不好容易跳帧屏幕/窗口捕获插件如 UnityCapture捕获整个应用窗口画面适合做录屏不能精确捕获指定 Camera叠加 UI 会和画面对不上OnRenderImage / CommandBuffer 截帧在渲染管线内截取中间画面可控性高需要自己管理 Blit 时机移动端兼容性较复杂我最终选了AsyncGPUReadback。原因很简单不卡主线程而且对 Render Pipeline 的依赖最小。URP 项目里OnRenderImage这套旧接口基本废了但AsyncGPUReadback在 Built-in、URP、HDRP 下都能正常工作。Unity 2019 以上版本它已经进入UnityEngine.Rendering命名空间直接用就行。这里有一个很容易忽略的点RenderTexture 的格式和读取时的 TextureFormat 一定要对齐。如果你在 C 编码层按 BGRA 处理但 RT 创建时用了 ARGB32读出来的颜色通道就是乱的。我项目里统一用RenderTextureFormat.BGRA32渲染读取时指定TextureFormat.RGBA32在 native 层再按实际传入格式做转换保证跨平台颜色不飘。另外如果项目跑在 WebGL 上AsyncGPUReadback不一定可用这时候要么用 Unity WebRTC 的方案要么退而求其次用 ReadPixels但要做好掉帧的心理准备。2.2 编码与推流绕不开的 FFmpeg Native 插件有了像素数据接下来是编码和推流。这块我一开始也想找纯 C# 的库省得写 C但实测下来路走不通。纯托管代码做 RTMP 协议封装其实不难难的是 H.264 编码。网上虽然有一些 C# 的 H.264 实现性能和稳定性都远达不到实时推流要求。所以正路只有一条用 FFmpeg 的 C/C 原生库封装一个 Native PluginC# 这边通过 P/Invoke 调用。FFmpeg 里完整的链路是这样的avformat_alloc_output_context2创建输出上下文指定输出格式为flvavcodec_find_encoder(AV_CODEC_ID_H264)找编码器通常用 libx264配置编码参数宽高、帧率、码率、GOP、像素格式avcodec_open2打开编码器每来一帧像素数据用sws_scale把 RGB/BGRA 转成 YUV420P填进AVFrameavcodec_send_frameavcodec_receive_packet得到编码后的AVPacketav_interleaved_write_frame写到 RTMP 服务器或本地文件你不需要把所有 FFmpeg 都导入工程只需要库文件里包含 libavcodec、libavformat、libavutil、libswscale、libx264 这几个模块即可。Windows 下我编译了ffmpeg.dll放进Assets/Plugins/x86_64/Android 下编了libffmpeg.so放进Assets/Plugins/Android/libs/arm64-v8a/iOS 这边因为东西比较多我是把 FFmpeg 静态库编进 Xcode 工程的这里不展开。还有一个点必须提醒FFmpeg 的 GPL/LGPL 版本问题。如果你用的是包含 libx264 的 GPL 版本发布商业项目时要注意开源协议合规如果只是内部工具或测试 Demo那随便用问题不大。商用你可以换 openh264 或者其他编码器接口层面的代码基本不用变。2.3 录制功能的两种落地方式录制和推流复用编码链路是理想情况但实际工程里还要看你能不能接受多写一套 muxer。我的建议是如果推流和录制是同时进行的最好在 Native 层让一路编码后的AVPacket同时写两个AVFormatContext一个走 RTMP 网络流一个走 MP4 文件。这样最省 CPU。如果录制只是“顺便”做一下不追求完美那可以用更流氓的方案在 C# 里直接拉起一个 ffmpeg 命令行子进程从标准输入stdin把原始像素数据喂进去让 ffmpeg 自己完成编码和写文件。这在 Windows 原型阶段特别省事但移动端不可用只能当调试工具。我最终选择了 Native 层同时写两个 muxer 的方案但原型阶段确实是用 ffmpeg 子进程偷懒的后面专门有一节讲这个写法。2.4 测试服务器搭建SRS 和 nginx-rtmp 二选一本地测试我用过 SRS 和 nginx-rtmp两者都可以但推荐 SRS文档清晰、Docker 一键启动、支持 HTTP-FLV 拉流方便调试。命令如下docker run -p 1935:1935 -p 8080:8080 ossrs/srs:4 ./objs/srs -c conf/rtmp.conf启动后推流地址填rtmp://127.0.0.1/live/test播放地址可以用http://127.0.0.1:8080/live/test.flv也可以用 VLC 直接拉rtmp://127.0.0.1/live/test。用 nginx-rtmp 的话需要自己在nginx.conf里加一段rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; } } }配置完重启 nginx 就能用。我建议测试阶段把推流和播放在同一台机器上能减少网络排查的干扰因素。3. 实操过程与核心环节实现3.1 工程目录规划写代码之前先规划好目录。我的工程结构大致是这样Assets/ ├── Plugins/ │ ├── x86_64/ │ │ └── FFmpegPublisher.dll # Windows 原生插件 │ └── Android/ │ └── libs/ │ └── arm64-v8a/ │ └── libFFmpegPublisher.so ├── Scripts/ │ └── Streaming/ │ ├── FrameGrabber.cs # Camera 画面采集 │ ├── NativeBridge.cs # C# 与原生插件的 P/Invoke 桥接 │ ├── RtmpPublisher.cs # 推流主逻辑调度帧数据 │ └── StreamConfig.cs # 配置参数分辨率、码率、URL 等 └── StreamingDemo.unity # 测试场景 NativeSrc/ ├── FFmpegPublisher.h ├── FFmpegPublisher.cpp └── CMakeLists.txt原生插件独立放在NativeSrc里用 CMake 编译而不是放在 Unity 工程里直接编译。这样解耦比较干净C 侧也不用装 Unity 编辑器依赖。发布时把编译好的库拷到对应平台目录即可。3.2 第一步把 Camera 画面读回 CPU先写FrameGrabber.cs负责把目标 Camera 渲染到 RenderTexture然后异步读回像素。核心代码我给的是能跑的版本但你要根据自己项目的渲染管线微调。using UnityEngine; using UnityEngine.Rendering; public class FrameGrabber : MonoBehaviour { public Camera targetCamera; public int width 1280; public int height 720; private RenderTexture _rt; private AsyncGPUReadbackRequest _readbackRequest; private bool _isPending; private System.Actionbyte[] _onFrameReady; void Start() { _rt new RenderTexture(width, height, 0, RenderTextureFormat.BGRA32); _rt.name CameraCaptureRT; targetCamera.targetTexture _rt; } void LateUpdate() { if (_isPending _readbackRequest.done) { var nativeArray _readbackRequest.GetDatabyte(); var buffer new byte[nativeArray.Length]; nativeArray.CopyTo(buffer); _onFrameReady?.Invoke(buffer); _isPending false; } if (!_isPending) { _readbackRequest AsyncGPUReadback.Request(_rt, 0, TextureFormat.RGBA32); _isPending true; } } public void SetOnFrameReady(System.Actionbyte[] callback) { _onFrameReady callback; } void OnDestroy() { targetCamera.targetTexture null; _rt.Release(); Destroy(_rt); } }说明几个关键点为什么用LateUpdate而不是UpdateUpdate里相机可能还没渲染完成LateUpdate保证这一帧的渲染已经结束读回来的才是完整画面。AsyncGPUReadback.Request的第二个参数 0 表示读取第 0 个 mip 层我们不需要 mipmap所以传 0。GetDatabyte()返回的是NativeArraybyteToArray()会有一次拷贝原型阶段无所谓但高频推流时最好用对象池复用byte[]避免 GC 压力。后面“常见问题”里我会讲怎么优化。如果你用的渲染管线比较特殊尤其在 HDRP 下相机渲染到 RenderTexture 时可能要考虑实际色彩空间。我默认项目用的是 Linear 色彩空间、RT 创建时不带 sRGB 标记最终颜色偏灰的话可以在相机上挂一个简单的后处理或者把读取出来的像素做一次颜色空间转换。这个问题比较看项目配置先跑通链路再细抠。3.3 第二步Native 插件里的编码和推流核心C 侧代码量比较大我这里不贴全部只把最核心的两个函数写出来一个是初始化一个是送帧。extern C __declspec(dllexport) void* FFP_Create() { Publisher* p new Publisher(); return p; } extern C __declspec(dllexport) bool FFP_Init(void* handle, const char* url, int width, int height, int fps, int bitrate) { Publisher* p static_castPublisher*(handle); // 1. 创建 FLV 输出上下文 avformat_alloc_output_context2(p-fmtCtx_, nullptr, flv, url); // 2. 创建 H.264 编码器 const AVCodec* codec avcodec_find_encoder(AV_CODEC_ID_H264); p-codecCtx_ avcodec_alloc_context3(codec); p-codecCtx_-width width; p-codecCtx_-height height; p-codecCtx_-time_base { 1, fps }; p-codecCtx_-framerate { fps, 1 }; p-codecCtx_-pix_fmt AV_PIX_FMT_YUV420P; p-codecCtx_-bit_rate bitrate; p-codecCtx_-gop_size fps * 2; // 2秒一个关键帧 p-codecCtx_-max_b_frames 0; // 直播场景建议关掉B帧降低延迟 // 3. 打开编码器 avcodec_open2(p-codecCtx_, codec, nullptr); // 4. 创建视频流并配置参数 p-stream_ avformat_new_stream(p-fmtCtx_, codec); p-stream_-time_base { 1, fps }; avcodec_parameters_from_context(p-stream_-codecpar, p-codecCtx_); // 5. 打开网络输出 avio_open(p-fmtCtx_-pb, url, AVIO_FLAG_WRITE); // 6. 写 FLV 头 avformat_write_header(p-fmtCtx_, nullptr); // 7. 准备像素格式转换Unity 传进来的数据按 RGBA 处理 p-swsCtx_ sws_getContext(width, height, AV_PIX_FMT_RGBA, width, height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, nullptr, nullptr, nullptr); return true; }送帧的逻辑也不复杂每次从 C# 拿到一帧像素数据后先sws_scale转成 YUV420P然后交给编码器。extern C __declspec(dllexport) bool FFP_SendVideoFrame(void* handle, const uint8_t* data, int size, int64_t ptsMs) { Publisher* p static_castPublisher*(handle); // 1. 把 RGBA 数据转成 YUV420P uint8_t* srcData[1] { const_castuint8_t*(data) }; int srcLinesize[1] { p-codecCtx_-width * 4 }; sws_scale(p-swsCtx_, srcData, srcLinesize, 0, p-codecCtx_-height, p-frame_-data, p-frame_-linesize); // 2. 设置 PTS这里用毫秒作为输入按期望帧率换算 p-frame_-pts av_rescale_q(ptsMs, { 1, 1000 }, p-stream_-time_base); p-frame_-width p-codecCtx_-width; p-frame_-height p-codecCtx_-height; p-frame_-format AV_PIX_FMT_YUV420P; // 3. 编码并写包 avcodec_send_frame(p-codecCtx_, p-frame_); AVPacket pkt; av_init_packet(pkt); while (avcodec_receive_packet(p-codecCtx_, pkt) 0) { pkt.pts av_rescale_q(pkt.pts, p-codecCtx_-time_base, p-stream_-time_base); pkt.dts av_rescale_q(pkt.dts, p-codecCtx_-time_base, p-stream_-time_base); pkt.stream_index p-stream_-index; av_interleaved_write_frame(p-fmtCtx_, pkt); av_packet_unref(pkt); } return true; }PTS是很容易出错的地方。很多同学第一次写推流画面老是卡顿、跳帧、花屏多半是 PTS 不对。这里的逻辑是C# 侧负责按帧间隔提供一个单调递增的毫秒时间戳Native 层再用av_rescale_q把毫秒换算成视频流的 time_base。注意ptsMs必须是单调递增而且尽量均匀如果你在 C# 里偷懒每帧传Time.time * 1000某些平台上浮点误差累计会造成 PTS 回退播放端就会抽风。3.4 第三步C# 桥接与主逻辑调度有了 Native 接口C# 侧就很简单了。NativeBridge.cs专门管 P/Invokeusing System; using System.Runtime.InteropServices; public static class NativeBridge { [DllImport(FFmpegPublisher)] public static extern IntPtr FFP_Create(); [DllImport(FFmpegPublisher, CharSet CharSet.Ansi)] public static extern bool FFP_Init(IntPtr handle, string url, int width, int height, int fps, int bitrate); [DllImport(FFmpegPublisher)] public static extern bool FFP_SendVideoFrame(IntPtr handle, byte[] data, int size, long ptsMs); [DllImport(FFmpegPublisher)] public static extern void FFP_Stop(IntPtr handle); [DllImport(FFmpegPublisher)] public static extern void FFP_Destroy(IntPtr handle); }这里有个小坑C# 默认字符串在 P/Invoke 时会按 UTF-16 转成 ANSI 再把指针传过去如果 RTMP 地址里带中文或特殊字符很容易乱码。我的做法是把 URL 用byte[]传进去或者直接用[MarshalAs(UnmanagedType.LPUTF8Str)]标记string参数[DllImport(FFmpegPublisher)] public static extern bool FFP_Init(IntPtr handle, [MarshalAs(UnmanagedType.LPUTF8Str)] string url, int width, int height, int fps, int bitrate);然后RtmpPublisher.cs负责把两个模块串起来public class RtmpPublisher : MonoBehaviour { public FrameGrabber grabber; public string rtmpUrl rtmp://127.0.0.1/live/test; public int fps 30; public int bitrate 2_000_000; public int width 1280; public int height 720; private IntPtr _handle; private bool _started; private float _frameInterval; private float _lastSendTime; private long _ptsMs; void Start() { _handle NativeBridge.FFP_Create(); bool ok NativeBridge.FFP_Init(_handle, rtmpUrl, width, height, fps, bitrate); if (!ok) { Debug.LogError(推流初始化失败); return; } _frameInterval 1f / fps; _lastSendTime 0f; _started true; grabber.SetOnFrameReady(OnFrameReady); } private void Update() { if (!_started) return; // 推流节奏不跟渲染帧走而是按固定帧率走 _lastSendTime Time.deltaTime; if (_lastSendTime _frameInterval) { _lastSendTime 0f; _ptsMs (long)(_frameInterval * 1000f); // 真正的发送在 OnFrameReady 里做 } } private void OnFrameReady(byte[] frameData) { if (!_started) return; NativeBridge.FFP_SendVideoFrame(_handle, frameData, frameData.Length, _ptsMs); } void OnDestroy() { _started false; if (_handle ! IntPtr.Zero) { NativeBridge.FFP_Stop(_handle); NativeBridge.FFP_Destroy(_handle); _handle IntPtr.Zero; } } }这段代码的目的是让你理解整体结构实际产品里还缺几个东西网络断线重连、音视频同步我这里没加音频、断流后的状态上报。后面我会在常见问题里补充。3.5 第四步本地录制怎么接进去前面说录制有两种方式我先讲最简单的原型方案用 ffmpeg 子进程录制。这种方案适合 Windows 调试省去在 Native 插件里同时开两个 muxer 的工作量。思路是Unity 采集到一帧像素后写到Process.StandardInput.BaseStream而 ffmpeg 子进程负责从 stdin 读原始像素并编码成 mp4。伪代码是这样string ffmpegPath Application.streamingAssetsPath /ffmpeg.exe; ProcessStartInfo psi new ProcessStartInfo(ffmpegPath, -f rawvideo -pix_fmt bgra -s 1280x720 -r 30 -i - -c:v libx264 -preset ultrafast -pix_fmt yuv420p record.mp4); psi.UseShellExecute false; psi.RedirectStandardInput true; Process proc Process.Start(psi); // 每帧数据写入 proc.StandardInput.BaseStream.Write(frameData, 0, frameData.Length); proc.StandardInput.BaseStream.Flush(); // 结束时 proc.StandardInput.Close(); proc.WaitForExit();注意-pix_fmt bgra要根据你 C# 传过去的实际像素格式改如果颜色不对检查这一行。子进程方案的优点是少写很多 C 代码缺点也很明显只能本地用移动端别想而且启动 ffmpeg 进程有额外开销。产品化的话还是老老实实在 Native 插件里同时开两个AVFormatContext一个写 RTMP一个写 MP4。两个 muxer 共用同一个编码器输出CPU 开销增加并不多。3.6 第五步直播参数怎么定参数这个事很多人直接照抄 OBS 的默认值结果在自己项目里卡成狗。我整理了一个常用的参数表你可以按项目场景选分辨率帧率码率适用场景720p301.5 Mbps普通 3D 场景、人物访谈、低码率优先1080p304 Mbps普通游戏直播、产品演示1080p608 Mbps高速运动画面、竞技游戏、仿真模拟2K308~12 Mbps数字孪生大屏、高清监控画面内容越复杂、运动越快码率要求越高。如果你的场景是静止的监控画面1.5Mbps 的 720p 完全够用如果有大面积草、树叶这类高频细节码率要往上加否则画面全是马赛克。编码器侧我建议把max_b_frames设为 0也就是不用 B 帧直播场景延迟会更低。GOP 大小我习惯设置成fps * 2两秒一个关键帧兼顾秒开和码率。如果对延迟要求很高可以缩短到 1 秒一个关键帧但码率会相应上涨。4. 常见问题排查与避坑实录4.1 推流失败Could not connect to server这个报错 90% 不是代码问题先按顺序排查服务器起没起本地执行ss -lntp | grep 1935看看端口有没有监听。推流地址写没写对RTMP 地址格式是rtmp://ip:port/app/streamKeyapp和streamKey是服务器端定义的不能乱填。SRS 默认 app 是livestream key 随便填但播放时要保持一致。防火墙放行 1935 端口没有Windows 防火墙、云服务器安全组都要放行。如果以上都没问题再用 ffmpeg 命令推流测试如果 ffmpeg 能推上去而你 Unity 推不上去那就是 Unity 端的问题查 Native 插件初始化的返回值。4.2 画面黑屏或颜色异常黑屏大概率是没拿到像素数据。检查三处Camera.targetTexture是否设置成功、RenderTexture 是否被意外释放、AsyncGPUReadback回调里GetData的长度是否为 0。还有一种情况是相机没有渲染到 RT比如你把targetTexture设置在一个不活跃的相机或者相机被裁剪到了层外颜色异常最常见的是红蓝互换或者偏绿原因就是像素格式没对上。Unity 里 RT 创建格式和读取格式、以及 Native 层sws_scale的输入格式这三者必须一致。我建议你在 Native 层加一个pixelFormat参数调试时传AV_PIX_FMT_RGBA和AV_PIX_FMT_BGRA各试一版哪个颜色对用哪个。平台不同默认格式是有差异的别想在代码里一劳永逸。4.3 延迟越来越高甚至卡顿直播延迟和播放缓冲强相关但如果你发现延迟是从 Unreal 到越来越卡那大概率是推流速度跟不上采集速度。我遇到过的情况是 Unity 主线程偶尔掉帧但我还在按 30fps 的节奏往 Native 层送数据导致数据慢慢堆积。解决思路有两个一是 Native 层维护一个发送队列如果队列积压超过 N 帧就丢掉非关键帧也就是只丢 P 帧不丢 IDR 帧保证实时性二是 C# 侧严格按固定帧率送帧不要渲染多少帧就推多少帧。简单实现在OnFrameReady里比较当前时间和上一次发送的实际时间没到1/fps就跳过这一帧。跳过普通帧影响不大播放端最多微卡一下。播放端建议用ffplay -fflags nobuffer验证延迟VLC 默认缓冲太大不能用来测真实延迟。4.4 内存上涨和 GC 抖动这个问题经常出现在采集侧。AsyncGPUReadback.GetDatabyte()每次拷贝出来的数组都是新的如果按 30fps 跑等于每秒产生 30 个byte[]对象给 GC 压力。我用对象池解决了这个问题预分配几个大字节数组读回来的数据拷贝到池子里面Native 层立刻处理处理完归还。如果你 Native 层FFP_SendVideoFrame是同步的那么数据拷贝完函数返回后NativeArray和托管byte[]都能安全释放。一定要避免的是把GetData返回的NativeArray直接传到 P/Invoke 里再用异步方式处理稍不留神就是野指针。4.5 录制的 MP4 文件打不开MP4 容器有一个特点文件索引 moov 信息默认写在文件末尾必须正常结束录制才能完整写入。如果你直接杀掉进程或者视频还没 Stop 就断电文件基本就废了。所以录制结束一定要走正规流程av_write_trailer→avio_close在 Unity 里就是OnDestroy里调FFP_Stop时要保证 native 层把 trailer 完整写出来。我用子进程录制时也必须在关闭 stdin 后等待 ffmpeg 自己退出不能直接 Kill。如果你需要随时可读的录制文件可以考虑用fMP4fragmented MP4格式它把 moov 拆成多个 fragment 写进文件异常中断时前面的片段也能播。FFmpeg 里指定movflags frag_keyframeempty_moov就能做到。4.6 移动端 IL2CPP 的坑Android 上用 IL2CPP 发布时最常遇到的问题是DllNotFoundException一看日志是找不到libFFmpegPublisher.so。确认三件事so 文件放在了Assets/Plugins/Android/libs/arm64-v8a/下、Unity 的 Player Settings 里勾选了 ARM64、FFmpeg 本身没有依赖缺失。另外 Android 上读取摄像头或麦克风权限需要你在代码里动态申请但这里有个容易忽略的点即使你只是推流场景画面不需要麦克风如果你的 native lib 里链接了 FFmpeg 的音频相关库某些机型可能仍然会触发录音权限检查导致崩溃。解决方式是裁剪 FFmpeg 模块只编视频相关部分。一点收尾的实在话这套链路我前前后后折腾了两三周大部分时间不是花在 RTMP 协议上而是耗在格式对齐、PTS 节奏、线程模型和平台差异这些细碎问题上。如果你也想做类似的事我真心建议先跑通一条最小闭环管线用的默认配置就好不要一上来就追求 4K 60 帧、低延迟、跨平台一把梭。跑通了再慢慢把音频、断线重连、码率自适应这些“产品级”功能加上去。最后分享一个小技巧把推流地址封装成一个配置表不要到处写死字符串。我后来把所有 RTMP 地址收敛成GetRtmpUrl(host, app, streamKey)这样一个方法测试切换服务器只改一行配置。这个习惯帮我省了不少半夜上线的麻烦。祝你的画面推流一次就通。
返回列表