
简介这份资源是面向C#开发者与智能家居爱好者的家庭视频监控系统完整源代码基于C#语言构建涵盖视频流处理、网络通信、数据库管理、用户界面设计等核心模块适合希望深入理解监控系统架构或进行二次开发的中级学习者。压缩包为zip格式整体约5.34MB文件类型以C#源代码文件为主辅以项目配置与资源文件便于直接导入Visual Studio编译运行。项目涉及OpenCV或AForge.NET视频处理、RTSP/WebRTC远程传输、SQL Server或SQLite数据存储、Windows Forms或WPF界面以及多线程异步编程等关键技术点并包含用户认证、数据加密等安全机制与报警通知逻辑。目前已有168人学习下载读者可通过研读源码掌握从视频采集到远程监控的完整实现路径理解各模块间的协作方式积累C#综合项目开发经验为构建可扩展、兼容多设备的家庭安防方案提供参考。1. 拆开这个 C# 家庭视频监控源码包它到底能跑出什么效果很多人第一次拿到「C#源代码家庭视频监控系统.zip」这种包第一反应是双击 sln 然后 F5结果要么缺 DLL要么摄像头黑屏要么跑起来只有一个空窗体。我拆过不少同类项目这个包的核心价值不在于「监控」两个字而在于它把 C# 在 Windows 桌面端做实时视频采集、多线程渲染、RTSP 拉流、本地录像存储这几件事串成了一条完整链路。它适合两类人一类是想学 C# 上位机与视频处理结合的中级开发者另一类是需要快速搭一个局域网内可用的家庭监控原型的从业者。你不需要从零写 DirectShow 封装也不用自己啃 FFmpeg 的 C 接口源码里已经把 AForge.NET 或 OpenCVSharp 这类库的调用逻辑铺好了。但前提是你得先搞清楚它的技术栈边界——它大概率是 WinForms 而不是 WPF视频源大概率走的是本地 USB 摄像头或 RTSP 地址数据库大概率是 SQLite 而不是 SQL Server。这些判断直接决定你后面能不能顺利编译、能不能接上自己的硬件。2. 环境搭建与依赖还原从 sln 到第一个画面2.1 先判断项目类型和 .NET 版本拿到包之后别急着打开先看目录结构。常见做法是根目录下有一个 .sln 文件旁边跟着一个或多个 .csproj。用记事本打开 .csproj看TargetFramework标签。如果是net48或net472说明是 .NET Framework 项目必须用 Visual Studio 2019 或 2022 的「.NET 桌面开发」工作负载不能指望 dotnet CLI 一把梭。如果是net6.0-windows或net8.0-windows那可以用 VS 2022 直接打开也可以用dotnet build命令行编译。这一步判断错了后面报的错会非常玄学比如「找不到 System.Windows.Forms 引用」或者「当前 SDK 不支持 Windows 桌面目标」。# 查看项目目标框架在源码根目录执行 find . -name *.csproj -exec grep -H TargetFramework {} \;这条命令会列出所有 csproj 里的目标框架。如果输出里有net48你就老老实实装 VS 2019/2022 并勾选 .NET Framework 4.8 开发工具。如果输出是net6.0-windows那可以用 VS 2022 直接打开也可以用dotnet build命令行编译。参数说明-name *.csproj是匹配所有 csproj 文件-exec grep是对每个匹配文件执行 grep-H是强制显示文件名。这一步花两分钟能省掉后面两小时的排查。2.2 NuGet 包还原与常见缺包处理视频监控类项目依赖的 NuGet 包通常集中在几个方向视频采集AForge.Video、AForge.Video.DirectShow、OpenCvSharp4、图像处理OpenCvSharp4.Extensions、数据库System.Data.SQLite 或 Microsoft.Data.Sqlite、日志NLog 或 log4net。打开 VS 后右键解决方案选择「还原 NuGet 包」。如果还原失败大概率是包源问题。国内环境常见做法是加一个国内镜像源比如在NuGet.Config里配https://nuget.cdn.azure.cn/v3/index.json或者用华为云、腾讯云的 NuGet 镜像。!-- NuGet.Config 放在解决方案根目录和 .sln 同级 -- ?xml version1.0 encodingutf-8? configuration packageSources clear / add keynuget.org valuehttps://api.nuget.org/v3/index.json / add keyazure-cn valuehttps://nuget.cdn.azure.cn/v3/index.json / /packageSources /configuration这个配置的作用是清空默认源然后按顺序尝试官方源和国内镜像。clear /很重要否则可能继承全局配置里的旧源导致冲突。如果还原时提示某个包版本找不到比如AForge.Video.DirectShow 2.2.5那就去 NuGet 官网查一下可用版本手动在 csproj 里改成存在的版本号。注意不要盲目升级到最新版AForge 系列在 .NET Framework 下稳定但在 .NET 6 下可能需要换 OpenCvSharp 方案。2.3 摄像头设备枚举与 RTSP 地址配置项目跑起来后第一个要改的地方是视频源。源码里通常会有一个VideoSource相关的类或配置文件里面写死了0默认摄像头索引或者一个 RTSP 地址。如果你用的是笔记本自带摄像头索引0一般能直接出画面。如果你用的是网络摄像头需要把 RTSP 地址填进去格式通常是rtsp://admin:password192.168.1.64:554/Streaming/Channels/101。注意这里的用户名密码要换成你摄像头实际的不同品牌路径不一样海康是/Streaming/Channels/101大华是/cam/realmonitor?channel1subtype0。// 常见做法在配置类里读取 RTSP 地址而不是硬编码 public static class CameraConfig { // 从 App.config 或 appsettings.json 读取 public static string RtspUrl ConfigurationManager.AppSettings[RtspUrl]; // 本地 USB 摄像头用索引 public static int LocalCameraIndex 0; // 根据配置决定用本地还是网络源 public static IVideoSource CreateVideoSource() { if (!string.IsNullOrEmpty(RtspUrl)) { // 网络摄像头走 RTSP return new VideoSourcePlayer(); // 实际项目中可能是 FFmpeg 封装 } // 本地摄像头走 DirectShow var sources new FilterInfoCollection(FilterCategory.VideoInputDevice); return new VideoCaptureDevice(sources[LocalCameraIndex].MonikerString); } }这段代码的逻辑是先判断配置里有没有 RTSP 地址有就走网络流没有就枚举本地 DirectShow 设备。参数说明FilterCategory.VideoInputDevice是 DirectShow 的设备类别MonikerString是设备的唯一标识。如果你发现枚举出来的设备列表是空的检查摄像头驱动是否装好或者换一个 USB 口。另外RTSP 拉流在 WinForms 里如果直接在 UI 线程做画面会卡死必须放到后台线程或使用VideoSourcePlayer这类已经封装好异步的控件。3. 视频流处理与多线程渲染别让 UI 线程背锅3.1 视频帧回调与跨线程更新控件这个项目里最容易翻车的地方就是跨线程操作控件。AForge 的NewFrame事件是在后台线程触发的如果你直接在事件处理里写pictureBox1.Image frame运行时会抛InvalidOperationException提示「线程间操作无效」。血泪经验是所有 UI 更新必须走Invoke或BeginInvoke。但Invoke是同步的如果视频帧率是 25fps每帧都同步等待 UI 线程画面会一卡一卡。常见做法是用BeginInvoke异步投递或者用双缓冲队列解耦。// 视频帧回调注意跨线程问题 private void OnNewFrame(object sender, NewFrameEventArgs eventArgs) { // 克隆帧避免后台线程复用同一块内存导致画面撕裂 Bitmap frame (Bitmap)eventArgs.Frame.Clone(); // 异步更新 UI不阻塞视频采集线程 if (pictureBox1.IsHandleCreated !pictureBox1.IsDisposed) { pictureBox1.BeginInvoke(new Action(() { var old pictureBox1.Image; pictureBox1.Image frame; old?.Dispose(); // 及时释放旧帧防止内存暴涨 })); } else { frame.Dispose(); // 控件已销毁直接释放 } }这段代码的关键点有三个第一Clone()是必须的因为 AForge 传过来的Frame对象在回调结束后会被复用不克隆的话你保存下来的画面会变成花屏。第二BeginInvoke是异步的不会卡住采集线程。第三旧帧要Dispose()否则跑一晚上内存能吃掉几个 G。参数说明IsHandleCreated判断控件句柄是否已创建IsDisposed判断控件是否已释放这两个条件缺一不可否则关闭窗体时会报「访问已释放对象」。3.2 录像存储与磁盘 IO 优化录像功能一般有两种实现一种是每帧存成 JPEG 图片序列另一种是封装成 AVI 或 MP4。这个源码包里大概率用的是 AVIWriter 或者自己写的帧队列。不管哪种磁盘 IO 都是瓶颈。如果你直接在主采集线程里写文件帧率会掉得厉害。常见做法是开一个BlockingCollectionBitmap作为帧队列采集线程只管往里塞另一个写盘线程从队列里取批量写入。// 用阻塞集合解耦采集和写盘 private BlockingCollectionBitmap _frameQueue new BlockingCollectionBitmap(100); // 采集线程入队 private void OnNewFrame(object sender, NewFrameEventArgs eventArgs) { if (_isRecording) { // 队列满了就丢弃当前帧避免内存无限增长 _frameQueue.TryAdd((Bitmap)eventArgs.Frame.Clone()); } } // 写盘线程出队并写入 private void RecordWorker() { while (!_frameQueue.IsCompleted) { if (_frameQueue.TryTake(out Bitmap frame, 100)) { // 写入 AVI 或保存为 JPEG _writer.WriteVideoFrame(frame); frame.Dispose(); } } }参数说明BlockingCollection的容量设为 100 是经验值太小会丢帧太大内存扛不住。TryAdd不阻塞队列满时直接丢弃保证采集线程不被拖慢。TryTake带超时避免线程无法退出。如果你发现录像文件播放时速度不对检查写入时的帧率设置是否和实际采集帧率一致很多 AVIWriter 默认是 25fps但你的摄像头可能是 30fps这会导致音画不同步或快放。3.3 运动检测的帧差法实现与阈值调参运动检测是这个项目里比较有技术含量的部分。常见做法是帧差法把当前帧和上一帧转成灰度做绝对差然后二值化最后统计非零像素比例。如果比例超过阈值就判定为有运动。这个方法的坑在于光照突变和摄像头噪声。比如晚上开灯瞬间整个画面亮度跳变帧差法会误报。解决办法是加一个高斯模糊先平滑噪声再用AbsDiff和Threshold。// 帧差法运动检测基于 OpenCvSharp private Mat _previousFrame new Mat(); public bool DetectMotion(Mat currentFrame) { using (var gray new Mat()) using (var diff new Mat()) using (var thresh new Mat()) { // 转灰度 Cv2.CvtColor(currentFrame, gray, ColorConversionCodes.BGR2GRAY); // 高斯模糊去噪 Cv2.GaussianBlur(gray, gray, new Size(21, 21), 0); if (_previousFrame.Empty()) { _previousFrame gray.Clone(); return false; } // 帧差 Cv2.Absdiff(_previousFrame, gray, diff); // 二值化阈值一般取 25 左右 Cv2.Threshold(diff, thresh, 25, 255, ThresholdTypes.Binary); // 统计非零像素 int nonZero Cv2.CountNonZero(thresh); double ratio (double)nonZero / (thresh.Rows * thresh.Cols); // 更新上一帧 _previousFrame.Dispose(); _previousFrame gray.Clone(); // 比例超过 0.02 认为有运动 return ratio 0.02; } }参数说明GaussianBlur的核大小21x21是经验值太小去噪不够太大运动物体边缘会被抹掉。Threshold的阈值25也是经验值光线变化剧烈时可以调到40以上。ratio 0.02表示画面中 2% 的像素发生变化就报警这个值要根据摄像头分辨率和场景调整1080P 下可以降到0.01。注意_previousFrame要Dispose否则非托管内存泄漏跑几个小时就崩。4. 数据库与报警通知SQLite 落盘和邮件推送的坑4.1 SQLite 表结构与录像索引设计监控系统的数据库一般存三类数据报警事件、录像文件索引、用户配置。这个源码包大概率用 SQLite因为免安装、单文件、适合家庭场景。常见表结构是AlarmEvents存报警时间、类型、截图路径RecordFiles存录像开始时间、结束时间、文件路径、文件大小。坑在于时间字段的类型SQLite 没有原生 DateTime一般存TEXT格式的 ISO8601 字符串或者INTEGER的 Unix 时间戳。如果你用TEXT查询时要注意时区问题建议统一存 UTC 时间显示时再转本地。-- 报警事件表 CREATE TABLE AlarmEvents ( Id INTEGER PRIMARY KEY AUTOINCREMENT, EventTime TEXT NOT NULL, -- ISO8601 UTC 时间 EventType INTEGER NOT NULL, -- 0运动检测 1遮挡 2离线 SnapshotPath TEXT, -- 报警截图路径 IsRead INTEGER DEFAULT 0 -- 是否已读 ); -- 录像文件索引表 CREATE TABLE RecordFiles ( Id INTEGER PRIMARY KEY AUTOINCREMENT, StartTime TEXT NOT NULL, EndTime TEXT NOT NULL, FilePath TEXT NOT NULL, FileSize INTEGER DEFAULT 0, CameraId INTEGER DEFAULT 0 ); -- 按时间范围查录像 CREATE INDEX idx_record_time ON RecordFiles(StartTime, EndTime);这个表结构的关键是索引。idx_record_time能让你在回放时按时间范围快速检索不然数据量大了之后查询会慢到无法接受。参数说明EventType用整数枚举而不是字符串省空间且查询快。IsRead用于标记报警是否已处理UI 上可以高亮未读。注意 SQLite 的并发写入能力弱如果多个线程同时写要用lock或者单开一个写线程否则会报database is locked。4.2 邮件报警的 SMTP 配置与异步发送报警通知常见做法是发邮件。C# 里用SmtpClient就行但坑在于很多邮箱服务商要求授权码而不是登录密码而且SmtpClient在 .NET 6 里已经标记过时推荐用MailKit。如果你用的是 .NET Framework 4.8SmtpClient还能用但要注意EnableSsl和端口号。QQ 邮箱是smtp.qq.com:587163 邮箱是smtp.163.com:465或994。发送时不要在主线程同步发否则网络一慢整个 UI 卡住。// 异步发送报警邮件 public async Task SendAlarmEmailAsync(string subject, string body, string attachmentPath) { using (var client new SmtpClient(smtp.qq.com, 587)) { client.EnableSsl true; client.Credentials new NetworkCredential(yourqq.com, 授权码不是密码); var mail new MailMessage(); mail.From new MailAddress(yourqq.com); mail.To.Add(receiverexample.com); mail.Subject subject; mail.Body body; if (File.Exists(attachmentPath)) { mail.Attachments.Add(new Attachment(attachmentPath)); } // 异步发送不阻塞 UI await client.SendMailAsync(mail); } }参数说明EnableSsl必须为true否则 QQ 邮箱会拒绝。Credentials里的密码是授权码在邮箱设置里生成不是登录密码。SendMailAsync是异步的配合async/await使用。如果你发现邮件发送失败先检查端口和 SSL 设置再检查授权码是否过期。另外附件不要超过 20MB否则大部分邮箱会拒收。报警截图建议压缩到 200KB 以内再发。4.3 报警去重与冷却时间运动检测如果每帧都触发报警你的邮箱会被轰炸。常见做法是加冷却时间第一次触发后N秒内不再重复报警。这个逻辑可以用一个DateTime _lastAlarmTime字段实现。另外报警截图要保存到本地邮件里只发缩略图或路径不然邮件体积太大。private DateTime _lastAlarmTime DateTime.MinValue; private readonly TimeSpan _alarmCooldown TimeSpan.FromSeconds(30); public void OnMotionDetected(Mat frame) { var now DateTime.UtcNow; if (now - _lastAlarmTime _alarmCooldown) { return; // 冷却期内忽略 } _lastAlarmTime now; // 保存截图 string snapshotPath Path.Combine(Snapshots, ${now:yyyyMMdd_HHmmss}.jpg); Cv2.ImWrite(snapshotPath, frame); // 写数据库 SaveAlarmEvent(now, 0, snapshotPath); // 异步发邮件 _ SendAlarmEmailAsync(家庭监控报警, $检测到运动{now:yyyy-MM-dd HH:mm:ss}, snapshotPath); }参数说明_alarmCooldown设为 30 秒是经验值太短会频繁报警太长会漏掉连续事件。_ SendAlarmEmailAsync(...)是丢弃 Task 的写法表示不等待邮件发送完成避免阻塞检测线程。注意Cv2.ImWrite保存的路径要确保目录存在否则会静默失败。建议在程序启动时创建Snapshots目录。5. 避坑与排查那些让我加班到凌晨的坑5.1 摄像头被其他程序占用导致黑屏现象程序启动后VideoSourcePlayer一直是黑屏但日志里没有报错。原因摄像头被其他程序占用了比如你之前开的 Zoom、Teams 或者另一个监控软件没关干净。Windows 的摄像头设备是独占模式同一时间只能有一个程序访问。解决打开「任务管理器」结束所有可能占用摄像头的进程或者重启电脑。更彻底的办法是在代码里捕获VideoSourcePlayer.VideoSourceError事件把错误信息弹出来而不是静默黑屏。5.2 编译时报「找不到 AForge.Video.FFMPEG」现象还原 NuGet 包后代码里using AForge.Video.FFMPEG;报红线。原因AForge.Video.FFMPEG 这个包在 NuGet 上已经很久没更新了而且它依赖本地的ffmpeg.dll不是纯托管包。解决换用OpenCvSharp4的VideoWriter来写 MP4或者手动把ffmpeg.dll放到输出目录。如果你坚持用 AForge去 GitHub 上找AForge.NET的完整发布包里面包含所有原生 DLL。5.3 录像文件播放时花屏或快进现象录出来的 AVI 文件用播放器打开画面花屏或者速度飞快。原因AVIWriter的帧率和实际采集帧率不匹配或者写入的帧尺寸和声明的不一致。解决在创建AVIWriter时frameRate参数要和摄像头实际帧率一致width和height要和Bitmap的尺寸一致。如果你不确定摄像头帧率可以在NewFrame事件里统计每秒触发次数。另外写入的Bitmap格式必须是24bppRgb其他格式会导致花屏。5.4 SQLite 并发写入报 database is locked现象程序跑一段时间后写数据库时报SQLiteException: database is locked。原因多个线程同时写同一个 SQLite 文件SQLite 默认的锁机制不支持高并发写。解决把所有写操作集中到一个线程用BlockingCollection做队列或者每次写操作都using一个新的SQLiteConnection并且设置CacheShared和Journal ModeWAL。WAL 模式能显著提升并发读性能但写仍然是串行的。5.5 程序跑一晚上内存暴涨到几个 G现象任务管理器里看到进程内存从 100MB 涨到 3GB最后崩溃。原因Bitmap对象没有DisposeGDI 对象泄漏。解决所有Clone()出来的Bitmap在用完后必须Dispose()包括pictureBox1.Image替换下来的旧帧。另外Mat对象也要DisposeOpenCvSharp 的非托管内存不受 GC 管理。建议用using包裹所有Mat和Bitmap的创建或者在finally里手动释放。6. 进阶技巧把监控画面推到 Web 端和移动端6.1 用 MJPEG 流实现浏览器实时预览如果你想让手机浏览器也能看监控画面最轻量的方案是 MJPEG over HTTP。原理很简单起一个HttpListener对每个请求返回multipart/x-mixed-replace内容类型然后不断把 JPEG 帧写进响应流。浏览器收到后会自动刷新画面。这个方案不需要 WebRTC不需要 RTMP代码量不到 100 行。// 简易 MJPEG 服务器 private HttpListener _listener new HttpListener(); private ConcurrentDictionaryHttpListenerContext, bool _clients new(); public void StartMjpegServer(int port) { _listener.Prefixes.Add($http://:{port}/); _listener.Start(); Task.Run(async () { while (_listener.IsListening) { var ctx await _listener.GetContextAsync(); _ Task.Run(() StreamToClient(ctx)); } }); } private async Task StreamToClient(HttpListenerContext ctx) { ctx.Response.ContentType multipart/x-mixed-replace; boundaryframe; var output ctx.Response.OutputStream; while (ctx.Response.OutputStream.CanWrite) { // 从共享的当前帧获取 JPEG 字节 byte[] jpeg GetCurrentFrameJpeg(); if (jpeg null) { await Task.Delay(50); continue; } // 写入边界和帧数据 string header --frame\r\nContent-Type: image/jpeg\r\n\r\n; byte[] headerBytes Encoding.ASCII.GetBytes(header); await output.WriteAsync(headerBytes, 0, headerBytes.Length); await output.WriteAsync(jpeg, 0, jpeg.Length); await output.WriteAsync(Encoding.ASCII.GetBytes(\r\n), 0, 2); await output.FlushAsync(); await Task.Delay(100); // 10fps 足够预览 } }参数说明boundaryframe是 MJPEG 的固定分隔符浏览器靠它识别每帧的边界。Task.Delay(100)控制推送帧率10fps 对预览来说够用太高会占带宽。GetCurrentFrameJpeg()需要你维护一个全局的当前帧变量在NewFrame事件里更新。注意HttpListener在 Windows 上需要管理员权限或者提前用netsh注册 URL ACL否则会报Access Denied。6.2 用 Xamarin 或 MAUI 做移动端拉流如果你想要原生移动端体验可以用 Xamarin.Forms 或 .NET MAUI 做一个简单的播放器。视频流走 RTSP 的话Android 上可以用LibVLCSharpiOS 上可以用FFmpegKit。但跨平台视频渲染的坑很多比如 Android 的 SurfaceView 和 iOS 的 AVPlayerLayer 生命周期管理。我一般会建议先用 MJPEG 方案验证需求确实需要低延迟再上 RTSP。MAUI 里可以用WebView直接加载 MJPEG 地址这是最省事的做法。!-- MAUI 里用 WebView 加载 MJPEG 流 -- WebView Sourcehttp://192.168.1.100:8080/ HeightRequest300 WidthRequest400 /这个方案的好处是不用处理原生视频解码坏处是延迟比 RTSP 高大概 1-2 秒。对于家庭监控来说这个延迟可以接受。如果你发现 WebView 加载不出来检查手机和电脑是否在同一局域网以及 Windows 防火墙是否放行了对应端口。6.3 验证清单与我的习惯每次改完代码我会按这个清单走一遍第一用dotnet build或 VS 编译确保零警告零错误第二启动程序确认摄像头画面正常帧率稳定在 15fps 以上第三触发一次运动检测确认数据库有记录、邮件能收到、截图文件存在第四跑 30 分钟看内存增长是否超过 200MB第五用手机浏览器打开 MJPEG 地址确认能看。这套流程走下来基本能覆盖 90% 的翻车场景。从那以后我每次拿到新的监控源码包都强制先跑一遍这个清单再动手改业务逻辑。希望帮到你。本文还有配套的精品资源点击获取