简介:这份资源是面向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 version="1.0" encoding="utf-8"?> <configuration> <packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="azure-cn" value="https://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:password@192.168.1.64:554/Streaming/Channels/101。注意这里的用户名密码要换成你摄像头实际的,不同品牌路径不一样,海康是/Streaming/Channels/101,大华是/cam/realmonitor?channel=1&subtype=0。
// 常见做法:在配置类里读取 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 都是瓶颈。如果你直接在主采集线程里写文件,帧率会掉得厉害。常见做法是开一个BlockingCollection<Bitmap>作为帧队列,采集线程只管往里塞,另一个写盘线程从队列里取,批量写入。
// 用阻塞集合解耦采集和写盘 private BlockingCollection<Bitmap> _frameQueue = new BlockingCollection<Bitmap>(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.8,SmtpClient还能用,但要注意EnableSsl和端口号。QQ 邮箱是smtp.qq.com:587,163 邮箱是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("your@qq.com", "授权码不是密码"); var mail = new MailMessage(); mail.From = new MailAddress("your@qq.com"); mail.To.Add("receiver@example.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,并且设置Cache=Shared和Journal Mode=WAL。WAL 模式能显著提升并发读性能,但写仍然是串行的。
5.5 程序跑一晚上内存暴涨到几个 G
现象:任务管理器里看到进程内存从 100MB 涨到 3GB,最后崩溃。原因:Bitmap对象没有Dispose,GDI+ 对象泄漏。解决:所有Clone()出来的Bitmap在用完后必须Dispose(),包括pictureBox1.Image替换下来的旧帧。另外,Mat对象也要Dispose,OpenCvSharp 的非托管内存不受 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 ConcurrentDictionary<HttpListenerContext, 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; boundary=frame"; 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 足够预览 } }参数说明:boundary=frame是 MJPEG 的固定分隔符,浏览器靠它识别每帧的边界。Task.Delay(100)控制推送帧率,10fps 对预览来说够用,太高会占带宽。GetCurrentFrameJpeg()需要你维护一个全局的当前帧变量,在NewFrame事件里更新。注意HttpListener在 Windows 上需要管理员权限或者提前用netsh注册 URL ACL,否则会报Access Denied。
6.2 用 Xamarin 或 MAUI 做移动端拉流
如果你想要原生移动端体验,可以用 Xamarin.Forms 或 .NET MAUI 做一个简单的播放器。视频流走 RTSP 的话,Android 上可以用LibVLCSharp,iOS 上可以用FFmpegKit。但跨平台视频渲染的坑很多,比如 Android 的 SurfaceView 和 iOS 的 AVPlayerLayer 生命周期管理。我一般会建议先用 MJPEG 方案验证需求,确实需要低延迟再上 RTSP。MAUI 里可以用WebView直接加载 MJPEG 地址,这是最省事的做法。
<!-- MAUI 里用 WebView 加载 MJPEG 流 --> <WebView Source="http://192.168.1.100:8080/" HeightRequest="300" WidthRequest="400" />这个方案的好处是不用处理原生视频解码,坏处是延迟比 RTSP 高,大概 1-2 秒。对于家庭监控来说,这个延迟可以接受。如果你发现 WebView 加载不出来,检查手机和电脑是否在同一局域网,以及 Windows 防火墙是否放行了对应端口。
6.3 验证清单与我的习惯
每次改完代码,我会按这个清单走一遍:第一,用dotnet build或 VS 编译,确保零警告零错误;第二,启动程序,确认摄像头画面正常,帧率稳定在 15fps 以上;第三,触发一次运动检测,确认数据库有记录、邮件能收到、截图文件存在;第四,跑 30 分钟,看内存增长是否超过 200MB;第五,用手机浏览器打开 MJPEG 地址,确认能看。这套流程走下来,基本能覆盖 90% 的翻车场景。从那以后我每次拿到新的监控源码包,都强制先跑一遍这个清单,再动手改业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取