简介C视频监控系统开发源码是一套基于MFC框架与C语言实现的多路视频监控工程适合Windows桌面应用开发者、计算机视觉初学者以及需要快速搭建监控原型的项目人员。工程在VC6.0环境下编写涵盖摄像头采集、视频显示、录像保存、多路并发预览等核心模块并涉及DirectShow/OpenCV类库调用、多线程处理与网络扩展思路可帮助读者理解从设备驱动交互到界面集成的完整链路。压缩包共85个文件以h头文件、cpp源文件、rc资源脚本为主另有bmp位图、ico图标、ini配置、mdb数据库及可执行调试文件结构上区分源程序与调试输出便于对照学习。资源大小仅2.78MB目前已有620人学习下载适合想通过实际源码快速熟悉MFC界面开发、视频流处理及简单报警事件管理的开发者参考。 做了几年嵌入式视觉方向的开发一直想把手头那套基于RK3588的实时视频监控系统整理成文。这期间断断续续有同行问我要源码、问方案选型索性把整个系统的开发思路、关键代码片段和踩过的坑一次性写清楚。这篇文章不聊虚的直接讲C视频监控系统从摄像头采集、图像处理、硬件编码到RTSP推流的完整实现路径包括线程模型怎么设计、RKMPP硬编参数怎么调、帧率不稳怎么排查以及开发环境怎么搭。不论你是刚入门想做一个小型监控demo还是已经在做嵌入式视频产品这篇都能给你一套可以直接上手的参考。1. 监控系统整体架构从摄像头到播放端的完整链路先别急着写代码把架构理清楚再动手后面能省好几周的返工时间。视频监控系统说到底是一条数据处理流水线采集端拿到原始图像帧中间做必要的图像预处理可能是画质增强、移动侦测、目标检测的区域裁剪然后交给编码器压缩成H.264或H.265码流最后通过网络协议推给客户端播放。绝大多数监控系统的核心骨架都是这一条链路区别只在于中间每个环节用了什么硬件加速、什么算法、什么协议。1.1 模块划分与数据流向我手里的这套系统跑在RK3588上整体拆成四个独立模块模块之间用队列解耦。这样做的好处是任何单一模块出了问题不会把整个进程拖死而且每个模块可以独立压测、独立优化。采集模块通过V4L2框架读取MIPI或USB摄像头数据输出YUV或RAW格式帧。处理模块基于OpenCV做图像预处理比如降噪、亮度均衡、畸变校正输出RGB或YUV帧。编码模块调用RKMPP硬件编码器把图像帧压缩为H.264/H.265码流。推流模块使用Live555或自研RTSP服务把编码后的帧封装成RTP包发给客户端。数据流上就是一个生产者-消费者链条采集线程往队列A丢帧处理线程从队列A取帧、处理完丢进队列B编码线程从队列B取帧、编码完丢进队列C推流线程从队列C取走已经编码的帧。每条队列都设置最大深度比如2到3帧满则丢旧帧。监控场景下实时性优先宁可丢帧不可延迟累积。1.2 为什么用队列解耦而不是直接函数调用很多新手会把采集、处理、编码写成一个串行的for循环一帧处理完再取下一帧。这在帧率要求不高的场景下能跑但一旦遇到高分辨率比如4K30fps串行链路里任何一个环节抖动整个系统帧率就跟着掉。我当时实测过串行结构在4K分辨率下帧率只能稳定在12到15帧采用队列解耦的多线程流水线后稳定跑满30帧。队列解耦的本质是把每一帧的完整处理时间从累加变成最大值。三线程流水线的帧间隔理论上只取决于最慢的那个环节而不是三个环节的时间之和。这个思路参考了muduo网络库的one loop per thread模型每一路都有自己独立的事件循环和缓冲区避免多线程竞争。2. 采集环节V4L2帧获取与零拷贝策略采集模块是整个系统的输入源头这块写不好后面编码推流再优化都是白搭。Linux下摄像头采集绕不开V4L2框架RK3588平台的MIPI-CSI摄像头默认也是走这个接口。2.1 V4L2采集的初始化要点V4L2采集初始化有几处容易踩坑的地方依次说首先打开设备节点后要检查设备能力确认支持视频采集然后设置采集格式这里要注意像素格式的选择。RK3588平台建议优先使用V4L2_PIX_FMT_NV12因为RKMPP硬件编码器的原生输入格式就是NV12直接用这个格式能省掉一次格式转换的开销。我刚开始用V4L2_PIX_FMT_YUYV结果后面编码前还得转一次NV12白白浪费CPU。然后是申请帧缓冲。V4L2的采集缓冲有两种方式mmap和userptr。mmap方式由驱动分配物理连续内存并映射到用户空间userptr方式由用户态自己分配内存再传给驱动。监控场景强烈建议用mmap因为支持零拷贝路径驱动直接DMA写入那块内存用户态读到的就是最终数据。还有个read/write方式每帧都要从内核态拷贝到用户态性能差一个数量级直接放弃。// 申请V4L2 mmap缓冲的核心步骤 struct v4l2_requestbuffers req; req.count 4; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, req); // 映射缓冲 for (int i 0; i 4; i) { struct v4l2_buffer buf; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; ioctl(fd, VIDIOC_QUERYBUF, buf); mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); }缓冲数量申请4个是我实测比较稳妥的值。太少会导致驱动来不及填充应用还没来得及取走新帧就覆盖上来了太多则浪费内存4K分辨率下每帧NV12大约占用8MB左右4个缓冲约32MB在RK3588这种内存配置下完全能接受。2.2 多路摄像头采集的帧率同步监控系统往往不止一路摄像头。我在系统里接了4路摄像头主码流做存储子码流做预览。多路采集最容易出现的问题是各路启动时间不一致导致帧率在视觉上不同步。我用的方案是所有采集线程启动后先在条件变量上等待一个统一的开始采集信号由主线程在确认所有线程就绪后统一释放。这样各路的首帧时间差可以控制在1毫秒以内。还有一个容易被忽视的参数是V4L2的帧间隔设置通过VIDIOC_S_PARM设置timeperframe。部分USB摄像头对帧间隔设置不敏感设置了30fps实际只跑25fps最好在采集起来后做一次实际帧率统计用帧计数器除以运行时间如果偏差超过5%说明驱动或传感器没有按预期工作。3. 多线程流水线线程模型选择与帧同步的实战血泪系统跑起来以后你很快会发现瓶颈不在采集也不在编码而在线程模型设计得不合理。这一节是我踩坑最多的地方写出来帮大家少走弯路。3.1 生产者-消费者模型与无锁队列每个环节之间用带互斥锁的std::queue数据量小的时候没问题但在4路4K30fps的场景下每秒钟有120帧在队列间流转锁竞争变得不可忽视。我实测过用std::mutex保护的普通队列在四路视频流同时跑的时候锁等待占掉了接近10%的CPU时间。后来把队列换成了基于环形缓冲的无锁队列。核心思路是用原子变量维护读指针和写指针生产者写入时更新写指针消费者读取时更新读指针。生产者和消费者各自只操作自己的那个指针读指针只有消费者改写指针只有生产者改配合原子操作保证可见性就能避免锁竞争。template typename T, size_t N class RingBuffer { public: bool push(const T item) { size_t currentWrite writeIndex.load(std::memory_order_relaxed); size_t nextWrite (currentWrite 1) % N; if (nextWrite readIndex.load(std::memory_order_acquire)) return false; // 满丢弃 buffer[currentWrite] item; writeIndex.store(nextWrite, std::memory_order_release); return true; } bool pop(T item) { size_t currentRead readIndex.load(std::memory_order_relaxed); if (currentRead writeIndex.load(std::memory_order_acquire)) return false; // 空 item buffer[currentRead]; readIndex.store((currentRead 1) % N, std::memory_order_release); return true; } private: std::vectorT buffer{N}; std::atomicsize_t readIndex{0}; std::atomicsize_t writeIndex{0}; };3.2 线程数与CPU亲和性绑定RK3588是8核CPU4个A76大核4个A55小核线程数不是越多越好。我把四路视频流的处理线程分别绑定到四个A76大核上推流和主线程放在A55小核上编码线程建议绑定到独立大核。CPU亲和性绑定的作用是防止线程在核间频繁迁移避免缓存失效。实测绑定后编码线程的帧处理时间抖动明显减少从±5毫秒降到±1毫秒以内。这对视频流来说很关键因为帧处理时间抖动最终会反映为推流端看到的卡顿。线程优先级也要设。采集线程用SCHED_RR实时调度策略优先级设到80左右保证它不会被普通线程抢占。如果系统里采集线程没有实时优先级一旦其他模块CPU占用偶发飙高采集线程就会延迟表现出来就是画面整体卡顿。3.3 帧率不稳的排查链路我先分享一个真实的排坑经历帮助大家理解帧率不稳的完整排查思路。有次系统跑着跑着客户端画面从流畅变成隔几秒卡一下帧率统计从30掉到18-20左右来回跳。我的排查步骤如下先看采集线程的实际帧率。在采集循环里加一个计数器每取到一帧就加1每秒钟打印一次计数。这台设备采集帧率稳定在29-30说明问题不在采集环节。再看处理环节耗时。用std::chrono::steady_clock记录每帧图像处理的实际耗时发现偶尔有帧处理耗时飙到90毫秒。看处理函数发现OpenCV的resize在4K分辨率下偶尔会触发内存重新分配分配内存的耗时不稳定。改用预分配的cv::Mat作为目标缓冲区每次都写入同一个Mat问题解决。这说明帧率不稳不一定是摄像头或编码器的问题往往出在你以为性能足够的图像处理环节。内存分配、格式转换、缓存未命中这类隐藏耗时才是罪魁祸首。4. 硬编码关键实践RKMPP的参数调优与码流封装RK3588自带的RKMPP硬件编码器是整个系统性能的核心。同样一个4K视频用x264软编码CPU占用接近80%而RKMPP硬编码CPU占用不到10%画面质量还更好。这篇重点讲RKMPP使用时容易踩的参数陷阱。4.1 RKMPP编码链路与参数配置RKMPP编码的基本流程是创建编码会话、设置编码参数分辨率、帧率、码率、GOP、循环送入原始帧NV12格式、取回编码后的码流。核心参数里最需要留意的是码率控制模式和GOP间隔。码率控制监控场景选CBR恒定码率更合适码率波动小对网络传输友好。VBR可变码率虽然能在画面静止时节省码率但码率突发上涨时容易撑爆网络缓冲区造成推流卡顿。GOP间隔GOP关键帧间隔决定了客户端收到视频流后多久能开始解码。间隔越大相同码率下画质越好但客户端连接拉流时等待关键帧的时间也更长。监控场景建议设成帧率的整数倍比如2秒一个关键帧30帧每秒的话GOP设60。SPS/PPSH.264码流中SPS和PPS参数集是解码的关键。RKMPP默认在码流前面带上SPS/PPS但有的播放器只认包含SPS/PPS的关键帧。建议开启动态SPS/PPS插入也就是每个IDR帧前都附带SPS/PPS这样新接入的客户端能快速解码。MppEncCfgImpl* cfg nullptr; mpp_enc_cfg_init(cfg); mpp_enc_cfg_set_s32(cfg, prep:width, width); mpp_enc_cfg_set_s32(cfg, prep:height, height); mpp_enc_cfg_set_s32(cfg, prep:format, MPP_FMT_NV12); mpp_enc_cfg_set_s32(cfg, rc:mode, MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, rc:bps, bitrate); mpp_enc_cfg_set_s32(cfg, codec:type, MPP_VIDEO_CodingAVC); mpp_enc_cfg_set_s32(cfg, rc:gop, gop);4.2 H.265与H.264的选择RK3588的硬编码器同时支持H.264和H.265参数上是看编码器有差异的。监控场景如果是局域网内看H.264足够兼容性最好如果是要远程传输或者长时间存储H.265的优势就很明显——同等画质下码率大约只有H.264的一半。我在系统里提供双码流输出主码流用H.265做存储子码流用H.264做Web端预览。H.265的主码流在1080p分辨率下码率设成2Mbps画质基本无损H.264的子码流在720p下码率设成1Mbps流畅度足够。有一件事必须注意RKMPP编码H.265时很多播放器在没有VPS视频参数集的情况下无法解码所以推流时要把VPS、SPS、PPS作为附加数据一起封装进RTSP的SDP中。我遇到过一个坑VLC能正常播放H.264码流但换到H.265就黑屏检查后发现就是SDP里缺少VPS。这个细节很隐蔽排查了将近一天。5. 推流方案RTSP服务的搭建与延迟优化编码器出来的H.264/H.265裸流需要封装成网络协议才能被播放器识别。RTSPRTP是目前监控行业最通用的方案。RK3588平台上我用过两种推流方式一种是自己基于Live555搭RTSP服务另一种是直接把编码流写入FFmpeg让FFmpeg完成RTSP推流。两种我都实验过各有利弊。5.1 两种推流方式的取舍先用表格做对比再详细说方案优点缺点适用场景Live555自建RTSP服务轻量灵活完全掌控RTP打包细节代码量大RTP时间戳处理复杂高并发、低延迟要求的专业设备FFmpeg推流代码量小协议支持全多一层内存拷贝CPU占用略高快速原型验证、非核心业务我在正式产品里选了Live555。原因有两个一是FFmpeg推流默认的传输缓冲较大推流端到播放端的延迟会多出200到400毫秒二是Live555可以直接把RKMPP输出的码流按照RTP包格式打包不需要经过FFmpeg内部的flv或mp4封装再解封装少一层转换就能降低延迟。5.2 RTSP延迟优化的几个关键参数延迟是监控系统体验的核心指标。客户端看到画面比现场晚太多监控就失去了意义。我在优化延迟时主要调了这些参数RTP打包大小。RTP包默认MTU是1500字节一个H.264 NAL单元如果大于MTU需要分片发送。分片越多网络传输效率越低延迟越大。我把RTP包大小调到1400字节配合TCP传输实测在局域网内延迟能控制在200毫秒以内。关键帧间隔。如果GOP设得太长客户端在关键帧之间接入必须先等待下一个关键帧才能出画面。这个等待时间最长可能达到几秒。我把GOP设为1到2秒虽然码率会稍高一点但拉流体验明显更好。TCP_NODELAY选项。推流socket要开启TCP_NODELAY禁用Nagle算法避免小包被延迟合并发送。Nagle算法对视频这种小包高频场景特别不友好不关掉的话延迟直接飚到1秒以上。int enable 1; setsockopt(rtspSocket, IPPROTO_TCP, TCP_NODELAY, enable, sizeof(enable));5.3 多客户端并发拉流的线程安全当有多个客户端同时拉流时推流模块会为每个客户端创建独立的会话线程各自从编码队列里取数据发送。这里要特别小心编码队列的数据被一个客户端取走后其他客户端就取不到了。我的处理方案是推流模块内部维护一个订阅列表每个客户端一个独立的环形缓冲编码模块产出的每一帧都广播到所有订阅缓冲中客户端线程只从自己的缓冲读数据发送缓冲满则丢弃最旧帧。这样某个客户端网络阻塞不会影响其他客户端也不会阻塞编码链路。这里的取舍是每个客户端都要复制一份数据到自己的缓冲多客户端时内存占用会上升。4路视频流 4个客户端的情况下多出来的内存大约在50到80MB之间在嵌入式平台上可以接受。相比所有客户端共享一个缓冲的方案这个方案的稳定性和隔离性要好得多。6. 开发环境与调试从VSCode远程开发到崩溃排查最后分享一下开发环境搭建和调试技巧。这部分看似小事实际上直接影响开发效率。我的开发环境是Windows笔记本上装VSCode Remote SSH插件远程连接RK3588开发板代码在开发板上直接编译运行。强烈推荐这个组合比在Windows上交叉编译再拷到板子上跑要高效得多。6.1 VSCode远程开发配置的几个要点VSCode远程开发第一个要配置的是C/C插件的IntelliSense。因为RK3588的交叉编译工具链和板子上的系统库路径和PC不一样直接在VSCode里打开源码会看到大量红色波浪线。解决方法是写一个c_cpp_properties.json指定编译器路径、includePath和宏定义。第二个要配置的是调试。VSCode远程开发天然支持远程gdb调试在launch.json里配置program为开发板上的可执行文件路径miDebuggerPath指向gdb。内存可能设定的RTTI、异常关闭等编译选项IDE中的错误提示和外层的调试断点联动也很顺手。{ configurations: [ { name: Linux, includePath: [ ${workspaceFolder}/**, /usr/include/opencv4, /usr/include/rkmpp, /usr/include/live555 ], defines: [], compilerPath: /usr/bin/g, cStandard: c17, cppStandard: c17, intelliSenseMode: linux-gcc-x64 } ], version: 4 }6.2 段错误与内存问题的排查工具视频监控系统跑长跑着突然崩溃多半是内存问题。我遇到的几类崩溃场景和排查方法列出来大家可以对号入座空指针解引用。图像处理模块和编码模块使用OpenCV的cv::Mat时最容易在访问空Mat的data指针时崩溃。排查方法是开启编译器警告并且每次取帧后都检查帧是否为空再处理。缓冲区越界。网络推流模块的接收缓冲区在处理畸形RTP包时可能越界写入。这类问题用AddressSanitizerASAN编译一次就能暴露。内存泄漏。长时间运行后内存占用持续增长多半是智能指针循环引用或者第三方库内部缓存。我排查内存泄漏用的工具是valgrind的massif和leak-check。RK3588上运行valgrind会很慢建议单独编译一个debug版本只在排查时跑。# 开启ASAN编译快速定位越界和释放后使用 g -fsanitizeaddress -g -o monitor main.cpp # 运行程序崩溃时会打印详细的出错位置 ./monitorASAN跑出来的报错会直接打印出出错的那一行代码和内存地址比对着日志猜要高效太多。6.3 日志系统的设计建议最后是我一开始疏忽、后来吃了亏才补上的日志。视频监控系统是长时间运行的没有一套结构化的日志系统出了问题只能瞎猜。我的日志系统设计很简单但有效分级DEBUG、INFO、WARN、ERROR四级日常跑INFO排查问题再开DEBUG。带时间戳每行日志精确到毫秒。分模块打标签采集、处理、编码、推流各自一个标签方便grep过滤。支持环形文件写入日志文件按大小轮转保留最近24小时避免日志把存储撑满。遇到崩溃或者卡顿第一步永远是看日志里最后一个WARN/ERROR发生在哪里。80%的问题靠这一步就能定位到模块级别再配合gdb和ASAN做精确排查。另外有一个小技巧在每个处理循环的入口和出口各打一条DEBUG日志记录耗时。这样如果某个环节耗时异常日志里一眼就能看到。我曾经用这个方法发现了OpenCV的cvtColor在高分辨率下偶发性能退化的问题靠的就是这个简单的耗时日志。本文还有配套的精品资源点击获取