ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity3D 播 RTSP 实战:LibVLCSharp 拉流解码上屏与避坑指南

Unity3D 播 RTSP 实战:LibVLCSharp 拉流解码上屏与避坑指南 简介这份资源面向需要在Unity3D中播放RTSP实时视频流的开发者尤其适合做视频监控、在线直播、VR/AR交互展示等项目的初中级工程师。由于Unity原生播放器不支持RTSP协议资源借助VLCMediaPlayer for Unity插件打通播放链路并给出从插件导入、平台依赖配置到C#脚本控制播放、暂停、停止的完整实现思路同时涉及视频输出到相机纹理、UI Image显示以及网络缓存、解码方式等性能调优与错误处理要点。压缩包为rar格式共2274个文件约36.54MB其中dll动态库、meta与info配置、luac与bin资源、png贴图、asset与prefab场景资源、cs脚本等构成插件运行与示例工程的主体另有html、xml、json等说明与配置文档。目前已有1452人学习下载可作为Unity接入RTSP视频流的可运行参考工程帮助读者快速验证方案并排查平台兼容问题。1. Unity3D 里播 RTSP为什么你直接塞 URL 给 VideoPlayer 一定翻车如果你在 Unity 里做过摄像头接入大概率踩过这个坑把海康或萤石的 RTSP 地址往VideoPlayer.url一填编辑器里黑屏打包到安卓直接闪退。这不是你代码写错了而是 Unity 内置的 VideoPlayer 根本不支持 RTSP 拉流协议——它只认本地文件和 HTTP 渐进式下载那套。RTSP 是实时流协议需要专门的会话协商、RTP 拆包、时间戳同步这些 Unity 没做。那怎么办一线常见做法就两条路一是引入 VLC 的 Unity 绑定VideoLAN 官方维护的 LibVLCSharp 或社区版 UnityVLC让 VLC 内核去处理 RTSP 会话再把解码后的帧回传给 Unity 的 Texture二是用 FFmpeg 自己拉流解码通过管道把 RGB 数据送进 Texture2D。前者省事、格式兼容性好后者可控性高但工作量大。这篇笔记围绕第一条路展开因为关键词里出现了unity vlcmediaplayer说明你大概率已经在往这个方向找方案了。适合谁看做安防监控大屏、工业巡检、车载视频回传、Unity 数字孪生接实时摄像头的同学。你需要有基本的 Unity C# 基础知道 Texture、Material 怎么用剩下的跟着走就行。下面从环境搭建、核心代码、参数调优到避坑一步步拆。2. 环境搭建与 VLC 插件选型别在版本兼容上浪费一整天2.1 为什么选 LibVLCSharp 而不是随便找个 UnityVLC 包网上能搜到好几个 Unity 播放 RTSP 的插件包质量参差不齐。有的年久失修只支持到 Unity 2018有的把 VLC 的 dll 硬编码成 32 位你打包 64 位安卓直接崩。我一般推荐两条线第一条线是LibVLCSharp VideoLAN.LibVLC 原生库。LibVLCSharp 是 VideoLAN 官方 .NET 绑定API 稳定社区活跃。Unity 里通过 NuGet 包或者手动导入 dll 都能用。缺点是它返回的是 VLC 的MediaPlayer对象你需要自己把帧数据桥接到 Unity 的 Texture官方没给现成的 Unity 渲染组件。第二条线是UnityVLC 社区封装比如 GitHub 上一些高星仓库。这类包通常已经帮你做好了RenderTexture更新逻辑导入就能用适合快速验证。但要注意看它的 LibVLC 版本和你的目标平台是否匹配。选型判断标准很简单如果你只是想在 PC 端编辑器里快速跑通 demo社区封装包十分钟搞定如果你要打包安卓或 iOS并且对延迟、分辨率有要求老老实实走 LibVLCSharp 手动桥接可控。2.2 导入 LibVLC 原生库的正确姿势不管走哪条线核心都是把 VLC 的原生库放到 Unity 能识别的位置。以 Windows 64 位为例你需要这些文件文件放置路径作用libvlc.dllAssets/Plugins/x86_64/VLC 核心库libvlccore.dllAssets/Plugins/x86_64/VLC 核心依赖plugins 文件夹Assets/Plugins/x86_64/plugins/解码器、demuxer 等模块LibVLCSharp.dllAssets/Plugins/C# 绑定层安卓的话需要把.so文件放到Assets/Plugins/Android/libs/arm64-v8a/下面plugins 文件夹放到Assets/Plugins/Android/libs/arm64-v8a/plugins/。注意安卓的 VLC 原生库版本要和你的 LibVLCSharp 版本对应混用必崩。# 以 Windows 为例从 VLC 安装目录拷贝核心文件 # 假设 VLC 装在默认路径 cp C:/Program Files/VideoLAN/VLC/libvlc.dll ./Assets/Plugins/x86_64/ cp C:/Program Files/VideoLAN/VLC/libvlccore.dll ./Assets/Plugins/x86_64/ cp -r C:/Program Files/VideoLAN/VLC/plugins ./Assets/Plugins/x86_64/这段命令做的是把 VLC 安装目录下的核心 dll 和插件目录复制到 Unity 的 Plugins 目录。libvlc.dll是入口libvlccore.dll是它依赖的核心plugins目录里是各种解码器和协议处理模块——RTSP 的 demuxer 就在里面。少了 plugins 目录VLC 初始化时会报 “cannot load module” 或者直接返回 null。提示不要从不同版本的 VLC 里混拷文件。libvlc.dll 和 plugins 必须来自同一个安装包否则初始化阶段就会失败。2.3 初始化 LibVLC 实例的最小代码环境放好之后先写一个最简单的初始化脚本确认 VLC 能起来。using LibVLCSharp.Shared; using UnityEngine; public class VLCInitializer : MonoBehaviour { private LibVLC _libVLC; void Start() { // 指定 plugins 目录安卓上路径不同 string pluginPath System.IO.Path.Combine( Application.streamingAssetsPath, vlc/plugins); // 核心初始化必须在使用任何 VLC 功能前调用 Core.Initialize(); // 创建 LibVLC 实例可传入参数 _libVLC new LibVLC( --no-video-title-show, // 不显示视频标题叠加 --rtsp-tcp, // RTSP 强制走 TCP减少丢包 --network-caching300 // 网络缓存 300ms ); Debug.Log(LibVLC 初始化成功版本 _libVLC.Version); } void OnDestroy() { _libVLC?.Dispose(); } }Core.Initialize()是必须调用的它会去加载原生库并解析 plugins 路径。如果你在编辑器里跑它会自动从系统 PATH 或当前目录找打包后需要确保 plugins 目录在正确位置。--rtsp-tcp这个参数很关键默认 RTSP 走 UDP在跨网段或无线环境下丢包严重强制 TCP 能明显改善花屏和卡顿。--network-caching控制缓冲时长太小容易卡太大延迟高300ms 是个折中值后面还会细说。如果这一步报DllNotFoundException九成是 dll 放错目录或者位数不对。检查 Unity 的 Inspector 里 Plugins 平台设置确保 x86_64 被勾选。3. 从 RTSP 地址到 Unity Texture拉流、解码、上屏的完整链路3.1 创建 MediaPlayer 并绑定 RTSP 源初始化 LibVLC 之后下一步是创建MediaPlayer和Media对象。RTSP 地址的格式通常是rtsp://用户名:密码IP:554/路径。海康的取流地址常见格式是rtsp://admin:password192.168.1.64:554/Streaming/Channels/101其中 101 表示主码流102 表示子码流。萤石的话一般是rtsp://admin:验证码IP:554/h264/ch1/main/av_stream。using LibVLCSharp.Shared; using UnityEngine; public class RTSPPlayer : MonoBehaviour { private LibVLC _libVLC; private MediaPlayer _mediaPlayer; private Media _media; [SerializeField] private string rtspUrl rtsp://admin:password192.168.1.64:554/Streaming/Channels/101; void Start() { Core.Initialize(); _libVLC new LibVLC(--rtsp-tcp, --network-caching300); // 创建 MediaPlayer _mediaPlayer new MediaPlayer(_libVLC); // 用 URL 创建 Media类型指定为 Network _media new Media(_libVLC, new System.Uri(rtspUrl), FromType.FromLocation); // 开始播放 bool started _mediaPlayer.Play(_media); Debug.Log(播放启动 started); } void OnDestroy() { _mediaPlayer?.Stop(); _media?.Dispose(); _mediaPlayer?.Dispose(); _libVLC?.Dispose(); } }new Media(_libVLC, new Uri(rtspUrl), FromType.FromLocation)这行是关键。FromType.FromLocation告诉 VLC 这是一个网络位置它会自动选择合适的 demuxer 去处理 RTSP 握手。如果你用FromType.FromPathVLC 会当成本地文件直接失败。_mediaPlayer.Play(_media)返回一个 bool表示播放命令是否成功下发。注意它不代表视频已经出画面了只是说 VLC 接受了这个请求。真正的画面出来需要等Playing事件或者TimeChanged事件触发。3.2 把 VLC 解码帧转成 Unity Texture2DLibVLCSharp 在 C# 层没有直接暴露“给我一帧 RGB 数据”的简单 API。常见做法有两种方案 A用SetVideoFormatSetVideoCallbacks自定义渲染。这是 LibVLC 提供的底层回调机制你告诉 VLC “别自己渲染把帧数据给我我来处理”。在 C# 里通过MediaPlayer.SetVideoFormat设置格式比如 RV32然后SetVideoCallbacks注册 lock/unlock/display 三个回调。在 display 回调里把帧数据拷贝到 Texture2D 并Apply()。方案 B用MediaPlayer.TakeSnapshot定时抓帧。这个 API 本来是给截图用的但可以每隔几十毫秒调一次拿到 BGRA 数据再上传 Texture。缺点是性能差、延迟高只适合低帧率场景。我一般用方案 A虽然代码量大一点但帧率和延迟都可控。下面是一个简化版的实现思路using System; using System.Runtime.InteropServices; using LibVLCSharp.Shared; using UnityEngine; public class VLCToTexture : MonoBehaviour { private Texture2D _texture; private MediaPlayer _mediaPlayer; private IntPtr _frameBuffer IntPtr.Zero; private uint _width 1920; private uint _height 1080; void Start() { _texture new Texture2D((int)_width, (int)_height, TextureFormat.BGRA32, false); GetComponentRenderer().material.mainTexture _texture; // 假设 _mediaPlayer 已经创建 // 设置视频格式RV32 对应 BGRA _mediaPlayer.SetVideoFormat(RV32, _width, _height, _width * 4); // 注册回调 _mediaPlayer.SetVideoCallbacks(LockCallback, UnlockCallback, DisplayCallback); } private IntPtr LockCallback(IntPtr opaque, IntPtr planes) { // 分配或复用缓冲区 if (_frameBuffer IntPtr.Zero) _frameBuffer Marshal.AllocHGlobal((int)(_width * _height * 4)); Marshal.WriteIntPtr(planes, _frameBuffer); return _frameBuffer; } private void UnlockCallback(IntPtr opaque, IntPtr picture, IntPtr planes) { // 这里可以做帧同步暂时空着 } private void DisplayCallback(IntPtr opaque, IntPtr picture) { // 把帧数据拷到 Texture2D byte[] data new byte[_width * _height * 4]; Marshal.Copy(_frameBuffer, data, 0, data.Length); _texture.LoadRawTextureData(data); _texture.Apply(); } void OnDestroy() { if (_frameBuffer ! IntPtr.Zero) Marshal.FreeHGlobal(_frameBuffer); } }SetVideoFormat的第一个参数RV32是 VLC 的像素格式标识对应 BGRA 四通道。_width * 4是每行字节数pitch这个值必须传对否则画面会错位。LockCallback里分配非托管内存并告诉 VLC 往哪写DisplayCallback里把数据拷到 Texture2D。注意DisplayCallback是在 VLC 的渲染线程上调用的Unity 的 Texture2D 操作必须在主线程所以实际项目中需要用ConcurrentQueue或者MainThreadDispatcher把数据转到主线程再Apply()。上面为了简洁省略了这一步但你在真项目里一定要处理否则会报 “can only be called from main thread”。3.3 参数调优延迟、缓存、分辨率怎么配RTSP 播放的三个核心指标是延迟、流畅度、分辨率。这三个互相拉扯需要根据场景取舍。参数作用低延迟场景稳定优先场景--network-caching网络缓冲毫秒数100~200500~1000--rtsp-tcp强制 TCP 传输开启开启--live-caching直播流缓冲100500--clock-jitter时钟抖动补偿05000--clock-synchro时钟同步0关闭1开启--network-caching是最常调的。设成 100 延迟大概在 200~400ms但网络一抖就卡设成 1000 延迟上秒级但基本不卡。安防监控场景一般 300~500 够用。--clock-jitter和--clock-synchro在低延迟场景下建议关掉因为时钟同步本身会引入额外缓冲。分辨率方面如果摄像头支持子码流优先拉子码流做预览主码流做录制或 AI 分析。海康的 102 通道就是子码流分辨率通常是 640x480 或 704x576解码压力小很多。在Media创建之前可以通过media.AddOption(:rtsp-frame-buffer-size500000)调整帧缓冲大小但一般不用动。注意--network-caching在 LibVLC 里是全局参数创建LibVLC实例时传入。如果你有多个播放器实例需要不同缓存得创建多个LibVLC实例但这样内存开销会翻倍。4. 避坑与排查安卓黑屏、花屏、内存泄漏的根因4.1 安卓打包后黑屏编辑器正常现象编辑器里播放正常打包到安卓手机后画面全黑日志里可能有libvlc: cannot load module或者没有任何报错。原因安卓的 VLC 原生库路径和 Windows 完全不同。Unity 打包后Application.streamingAssetsPath在安卓上是一个 jar 内部路径VLC 无法直接从那里加载 plugins。另外安卓需要.so文件而不是.dll并且要匹配 CPU 架构arm64-v8a 或 armeabi-v7a。解决把 VLC 的安卓原生库放到Assets/Plugins/Android/libs/arm64-v8a/下面plugins 文件夹放到Assets/Plugins/Android/libs/arm64-v8a/plugins/。然后在初始化时显式指定插件路径string pluginPath System.IO.Path.Combine( Application.persistentDataPath, vlc/plugins); // 首次运行时把 streamingAssets 里的 plugins 解压到 persistentDataPath // 再传给 LibVLC _libVLC new LibVLC( --no-video-title-show, --rtsp-tcp, --network-caching300, :avcodec-hwany // 尝试硬件解码 );安卓上还需要在AndroidManifest.xml里加网络权限uses-permission android:nameandroid.permission.INTERNET /。这个容易忘忘了就是连不上。4.2 画面花屏、绿屏、下半部分撕裂现象视频能播但画面上有绿色块、条纹或者下半部分图像错位。原因九成是SetVideoFormat里的 pitch 参数不对。pitch 是每行像素占用的字节数如果 VLC 输出的实际 pitch 和你告诉它的不一致就会出现错位。另外RV32格式在某些 VLC 版本上实际输出的是 BGRA 还是 RGBA 可能有差异导致颜色通道错乱。解决先确认 VLC 版本和像素格式的对应关系。可以在DisplayCallback里打印实际收到的planes指针和 pitch 值和你的预期对比。如果颜色不对试试把RV32换成RGBA或者手动交换 R 和 B 通道。pitch 一般等于width * 4但如果 VLC 做了行对齐比如对齐到 32 字节实际 pitch 会更大需要用 VLC 回调里给出的值。4.3 播放几分钟后内存暴涨然后崩溃现象任务管理器里内存持续上升几分钟后 Unity 崩溃或者被系统杀掉。原因DisplayCallback里每帧都new byte[]和Marshal.CopyGC 压力巨大。另外Texture2D.LoadRawTextureData每帧调用也会产生大量临时内存。如果LockCallback里每次都Marshal.AllocHGlobal而不释放就是直接的内存泄漏。解决缓冲区复用。_frameBuffer只分配一次byte[]也预分配好每帧只做Marshal.Copy到已有数组。Texture2D 用LoadRawTextureData(IntPtr, int)的重载直接传指针避免中间数组。另外DisplayCallback里不要做任何 Unity API 调用只把数据塞进队列主线程Update里再Apply()。// 预分配避免每帧 GC private byte[] _frameData; private ConcurrentQueuebyte[] _frameQueue new(); void Start() { _frameData new byte[_width * _height * 4]; // ... } private void DisplayCallback(IntPtr opaque, IntPtr picture) { // 只拷贝不调 Unity API Marshal.Copy(_frameBuffer, _frameData, 0, _frameData.Length); _frameQueue.Enqueue((byte[])_frameData.Clone()); } void Update() { if (_frameQueue.TryDequeue(out byte[] data)) { _texture.LoadRawTextureData(data); _texture.Apply(); } }4.4 RTSP 连接成功但一直不出画面现象Play返回 trueMediaPlayer状态变成 Playing但DisplayCallback一直不触发。原因可能是 RTSP 协商完成了但 RTP 包没收到。常见于摄像头开启了双码流但地址写错通道或者防火墙拦了 UDP 端口。另外有些摄像头要求 RTSP over TCP但 VLC 默认走 UDP。解决先加--rtsp-tcp强制 TCP。然后用 VLC 桌面版直接打开同一个 RTSP 地址确认地址本身没问题。如果 VLC 桌面版能播但 Unity 里不行检查 Unity 的 Plugins 目录是否完整特别是plugins/access和plugins/demux这两个子目录。还不行就抓包看 RTP 端口有没有数据进来。4.5 多个摄像头同时播放时卡顿现象单个摄像头流畅同时播 4 路以上开始掉帧、卡顿。原因每个MediaPlayer实例都会创建独立的解码线程和缓冲区。CPU 解码 1080p 流4 路就是 4 个解码器满载。如果没开硬件解码CPU 扛不住。解决优先拉子码流降低分辨率。开启硬件解码--avcodec-hwany或--avcodec-hwdxva2Windows。另外可以复用LibVLC实例多个MediaPlayer共享同一个LibVLC减少重复的插件加载开销。但注意--network-caching是LibVLC级别的共享实例意味着所有播放器用同一个缓存值。5. 进阶技巧用 RenderTexture 做多路拼接与延迟测量5.1 把多路 RTSP 拼到一张大屏上安防大屏经常需要 4 分屏、9 分屏。如果每路都用一个RawImage单独渲染UI 开销大且不好做统一后处理。更好的做法是把每路视频渲染到独立的RenderTexture然后用一个 shader 或者Graphics.Blit拼到一张大RenderTexture上最后用一个RawImage显示。// 为每路视频创建 RenderTexture RenderTexture[] _rtArray new RenderTexture[4]; for (int i 0; i 4; i) { _rtArray[i] new RenderTexture(960, 540, 0, RenderTextureFormat.BGRA32); _rtArray[i].Create(); } // 在 DisplayCallback 对应的主线程更新逻辑里 // 把 Texture2D 数据 Blit 到对应的 RenderTexture Graphics.Blit(_texture, _rtArray[cameraIndex]); // 最后拼接用 Blit 加材质或者直接在一个大 RT 上分四个区域画 RenderTexture.active _bigRT; GL.PushMatrix(); GL.LoadPixelMatrix(0, 1920, 1080, 0); for (int i 0; i 4; i) { int x (i % 2) * 960; int y (i / 2) * 540; Graphics.DrawTexture(new Rect(x, y, 960, 540), _rtArray[i]); } GL.PopMatrix(); RenderTexture.active null;RenderTexture的好处是它活在 GPU 上Graphics.Blit和Graphics.DrawTexture都是 GPU 操作比每帧Texture2D.Apply()再上传到 UI 效率高得多。4 路 960x540 的 RT 显存占用大概几十 MB现代显卡完全扛得住。5.2 测量端到端延迟的土办法RTSP 延迟是安防和工业场景的硬指标。VLC 本身不提供端到端延迟数据但可以用一个土办法测拿手机秒表放在摄像头前面Unity 画面里也显示秒表两个时间差就是端到端延迟。更工程化的做法是在DisplayCallback里记录帧到达的System.DateTime.Now和 VLC 的MediaPlayer.Time流时间戳做差。但这个差值包含了解码和缓冲时间不完全等于端到端延迟。我一般两个方法都用秒表法做验收时间戳法做日常监控。private long _lastFrameTicks; private void DisplayCallback(IntPtr opaque, IntPtr picture) { long now DateTime.Now.Ticks; if (_lastFrameTicks ! 0) { double intervalMs (now - _lastFrameTicks) / 10000.0; // 帧间隔正常应该在 33ms 左右30fps if (intervalMs 100) Debug.LogWarning($帧间隔异常{intervalMs:F1}ms); } _lastFrameTicks now; // ... 拷贝帧数据 }帧间隔突然变大说明网络抖动或者解码卡了。这个日志在调试阶段很有用上线后可以关掉或者写到文件里。5.3 断线重连RTSP 不会自己恢复RTSP 连接断了之后MediaPlayer不会自动重连。你需要监听EncounteredError事件或者EndReached事件然后手动重建Media和MediaPlayer。_mediaPlayer.EncounteredError (sender, e) { Debug.LogWarning(RTSP 连接断开3 秒后重连); Invoke(nameof(Reconnect), 3f); }; private void Reconnect() { _mediaPlayer.Stop(); _media?.Dispose(); _media new Media(_libVLC, new Uri(rtspUrl), FromType.FromLocation); _mediaPlayer.Play(_media); }重连间隔别设太短3~5 秒比较合适。太短了摄像头可能还没恢复频繁重连反而加重负担。另外重连前一定要Stop并Dispose旧的Media否则会泄漏。从那以后我每次接 RTSP 项目都强制走一遍“桌面 VLC 验证地址 → Unity 编辑器跑通 → 安卓真机测延迟 → 断线重连压测”这个流程少一步后面就要还债。希望帮到你。本文还有配套的精品资源点击获取
返回列表