简介本资源面向具备一定C#基础的开发者聚焦于使用OpenCvSharp读取RTSP视频流并录制为MP4文件、支持按时间或大小分段保存的完整实现方案适用于安防监控、直播录制、视频采集等需要长时间稳定拉流与本地存储的场景。压缩包共198个文件约146.26MB包含50个dll动态库、42个xml配置与文档、10个cs源码文件以及nupkg包、props/targets工程配置、png截图、exe可执行程序等覆盖从依赖库到示例工程的完整结构。资源基于VS2019、.NET Framework 4.7.2与OpenCvSharp 4.8环境搭建源码更新于2024年4月并配有博客说明与B站演示视频便于对照理解拉流、解码、编码与分段落盘的实现思路。目前已有947人学习下载适合需要快速集成RTSP录制能力或研究OpenCvSharp视频处理的中高级开发者参考。1. 从一路 RTSP 到可回看的 MP4为什么“分段保存”才是监控落地的分水岭很多做 C# 上位机的同行第一次接监控摄像头都会卡在同一个地方用 OpenCvSharp 的VideoCapture打开 RTSP 地址预览窗口能出画面可一旦想把它录成能回看的文件要么录出来的 MP4 播到一半花屏要么程序跑几个小时内存爆掉要么断电后整个文件直接损坏打不开。标题里的“读取 rtsp 流录制 mp4 可分段保存”讲的正是把这件事做扎实的完整链路拉流、解码、编码写盘、按时间或大小切片、异常恢复。它解决的不是“能不能录”而是“能不能长时间稳定地录、录完能直接播、坏一段不牵连全部”。适合做安防上位机、机器视觉工位录像、无人值守监控回传的 C# 开发者尤其是已经会用 OpenCvSharp 做基础图像处理、但还没把视频落盘这条链路跑通的人。下面按我实际踩过的顺序把选型、代码、参数和坑一次讲清。2. 拉流与写盘的技术选型OpenCvSharp 到底能扛到哪一步2.1 为什么不用 VideoWriter 直接写 RTSPOpenCvSharp 自带的VideoWriter是最省事的写法VideoCapture读一帧、VideoWriter.Write写一帧几行代码就能出 MP4。但它在 RTSP 场景下有三个硬伤。第一VideoWriter依赖 FFmpeg 的封装写 MP4 时 moov box 默认写在文件末尾程序被强杀或断电文件头没写完整个 MP4 直接报废。第二它没有内置的分段能力想按 10 分钟切一个文件只能自己关掉再新建而关闭瞬间的缓冲帧容易丢。第三长时间运行下VideoWriter的帧队列会堆积尤其是编码速度跟不上解码速度时内存曲线一路向上。所以我的做法是用 OpenCvSharp 负责拉流和解码用 FFmpeg 进程负责编码和封装。OpenCvSharp 把解码后的Mat通过标准输入管道喂给 FFmpegFFmpeg 用-f segment做切片。这样分段、封装、异常恢复都交给成熟的 FFmpegC# 侧只关心“拿到帧、转成字节、写进管道”。2.2 拉流参数别让默认值拖垮稳定性VideoCapture打开 RTSP 时默认走 TCP 还是 UDP 取决于 FFmpeg 编译选项很多环境默认 UDP丢包就花屏。我一般强制 TCP并设置超时和缓冲。using OpenCvSharp; // 打开 RTSP强制 TCP 传输避免 UDP 丢包导致的花屏 var capture new VideoCapture(); // 通过环境变量让底层 FFmpeg 走 TCP Environment.SetEnvironmentVariable(OPENCV_FFMPEG_CAPTURE_OPTIONS, rtsp_transport;tcp|stimeout;5000000); bool ok capture.Open(rtsp://admin:password192.168.1.64:554/Streaming/Channels/101, VideoCaptureAPIs.FFMPEG); if (!ok) { Console.WriteLine(拉流失败检查地址、账号密码、网络连通性); return; } // 读取一帧确认流可用 using var testFrame new Mat(); if (!capture.Read(testFrame) || testFrame.Empty()) { Console.WriteLine(流已打开但读不到帧可能是子码流地址或编码格式问题); return; }OPENCV_FFMPEG_CAPTURE_OPTIONS是关键rtsp_transport;tcp强制 TCPstimeout;5000000是 socket 超时 5 秒单位微秒。注意不同 OpenCvSharp 版本对超时参数名支持不同有的用timeout如果设了不生效先确认底层 FFmpeg 版本。地址里的Channels/101是海康主码流102是子码流主码流清晰但码率高做录像一般用主码流做实时分析可以用子码流降负载。2.3 编码封装FFmpeg 进程 管道是最稳的组合把 FFmpeg 作为子进程启动标准输入接管道C# 把 BGR 帧写成原始字节流喂进去。FFmpeg 命令里用-f segment做分段-segment_time控制每段时长-reset_timestamps让每段时间戳归零播放器兼容性更好。using System.Diagnostics; var ffmpeg new Process(); ffmpeg.StartInfo.FileName ffmpeg.exe; ffmpeg.StartInfo.Arguments -f rawvideo -pix_fmt bgr24 -s 1920x1080 -r 25 -i pipe:0 -c:v libx264 -preset ultrafast -tune zerolatency -crf 23 -f segment -segment_time 600 -reset_timestamps 1 -segment_format mp4 -movflags faststart D:\\record\\cam01_%Y%m%d_%H%M%S.mp4; ffmpeg.StartInfo.UseShellExecute false; ffmpeg.StartInfo.RedirectStandardInput true; ffmpeg.StartInfo.CreateNoWindow true; ffmpeg.Start(); var stdin ffmpeg.StandardInput.BaseStream;参数逐个说-f rawvideo -pix_fmt bgr24告诉 FFmpeg 输入是 OpenCV 默认的 BGR 三通道原始帧-s 1920x1080必须和实际帧尺寸一致不一致会花屏-r 25是输入帧率要和摄像头实际帧率匹配写高了会重复帧、写低了会加速-c:v libx264 -preset ultrafast是编码器ultrafast降低 CPU 占用适合多路-crf 23是质量18 到 28 之间越小越清晰文件越大-f segment -segment_time 600每 600 秒切一段-reset_timestamps 1每段时间戳从零开始-movflags faststart把 moov 放到文件头边录边播更友好。提示-movflags faststart在分段模式下对每个分片生效但需要 FFmpeg 版本支持老版本可能报错先本地跑一条命令验证。2.4 帧写入循环别在主线程里阻塞拉流和写盘要放在独立线程主线程只做 UI 或状态监控。写入时用Mat的连续内存直接写避免逐像素拷贝。using var frame new Mat(); var buffer new byte[1920 * 1080 * 3]; // 预分配避免每帧 GC while (isRunning) { if (!capture.Read(frame) || frame.Empty()) { // 读帧失败尝试重连不要直接退出 Thread.Sleep(1000); Reconnect(capture); continue; } // 确保 Mat 连续否则内存布局和 FFmpeg 预期不一致 if (!frame.IsContinuous()) frame frame.Clone(); // 把 Mat 数据拷进 byte 缓冲 System.Runtime.InteropServices.Marshal.Copy( frame.Data, buffer, 0, buffer.Length); try { stdin.Write(buffer, 0, buffer.Length); } catch (IOException) { // FFmpeg 进程挂了重启进程 RestartFFmpeg(); } }frame.IsContinuous()这个判断很多人忽略ROI 裁剪后的 Mat 往往不连续直接取Data会读到错误内存。预分配buffer是为了避免每帧 new 一个几 MB 的数组否则 GC 压力会让程序周期性卡顿。stdin.Write是阻塞的如果 FFmpeg 编码慢这里会自然限速反而起到背压作用比无界队列安全。3. 分段保存的落地实现时间切片、文件命名与断流恢复3.1 按时间分段还是按大小分段FFmpeg 的-f segment支持两种切法-segment_time按秒切-segment_size按字节切。监控场景我推荐按时间因为回看时按时间定位最直观而且文件大小受码率波动影响按大小切会出现有的段 3 分钟、有的段 15 分钟。-segment_time 600就是 10 分钟一段一天 144 段单段 1080P 大约 200 到 400 MB既不会太大导致拷贝慢也不会太碎导致文件数量爆炸。如果确实要按大小比如限制单文件不超过 500 MB可以写-segment_size 500M但要注意它和-segment_time同时存在时以先到者为准实际段长会不可预测。3.2 文件命名让运维一眼看出是哪路、哪段时间FFmpeg 的 segment 模式支持在文件名里用时间占位符%Y%m%d_%H%M%S会在每段开始时替换成当前时间。我一般还会加上摄像头编号方便多路区分。# 单路命令示例多路就是启动多个进程各自独立 ffmpeg -f rawvideo -pix_fmt bgr24 -s 1920x1080 -r 25 -i pipe:0 \ -c:v libx264 -preset ultrafast -crf 23 \ -f segment -segment_time 600 -reset_timestamps 1 \ -segment_format mp4 -movflags faststart \ D:\record\cam01_%Y%m%d_%H%M%S.mp4命名规则建议固定成cam{编号}_{开始时间}.mp4不要用中文和空格否则某些播放器或脚本处理会出问题。目录按天分比如D:\record\20250115\cam01_...mp4这样清理旧文件时直接删整个日期目录不用遍历匹配。3.3 断流重连RTSP 不会一直稳定RTSP 流断的原因很多网络抖动、摄像头重启、交换机端口协商、NVR 踢流。capture.Read返回 false 不代表流永久不可用可能只是暂时读不到。我的重连策略是连续失败 3 次后关闭VideoCapture等 2 秒重新Open重连成功继续写同一个 FFmpeg 进程如果 FFmpeg 进程也挂了就重启进程新文件自然接上。int failCount 0; while (isRunning) { if (!capture.Read(frame) || frame.Empty()) { failCount; if (failCount 3) { capture.Release(); Thread.Sleep(2000); capture.Open(rtspUrl, VideoCaptureAPIs.FFMPEG); failCount 0; } Thread.Sleep(200); continue; } failCount 0; // ... 写入逻辑 }注意重连后帧尺寸可能变化比如摄像头从主码流切到子码流如果尺寸和 FFmpeg 启动参数不一致写进去就是花屏。稳妥做法是重连后比对frame.Width/Height不一致就重启 FFmpeg 进程并更新-s参数。3.4 优雅退出别让最后一段变成坏文件程序退出时如果直接KillFFmpeg正在写的那个 MP4 的 moov 没写完文件打不开。正确做法是先停止写帧然后关闭 FFmpeg 的标准输入等它自己退出。isRunning false; Thread.Sleep(500); // 等写入循环退出 stdin.Close(); // 关闭管道FFmpeg 收到 EOF 会写完文件头并退出 ffmpeg.WaitForExit(5000); if (!ffmpeg.HasExited) ffmpeg.Kill(); // 兜底但尽量别走到这stdin.Close()会让 FFmpeg 认为输入结束它会把当前分段的 MP4 正常收尾。WaitForExit给 5 秒正常情况几百毫秒就退出了。如果超时再 Kill至少大部分段是完整的只有最后一段可能损坏。注意Windows 服务或计划任务里跑要处理系统关机事件在OnShutdown里执行同样的关闭流程否则每次关机都留一个坏文件。4. 避坑与排查分段录制里最容易翻车的 5 个点4.1 录出来的 MP4 能播但花屏、绿屏现象文件能打开播放器显示花屏、绿块或者画面颜色不对偏红偏蓝。原因最常见是-pix_fmt和实际帧格式不匹配。OpenCvSharp 的Mat默认是 BGR 三通道如果 FFmpeg 输入写成rgb24红蓝通道就反了。其次是-s尺寸和实际帧不一致FFmpeg 按错误行宽解析画面错位。解决确认-pix_fmt bgr24确认-s等于frame.Width x frame.Height。如果摄像头输出的是 YUV 或灰度VideoCapture默认会转成 BGR所以统一按 BGR 处理即可。花屏还可能是 TCP 没强制UDP 丢包导致解码器输出损坏帧加上rtsp_transport;tcp。4.2 程序跑几小时后内存暴涨然后崩溃现象任务管理器里进程内存从几百 MB 涨到几 GB最后 OutOfMemory。原因每帧new byte[]或者new Mat()没释放GC 跟不上分配速度。另一个隐蔽原因是VideoCapture内部缓冲队列堆积如果读取速度慢于摄像头推流速度OpenCV 会缓存帧越积越多。解决预分配buffer复用Mat用using或手动Dispose在读取循环里不要做耗时操作如果确实处理不过来设置capture.Set(VideoCaptureProperties.BufferSize, 1)限制缓冲或者用子码流降低分辨率。FFmpeg 侧如果编码慢stdin.Write会阻塞这是正常的背压不要改成异步无界队列。4.3 分段文件时间戳不对播放器显示时长异常现象每个 MP4 能播但播放器显示时长是几小时甚至负数进度条拖不动。原因没有加-reset_timestamps 1每段继承了上一段的 PTS导致时间戳越来越大。或者-r输入帧率和实际不符FFmpeg 按错误帧率推算时长。解决分段模式必加-reset_timestamps 1-r设成摄像头实际帧率海康主码流常见 25子码流可能 25 或 30用capture.Fps读出来确认。如果capture.Fps返回 0说明流信息没解析出来手动指定已知值。4.4 断电或强杀后最后一段 MP4 打不开现象正常退出前的文件都能播最后正在录的那个文件双击没反应或者提示文件损坏。原因MP4 的 moov box 在文件末尾FFmpeg 被强杀时还没写入。这是 MP4 格式的固有问题不是代码 bug。解决尽量优雅退出stdin.Close()让 FFmpeg 收尾。如果业务上无法保证优雅退出比如车载、户外设备可以改用-f segment配合-segment_format mpegts先写 TS再用脚本转 MP4TS 格式对断电更宽容。或者接受最后一段损坏靠分段时长控制损失10 分钟一段最多丢 10 分钟。4.5 多路同时录制时 CPU 跑满、丢帧现象单路正常开到 4 路以上 CPU 100%画面卡顿录制文件丢帧。原因libx264的ultrafast虽然快但 1080P 25 帧多路编码仍然吃 CPU。如果还用-preset medium或更高单路就能吃满一个核。解决多路场景优先用子码流分辨率降到 704x576 或 1280x720或者用硬件编码。NVIDIA 显卡可以用-c:v h264_nvencIntel 核显用-c:v h264_qsv把编码从 CPU 卸载到 GPU。命令里把libx264换成对应硬件编码器-preset换成-preset p1NVENC 的预设或-preset veryfastQSV。注意硬件编码器需要 FFmpeg 编译时带对应模块先用ffmpeg -encoders | findstr nvenc确认。5. 进阶技巧用分段索引和快速校验把回看体验做顺分段录完之后真正的痛点在“找”。一天 144 个文件运维要回看某个时间点靠文件名翻很慢。我一般会在 C# 侧维护一个轻量索引每段文件关闭时往 SQLite 或 CSV 里写一条记录文件名、开始时间、结束时间、文件大小、摄像头编号。回看界面按时间查询直接定位到文件再用Process.Start调系统播放器打开。// 每段文件生成后写索引FFmpeg 的 segment 模式会在切换时输出新文件名 // 简单做法监听目录变化或者按 segment_time 估算 using var conn new SQLiteConnection(Data Sourcerecord_index.db); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO segments (cam_id, file_name, start_time, file_size) VALUES (cam, file, start, size); cmd.Parameters.AddWithValue(cam, cam01); cmd.Parameters.AddWithValue(file, fileName); cmd.Parameters.AddWithValue(start, startTime); cmd.Parameters.AddWithValue(size, new FileInfo(fileName).Length); cmd.ExecuteNonQuery();索引表按start_time建索引查询某时间段就一句 SQL。文件清理也方便按日期删除目录后同步删索引记录不会出现“索引里有、文件没了”的情况。另一个技巧是快速校验。分段录制最怕某一段静默损坏平时不发现真要看的时候打不开。我习惯在每段文件生成后跑一次轻量校验用 FFmpeg 的-v error解码一遍不输出文件只检查有没有解码错误。ffmpeg -v error -i D:\record\cam01_20250115_100000.mp4 -f null -如果命令没有输出说明文件完整有报错就标记该段异常触发告警或重新拉流补录。这个校验很轻只解码不编码单段几秒钟跑完放在后台线程做不影响录制。最后说个我自己的习惯所有参数先在一路摄像头上跑满 24 小时看内存曲线、CPU 曲线、文件完整性再复制到多路。监控这行没有“应该没问题”只有“跑过 24 小时没问题”。分段时长我一般设 600 秒但如果是重要工位会缩到 300 秒牺牲文件数量换更小的损坏粒度。这套方案我用了几年从海康、大华到萤石、小米只要 RTSP 地址对、编码是 H.264基本都能跑通。希望帮到你。本文还有配套的精品资源点击获取