1. 项目背景与核心需求拆解
1.1 为什么要在 Unity3D 里播放 RTSP 流
做过数字孪生、智慧园区、工业巡检这类项目的朋友大概率都遇到过同一个需求:把海康、大华这些网络摄像头的实时画面接进 Unity3D 场景里。不管是做三维可视化大屏,还是在 VR 里叠加监控视角,核心诉求就一个——让 Unity 能稳定地播放 RTSP 视频流。
RTSP 是安防行业事实上的标准拉流协议,海康威视摄像头的 RTSP 地址格式通常是rtsp://admin:密码@IP:554/Streaming/Channels/101,大华则是rtsp://admin:密码@IP:554/cam/realmonitor?channel=1&subtype=0。这些地址在浏览器里打不开,VLC 和 PotPlayer 能播,但 Unity 原生不支持。Unity 自带的 VideoPlayer 组件只认本地文件和有限的几个 HTTP 流格式,直接喂 RTSP 地址进去,要么黑屏要么报错。
所以问题的本质是:需要一个能把 RTSP 流解码成 Unity 能渲染的纹理的中间层。这个中间层要解决三件事——拉流、解码、把解码后的帧数据送进 Unity 的渲染管线。
1.2 三种主流技术路线的取舍
在动手之前,我先把市面上能走通的几条路都摸了一遍,这里直接给结论和理由。
| 方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Unity VideoPlayer + 转协议 | 用 FFmpeg 把 RTSP 转成 HTTP/RTMP 再播 | 实现简单 | 延迟高、需额外转流服务 | 对延迟不敏感的展示 |
| LibVLCSharp | 集成 VLC 内核,VLC 原生支持 RTSP | 格式兼容性极强、代码量少 | 移动端支持一般、包体较大 | PC 端项目首选 |
| FFmpeg + 自研解码 | 自己调 FFmpeg 拉流解码,送纹理 | 延迟最低、可控性最强 | 开发量大、坑多 | 对延迟和性能要求极高 |
我最终选的是LibVLCSharp,原因很直接:5 分钟能跑起来,格式兼容性覆盖海康大华几乎所有型号,PC 端稳定性经过大量项目验证。FFmpeg 自研方案虽然延迟能压到 200ms 以内,但光是处理解码线程和纹理更新就能耗掉一整天,不适合快速验证需求。
提示:如果你的项目是安卓端或者对延迟要求苛刻(比如云台控制需要实时反馈),那 FFmpeg 自研路线更合适。LibVLCSharp 在安卓上也能用,但配置复杂度会上升一个量级。
1.3 适合谁来参考
这篇内容适合三类人:一是刚接触 Unity 音视频、需要快速把监控画面接进场景的开发者;二是做数字孪生、工业可视化、智慧安防的从业者;三是有 C# 基础但没怎么碰过原生库集成的朋友。代码部分我会给完整可运行的版本,参数也会解释清楚为什么这么设。
2. 环境准备与依赖安装
2.1 Unity 版本与平台选择
Unity 版本我实测下来 2021.3 LTS 和 2022.3 LTS 都稳,推荐用 LTS 版本,别追最新的 Tech Stream。平台方面,Windows 是首选,LibVLCSharp 在 Windows 上的原生库最完整。如果你的项目必须跑在 Linux 上,Ubuntu 20.04 以上也能用,但需要额外装libvlc-dev和vlc-plugin-*系列包。
创建项目时选3D (Built-in Render Pipeline)就行,URP 也能用,但要注意材质和 Shader 的兼容性。我下面给的代码在 Built-in 管线下测试通过,URP 下只需要把 RawImage 换成对应的 UI 材质即可。
2.2 NuGet 包安装的坑
LibVLCSharp 通过 NuGet 分发,Unity 本身不直接支持 NuGet,所以需要借助NuGetForUnity这个插件。安装步骤:
- 从 GitHub 下载 NuGetForUnity 的
.unitypackage,导入项目 - 菜单栏
NuGet->Manage NuGet Packages - 搜索并安装以下三个包:
LibVLCSharp(核心托管库)LibVLCSharp.Unity(Unity 专用封装)VideoLAN.LibVLC.Windows(Windows 原生库)
这里有个大坑:VideoLAN.LibVLC.Windows装完后,原生 DLL 不会自动放到正确位置。你需要手动把Assets/Packages/VideoLAN.LibVLC.Windows.*/build/x64/下的所有 DLL 复制到Assets/Plugins/x86_64/目录。如果项目要出 32 位版本,还要复制 x86 的。漏了这一步,运行时会报DllNotFoundException: libvlc。
注意:Unity 2021 之后默认只勾选 x86_64 平台,如果你在 Plugins 设置里看到 DLL 的平台选项,确保 x86_64 是勾上的,否则打包后运行会找不到库。
2.3 原生库依赖清单
VideoLAN.LibVLC.Windows包里包含的 DLL 不止一个libvlc.dll,还有libvlccore.dll和plugins文件夹。plugins 文件夹里是各种解码器、渲染器插件,必须整个文件夹一起复制,不能只挑几个。我见过有人只复制了主 DLL,结果 RTSP 能连上但画面出不来,就是因为缺了liblive555_plugin.dll这个 RTSP 解复用插件。
复制完成后的目录结构应该是:
Assets/ Plugins/ x86_64/ libvlc.dll libvlccore.dll plugins/ access/ codec/ demux/ ...2.4 测试用 RTSP 地址准备
手头没有摄像头的话,可以用公开的测试流地址验证。不过公开地址经常失效,更稳妥的做法是本地搭一个 RTSP 服务器。用 FFmpeg 配合mediamtx(原 rtsp-simple-server)就能快速搭起来:
# 下载 mediamtx 后直接运行 ./mediamtx # 另开终端,用 FFmpeg 推一个本地视频文件进去 ffmpeg -re -stream_loop -1 -i test.mp4 -c copy -f rtsp rtsp://localhost:8554/live这样就有了一个稳定的测试地址rtsp://localhost:8554/live。海康摄像头的取流地址格式前面提过,大华的也给了,实际项目里把 IP、账号、密码替换掉就行。
3. 核心代码实现与逐行解析
3.1 整体架构设计
代码结构上我分成三层:VLC 实例层负责初始化和全局配置,MediaPlayer 层负责单个流的播放控制,Unity 渲染层负责把视频帧转成纹理贴到 UI 或模型上。这样分层的好处是,一个场景里要同时播多路摄像头时,只需要创建多个 MediaPlayer,共享一个 LibVLC 实例即可,资源占用更合理。
LibVLCSharp 在 Unity 里的渲染方式有两种:一种是它自带的LibVLCSharp.Unity提供的VLCRenderer组件,另一种是自己接管帧数据。前者开箱即用但灵活性差,后者可控性强但代码多。我选的是前者,因为标题说了 5 分钟搞定,自接管帧数据那套至少得写两百行。
3.2 完整可运行代码
先上代码,后面逐段解释。
using System; using System.Threading; using System.Threading.Tasks; using LibVLCSharp.Shared; using UnityEngine; using UnityEngine.UI; public class RTSPPlayer : MonoBehaviour { [Header("RTSP 配置")] public string rtspUrl = "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101"; public RawImage displayImage; [Header("播放参数")] public int latencyMs = 300; public bool enableHardwareDecode = true; private LibVLC _libVLC; private MediaPlayer _mediaPlayer; private RenderTexture _renderTexture; private bool _isPlaying; void Start() { Core.Initialize(); InitializeVLC(); InitializeRenderTexture(); StartPlay(); } private void InitializeVLC() { string[] options = new string[] { "--rtsp-tcp", "--no-audio", $"--network-caching={latencyMs}", "--live-caching=300", "--sout-mux-caching=300", "--clock-jitter=0", "--clock-synchro=0", "--avcodec-hw=any", "--no-drop-late-frames", "--no-skip-frames", "--rtsp-frame-buffer-size=1000000" }; _libVLC = new LibVLC(options); } private void InitializeRenderTexture() { _renderTexture = new RenderTexture(1920, 1080, 0, RenderTextureFormat.ARGB32); _renderTexture.Create(); displayImage.texture = _renderTexture; } private void StartPlay() { _mediaPlayer = new MediaPlayer(_libVLC); _mediaPlayer.EnableHardwareDecoding = enableHardwareDecode; using (var media = new Media(_libVLC, new Uri(rtspUrl), FromType.FromLocation)) { media.AddOption(":rtsp-tcp"); media.AddOption(":network-caching=" + latencyMs); media.AddOption(":live-caching=300"); media.AddOption(":no-audio"); _mediaPlayer.Play(media); } _isPlaying = true; StartCoroutine(UpdateTextureCoroutine()); } private System.Collections.IEnumerator UpdateTextureCoroutine() { while (_isPlaying) { if (_mediaPlayer != null && _mediaPlayer.IsPlaying) { // LibVLCSharp.Unity 的纹理更新由内部回调处理 // 这里留作扩展点,比如做帧率统计或异常检测 } yield return new WaitForSeconds(0.1f); } } void OnDestroy() { _isPlaying = false; _mediaPlayer?.Stop(); _mediaPlayer?.Dispose(); _libVLC?.Dispose(); if (_renderTexture != null) { _renderTexture.Release(); Destroy(_renderTexture); } } }3.3 关键参数逐个说清楚
上面代码里那一堆 VLC 参数不是随便写的,每个都有明确目的。
--rtsp-tcp是最重要的一个。RTSP 默认走 UDP 传输,但 UDP 在复杂网络环境下丢包严重,画面会出现马赛克、绿屏甚至直接断流。强制走 TCP 后,虽然延迟会略微增加(大概多 50-100ms),但稳定性提升非常明显。海康和大华的摄像头都支持 TCP 模式,实际项目里我基本都开这个。
--network-caching控制网络缓冲时长,单位毫秒。这个值直接决定延迟和流畅度的平衡。设太小(比如 100ms)画面容易卡顿,设太大(比如 2000ms)延迟高到没法做云台控制。我实测下来 300ms 是个甜点值,局域网环境下画面流畅且延迟可接受。如果是跨公网访问,建议调到 800-1000ms。
--clock-jitter=0和--clock-synchro=0这两个是关掉时钟同步的。RTSP 流的时间戳有时候和本地时钟对不上,开着同步会导致画面反复缓冲。关掉后由 VLC 自己按帧率渲染,反而更稳。这个技巧在 PotPlayer 反复缓冲的问题上同样适用。
--avcodec-hw=any是开启硬件解码。H.264/H.265 用 GPU 解码能大幅降低 CPU 占用,尤其是多路播放时效果明显。不过有些老显卡驱动对硬解支持不好,如果出现花屏可以改成--avcodec-hw=none回退到软解。
--no-drop-late-frames和--no-skip-frames是防止 VLC 主动丢帧。默认情况下 VLC 为了保持实时性会丢掉来不及渲染的帧,但监控场景下每一帧都可能包含关键信息,丢帧不可接受。这两个参数强制 VLC 渲染所有帧,代价是极端情况下延迟会累积。
3.4 渲染纹理的尺寸选择
RenderTexture我设的是 1920x1080,这是主流摄像头的输出分辨率。但实际项目里要根据摄像头实际分辨率来设,设大了浪费显存,设小了画面模糊。海康的Streaming/Channels/101是主码流,通常是 1080P 或 4K;Streaming/Channels/102是子码流,一般是 720P 或 D1。
如果场景里要同时播 16 路,每路都开 1080P 的 RenderTexture,显存占用会非常可观。这时候建议用子码流,或者把 RenderTexture 降到 1280x720。显存计算公式是宽 x 高 x 4 字节 x 路数,1080P 单路约 8MB,16 路就是 128MB,加上 VLC 自己的解码缓冲,整体显存占用要留够 500MB 以上。
4. 常见问题排查与实战避坑
4.1 连接类问题速查
实际项目里遇到的问题,八成集中在连接阶段。我整理了一张速查表,按现象对原因。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 黑屏无画面 | 地址错误或网络不通 | 先用 VLC 桌面版测试同一地址 | 确认 IP、端口、账号密码、通道号 |
| 报 DllNotFoundException | 原生库未正确放置 | 检查 Plugins/x86_64 目录 | 复制完整 DLL 和 plugins 文件夹 |
| 连接超时 | 摄像头未开启 RTSP 或防火墙拦截 | telnet IP 554 测试端口 | 摄像头后台开启 RTSP,放行 554 端口 |
| 画面卡顿马赛克 | UDP 丢包 | 抓包看是否有丢包 | 加--rtsp-tcp强制 TCP |
| 播放几秒后断开 | 认证失败或会话超时 | 看 VLC 日志 | 检查账号权限,加--rtsp-frame-buffer-size |
海康摄像头有个常见坑:默认的 RTSP 认证方式是 Digest,但有些固件版本对 Digest 支持有问题,会反复弹认证导致断流。解决办法是在摄像头后台把认证方式改成 Basic,或者用rtsp://admin:password@IP:554/...这种把账号密码嵌在 URL 里的方式。
4.2 延迟优化实战记录
延迟是监控播放最核心的指标。我做过一组对比测试,同一路 1080P 海康流,不同参数组合下的端到端延迟(从摄像头到 Unity 画面):
| 参数组合 | 平均延迟 | 流畅度 | 备注 |
|---|---|---|---|
| 默认 UDP + 1000ms 缓存 | 1200ms | 好 | 延迟太高 |
| TCP + 300ms 缓存 | 450ms | 好 | 推荐配置 |
| TCP + 100ms 缓存 | 280ms | 一般,偶有卡顿 | 局域网可用 |
| TCP + 0 缓存 + 硬解 | 180ms | 差,频繁卡顿 | 不推荐 |
实测下来,TCP + 300ms 缓存 + 硬解是综合最优解。延迟 450ms 左右,做云台控制时手感基本跟得上,画面也稳定。如果非要压到 300ms 以内,就得走 FFmpeg 自研解码那条路了,LibVLC 的架构决定了它很难做到极低延迟。
提示:
--network-caching和 media 上的:network-caching要设成一样的值,只设一个有时候不生效。这是 LibVLC 的一个已知行为,两个地方都写保险。
4.3 断流重连机制
监控流最怕的就是断流不恢复。摄像头重启、网络抖动、NVR 切换主码流,都会导致流中断。LibVLC 本身有重连机制,但默认行为不够积极。我的做法是监听MediaPlayer.EndReached事件,在里面做延迟重连:
_mediaPlayer.EndReached += (sender, args) => { Task.Run(async () => { await Task.Delay(2000); if (_isPlaying) { using (var media = new Media(_libVLC, new Uri(rtspUrl), FromType.FromLocation)) { media.AddOption(":rtsp-tcp"); media.AddOption(":network-caching=" + latencyMs); _mediaPlayer.Play(media); } } }); };这里有个关键点:重连必须在后台线程做,且要延迟 2 秒以上。因为EndReached回调是在 VLC 内部线程触发的,直接在里面调Play会导致死锁。延迟 2 秒是给 VLC 释放资源的时间,太短了会重连失败。
另外,EndReached事件在正常停止播放时也会触发,所以要用_isPlaying标志位区分是主动停止还是意外断流。主动停止时把_isPlaying置 false,就不会触发重连。
4.4 多路播放的性能调优
单路播放随便怎么搞都行,但项目里经常要同时播 4 路、9 路甚至 16 路。这时候性能问题就暴露出来了。
首先是LibVLC 实例要共享。每new LibVLC()一次都会创建一套完整的解码器实例,内存和线程开销很大。正确做法是全局只创建一个 LibVLC,所有 MediaPlayer 共用它。
其次是解码方式的选择。16 路 1080P 硬解,中端显卡(比如 GTX 1650)基本能扛住,CPU 占用在 30% 左右。如果换成软解,CPU 直接飙到 100%,画面开始丢帧。所以多路场景下硬解是必须的。
再就是RenderTexture 的复用。如果多路画面尺寸一样,可以考虑用 RenderTexture 池来复用,减少创建销毁的开销。不过实测下来这块优化收益不大,主要瓶颈还是在解码。
最后提一个容易忽略的点:Unity 的 UI 渲染开销。如果用 RawImage 显示,每路都是一个 DrawCall,16 路就是 16 个 DrawCall。可以合并到一个 Canvas 下用图集优化,或者干脆用 Mesh 渲染代替 UI。这个看项目具体情况,不是必须的。
5. 进阶扩展与工程化建议
5.1 从 Demo 到生产环境的差距
上面那套代码跑通 Demo 没问题,但直接上生产环境还差几块。第一块是配置管理,RTSP 地址、账号密码不能硬编码在脚本里,要抽到配置文件或者 ScriptableObject 里。第二块是状态监控,要能实时知道每路流的状态(连接中、播放中、断流、重连中),方便上层做告警。第三块是日志系统,VLC 的日志要能捕获出来,出问题时才有据可查。
LibVLC 的日志捕获可以这样写:
_libVLC.Log += (sender, e) => { Debug.Log($"[VLC-{e.Level}] {e.Message}"); };不过 VLC 日志量很大,生产环境建议只捕获 Error 和 Warning 级别,或者写到独立文件里轮转。
5.2 与 FFmpeg 方案的混合使用
LibVLCSharp 虽然方便,但有两个场景它搞不定:一是需要把流录制下来存成文件,二是需要做视频分析(比如 AI 识别)。这两个场景 FFmpeg 更合适。
我的做法是混合使用:LibVLC 负责实时预览,FFmpeg 负责录制和分析。FFmpeg 可以用命令行方式调用,也可以用FFmpeg.AutoGen做 C# 绑定。命令行方式简单粗暴,适合录制:
ffmpeg -rtsp_transport tcp -i "rtsp://..." -c copy -f segment -segment_time 300 -segment_format mp4 "record_%03d.mp4"这条命令每 5 分钟切一个文件,-c copy不重新编码,CPU 占用极低。注意-rtsp_transport tcp和 VLC 的--rtsp-tcp是一个意思,都是强制 TCP。
5.3 跨平台部署注意事项
Windows 部署最简单,把 Plugins 目录整个带上就行。Linux 部署要注意系统里得装 VLC 的运行库,apt install libvlc-dev vlc-plugin-base基本够用。安卓部署最麻烦,需要把 VLC 的安卓原生库(.so 文件)放到Plugins/Android/libs/下对应架构的目录里,而且安卓上的硬解支持因设备而异,需要做兼容性测试。
有个跨平台的坑要提前说:路径分隔符。Windows 用反斜杠,Linux 和安卓用正斜杠。如果代码里有拼接路径的地方,统一用Path.Combine,别手写字符串拼接。
5.4 我个人的几条经验
第一,别在 Update 里做任何和 VLC 相关的操作。VLC 的回调和 Unity 主线程是分离的,在 Update 里调 MediaPlayer 的方法容易出竞态问题。所有 VLC 操作要么在 Start 里做,要么在事件回调里做。
第二,测试阶段一定要用真实摄像头。公开的测试流地址和真实摄像头的流特性差别很大,尤其是海康的私有码流格式,测试流跑通了不代表摄像头能跑通。
第三,延迟和稳定性是一对矛盾。想要低延迟就得牺牲缓冲,想要稳定就得加大缓冲。项目初期先把稳定性做够,上线前再根据实际网络环境调延迟参数。我见过太多项目为了追求低延迟把缓存调到 100ms,结果现场网络一抖动就花屏,返工重调。
第四,多留一路子码流做备份。主码流断了的时候,子码流往往还能连上。代码里可以做个降级逻辑,主码流连续重连失败三次就切到子码流地址,保证画面不中断。
这套方案我在三个实际项目里用过,最长的已经稳定跑了两年多,7x24 小时不间断。核心就是那几个参数调对了,剩下的就是重连机制兜底。代码不复杂,难的是知道每个参数为什么这么设,以及出问题时往哪个方向排查。