
简介这是一份面向C#开发者与安防监控初学者的视频监控上位机源码覆盖视频流捕获、实时预览与基础画面处理等核心环节适合正在学习.NET多媒体编程或需要快速搭建监控演示项目的读者。压缩包为zip格式共收纳若干源码与工程配置文件整体大小约5.28MB下载后解压即可在Visual Studio中打开编译插上USB摄像头便能直接看到实时监控画面。项目基于.NET框架实现涉及Media Foundation或AForge.NET类库调用代码结构清晰包含窗体界面、视频采集逻辑与必要的配置说明便于逐段理解流程并二次扩展例如加入图像滤波、运动检测或网络传输等功能。资源已获得313人关注学习作为入门级可用示例能帮助开发者省去环境搭建与底层API调研的时间更快上手C#视频监控应用的开发思路。1. 从“C#视频监控”到能看的系统先拆模块再选武器之前某公司的车间改造找到我说老板要上 8 路网络摄像头的视频监控想能实时看、能录像、能报警。问了一圈商业软件年费加授权费不便宜而且摄像头品牌一换就得重新谈。最后我用 C#从零搭了一套监控系统从取流、预览、录像到回放全部自己控制。这块技术方向用一句话说就是用 C# 和 .NET 生态把视频采集、编码解码、画面预览、录像落盘、时间检索这几个模块串成一套可运行的服务它解决的是商业监控软件太贵、定制困难、没法按自己业务逻辑做联动报警的问题。适合有 C# 基础想自己掌控监控链路的开发者和集成商。这套方案的起点不是写代码而是先把取流这件事彻底搞清楚。2. 取流前的三个确认协议、参数、最小可读代码2.1 摄像头取流协议怎么选RTSP、RTMP、HTTP 的场景边界网络摄像头取流第一件事不是写 C# 代码而是确认设备支持什么协议。现在主流网络摄像头基本都支持 RTSP这是视频监控领域的标准协议适合一对一拉流、低延迟。RTMP 更多用于推流到服务器再分发适合直播场景监控里直接用得少。还有一部分摄像头厂商提供 HTTP 取流接口返回 MJPEG 或静态图片这种接口延迟高、帧率不稳定做实时监控会很吃力。所以我的结论是做 C# 视频监控优先走 RTSP除非设备确实不支持。RTSP 地址的典型长这样rtsp://用户名:密码IP:端口/路径。默认端口是 554。路径部分不同品牌差异很大有的在 / 后面直接跟 Stream有的走 ONVIF 标准你需要登录摄像头的管理页去查不要靠猜。摄像头通常提供两路码流主码流分辨率高、码率高适合录像子码流分辨率低适合墙上的实时预览和多画面拼接。我一般会先记录这两个地址因为后面做录像和预览会分别用到。RTSP 传输还有一个隐藏选项RTP 底层走 TCP 还是 UDP。默认很多设备用 UDP因为开销小、实时性好但在 Wi-Fi 或者跨交换机传输时UDP 丢包会造成花屏和画面卡顿。C# 侧做取流时一定要把传输模式显式指定为 TCP。这个点后面避坑章节会再展开说。2.2 用 ffprobe 验证流信息分辨率、编码、帧率三个参数别靠设备管理页拿到 RTSP 地址后不要直接开代码先用 FFmpeg 自带的 ffprobe 工具把流信息摸清楚。这一步能确认地址对不对、编码是什么、帧率分辨率到底多少。很多设备管理页上的参数和实际流输出不一致只有 ffprobe 的输出才是真实的。ffprobe -rtsp_transport tcp -i rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101 -show_streams -show_format命令里-rtsp_transport tcp强制走 TCP避免 UDP 探测时丢包导致信息不完整-show_streams列出视频流详细信息-show_format显示封装层信息。执行后重点看这几个字段codec_name是视频编码H264 最常见也可能是 hevc/h265width和height是实际分辨率avg_frame_rate是平均帧率比如 25/1 代表 25fps。把这些记录下来后面 C# 侧设置缓冲区大小和帧率控制全靠它们。摄像头取流地址常见有两种风格一种是/Streaming/Channels/101这样的厂商私有路径其中 101 表示主码流102 表示子码流另一种是 ONVIF 标准的/media/video1或者/cam/realmonitor?channel1subtype0。如果 ffprobe 报认证失败先检查 URL 里是否带了特殊字符密码如果有 或者 : 需要做百分号编码。这一步是玄学最少的一步只要 ffprobe 能读出流后面 C# 基本不会出大问题。2.3 用 OpenCvSharp 打开 RTSP 流最小可运行的 C# 取帧代码确认流没问题后进入 C# 侧。最简单的取帧方案是用 OpenCvSharp 的 VideoCapture 类它底层封装了 FFmpeg所以 RTSP、H264、H265 通吃不需要自己写解码逻辑。下面这段代码是最小可运行的取帧示例也适合用来验证环境是否装对。using OpenCvSharp; // 用摄像头主码流地址创建 VideoCapture using var capture new VideoCapture(rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101); capture.Set(VideoCaptureProperties.FrameWidth, 1280); capture.Set(VideoCaptureProperties.FrameHeight, 720); capture.Set(VideoCaptureProperties.BufferSize, 1); using var frame new Mat(); if (capture.Read(frame)) { // 读到了第一帧写成 jpg 验证取流成功 Cv2.ImWrite(first_frame.jpg, frame); } else { Console.WriteLine(取流失败检查网络和 RTSP 地址); }这段代码的逻辑是创建 VideoCapture 并传入 RTSP 地址随后设置分辨率、缓冲区大小再调用capture.Read(frame)阻塞式读取一帧。FrameWidth和FrameHeight只是请求值实际分辨率以摄像头输出为准如果设备不支持会静默忽略所以读取后一定要检查 frame 的尺寸。BufferSize设为 1 很关键它让底层解码缓冲区只保留最近一帧对实时监控来说能明显降低延迟默认值可能攒好几帧画面看起来会慢半拍。Read是阻塞调用卡住时可能是在等下一帧也可能是在等网络重连不能直接放在 UI 线程里。首次取流偶尔会超时常见做法是加一个重试循环间隔 2 到 3 秒重新 create 一次。3. 预览与播放OpenCvSharp 和 LibVLC 两条路线选哪条3.1 方案一OpenCvSharp 逐帧显示适合带算法处理的预览取帧之后最直接的需求是实时预览。OpenCvSharp 的优势是每一帧都是 Mat你可以直接在画面上叠加运动检测框、人数统计、区域入侵报警缺点是它本质是把每一帧解码成图像再交给 UI 显示CPU 占用比专用播放方案更高。如果只是把画面扔到界面上这个方案足够用如果后面还要做算法联动它是唯一省事的路。下面是一段在 WinForms 里显示实时画面的最小实现核心是把解码循环放到后台线程避免阻塞 UI。private bool _stop; private readonly object _lock new object(); private void StartPreview() { var thread new Thread(CameraLoop) { IsBackground true }; thread.Start(); } private void CameraLoop() { using var capture new VideoCapture(rtsp://admin:123456192.168.1.64:554/Streaming/Channels/102); capture.Set(VideoCaptureProperties.BufferSize, 1); using var frame new Mat(); while (!_stop) { if (capture.Read(frame)) { // 转成 Bitmap 后复制一份跨线程传给 UI using var bmp BitmapConverter.ToBitmap(frame); var snapshot (Bitmap)bmp.Clone(); pictureBox.BeginInvoke(new Action(() { var old pictureBox.Image; pictureBox.Image snapshot; old?.Dispose(); })); } else { Thread.Sleep(1000); // 读取失败休息 1 秒后重试 } } }这段代码的逻辑是后台线程持续调用capture.Read(frame)每次读到一帧就转成 Bitmap再用BeginInvoke丢给 UI 线程更新 PictureBox。这里强制 Clone 一份是因为BitmapConverter.ToBitmap(frame)复用了 Mat 的托管内存Mat 在下一次Read时会被覆盖不复制的话 UI 上显示的图像会闪或花。UI 线程替换 PictureBox.Image 的时候顺手把旧图 Dispose 掉否则内存每秒涨十几张图的量几分钟就能把内存吃满。BeginInvoke是异步的不会卡住解码线程但注意 UI 更新速度和解码速度不一致时事件队列会积压所以下面还会说丢帧策略。这个方案的实际延迟大概在 100 到 300 毫秒取决于解码速度和 UI 刷新。它的典型问题是 CPU 占用偏高因为每一帧都经历了 Mat → Bitmap → UI 三层拷贝对 8 路以上的需求建议只对需要做算法分析的通道用这种方式其他通道用下一节的方案。3.2 方案二LibVLC 拉流播放适合大屏低延迟监控墙如果目标是做监控墙、大屏多画面、低 CPU 占用我一般不会用 OpenCvSharp 做持续显示而是用 LibVLC 的跨平台绑定库。LibVLC 自带 H264、H265 解码器支持硬件解码延迟能做到 100 毫秒以内CPU 占用远低于逐帧转换的方案。对纯显示场景来说这是最省心的路线。using LibVLCSharp.Shared; // 全局只创建一个 LibVLC 实例不要每路摄像头都 new var libVLC new LibVLC(); libVLC.SetLogLevel(LogLevel.Warn); libVLC.SetOption(--rtsp-tcp); // 强制 RTSP 走 TCP libVLC.SetOption(--network-caching200); // 网络缓存 200ms过低容易卡顿 var mediaPlayer new MediaPlayer(libVLC); mediaPlayer.EnableHardwareDecoding true; mediaPlayer.Play(new Media(libVLC, rtsp://admin:123456192.168.1.64:554/Streaming/Channels/102, FromType.FromLocation));这段代码的逻辑是创建 LibVLC 实例设置 RTSP 走 TCP 并调整网络缓存然后创建 MediaPlayer 并直接传入 RTSP 地址播放。--network-caching单位是毫秒200 到 300 是一个比较稳的经验值设太低了容易在丢包时卡住设高了延迟明显。EnableHardwareDecoding开启硬件解码对 H265 尤其重要否则高分辨率路数多了 CPU 会到极限。WPF 里把 MediaPlayer 绑定到 VideoView 控件即可显示WinForms 下也有对应封装如果你不使用 VideoView也可以从 MediaPlayer 里取出视频帧做自定义渲染但那就又回到复杂路线了。选这条路线时要注意LibVLC 只负责播放和显示不会给你一帧帧的 Mat如果想做运动检测或者叠加自定义图形得从渲染纹理或帧回调里拿数据开发成本明显比 OpenCvSharp 高。所以我的选型标准很简单纯预览用 LibVLC要做算法用 OpenCvSharp混合场景就预览走 LibVLC、算法通道单独用 OpenCvSharp 抓帧。3.3 为什么解码不能放 UI 线程卡顿背后的真相新手做视频监控最容易踩的坑是把取流和解码循环直接放进 Timer 或 UI 线程里。结果就是画面一卡一卡拖拽窗口时整个界面假死CPU 甚至打满。原因是capture.Read()是一个阻塞调用一次网络等待或解码耗时可能几十毫秒UI 线程在这个时间段里什么都不能干。帧率越高UI 被堵得越死。正解是让解码持续跑在后台线程UI 线程只负责取最新一帧做显示。上一节给的代码就是这么干的。再进阶一点的常见做法是解码线程把帧放入一个固定长度的队列UI 线程定时从队列取最新帧显示如果队列满了直接丢弃最旧的一帧保证 UI 永远显示最新画面而不是追着积压帧跑。这个模型在监控里叫“丢帧策略”。实现上可以不要队列直接在解码线程里用一个 volatile Bitmap 保存最新快照UI 的 Timer 每秒刷新 15 到 20 次就够。这样可以极大降低 UI 线程压力8 路同时开也不会把界面拖垮。4. 录像存储与回放切片、命名、索引与断线重连4.1 录像文件怎么命名按时间切片的三个理由录像落盘是整个监控系统的核心。最常见的方案不是让 C# 自己去拿每一帧编码写文件而是启动一个 FFmpeg 子进程由它负责拉流、转封装、切段存储。这样 C# 只管理进程生命周期和文件索引省掉大量底层工作而且 FFmpeg 对断流、异常退出的处理比我们手写的循环要成熟得多。录像文件一定要按时间切片而不是录成一个整天大的单文件。原因有三个第一单文件损坏的代价太大一个 24 小时的文件只要中间 IT 存储抖动一下可能整个文件都打不开切片后最多损失一个片段。第二回放定位时按时间段查找文件更高效不需要在超大文件里拖动进度条。第三如果后面要把录像上传或转存小文件方便断点续传。切片长度我一般设 5 到 15 分钟太短文件数太多太长损坏范围大。文件命名格式用通道号加 UTC 时间或服务器本地时间比如CH01_20260612_153000.mkv其中 CH01 是通道号20260612 是日期153000 是开始时间。命名里不要用摄像头自带的时间很多网络摄像头不带电池重启后时间会漂移这是视频监控里最坑的细节之一后面的避坑章节会专门讲。4.2 用 FFmpeg 子进程做录像命令、参数与退出重连录像模块我一般用 Process 启动 FFmpeg传入合适的参数让它自己拉流切片。命令行长这样ffmpeg -rtsp_transport tcp -i rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101 -c copy -f segment -segment_time 300 -segment_format mkv -strftime 1 D:/recordings/CH01_%Y%m%d_%H%M%S.mkv这里-c copy是关键的选型不重新编码直接把原始 H264 流封装进 mkvCPU 占用极低录像画质无损。-f segment启用切片模式-segment_time 300表示每 300 秒切一段-segment_format mkv指定输出封装格式-strftime 1允许文件名里用时间格式化符。如果摄像头输出 H265这个命令同样适用因为-c copy不关心编码格式只做封装。要注意的是 mkv 在流中断时容忍度比 mp4 高很多mp4 的索引通常在文件尾部一旦断电很容易整个文件损坏所以无人值守的监控录像优先 mkv 或 ts。C# 侧启动这个进程代码很直接var psi new ProcessStartInfo(ffmpeg, -rtsp_transport tcp -i \rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101\ -c copy -f segment -segment_time 300 -segment_format mkv -strftime 1 \D:/recordings/CH01_%Y%m%d_%H%M%S.mkv\) { UseShellExecute false, RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow true }; var proc Process.Start(psi); proc.Exited (s, e) Console.WriteLine(FFmpeg 退出准备重连);UseShellExecutefalse是为了重定向输出方便记录日志排查错误CreateNoWindowtrue让 ffmpeg 不在桌面弹黑色窗口。FFmpeg 的日志默认走 stderr所以RedirectStandardError开后要异步读取否则缓冲区满了进程会卡住。录像进程最怕的是静默退出断流、摄像头重启、网络闪断都会让 ffmpeg 自动退出。我一般在 Exited 事件里做重连等 10 秒再拉起新进程并且限制连续失败次数超过 10 次就发报警避免死循环无限重启。录像开始前要确认目录存在如果D:/recordings/不存在FFmpeg 会直接报错退出这种低级错误在无人值守的系统上特别容易发生。4.3 用 SQLite 记录录像索引回放定位的正确姿势录像文件到一定数量后光靠文件名没法高效检索。比如用户要查昨天上午 10 点到 10 点半的录像你得先解析文件名、再排除重叠段文件一多就慢到不可用。所以我会建一个 SQLite 索引表把每个录像文件的通道、开始时间、结束时间、路径和大小存进去回放时直接查库。建表语句和插入逻辑如下CREATE TABLE IF NOT EXISTS recordings ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel TEXT NOT NULL, start_time TEXT NOT NULL, end_time TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER, created_at TEXT DEFAULT (datetime(now)) ); CREATE INDEX IF NOT EXISTS idx_recordings_channel_start ON recordings(channel, start_time);using var conn new SqliteConnection(Data Sourcerecords.db); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO recordings (channel, start_time, end_time, file_path, file_size) VALUES ($ch, $st, $et, $fp, $sz); cmd.Parameters.AddWithValue($ch, CH01); cmd.Parameters.AddWithValue($st, startTime.ToString(yyyy-MM-dd HH:mm:ss)); cmd.Parameters.AddWithValue($et, endTime.ToString(yyyy-MM-dd HH:mm:ss)); cmd.Parameters.AddWithValue($fp, filePath); cmd.Parameters.AddWithValue($sz, fileSize); cmd.ExecuteNonQuery();我的习惯是每次 FFmpeg 正常切段后扫描生成的新文件把时间、路径写入 SQLite如果 FFmpeg 中途崩溃导致一个文件时间不完整也照写进去但 end_time 用文件实际修改时间。回放时查某段时间的录像用下面这条 SQLSELECT file_path FROM recordings WHERE channel $ch AND start_time $endTime AND end_time $startTime ORDER BY start_time;这条 SQL 用的是区间重叠判断能正确处理跨切片边界的查询。如果你把时间存成字符串格式必须统一我建议全部用yyyy-MM-dd HH:mm:ss存服务器本地时间虽然 UTC 更标准但监控使用方通常只看本地时间存本地时间可以少一次转换。索引建在(channel, start_time)上几万条记录查询也很快。等到文件数量上几十万SQLite 依然够用到时候再考虑换 PostgreSQL 也不迟。5. 视频监控常见坑与排查五个高频问题逐条拆5.1 坑一画面间歇性花屏、卡顿但 CPU 占用不高现象预览画面每隔几秒出现马赛克或者停住一两秒后跳帧录像文件里同样存在。排查时发现 CPU 和内存都正常网络带宽也够。原因RTSP 默认传输走 UDP。UDP 在跨交换机、Wi-Fi、高负载链路下会丢包丢包后解码器收到不完整帧就表现为花屏和卡顿。很多时候摄像头和客户端在同一局域网也会发生因为 Wi-Fi 本身就有丢包率。解决强制 RTSP 走 TCP。FFmpeg 和 ffprobe 都加-rtsp_transport tcpLibVLC 里用--rtsp-tcpOpenCvSharp 里通过设置属性强制capture.Set(VideoCaptureProperties.RtspTransport, 0); // 0 表示 TCP如果 TCP 之后仍然卡顿再检查网络延迟和交换机端口协商把网线和交换机端口换成千兆。我遇到过最隐蔽的情况是摄像头和电脑都在同一个交换机下但交换机某个端口处于半双工状态强制走 TCP 后反而触发大量重传画面更差这种就只能换端口或换交换机了。建议在网络不稳定的环境里把 TCP 做默认选项不要留给用户配置。5.2 坑二H265 摄像头在 WinForms 内置播放器上黑屏打不开现象摄像头换成 H265 编码后C# 里用系统自带的 MediaPlayer 控件播放 RTSP 地址画面一直是黑的有的伴随声音正常有的直接报找不到解码器。原因Windows 自带的 Media Foundation 对 H265 的支持依赖系统解码器而 H265 的版权授权让很多系统默认不带硬解或软解。WinForms 里的 MediaPlayer 控件本质就是调用系统解码能力所以对 H265 支持很差。这不是你代码的问题。解决切到 LibVLC 方案它自带 H265 解码器。或者在生产部署时把摄像头子码流编码强制设成 H264主码流保留 H265 用于录像预览走子码流的 H264录像走主码流 H265。这样既能保证画面可见又能节省存储。如果你已经在用 OpenCvSharp它底层走 FFmpeg对 H265 的软解支持是好的不需要换方案。5.3 坑三录像程序跑几小时后内存飙升最后崩溃现象程序刚启动内存 200MB跑 6 小时后涨到 2GB点击界面卡顿最后直接 OOM 崩溃。原因视频每帧都生成 Bitmap 或 Mat如果循环里没有显式释放或者 PictureBox.Image 替换时旧图没有 Dispose内存就会以每秒 25 帧的速度增长。另一个常见原因是BeginInvoke的委托队列积压解码线程生产帧的速度比 UI 线程消费快消息队列里存了成千上万个待显示帧每个帧都是一张图内存自然爆。解决在替换 PictureBox.Image 时显式 Dispose 旧图前面 3.1 的代码已经是这样写的。同时加丢帧策略解码线程里只保存最新帧UI 更新完就重置不要每次读帧都无脑 Invoke。具体做法是定义一个标志位UI 线程处理完一帧后置位解码线程看到标志位未复位就跳过当前帧。这样内存占用是稳定的CPU 也降下来。排查时用性能计数器观察“托管堆大小 #”和“线程队列长度”两个指标哪一个异常都能快速定位。5.4 坑四录像文件时间不对凌晨的录像跑到了前一天现象回放时发现凌晨 0 点到 2 点的录像文件名是前一天日期或者录像时间和实际时间差了好几个小时。索引表里的 start_time 也和界面显示不一致。原因用了摄像头自带时间生成文件名或索引时间。多数网络摄像头没有内置电池断电重启后时间会重置到出厂值有的摄像头时间走的是 UTC而监控界面显示的是 UTC8差 8 小时。摄像头时间一旦漂移所有依赖它的时间戳都会错。解决录像文件名和索引时间一律以服务器本地时间为准与摄像头时间完全解耦。FFmpeg 命令里的-strftime 1用的就是执行进程的系统时间所以只要服务器时间同步文件名就是对的。如果监控大屏显示的时间要精确到秒就统一做 NTP 校时把摄像头和服务器都指向同一个 NTP 服务器每天校时一次。还有一个细节切段文件名的结束时间不是文件系统时间而是按 start_time 加 segment_time 推算如果 FFmpeg 中途重连导致段长不固定要按文件实际修改时间更新 SQLite不能机械地 start_time 300 秒。5.5 坑五FFmpeg 录制进程不在了录像目录长期没有新文件现象某天查看录像目录发现最新的文件还是前天去服务器看进程列表ffmpeg 进程已经消失后台也没报错。原因断流、摄像头重启、网络抖动都会让 ffmpeg 进程退出而且退出时未必有非零退出码命令行窗口被 CreateNoWindow 吞掉后很容易被忽略。另外进程退出后如果不做自动拉起系统就静默失录。解决在 Exited 事件里做自动重连这是录像系统必须具备的能力。重连逻辑我这样写proc.EnableRaisingEvents true; proc.Exited (s, e) { Console.WriteLine($FFmpeg 退出退出码 {proc.ExitCode}10 秒后重连); Thread.Sleep(10000); StartRecording(channelId); // 重新拉起进程递归调用自身 };同时把 FFmpeg 的 stderr 输出重定向到日志文件排查具体错误。连续失败要设置退避策略第一次等 10 秒第二次 30 秒第三次 60 秒最多重试 10 次超过阈值就发邮件或企业微信机器人报警避免无限重启把日志刷爆。还有一个常见原因容易被忽略磁盘满了。ffmpeg 写入失败会退出但退出码不一定被捕获。所以录像程序要定时检查磁盘剩余空间低于 10% 就应该清理最老的文件或停止录像并报警。6. 让系统会“挑重点”存储运动检测联动录像的落地技巧做到这里一套能看、能录、能查的系统已经能跑了。但商业监控系统里很常见的需求是没有人的时候不录有人或物体移动时才录。这样存储空间大幅节省回放时也不需要在一堆静止画面里找事件。我用 OpenCvSharp 做最简单的帧差法运动检测效果对固定摄像头来说已经够用。核心思路是对比当前帧和上一帧的差异超过阈值就认为画面有运动。// frame 为当前帧prevFrame 为上一帧 using var diff new Mat(); Cv2.Absdiff(prevFrame, frame, diff); // 两帧做差 Cv2.CvtColor(diff, diff, ColorConversionCodes.BGR2GRAY); Cv2.GaussianBlur(diff, diff, new Size(5, 5), 0); // 降噪 using var thresh new Mat(); Cv2.Threshold(diff, thresh, 30, 255, ThresholdTypes.Binary); double motionRatio Cv2.CountNonZero(thresh) / (double)(thresh.Rows * thresh.Cols); if (motionRatio 0.005) { // 有运动记录当前帧时间戳或给 FFmpeg 发指令开始录像 Console.WriteLine($运动检测触发时间 {DateTime.Now:yyyy-MM-dd HH:mm:ss}); }Absdiff计算两帧像素差GaussianBlur过滤噪点Threshold把差异变成黑白图最后计算非零像素占比。阈值 30 是经验值光线稳定的室内可以降到 15~20室外有树叶晃动时可能要调到 40 以上。运动比例 0.005 表示整个画面有 0.5% 的区域发生变化太小容易误报太大会漏掉小的移动目标。要注意的是帧差法对光照突变特别敏感比如有人开灯、窗帘被风吹动都会误判。我一般会加一个“连续 N 帧触发才算真运动”的防抖逻辑例如连续 5 帧运动比例都超标才启动录像。我做这套系统最吃亏的一次就是把运动检测和录像联动做成“检测到运动才开始录像”导致事件发生前几秒的动作没录进去。后来改成环形缓冲预录程序始终在后台缓存最后 5 秒的关键帧检测触发时把这 5 秒一起写入录像文件。这样补上了事件前置时间回放时才知道前因后果。最后再分享一个习惯所有录像系统上线前我都会做一次 72 小时无人值守测试重点看内存曲线、磁盘增长速度和进程存活状态。排查问题不要靠猜把 FFmpeg 的 stderr 日志、Windows 事件日志、SQLite 里的录像记录三条线对齐大部分问题半小时内能定位。这套 C# 视频监控方案覆盖了取流、预览、录像、索引、运动检测五个核心模块做到了能看、能录、能报警并且所有代码都在自己手里后续要加人脸识别、车牌识别只要在现有帧处理管道上扩展即可。希望这些踩坑记录能帮你少走几段弯路。本文还有配套的精品资源点击获取