三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity UDP实时视频流传输:从摄像头采集到3D场景集成的完整实现

Unity UDP实时视频流传输:从摄像头采集到3D场景集成的完整实现

1. 项目概述与核心价值

最近在做一个智慧工地或者数字孪生相关的项目,客户提了个需求,想把现场摄像头的实时画面集成到Unity构建的3D场景里。这需求听起来挺常见,但真动手做起来,发现坑还真不少。市面上很多方案要么依赖特定的硬件或昂贵的中间件,要么就是延迟高得没法看,要么就是画质压缩得惨不忍睹。琢磨了一圈,决定自己动手,用Unity2020配合VS2019,基于UDP协议从零搭建一个带画面压缩的实时视频监控系统。这个方案的好处是轻量、灵活,对网络波动的容忍度相对TCP更高,特别适合局域网内或者对实时性要求高于绝对可靠性的场景,比如无人机图传、机器人远程监控或者我们这种需要将视频流嵌入到虚拟环境中的项目。

这个教程的目标,就是带你一步步走通从摄像头采集、编码、网络传输,到Unity端接收、解码、渲染显示的完整链路。我会重点讲解为什么选择UDP而不是TCP,如何选择合适的视频编码和压缩方案来平衡画质与带宽,以及在Unity中高效处理视频流的关键技巧。无论你是想为你的游戏增加一个监控室场景,还是为工业仿真项目集成真实的视频源,这套方案都能提供一个扎实的起点。整个过程会涉及到C# Socket编程、视频编解码基础,以及Unity的纹理和渲染管线,我会尽量讲得直白,确保即使你对网络编程不太熟,也能跟着做出来。

2. 技术选型与架构设计思路

在动手写代码之前,我们先得把整个系统的骨架搭好,搞清楚数据是怎么流动的。一个典型的视频监控系统,可以抽象为三个核心部分:视频源(发送端)网络传输层客户端(接收与渲染端)。我们的目标是在PC上用一个控制台程序模拟发送端,在Unity里实现接收和显示。

2.1 为什么是UDP而不是TCP?

这是第一个关键决策点。一提到可靠传输,大家第一反应可能就是TCP。但对于实时视频流,TCP的“可靠”特性恰恰成了它的负担。

  • TCP的拥塞控制与重传机制:TCP为了保证每个数据包都到达,会有复杂的确认、重传和流量控制逻辑。一旦发生丢包,后续的包会被阻塞,等待丢失的包重传成功。这会导致视频画面卡顿,甚至出现严重的延迟累积,用户体验就是“一卡一卡的”,或者画面比现实慢好几秒。
  • UDP的“尽力而为”:UDP不保证送达,也不保证顺序。听起来不靠谱,但对视频流反而是优点。视频编码(如H.264)本身有很强的容错性,丢失一两个帧或者几个数据包,可能只是造成瞬间的马赛克或花屏,但画面会立刻跟上最新的数据,保持实时性。对于监控场景,看到“现在”发生了什么,比完美地看到“刚才”发生了什么更重要。
  • 开销对比:UDP包头更小(8字节),没有连接建立和保持的开销,更节省带宽和CPU资源。

注意:选择UDP意味着你需要自己在应用层处理一些TCP帮你做的事,比如包顺序整理一定程度的心跳/状态检测。我们会用简单的序号来解决乱序问题。

2.2 视频压缩方案选型:MJPEG vs. H.264

原始摄像头数据(如RGB24格式)数据量巨大。一个1280x720的画面,一帧就有1280 * 720 * 3 ≈ 2.6 MB。30帧每秒就是近80MB/s的带宽,这显然不现实。必须压缩。

  • MJPEG(Motion JPEG):本质就是每一帧都进行JPEG静态图片压缩。优点是编解码非常简单,很多摄像头直接支持,在Unity里用Texture2D.LoadImage就能直接解码。缺点是压缩率相对较低,因为帧间没有利用冗余信息。适合对延迟极其敏感、CPU资源充足但带宽有限的场景。
  • H.264:主流的视频编码标准,压缩率极高。它通过帧内预测和帧间预测,能大幅减少连续帧之间的冗余数据。缺点是编解码复杂,需要引入专门的编解码库(如Intel Media SDK, NVIDIA Video Codec SDK,或软件库如FFmpeg的libx264)。在Unity端解码也更麻烦。

我们的选择:为了简化教程流程,降低实现门槛,我们选择MJPEG作为压缩格式。这样发送端只需要用System.DrawingOpenCVSharp捕获画面并编码成JPEG字节流,接收端Unity可以直接处理。这对于实现一个原型系统或对画质要求不是极端高的监控场景,是完全可行且高效的。后续如果需要提升压缩率,可以在此基础上替换为H.264编码。

2.3 整体架构流程图

整个系统的数据流可以这样描述:

[摄像头/模拟视频源] -> [发送端程序(C# Console)] | | (捕获画面,压缩为JPEG字节流) V [UDP Socket发送器] -> [网络] -> [UDP Socket接收器(Unity内)] | | (接收数据包,重组帧数据) V [Unity中的视频流管理器] -> [JPEG字节流解码为Texture2D] -> [RawImage UI组件/Material显示]

发送端负责抓帧、压缩、分包(如果一帧JPEG太大,超过UDP单包限制~64KB)并通过UDP发送。Unity端负责收包、组帧、解码并更新到纹理上。

3. 开发环境搭建与项目初始化

工欲善其事,必先利其器。我们先确保手头的工具都齐备。

3.1 软件安装清单

  1. Unity 2020.3 LTS:选择长期支持版更稳定。安装时记得勾选Windows Build Support下的IL2CPPWindows Mono两个后端,以及Visual Studio 2019编辑器支持。
  2. Visual Studio 2019:安装时工作负载选择“.NET 桌面开发”“使用C++的桌面开发”。后者是为了以防万一需要编译一些本地插件。
  3. .NET Framework 4.7.2 或更高版本:VS2019通常会自带。我们的发送端控制台程序将基于.NET Framework Console App。

3.2 创建Unity项目与设置

  1. 打开Unity Hub,新建一个3D项目,模板选3D (Core),项目名称比如UDPVideoStream
  2. 进入项目后,首先调整一些关键设置:
    • Edit -> Project Settings -> Player
      • Resolution and Presentation下,取消勾选Run in Background?不,对于监控客户端,我们需要勾选它,确保即使窗口失去焦点,UDP接收线程也能继续工作。
      • Other Settings下,将Api Compatibility Level设置为.NET 4.x.NET Framework。这是使用System.Net.Sockets等完整.NET库所必需的。
      • 同样在Other Settings,将Scripting Backend暂时设置为Mono,开发调试更方便。
  3. 在场景中创建一个UI用于显示视频:GameObject -> UI -> Raw Image,将其铺满屏幕,命名为VideoDisplay

3.3 创建发送端控制台项目

  1. 打开VS2019,新建项目,选择“控制台应用(.NET Framework)”,命名为VideoStreamSender,目标框架选择.NET Framework 4.7.2
  2. 我们需要一个库来处理图像捕获和JPEG编码。最简便的方法是使用NuGet包管理器。
    • 在解决方案资源管理器中右键项目 ->管理NuGet程序包
    • 浏览并安装OpenCvSharp4OpenCvSharp4.runtime.win。OpenCV提供了非常便捷的摄像头访问和图像编码功能。
    • 或者,如果你不想用OpenCV,也可以用System.Drawing配合AForge库,但OpenCV更强大和通用。

至此,软硬件环境就绪。接下来我们分别构建发送端和Unity接收端。

4. 发送端(VideoStreamSender)详细实现

发送端的核心任务就三步:抓图、压缩、发送。我们一步步来。

4.1 使用OpenCV捕获摄像头画面

首先,在VideoStreamSender项目的Program.cs中,引入必要的命名空间,并编写捕获逻辑。

using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using OpenCvSharp; namespace VideoStreamSender { class Program { // UDP相关 private static UdpClient udpSender; private static IPEndPoint remoteEndPoint; // Unity客户端的IP和端口 // 视频捕获相关 private static VideoCapture capture; private static bool isStreaming = false; private static int frameWidth = 640; private static int frameHeight = 480; private static int fps = 30; // 用于帧编号,解决乱序问题 private static ushort frameSequence = 0; static void Main(string[] args) { Console.WriteLine("UDP视频流发送端启动..."); Console.Write("请输入目标IP(Unity运行机器的IP): "); string ip = Console.ReadLine(); // 例如:192.168.1.100 Console.Write("请输入目标端口: "); int port = int.Parse(Console.ReadLine()); // 例如:8052 remoteEndPoint = new IPEndPoint(IPAddress.Parse(ip), port); udpSender = new UdpClient(); // 不绑定本地端口,系统自动分配 // 初始化摄像头 capture = new VideoCapture(0); // 0代表默认摄像头 if (!capture.IsOpened()) { Console.WriteLine("无法打开摄像头!"); return; } // 设置摄像头参数(非所有摄像头都支持) capture.Set(VideoCaptureProperties.FrameWidth, frameWidth); capture.Set(VideoCaptureProperties.FrameHeight, frameHeight); capture.Set(VideoCaptureProperties.Fps, fps); Console.WriteLine($"摄像头已打开,分辨率:{frameWidth}x{frameHeight}, FPS: {fps}"); Console.WriteLine("按任意键开始/停止流传输,按ESC退出。"); isStreaming = true; Thread streamThread = new Thread(new ThreadStart(StreamVideo)); streamThread.IsBackground = true; streamThread.Start(); while (true) { var key = Console.ReadKey(true); if (key.Key == ConsoleKey.Escape) { isStreaming = false; break; } else if (key.Key == ConsoleKey.Enter) { isStreaming = !isStreaming; Console.WriteLine(isStreaming ? "流传输恢复" : "流传输暂停"); } } // 清理 isStreaming = false; Thread.Sleep(100); // 给流线程一点时间结束 capture.Release(); udpSender.Close(); Console.WriteLine("发送端已关闭。"); } } }

4.2 JPEG压缩与UDP分包发送

这是核心函数StreamVideo的内容。一帧JPEG数据可能超过UDP的65507字节有效载荷限制(实际建议小于1400字节以避免IP分片),所以我们需要分包。

static void StreamVideo() { Mat frame = new Mat(); byte[] frameHeader = new byte[4]; // 用于存放帧序号和包序号 int maxPacketSize = 1024 * 63; // 每个UDP包最大约63KB,预留空间给包头 byte[] packetBuffer = new byte[maxPacketSize]; while (isStreaming) { capture.Read(frame); if (frame.Empty()) { Thread.Sleep(10); continue; } // 1. 将Mat(BGR格式)编码为JPEG字节数组 byte[] jpegBytes; Cv2.ImEncode(".jpg", frame, out jpegBytes, new int[] { ImwriteFlags.JpegQuality, 85 }); // 质量85% // 2. 计算需要多少个包来发送这一帧 ushort currentFrameSeq = frameSequence++; int totalPackets = (int)Math.Ceiling(jpegBytes.Length / (double)maxPacketSize); for (ushort packetIndex = 0; packetIndex < totalPackets; packetIndex++) { // 3. 构建包头:帧序号(2字节) + 包序号(2字节) // 使用大端序(网络字节序)以确保跨平台一致性 frameHeader[0] = (byte)(currentFrameSeq >> 8); frameHeader[1] = (byte)(currentFrameSeq & 0xFF); frameHeader[2] = (byte)(packetIndex >> 8); frameHeader[3] = (byte)(packetIndex & 0xFF); // 4. 计算当前包的数据起始位置和长度 int offset = packetIndex * maxPacketSize; int length = Math.Min(maxPacketSize, jpegBytes.Length - offset); // 5. 将包头和载荷数据复制到发送缓冲区 Buffer.BlockCopy(frameHeader, 0, packetBuffer, 0, 4); Buffer.BlockCopy(jpegBytes, offset, packetBuffer, 4, length); // 6. 发送UDP数据包 try { udpSender.Send(packetBuffer, length + 4, remoteEndPoint); } catch (SocketException ex) { Console.WriteLine($"发送数据包时出错: {ex.Message}"); } // 可选:微小的延迟以避免瞬间占满带宽,模拟恒定比特率 // Thread.Sleep(1); } // 控制帧率 Thread.Sleep(1000 / fps); } frame.Dispose(); }

关键点解析

  1. 包头设计:我们用了4个字节。前2字节是frameSequence(帧序号),用于区分不同帧。后2字节是packetIndex(包序号),用于将一帧的数据按顺序重组。这种简单设计足以应对乱序问题。
  2. JPEG质量ImwriteFlags.JpegQuality, 85是一个平衡点。质量越低,压缩率越高,带宽占用越小,但画质损失越大。你可以根据实际网络状况调整。
  3. 分包逻辑:循环切割jpegBytes数组,每次发送maxPacketSize大小的数据(加上4字节包头)。接收端需要根据frameSequencepacketIndex重新组装。
  4. UDP发送UdpClient.Send方法是同步的。在高速流媒体中,这可能会成为瓶颈。对于更高性能的需求,可以考虑使用BeginSend/EndSend异步方法,或者使用Socket类直接操作。

发送端到这里就完成了。编译运行,输入Unity客户端的IP和端口,它就会开始发送视频流。

5. Unity客户端(接收与渲染)核心实现

Unity端的任务更繁重一些:收包、组帧、解码、渲染。为了不阻塞主线程,网络接收必须在单独的线程中进行。

5.1 创建UDP视频流管理器

在Unity项目中,创建一个C#脚本,命名为UDPVideoReceiver.cs,并将其挂载到一个空的GameObject上(例如VideoManager)。

using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using UnityEngine; using UnityEngine.UI; public class UDPVideoReceiver : MonoBehaviour { [Header("网络设置")] public string listenIP = "0.0.0.0"; // 监听所有网络接口 public int listenPort = 8052; // 与发送端端口一致 [Header("显示设置")] public RawImage displayRawImage; // 拖拽赋值 public int targetWidth = 640; public int targetHeight = 480; // UDP接收相关 private UdpClient udpReceiver; private Thread receiveThread; private bool isReceiving = false; // 帧重组相关 private Dictionary<ushort, Dictionary<ushort, byte[]>> framePackets = new Dictionary<ushort, Dictionary<ushort, byte[]>>(); private ushort expectedFrameSeq = 0; private object frameLock = new object(); // 用于线程安全操作字典 private Texture2D displayTexture; private bool textureNeedsUpdate = false; private byte[] latestFrameData = null; void Start() { if (displayRawImage == null) { Debug.LogError("Display RawImage is not assigned!"); return; } // 初始化显示纹理 displayTexture = new Texture2D(targetWidth, targetHeight, TextureFormat.RGB24, false); displayRawImage.texture = displayTexture; // 启动UDP接收线程 StartReceiving(); } void StartReceiving() { try { udpReceiver = new UdpClient(new IPEndPoint(IPAddress.Parse(listenIP), listenPort)); isReceiving = true; receiveThread = new Thread(new ThreadStart(ReceiveData)); receiveThread.IsBackground = true; receiveThread.Start(); Debug.Log($"开始监听UDP端口: {listenPort}"); } catch (SocketException e) { Debug.LogError($"启动UDP监听失败: {e.Message}"); } } }

5.2 数据接收与帧重组逻辑

这是最核心的部分,在独立的线程中运行。

private void ReceiveData() { IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); while (isReceiving) { try { // 阻塞式接收数据 byte[] receivedBytes = udpReceiver.Receive(ref remoteEndPoint); if (receivedBytes.Length < 4) continue; // 忽略无效包 // 1. 解析包头 ushort frameSeq = (ushort)((receivedBytes[0] << 8) | receivedBytes[1]); // 大端序解析 ushort packetIndex = (ushort)((receivedBytes[2] << 8) | receivedBytes[3]); // 2. 提取载荷数据(去掉4字节包头) byte[] packetData = new byte[receivedBytes.Length - 4]; Buffer.BlockCopy(receivedBytes, 4, packetData, 0, packetData.Length); // 3. 线程安全地将数据包存入字典 lock (frameLock) { if (!framePackets.ContainsKey(frameSeq)) { framePackets[frameSeq] = new Dictionary<ushort, byte[]>(); } framePackets[frameSeq][packetIndex] = packetData; // 4. 尝试组装当前期望的帧 TryAssembleFrame(frameSeq); } } catch (SocketException e) { // 通常是因为线程被关闭时,Receive调用被中断 if (isReceiving) Debug.LogWarning($"接收数据时Socket异常: {e.Message}"); break; } catch (Exception e) { Debug.LogError($"接收数据时发生未知异常: {e.Message}"); } } Debug.Log("接收线程结束。"); }

TryAssembleFrame方法负责检查一帧的所有包是否到齐,并组装成完整的JPEG数据。

private void TryAssembleFrame(ushort arrivedFrameSeq) { // 我们采用简单的“按顺序处理”策略,只处理expectedFrameSeq if (!framePackets.ContainsKey(expectedFrameSeq)) return; var packets = framePackets[expectedFrameSeq]; // 我们需要知道这一帧总共有多少个包。这里采用一个简单的启发式方法: // 假设包序号是从0开始连续递增的。找到最大的包序号,并检查从0到最大序号是否都存在。 // 更健壮的做法是在发送端发送一个“帧结束”包或包含总包数的包头,这里为简化先这样处理。 int maxPacketIndex = -1; foreach (var idx in packets.Keys) { if (idx > maxPacketIndex) maxPacketIndex = idx; } // 检查是否收集了从0到maxPacketIndex的所有包 bool isFrameComplete = true; for (int i = 0; i <= maxPacketIndex; i++) { if (!packets.ContainsKey((ushort)i)) { isFrameComplete = false; break; } } if (isFrameComplete && maxPacketIndex >= 0) { // 5. 组装所有包的数据 int totalLength = 0; for (int i = 0; i <= maxPacketIndex; i++) { totalLength += packets[(ushort)i].Length; } byte[] completeFrameData = new byte[totalLength]; int offset = 0; for (int i = 0; i <= maxPacketIndex; i++) { byte[] pktData = packets[(ushort)i]; Buffer.BlockCopy(pktData, 0, completeFrameData, offset, pktData.Length); offset += pktData.Length; } // 6. 将组装好的帧数据传递给主线程处理 latestFrameData = completeFrameData; textureNeedsUpdate = true; // 7. 清理已处理的帧数据 framePackets.Remove(expectedFrameSeq); expectedFrameSeq++; // 期待下一帧 } // 可选:如果帧序号跳跃太大(丢了很多帧),可以重置expectedFrameSeq // 这里简单处理,只按顺序处理 }

5.3 在主线程中更新纹理

Unity中,创建和修改Texture2D必须在主线程进行。我们使用Update循环来检查标志位。

void Update() { if (textureNeedsUpdate && latestFrameData != null) { // 使用LoadImage方法从JPEG字节数组加载纹理(主线程安全) bool success = displayTexture.LoadImage(latestFrameData); if (success) { // 可选:如果纹理尺寸变化,需要调整RawImage的显示比例 // 但LoadImage会自动调整Texture2D的尺寸,所以通常不需要 displayRawImage.texture = displayTexture; } else { Debug.LogWarning("JPEG解码失败,可能数据损坏。"); } textureNeedsUpdate = false; // 注意:不清空latestFrameData,因为可能同一帧数据被用于重试(如果解码失败) // 在实际应用中,可能需要更精细的管理 } } void OnDestroy() { // 安全停止线程和关闭Socket isReceiving = false; if (udpReceiver != null) { udpReceiver.Close(); } if (receiveThread != null && receiveThread.IsAlive) { receiveThread.Join(500); // 等待线程结束,最多500ms } Debug.Log("UDP接收器已清理。"); }

UDPVideoReceiver脚本挂载到场景中的VideoManager对象,并将UI Canvas中的RawImage组件拖拽到脚本的Display RawImage字段。运行Unity项目,确保发送端也在运行并指向正确的IP和端口,你应该就能在Game视图中看到实时视频流了。

6. 性能优化与高级技巧

基础功能跑通后,我们来看看如何让它更稳定、更高效。

6.1 降低延迟与CPU占用

  • 发送端

    • 调整JPEG质量与分辨率:这是最直接的手段。将分辨率从1280x720降到640x480,JPEG质量从90降到70,带宽占用可能减少80%以上。
    • 使用双缓冲或队列capture.ReadCv2.ImEncode可能速度不一致。可以使用生产者-消费者模式,一个线程专门抓帧放入队列,另一个线程从队列取帧编码发送,避免因编码慢而丢帧。
    • 禁用Nagle算法:对UDP无效,但如果是TCP需要考虑。对于UDP,关键是控制发送速率,避免瞬间爆发。
  • Unity接收端

    • 限制帧率:在Update中更新纹理是昂贵的。可以设置一个目标渲染帧率(如25 FPS),只有在新帧到达且距离上一帧渲染时间超过间隔时才更新纹理。
    • 使用Texture2D.LoadRawTextureDataJpegDecoderLoadImage内部会进行完整的JPEG解码,CPU开销较大。可以尝试使用UnityEngine.Experimental.Rendering.JpegDecoder(如果版本支持)或第三方纯C# JPEG解码库(如ImageSharp)在后台线程解码,然后将RGB数据通过LoadRawTextureData加载到纹理,这能显著降低主线程负担。
    • 对象池管理帧数据:频繁创建byte[]数组会引发GC(垃圾回收),导致卡顿。可以为latestFrameData和包字典使用的缓冲区实现简单的对象池。

6.2 增强传输可靠性(简易版)

UDP不保证可靠,但我们可以在应用层增加一些轻量级的保障。

  • 心跳包与超时重连:发送端定期(如每秒)发送一个特殊的心跳包。Unity端如果超过一定时间(如3秒)没收到任何数据包(包括心跳和视频包),则认为连接断开,可以尝试重连或显示“信号丢失”。
  • 选择性重传:接收端发现某帧缺失了部分包,可以向发送端发送一个NACK(否定确认)包,包含缺失的帧序号和包序号。发送端收到后重传该包。这比TCP的全重传机制更高效。实现起来稍复杂,需要双向通信(Unity也需要一个发送Socket)。

6.3 处理多摄像头与画面布局

如果需要同时显示多个摄像头,架构可以这样扩展:

  1. 发送端:每个摄像头一个独立的发送线程和UDP端口(或使用同一个Socket但目标端口不同)。
  2. Unity端:创建多个UDPVideoReceiver实例,每个实例监听不同的端口,并关联到不同的RawImage
  3. 画面布局:使用Unity的UI系统(Grid Layout Group, Horizontal/Vertical Layout Group)或调整多个RawImage的Rect Transform,轻松实现画中画、四宫格、九宫格等布局。

7. 常见问题排查与调试心得

在实际搭建过程中,你几乎一定会遇到下面这些问题。这里是我踩过坑后的经验总结。

7.1 画面卡顿、延迟高

  • 检查发送端帧率:在发送端循环中打印每秒实际发送的帧数,确认是否达到设定的FPS。可能是摄像头本身帧率不足,或编码耗时太长。
  • 检查网络带宽:使用任务管理器或网络监控工具,查看发送端和接收端的网络利用率。如果接近或超过局域网带宽(考虑其他流量),必然卡顿。降低分辨率或码率。
  • Unity端性能瓶颈:在Unity编辑器的Profiler窗口中,查看UpdateRender阶段的CPU耗时。如果LoadImage耗时很长(比如超过10ms),就是瓶颈。考虑使用6.1中提到的高效解码方法。
  • UDP缓冲区溢出:如果发送太快,接收端的UDP缓冲区可能被塞满,导致丢包。可以尝试在Unity端初始化UdpClient时设置更大的接收缓冲区:udpReceiver.Client.ReceiveBufferSize = 1024 * 1024; // 1MB。发送端也可以设置发送缓冲区。

7.2 画面花屏、撕裂、显示不全

  • 包顺序错乱或丢失:这是最常见的原因。我们的重组逻辑依赖于包序号的连续性。如果网络抖动导致包乱序严重或丢失,就会组装出错误的JPEG数据,导致解码失败(花屏)或LoadImage返回false。增加日志:在TryAssembleFrame中打印收到的帧序号和包序号,观察是否连续。如果丢包严重,考虑6.2的可靠性增强。
  • JPEG数据损坏:确保在组帧时,Buffer.BlockCopy的源数据、目标数组和长度计算正确。一个字节错位都会导致解码失败。
  • 纹理格式不匹配Texture2D的格式是RGB24,但JPEG解码后的数据可能是RGBA顺序?实际上LoadImage会自动处理。如果使用LoadRawTextureData,则需要确保数据格式完全匹配。

7.3 Unity编辑器运行正常,打包后黑屏/无画面

  • .NET API兼容性:确保Player Settings中的Api Compatibility Level设置为.NET 4.x.NET Standard 2.0可能缺少某些API。
  • 防火墙/杀毒软件:打包后的可执行文件可能被系统防火墙拦截。需要在防火墙设置中允许该程序通过。
  • 后台运行权限:确保Player Settings中Run in Background已勾选。
  • 线程异常静默:在打包版本中,后台线程的异常可能不会弹出提示,导致线程静默死亡。在ReceiveDatacatch块中,将错误信息写入文件或通过某种方式反馈到UI上。

7.4 如何测试与模拟网络环境

  • 本地回环测试:发送端目标IP设为127.0.0.1,Unity也运行在同一台机器上。这排除了网络问题。
  • 使用网络调试工具:如Wireshark,可以过滤UDP端口,查看数据包是否正常收发,分析包大小、间隔和丢失情况。
  • 模拟网络损伤:在路由器或通过软件(如Clumsy)模拟丢包、延迟和乱序,测试你代码的健壮性。

最后,这个项目只是一个起点。你可以在此基础上,增加录制功能、运动检测、云台控制(通过发送指令给摄像头)等,构建一个功能完整的视频监控系统。关键是把基础的数据流管道搭建稳定,后续的扩展就水到渠成了。

← 返回列表