Unity3D 使用 LibVLCSharp 播放 RTSP 视频流实战
1. 项目缘起与整体设计思路做过安防监控或者工业上位机项目的朋友大概率都遇到过这个需求在 Unity3D 里实时播放网络摄像头的 RTSP 视频流。不管是做数字孪生、智慧园区、工厂可视化还是 VR 看监控这个需求出现的频率高得离谱。但 Unity3D 原生对 RTSP 的支持几乎为零VideoPlayer 组件只吃本地文件和有限的 HTTP 流直接喂 RTSP 地址进去基本就是黑屏或者报错。我最早接这类需求的时候试过好几种方案。一开始想用 FFmpeg 命令行拉流转成图片序列再在 Unity 里刷新延迟高得没法看而且 CPU 占用感人。后来试过把 RTSP 转成 RTMP 再走 Unity 的 VideoPlayer中间多了一层转发稳定性差断流重连还得自己写一堆逻辑。最后落到LibVLCSharp这个方案上实测下来是 C# 生态里做 RTSP 播放最省心、延迟最低、兼容性最好的路子。这个项目的核心思路很简单用 LibVLCSharp 在 Unity 里创建一个 VLC 播放器实例把 RTSP 地址丢给它VLC 解码后把视频帧回调给 Unity 的 Texture2D最终渲染到 RawImage 或者材质球上。整个链路是摄像头 RTSP 流 → LibVLC 解码 → 帧数据回调 → Unity Texture2D 更新 → UI/材质渲染。听起来步骤不少但实际代码量很小核心逻辑两百行以内能搞定。为什么选 LibVLCSharp 而不是其他方案我列个对比你就明白了方案延迟稳定性断流重连开发难度跨平台Unity VideoPlayer 直连不支持----FFmpeg 命令行转图片高1-3秒一般需自己写中好RTSP 转 RTMP 再播放中500ms-1s差需自己写中一般LibVLCSharp低100-300ms好内置低好LibVLCSharp 本质上是把 VLC 的播放核心封装成了 .NET 库VLC 本身对 RTSP 的支持是经过十几年打磨的各种摄像头厂商的私有 RTSP 实现它基本都兼容。海康、大华、宇视这些主流品牌的 RTSP 地址格式它都能直接吃。而且 VLC 内部自带缓冲管理和重连机制网络抖动的时候不会像自己写的拉流代码那样直接崩掉。这个方案适合谁如果你是有一定 C# 基础、正在做 Unity3D 项目、需要接入网络摄像头视频流的开发者那这篇内容就是给你写的。不需要你懂 FFmpeg 命令不需要你搭流媒体服务器只要会写 C# 脚本、会在 Unity 里拖组件就能跑起来。2. 环境准备与核心依赖解析2.1 Unity 版本与项目设置Unity 版本我建议用2021.3 LTS 或更高。2021.3 是长期支持版稳定性好而且对 .NET Standard 2.1 的支持比较完善LibVLCSharp 依赖的库能正常加载。我用过 2019.4 也能跑但偶尔会遇到 DLL 加载顺序的问题2021.3 之后就没再碰到过。项目设置里有几个关键点要注意。首先Api Compatibility Level 要设成 .NET Standard 2.1在 Edit → Project Settings → Player → Other Settings 里改。如果设成 .NET Framework某些 LibVLCSharp 的依赖会找不到。其次Scripting Backend 建议用 MonoIL2CPP 也能跑但配置麻烦一些Mono 在编辑器里调试方便出包的时候再切 IL2CPP 也不迟。还有一个坑我踩过Color Space 用 Linear 还是 Gamma。LibVLC 回调出来的帧数据是 BGRA 格式如果你项目是 Linear 空间直接塞进 Texture2D 颜色会偏暗。解决办法是在创建 Texture2D 的时候指定linear: false或者手动做一次颜色空间转换。我一般直接设成 Gamma 空间省事除非项目美术要求必须 Linear。2.2 LibVLCSharp 与 VideoLAN.LibVLC 的安装LibVLCSharp 在 NuGet 上有两个包需要装LibVLCSharp和VideoLAN.LibVLC.Windows如果你在 Windows 上开发。Mac 对应的是 VideoLAN.LibVLC.MacLinux 对应 VideoLAN.LibVLC.Linux。在 Unity 里装 NuGet 包不能直接像 Visual Studio 那样右键安装需要用NuGetForUnity这个插件。装好 NuGetForUnity 之后在 NuGet 窗口里搜 LibVLCSharp安装最新稳定版。截至我写这篇内容的时候LibVLCSharp 最新是 3.8.xVideoLAN.LibVLC.Windows 是 3.0.20 左右。装完之后你会发现在Assets/Packages目录下多了一堆东西。关键的是libvlc.dll、libvlccore.dll和plugins文件夹。这三个东西必须放在最终可执行文件同级目录下编辑器里跑的时候 Unity 会自动处理但出包的时候要手动拷贝。我一般写个 PostBuild 脚本自动拷贝省得每次忘了。注意VideoLAN.LibVLC.Windows 包很大装完之后项目体积会暴涨 100MB 以上。如果嫌大可以只保留plugins目录下access、codec、demux、video_output这几个子目录其他用不到的可以删掉能省一半空间。2.3 摄像头 RTSP 地址格式速查不同品牌的摄像头 RTSP 地址格式不一样我整理了一份常用的品牌RTSP 地址格式默认端口海康威视rtsp://admin:密码IP:554/Streaming/Channels/101554大华rtsp://admin:密码IP:554/cam/realmonitor?channel1subtype0554宇视rtsp://admin:密码IP:554/video1554通用 ONVIFrtsp://IP:554/onvif1554大华 NVRrtsp://admin:密码IP:554/cam/realmonitor?channel通道号subtype0554海康的101代表通道 1 主码流102是子码流。大华的subtype0是主码流subtype1是子码流。做 Unity 播放建议用子码流分辨率低、码率小、延迟低主码流 4K 的话解码压力大除非你确实需要高清。测试的时候如果没有真实摄像头可以用公开的 RTSP 测试地址或者用mediamtx原 rtsp-simple-server在本地搭一个。mediamtx 是个单文件可执行程序下载下来直接跑默认监听 8554 端口推流上去就能用。我本地测试经常用它比接真实摄像头方便。3. 核心代码实现与关键细节3.1 初始化 LibVLC 与 MediaPlayer整个流程的第一步是初始化 LibVLC 核心对象。这里有个关键点LibVLC 实例全局只能有一个不要在每个脚本里都 new 一个否则会出各种诡异的内存问题。我一般用一个单例管理器来持有它。using LibVLCSharp.Shared; using UnityEngine; public class VLCManager : MonoBehaviour { private static VLCManager _instance; public static VLCManager Instance { get { if (_instance null) { GameObject go new GameObject(VLCManager); _instance go.AddComponentVLCManager(); DontDestroyOnLoad(go); } return _instance; } } private LibVLC _libVLC; public LibVLC LibVLC _libVLC; private void Awake() { if (_instance ! null _instance ! this) { Destroy(gameObject); return; } _instance this; DontDestroyOnLoad(gameObject); Core.Initialize(); _libVLC new LibVLC( --no-audio, --no-xlib, --rtsp-tcp, --network-caching300, --clock-jitter0, --clock-synchro0 ); } private void OnDestroy() { _libVLC?.Dispose(); } }这里几个参数我解释一下为什么这么设--no-audio监控视频一般不需要声音关掉省资源。--no-xlibLinux 下避免 X11 依赖Windows 下无所谓但加上没坏处。--rtsp-tcp强制 RTSP 走 TCP 传输。默认是 UDPUDP 在网络抖动时丢包会导致花屏TCP 虽然延迟略高但稳定得多。这个参数是监控场景的必选项。--network-caching300网络缓存 300ms。太小容易卡顿太大延迟高。300ms 是我实测下来延迟和流畅度比较平衡的值。--clock-jitter0和--clock-synchro0关闭时钟同步减少因为音视频同步带来的额外延迟。监控场景不需要严格同步。3.2 视频帧回调与 Texture2D 更新MediaPlayer 创建好之后关键是把视频帧拿出来。LibVLCSharp 提供了SetVideoFormat和SetVideoCallbacks两个回调前者告诉 VLC 你要什么格式的帧后者在每一帧到达时被调用。using System; using System.Runtime.InteropServices; using LibVLCSharp.Shared; using UnityEngine; public class RTSPPlayer : MonoBehaviour { public string rtspUrl rtsp://admin:password192.168.1.100:554/Streaming/Channels/102; public UnityEngine.UI.RawImage targetImage; private MediaPlayer _mediaPlayer; private Texture2D _texture; private byte[] _frameBuffer; private GCHandle _frameHandle; private int _videoWidth 1920; private int _videoHeight 1080; private readonly object _frameLock new object(); private bool _frameReady false; private void Start() { _texture new Texture2D(_videoWidth, _videoHeight, TextureFormat.BGRA32, false, false); targetImage.texture _texture; _mediaPlayer new MediaPlayer(VLCManager.Instance.LibVLC); _mediaPlayer.SetVideoFormat(BGRA, (uint)_videoWidth, (uint)_videoHeight, (uint)(_videoWidth * 4)); _mediaPlayer.SetVideoCallbacks(LockCallback, null, DisplayCallback); using (var media new Media(VLCManager.Instance.LibVLC, new Uri(rtspUrl))) { media.AddOption(:rtsp-tcp); media.AddOption(:network-caching300); _mediaPlayer.Play(media); } } private IntPtr LockCallback(IntPtr opaque, IntPtr planes) { lock (_frameLock) { if (_frameBuffer null || _frameBuffer.Length ! _videoWidth * _videoHeight * 4) { if (_frameHandle.IsAllocated) _frameHandle.Free(); _frameBuffer new byte[_videoWidth * _videoHeight * 4]; _frameHandle GCHandle.Alloc(_frameBuffer, GCHandleType.Pinned); } Marshal.WriteIntPtr(planes, _frameHandle.AddrOfPinnedObject()); return _frameHandle.AddrOfPinnedObject(); } } private void DisplayCallback(IntPtr opaque, IntPtr picture) { lock (_frameLock) { _frameReady true; } } private void Update() { if (_frameReady _frameBuffer ! null) { lock (_frameLock) { _texture.LoadRawTextureData(_frameBuffer); _texture.Apply(false); _frameReady false; } } } private void OnDestroy() { _mediaPlayer?.Stop(); _mediaPlayer?.Dispose(); if (_frameHandle.IsAllocated) _frameHandle.Free(); } }这段代码有几个关键设计点值得展开说。为什么用 BGRA 而不是 RGBVLC 内部解码出来最接近的格式就是 BGRA用这个格式转换开销最小。如果设成 RGBVLC 内部会多做一次转换浪费 CPU。Unity 的 TextureFormat.BGRA32 正好对应直接 LoadRawTextureData 就行。为什么要用 GCHandle 固定内存VLC 回调是在非托管线程里执行的它需要一个稳定的内存指针来写帧数据。如果直接用 C# 数组GC 移动内存的时候指针就失效了会直接崩溃。GCHandle.Alloc 配合 Pinned 类型把数组钉在内存里保证地址不变。为什么用锁而不是直接更新 TextureVLC 的 DisplayCallback 在后台线程触发而 Unity 的 Texture2D 操作必须在主线程。所以用_frameReady标志位做线程间通信Update 里检查标志位再更新纹理。锁的粒度要小只保护标志位和缓冲区切换不要在锁里做 Texture 操作。分辨率问题上面代码写死了 1920x1080实际摄像头可能是其他分辨率。更稳妥的做法是先不设 SetVideoFormat让 VLC 自己协商然后在回调里根据实际尺寸重建 Texture。但这样代码复杂一些如果摄像头分辨率固定写死更简单。3.3 断流重连与状态监控监控场景最怕的就是网络抖动导致断流画面卡住不动。LibVLC 本身有重连机制但默认行为不一定符合预期需要自己加一层监控。private float _lastFrameTime; private float _reconnectCooldown 0f; private void Update() { if (_frameReady _frameBuffer ! null) { lock (_frameLock) { _texture.LoadRawTextureData(_frameBuffer); _texture.Apply(false); _frameReady false; _lastFrameTime Time.realtimeSinceStartup; } } // 超过3秒没有新帧判定为断流 if (Time.realtimeSinceStartup - _lastFrameTime 3f _reconnectCooldown 0f) { Debug.LogWarning(RTSP 流超时尝试重连...); Reconnect(); _reconnectCooldown 5f; } if (_reconnectCooldown 0f) _reconnectCooldown - Time.deltaTime; } private void Reconnect() { _mediaPlayer?.Stop(); using (var media new Media(VLCManager.Instance.LibVLC, new Uri(rtspUrl))) { media.AddOption(:rtsp-tcp); media.AddOption(:network-caching300); _mediaPlayer.Play(media); } _lastFrameTime Time.realtimeSinceStartup; }这个重连逻辑的核心是用最后一帧到达的时间做超时判断。3 秒是我实测下来比较合理的阈值太短容易误判比如摄像头关键帧间隔大太长用户能明显感觉到卡顿。重连冷却 5 秒是防止频繁重连把摄像头搞崩有些摄像头并发连接数有限制疯狂重连会被拉黑。实操心得海康摄像头默认同时只允许 6 路 RTSP 连接如果项目里要播多路记得用子码流并且控制重连频率。大华的限制宽松一些但也不建议超过 10 路。4. 常见问题排查与性能优化4.1 黑屏、花屏、绿屏问题速查这类问题我遇到过太多次了基本逃不出下面几种原因现象可能原因排查方法解决方案完全黑屏RTSP 地址错误用 VLC 桌面版测试同一地址检查用户名密码、通道号黑屏但有声音视频格式不兼容查看 VLC 日志换 BGRA 格式或降低分辨率花屏、马赛克UDP 丢包抓包看丢包率强制--rtsp-tcp绿屏颜色格式不匹配检查 TextureFormat改用 BGRA32 或做颜色转换画面偏暗Linear 色彩空间检查项目 Color SpaceTexture2D 构造时 linear 设 false卡在第一帧关键帧未到达等待或重启流设置--rtsp-frame-buffer-size黑屏最常见的原因是 RTSP 地址不对。我见过有人把Streaming/Channels/101写成Streaming/Channels/1少了个 01 就连不上。还有人密码里有特殊字符没做 URL 编码比如要写成%40。排查的时候先用 VLC 桌面版或者 PotPlayer 试一下地址能播再往 Unity 里塞。花屏问题九成是 UDP 丢包。RTSP 默认走 UDP网络稍微差一点就丢包VLC 解码出来就是马赛克。强制 TCP 之后基本能解决。如果 TCP 还花屏那就是摄像头编码有问题试试降低码率或者换子码流。4.2 延迟优化实战监控场景对延迟很敏感我做过一个对比测试不同参数组合下的端到端延迟配置平均延迟流畅度默认 UDP 1000ms 缓存1200ms好TCP 1000ms 缓存1300ms很好TCP 300ms 缓存500ms好TCP 100ms 缓存300ms一般偶有卡顿TCP 0ms 缓存150ms差经常卡最终我一般用TCP 300ms 缓存这个组合延迟 500ms 左右对于大多数监控场景够用了。如果要求极致低延迟可以降到 100ms但要接受偶尔的卡顿。还有一个隐藏的延迟来源是摄像头的关键帧间隔。如果摄像头设的 GOP 是 50 帧25fps 下就是 2 秒一个关键帧VLC 必须等到关键帧才能开始解码这 2 秒是硬延迟。把摄像头的关键帧间隔改成 25 或者更小能明显降低首帧延迟。4.3 多路播放的性能考量一个项目里播 4 路、8 路甚至 16 路 RTSP 是很常见的需求。这时候性能就是大问题。CPU 解码 vs 硬件解码LibVLC 默认用 CPU 软解1080p 一路大概占 5%-10% CPU。8 路就是 40%-80%直接吃满。解决办法是开硬件解码_libVLC new LibVLC( --no-audio, --rtsp-tcp, --network-caching300, --avcodec-hwdxva2 // Windows 下用 DXVA2 );Windows 下用dxva2或d3d11vaLinux 下用vaapiMac 下用videotoolbox。开了硬解之后 CPU 占用能降到原来的三分之一左右。分辨率策略多路场景一律用子码流。海康子码流默认是 704x576 或者 640x480解码压力小很多。如果 UI 上显示区域本来就小用子码流完全够看。帧率控制如果摄像头是 25fps但 UI 上只需要 15fps可以在 Update 里做跳帧每两帧取一帧更新 Texture能省不少 GPU 上传带宽。5. 打包部署与跨平台注意事项5.1 Windows 打包的 DLL 拷贝问题编辑器里跑得好好的一打包就报DllNotFoundException: libvlc这是最常见的问题。原因是 VideoLAN.LibVLC.Windows 包里的 DLL 和 plugins 目录没有自动拷贝到输出目录。解决办法是写一个 PostProcessBuild 脚本#if UNITY_EDITOR using UnityEditor; using UnityEditor.Callbacks; using System.IO; public class VLCPostBuild { [PostProcessBuild(1)] public static void OnPostProcessBuild(BuildTarget target, string path) { if (target ! BuildTarget.StandaloneWindows64) return; string buildDir Path.GetDirectoryName(path); string packageDir Path.Combine(Assets, Packages, VideoLAN.LibVLC.Windows, build, x64); File.Copy(Path.Combine(packageDir, libvlc.dll), Path.Combine(buildDir, libvlc.dll), true); File.Copy(Path.Combine(packageDir, libvlccore.dll), Path.Combine(buildDir, libvlccore.dll), true); CopyDirectory(Path.Combine(packageDir, plugins), Path.Combine(buildDir, plugins)); } private static void CopyDirectory(string source, string dest) { if (!Directory.Exists(dest)) Directory.CreateDirectory(dest); foreach (var file in Directory.GetFiles(source)) File.Copy(file, Path.Combine(dest, Path.GetFileName(file)), true); foreach (var dir in Directory.GetDirectories(source)) CopyDirectory(dir, Path.Combine(dest, Path.GetFileName(dir))); } } #endif这个脚本在打包完成后自动把 DLL 和 plugins 拷到输出目录。注意路径里的x64要根据你的目标平台改32 位是x86。5.2 安卓与 Linux 平台的差异安卓平台上 LibVLCSharp 也能跑但配置更麻烦。需要把.so文件和 plugins 放到Assets/Plugins/Android下并且 AndroidManifest 里要加网络权限。另外安卓上硬件解码用mediacodec性能比软解好很多。Linux 平台比如 RK3588 这类嵌入式板子要注意的是VLC 版本和系统库的兼容性。有些精简系统缺libxcb之类的库VLC 起不来。解决办法是装完整版 VLC 或者手动补依赖。RK3588 上有硬件解码器用--avcodec-hwv4l2m2m能调用起来。注意跨平台打包时plugins 目录下的插件不是全平台通用的。Windows 的 plugins 拿到 Linux 上用不了需要各自平台对应的版本。建议按平台分目录管理打包时按目标平台拷贝。5.3 内存泄漏排查LibVLC 用不好很容易内存泄漏表现是跑几个小时之后内存暴涨然后崩溃。几个常见的泄漏点Media 对象没释放每次new Media()都要using或者手动Dispose()。我见过有人在重连逻辑里反复 new Media 不释放一小时泄漏几百 MB。MediaPlayer 没释放场景切换的时候如果没调Dispose()MediaPlayer 会一直挂在后台。建议在 OnDestroy 里确保释放。GCHandle 没释放帧缓冲区的 GCHandle 如果没 Free那块内存永远回收不了。每次重建缓冲区的时候记得先 Free 旧的。排查工具我一般用Unity Profiler 的 Memory 模块看托管内存用Windows 任务管理器看非托管内存。如果托管内存稳定但进程内存一直涨那就是非托管泄漏重点查 LibVLC 相关对象。6. 实操心得与进阶方向6.1 几个让我少走弯路的经验第一先用 VLC 桌面版验证地址。任何 RTSP 地址往 Unity 里塞之前先用 VLC 桌面版播一下。能播再写代码不能播先解决地址问题。这一步能省掉 80% 的调试时间。第二日志一定要开。LibVLC 的日志通过_libVLC.Log (s, e) Debug.Log(e.Message);挂上出问题的时候日志里写得清清楚楚。我排查过一个花屏问题日志里直接显示picture is too late to be displayed一看就是缓存设太小了。第三分辨率不要写死。我早期代码写死 1920x1080结果接了个 1280x720 的摄像头画面只显示左上角一块。后来改成在SetVideoFormat回调里动态获取实际尺寸再重建 Texture兼容性好很多。第四重连逻辑要加退避。不要一断流就立刻重连加个递增的冷却时间。第一次 2 秒第二次 4 秒第三次 8 秒最多到 30 秒。这样既能快速恢复又不会把摄像头搞崩。6.2 还能怎么扩展这个基础方案跑通之后可以往上叠很多功能。比如多路画面拼接用 RenderTexture 把多路视频渲染到一张大图上做成监控墙。比如录像回放把帧数据同时写进视频文件LibVLC 本身支持 sout 输出。比如AI 分析叠加把视频帧送给目标检测模型检测结果画在 UI 上做叠加显示。还有一个方向是用 Compute Shader 做颜色转换和缩放。现在帧数据从 CPU 传到 GPU 再更新 Texture带宽占用不小。如果改成用 Compute Shader 直接在 GPU 上处理多路场景下性能能再上一个台阶。这个我还在折腾跑通了再单独写一篇。整体来说LibVLCSharp 这套方案在 Unity 里播 RTSP 是相当成熟的代码量小、稳定性好、跨平台支持也不错。核心就是把初始化、回调、重连这三块写扎实剩下的就是根据具体项目需求做调整。我上面给的代码都是实际项目里跑过的拿去改改就能用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →