☰
C# OpenCvSharp RTSP 录制 MP4 分段保存实战
2026/10/29 7:50:02 网站建设 项目流程

简介:本资源面向具备一定C#基础的开发者,聚焦于使用OpenCvSharp读取RTSP视频流并录制为MP4文件、支持按时间或大小分段保存的完整实现方案,适用于安防监控、直播录制、视频采集等需要长时间稳定拉流与本地存储的场景。压缩包共198个文件,约146.26MB,包含50个dll动态库、42个xml文档、10个cs源码文件以及nupkg包、props/targets配置、resx资源、png截图与exe可执行文件等,覆盖从依赖库到示例工程的完整结构,便于直接编译运行与二次开发。资源基于VS2019、.NET Framework 4.7.2与OpenCvSharp 4.8环境验证,源码更新于2024年4月,配套博客与B站演示视频可辅助理解。目前已有947人学习下载,读者可从中获取RTSP拉流、帧处理、视频编码与分段存储的完整代码逻辑及工程配置参考,快速搭建可用的录制模块。

1. 从一路 RTSP 到可回放的 MP4:为什么“分段保存”才是监控落地的分水岭

很多人第一次用 C# 接 OpenCvSharp 拉 RTSP,代码不到二十行就能在窗口里看到画面,于是以为大功告成。真正把它丢到现场跑一周,问题全冒出来:进程内存一路涨、单个 MP4 文件涨到几十 GB 打不开、断电后整个文件报废、想按时间段回放却只能从头拖。标题里的“读取 rtsp 流录制 mp4 可分段保存”,解决的正是从“能看”到“能存、能查、能交付”这一步。它适合做 C# 上位机、视频监控客户端、AI 拉流预处理(比如监控视频拉流 rtsp 接 yolo 推理)的工程师。核心思路很朴素:用 OpenCvSharp 的 VideoCapture 拉流,用 VideoWriter 写盘,再按时间或大小切片,让每一段 MP4 都是独立可播放的完整文件。难点不在拉流,而在切片时机、编码器选择、时间戳对齐和异常恢复,这几处决定了方案能不能上生产。

2. OpenCvSharp 拉 RTSP 与写 MP4 的最小闭环:先跑通再谈分段

2.1 为什么选 OpenCvSharp 而不是纯 FFmpeg 命令行

现场做 C# 上位机,常见做法有两类:一类是 Process 起 ffmpeg.exe 命令行,靠参数拼装;另一类是直接用 OpenCvSharp 这类托管封装。选 OpenCvSharp 的理由很实际:它把解码、色彩转换、写盘统一在一套 API 里,帧数据直接是 Mat,方便顺手接图像处理或 AI 推理;不用管外部进程的生命周期和管道读取;部署时只要带对 native 依赖(OpenCvSharpExtern 及 ffmpeg 相关 dll),不依赖用户机器装没装 ffmpeg。代价是它对 RTSP 的底层控制不如直接调 FFmpeg 精细,比如超时、重连、码流切换要靠自己补。所以我的定位是:OpenCvSharp 负责“拉+写”的主干,超时和重连用配置参数兜底,极端场景再考虑混合方案。

需要提醒的是,OpenCvSharp 的 VideoWriter 在 Windows 上写 MP4,底层走的是 FFmpeg 的封装,能否成功非常依赖你带的 OpenCvSharp 版本和它内置的编码器。常见坑是 FourCC 写 “mp4v” 能出文件但兼容性一般,写 “H264” 又经常因为找不到 openh264 而静默失败。所以第一步不是急着分段,而是先确认“单文件能稳定写出来”。

2.2 最小可运行:拉流 + 写单个 MP4

先给一个能直接抄的最小闭环,把 RTSP 拉进来写成 MP4,跑通再改。

using OpenCvSharp; using System; class Program { static void Main() { // RTSP 地址:海康主码流常见形如 rtsp://user:pwd@ip:554/Streaming/Channels/101 string rtsp = "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101"; string output = "record_single.mp4"; // 用 FFmpeg 后端拉流,超时参数通过环境变量控制更稳 using var capture = new VideoCapture(rtsp, VideoCaptureAPIs.FFMPEG); if (!capture.IsOpened()) { Console.WriteLine("拉流失败,检查地址、账号密码、网络"); return; } // 读取流参数:分辨率与帧率,写盘必须和源一致,否则时间轴会错 int width = (int)capture.Get(VideoCaptureProperties.FrameWidth); int height = (int)capture.Get(VideoCaptureProperties.FrameHeight); double fps = capture.Get(VideoCaptureProperties.Fps); if (fps <= 0 || double.IsNaN(fps)) fps = 25; // 有些流拿不到帧率,给默认值 // FourCC:mp4v 兼容性好;想上 H264 需确认 native 带 openh264 var fourcc = FourCC.FromString("mp4v"); using var writer = new VideoWriter(output, fourcc, fps, new Size(width, height)); if (!writer.IsOpened()) { Console.WriteLine("VideoWriter 打开失败,多半是编码器或路径问题"); return; } using var frame = new Mat(); while (true) { if (!capture.Read(frame) || frame.Empty()) { Console.WriteLine("读帧失败或流中断"); break; } writer.Write(frame); } capture.Release(); writer.Release(); } }

逻辑说明:VideoCapture 用 FFMPEG 后端打开 RTSP,Read 阻塞式取帧,取到就写。参数说明:FrameWidth/FrameHeight 必须和源一致,写盘尺寸不匹配会导致花屏或写失败;Fps 拿不到时给 25 是常见兜底,但要注意这会让写出的文件时间轴和真实时间有偏差,后面分段时要用真实时间而不是帧数来切。FourCC 用 mp4v 是保守选择,先保证能出文件。

2.3 分段保存的两种切法:按时间还是按大小

分段的核心是“什么时候关掉当前 writer、开一个新的”。常见两种策略:

策略触发条件优点缺点
按时间每 N 秒切一段回放按时间段好找,文件大小可预估需要真实时间基准,不能只数帧
按大小文件超过 N MB 切控制磁盘占用直接时长不均匀,回放定位麻烦

我一般用按时间为主、按大小为兜底:主逻辑每 300 秒切一段,同时监控文件大小,超过阈值也强制切。原因是监控回放场景用户习惯按“几点到几点”找录像,时间切片更贴合;而大小兜底是防止某些高码率流在 300 秒内就写出超大文件。

按时间切的关键是别用帧计数除以 fps 来估算时间,因为 RTSP 实际帧率会抖动、丢帧,累计误差很大。正确做法是记录每段开始的 DateTime,每读到一帧就比对当前时间。下面给分段版本的核心循环。

using OpenCvSharp; using System; using System.IO; class SegmentRecorder { static void Main() { string rtsp = "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101"; string outDir = @"D:\records"; Directory.CreateDirectory(outDir); int segmentSeconds = 300; // 每段 5 分钟 long maxSegmentBytes = 200L * 1024 * 1024; // 单段上限 200MB using var capture = new VideoCapture(rtsp, VideoCaptureAPIs.FFMPEG); if (!capture.IsOpened()) { Console.WriteLine("拉流失败"); return; } int width = (int)capture.Get(VideoCaptureProperties.FrameWidth); int height = (int)capture.Get(VideoCaptureProperties.FrameHeight); double fps = capture.Get(VideoCaptureProperties.Fps); if (fps <= 0 || double.IsNaN(fps)) fps = 25; var fourcc = FourCC.FromString("mp4v"); using var frame = new Mat(); VideoWriter writer = null; DateTime segmentStart = DateTime.Now; string currentPath = null; void OpenNewSegment() { writer?.Release(); writer?.Dispose(); // 文件名带时间戳,方便回放检索 currentPath = Path.Combine(outDir, $"rec_{DateTime.Now:yyyyMMdd_HHmmss}.mp4"); writer = new VideoWriter(currentPath, fourcc, fps, new Size(width, height)); segmentStart = DateTime.Now; Console.WriteLine($"新分段: {currentPath}"); } OpenNewSegment(); while (true) { if (!capture.Read(frame) || frame.Empty()) { Console.WriteLine("流中断,尝试重连逻辑放这里"); break; } writer.Write(frame); bool timeUp = (DateTime.Now - segmentStart).TotalSeconds >= segmentSeconds; bool sizeUp = false; if (File.Exists(currentPath)) { sizeUp = new FileInfo(currentPath).Length >= maxSegmentBytes; } if (timeUp || sizeUp) { OpenNewSegment(); } } writer?.Release(); writer?.Dispose(); capture.Release(); } }

逻辑说明:OpenNewSegment 负责关旧开新,文件名用时间戳。参数说明:segmentSeconds 决定回放粒度,监控场景 300 秒比较均衡;maxSegmentBytes 是兜底,注意 FileInfo.Length 在 writer 未 Release 时可能拿不到最新值,所以它更适合做“粗兜底”,精确控制还是靠时间。这里有个血泪经验:切段瞬间如果正好在写关键帧之间,新文件开头可能缺 I 帧导致前几秒花屏,后面避坑章节细说。

3. 让分段真正可用的四个工程细节:编码器、时间戳、重连、落盘

3.1 编码器与 FourCC 的选择边界

FourCC 选错是新手最常见的翻车点。mp4v(MPEG-4 Part 2)几乎必成,但压缩率一般,同样画质文件偏大;H264 压缩好、兼容广,但 OpenCvSharp 默认 native 包不一定带 openh264 编码器,writer.IsOpened() 会返回 false 或写出 0 字节文件。判断方法很简单:打开后立刻检查 IsOpened,再写一帧后看文件大小是否增长。

如果确实需要 H264,常见做法是确认所用 OpenCvSharp 运行库自带的 FFmpeg 支持 libx264,或者退一步用 mp4v 录制、后期再转码。转码这一步可以用 ffmpeg 命令行批量做,但那是离线流程,不影响录制主链路。我的建议是:录制阶段优先保证稳定出文件,编码效率放到次要位置,除非磁盘非常紧张。

3.2 时间戳与帧率:别让回放时间轴漂移

RTSP 流的真实帧率经常不等于标称帧率。如果写盘时固定用 25fps,而实际流是 20fps 波动,写出的 MP4 播放时会忽快忽慢,分段边界的时间也对不上。缓解办法有两个:一是尽量从 capture.Get 拿到真实 fps,拿不到再用默认;二是分段用真实墙钟时间而不是帧数。更严谨的做法是记录每段实际写入帧数和耗时,事后能算出真实平均帧率。对于要求不高的监控回放,墙钟时间切片已经够用;对时间敏感的场景,就得考虑直接用 FFmpeg 的 -segment 能力,那是另一条路线。

3.3 断流重连:别让一次网络抖动终止整晚录制

现场网络抖动、摄像头重启都会让 Read 返回 false。最小闭环里直接 break 退出,生产环境必须重连。常见做法是外层套一个 while,Read 失败后 Release 当前 capture,等待几秒重新 new VideoCapture,成功则继续写新分段(注意重连后分辨率可能变化,要重新读参数并重开 writer)。重连要有退避,比如 2 秒、5 秒、10 秒递增,避免疯狂重试打爆摄像头。同时记录日志,方便排查是网络问题还是设备问题。

3.4 落盘与磁盘管理:写满磁盘比断流更致命

分段保存的一个隐藏收益是方便清理旧文件。可以加一个后台任务,扫描 outDir,删除超过保留天数或总容量超限的最旧分段。注意删除时避开正在写入的当前文件。另外,写盘路径尽量用本地磁盘或稳定的网络盘,写到 U 盘或劣质存储卡上,长时间写入容易掉速甚至写坏文件。FileInfo.Length 轮询别太频繁,每秒一次足够,太频繁反而增加 IO 负担。

4. 分段录制避坑清单:五个现场真实翻车记录

4.1 现象:分段文件开头几秒花屏或黑屏

原因:切段时正好落在两个关键帧(I 帧)之间,新文件缺少起始 I 帧,解码器无法立即还原画面。 解决:切段尽量对齐关键帧。OpenCvSharp 层面拿不到关键帧信息时,可以接受这个瑕疵,或把分段时长设长一点降低概率;要求高就改用 FFmpeg 的 segment 模式,它支持按关键帧切。

4.2 现象:录制几分钟后内存持续上涨

原因:Mat 或 VideoWriter 没及时释放,或者每帧 new Mat 造成 GC 压力;也可能是 native 层缓冲未释放。 解决:Mat 用 using 复用同一个实例,writer 切段时显式 Release 再 Dispose,别只置 null。长时间运行的程序要定期观察私有工作集。

4.3 现象:VideoWriter.IsOpened() 为 false 或写出 0 字节文件

原因:FourCC 对应的编码器不存在,或输出路径无权限,或分辨率/帧率参数非法(比如 width 为 0)。 解决:先打印 width/height/fps 确认有效,再换 mp4v 测试,检查目录写权限。路径别用中文和空格混排的奇怪组合,虽然多数情况支持,但能避则避。

4.4 现象:断电或进程被杀后,最后一个 MP4 无法播放

原因:MP4 的 moov box 在文件末尾,正常 Release 时才写入,异常退出导致文件不完整。 解决:这是 MP4 格式的固有限制。缓解办法是缩短分段时长,让损失最多只有一段;或改用对异常容忍更好的封装格式(如某些场景用 MKV),但标题锁定 MP4,所以只能靠短分段降低损失。

4.5 现象:回放时时间轴和真实时间对不上

原因:写盘 fps 用了默认值,和真实流帧率不符;或分段用帧数估算时间,累计误差。 解决:优先取真实 fps,分段用 DateTime 墙钟时间,必要时记录每段实际帧数和时长做校准。

5. 进阶:把分段录制做成可检索、可验证的录像服务

走到这里,基本录制已经能跑。但要让这套东西真正交付,还得补两块:可检索和可验证。

可检索指的是文件名和目录结构要能直接回答“某时间段录像在哪”。我习惯用日期/小时/rec_yyyyMMdd_HHmmss.mp4的层级,配合一个简单的索引文件或数据库记录每段的起止时间、大小、帧数。这样回放程序不用扫描全部文件,查索引就能定位。索引可以用 SQLite 或直接写 JSON 行,别用重量级方案。

可验证指的是你得有办法确认录像是完整、可播放的。一个实用技巧是:每段写完后,用 OpenCvSharp 重新打开该 MP4,读取 FrameCount 和 Fps,和录制时记录的值比对,偏差过大就标记异常。这能提前发现编码器静默失败、文件截断等问题。下面给一个校验片段。

using OpenCvSharp; static bool VerifySegment(string path, double expectFps, int expectFrames, double tolerance = 0.1) { using var cap = new VideoCapture(path); if (!cap.IsOpened()) return false; double fps = cap.Get(VideoCaptureProperties.Fps); int frames = (int)cap.Get(VideoCaptureProperties.FrameCount); // 帧数偏差超过容忍度视为异常 bool fpsOk = Math.Abs(fps - expectFps) / expectFps < tolerance; bool frameOk = Math.Abs(frames - expectFrames) / (double)expectFrames < tolerance; return fpsOk && frameOk; }

参数说明:expectFps 和 expectFrames 来自录制时的记录,tolerance 按现场容忍度调,一般 0.1 够用。这个校验可以放在切段后异步执行,不阻塞录制主循环。

还有一个容易被忽略的点:多路 RTSP 同时录制时,别用一堆独立线程各自 new VideoCapture 硬扛,CPU 和网络会先撑不住。常见做法是控制并发路数,或者把解码和写盘拆成生产者消费者队列,写盘单独线程串行化,减少磁盘随机写。至于 RTSP 地址格式,海康主码流常见是/Streaming/Channels/101,子码流是/Streaming/Channels/102,录制一般用主码流保证清晰度,预览可以用子码流省资源,这个区分在配置里做成可选项会省很多事。

我自己踩过最深的一次,是没做磁盘清理,一台机器录了半个月把系统盘写满,连带其他服务一起挂掉。从那以后,任何录制程序我都先加保留策略和磁盘水位告警,再谈功能。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询