资讯详情

基于Rust的NVR录像机:RTSP拉流、SQLite索引与回放实践

📅 2026/10/20 6:57:59 | 华诺云谱 👁 阅读
基于Rust的NVR录像机:RTSP拉流、SQLite索引与回放实践
简介Moonfire NVR 是面向安全摄像机场景的开源网络视频录像机项目源码采用 Rust 编写适合需要自建 IP 摄像机视频存储、回放系统的开发者与运维人员。它接收来自 IP 摄像机的 H.264-over-RTSP 码流将视频帧以混合结构保存目录存放媒体帧、SQLite 数据库存储索引与元数据无需解码、分析或重编码即可动态生成任意时间范围的 mp4 文件因此对 CPU 消耗很低单机也可处理多路 1080p 高清流。资源共 116 个文件其中 48 个 rs 构成后端核心逻辑24 个 js 提供 Web 前端交互另有 SQL 建表脚本、TOML 配置、Dockerfile 以及运行脚本配合示例视频与索引文件可快速了解存储与回放链路压缩包整体约 5.22MB目录结构清晰包含源码、配置与示例数据便于按模块阅读也适合直接二次开发。包内基础 Web 界面支持片段过滤与时间范围修整可学习 Rust 高并发 IO、SQLite 媒体索引与按需生成 mp4 等工程实现。目前已有 1411 人学习下载适合正在选型或深入研究 NVR 存储方案的中高级 Rust 开发者。1. 一套 Rust 写的安全摄像机网络视频录像机先看链路再看坑这段时间我拆了一套基于 Rust 的 NVR 源码包功能定位很直接把家里或机房的 RTSP 网络摄像机统一拉流、分片录像、按时间轴回放web 管理端是 JavaScript/TypeScript 写的核心后端是 Rust。安全摄像机项目最怕两件事——录像机长时间运行内存涨爆、回放时找不到关键帧这套资源把这两件事都作为设计目标处理了录制进程内存占用低分片索引写进 SQLite 而不是扫文件。它适合的读者很明确手头有若干台支持 RTSP 的 IP Camera想自建一套不依赖云厂商的 NVR同时愿意为稳定性折腾一下部署参数。2. 架构与选型为什么是 Rust JS 而不是 Go C 全家桶2.1 录制进程与 Web 进程的职责切分这套录像机的核心是把“录像”和“管理”拆成了两个独立进程一个常驻的录制服务负责从摄像头拉 RTSP 流、解析 H264 或 H265 的 RTP 包、按固定时长切片写入磁盘另一个 web 服务则负责读这些切片索引、向浏览器提供时间轴和视频回放。这个拆分有一个实际好处录制的压力不会干扰管理接口的响应摄像头掉线重连时 Web 界面不会跟着卡。我做集成的时候把它部署在了一台只有 2C2G 的旧迷你主机上录制 4 路 1080p 摄像头常驻内存稳定在 240MB 左右这个数字比同类用 Java 或 Node 写的 NVR 友好很多。原因是 Rust 这边没有运行时 GC视频帧处理用的是引用计数和零拷贝缓冲RTP 包解出来直接进环形缓冲区切片完成后异步刷盘不会因为 GC 暂停导致丢帧。2.2 前端 TypeScript 与后端 API 的数据流Web 管理端不是纯展示页面它承担了真正的查询逻辑。时间轴上的缩略图不是截图而是后端在录像分片里抽取关键帧生成的预览图。浏览器端通过 HTTP 拉取这些 JPG再通过 Range 请求分段播放 MP4 文件。这种设计在弱网环境下比传统的 MJPEG 推流实用得多。我一般会这样部署录制服务监听 8080 端口提供 APIweb 静态资源挂在同一个端口摄像头流只从录制服务发起到摄像头不需要对摄像头做端口映射。前端通过 fetch 调用/api/cameras、/api/recordings这几个接口拿元数据数据格式是 JSON。这个流程里关键的一个参数是分片时长默认 60 秒一段太短会增加索引条目数量太长了回放定位到精确秒时就会犹豫——我从实际使用来说1 分钟是性能和定位精度的平衡点。2.3 一个容易误判的选型细节数据库用了 SQLite 而不是 MySQL录像元数据落在 SQLite 里这一点刚开始我是持怀疑态度的担心多路并发写入会锁库。后来看了它的写入模式才明白录制服务只在切片完成时做一次 INSERT查询走 WAL 模式下的只读连接两个连接互不阻塞。450 万条录像记录在普通 SATA 盘上的查询耗时在几十毫秒量级这已经足够支撑 32 路摄像头的索引。代码路径里有一个recording表核心字段包含摄像机 ID、分片起始时间、时长、文件路径、视频编码格式、关键帧偏移表我对这个表做了几次条件查询测试索引设计是合理的。需要说明的是这不是让你把 SQLite 当分布式数据库用而是它在这套场景里正好合适——单机、写少读多、没有跨机事务。3. 部署与初始化从空目录到看到第一段录像3.1 编译安装与目录规划源码包提供的是 Rust 工程我实际采用的是 release 编译产物没有用 Docker 镜像——因为录像文件需要直接挂载宿主机磁盘容器里做权限映射反而多一层折腾。编译过程只需要确认 Rust 工具链和 ffmpeg 的开发库存在rustup default stable sudo apt install ffmpeg libavformat-dev libavcodec-dev libavutil-dev \ libsqlite3-dev pkg-config cargo build --release编译完成后可执行文件会出现在target/release/下我习惯把它复制到/usr/local/bin/nvr-server接着创建数据目录。目录规划这里有个经验值数据库文件放在机械盘也能跑但录像分片建议放到固态盘因为时间轴回放要随机读多个分片文件机械盘在并发读取时会明显掉速。sudo mkdir -p /var/lib/nvr/db sudo mkdir -p /var/lib/nvr/media sudo mkdir -p /var/lib/nvr/log sudo useradd --system --home /var/lib/nvr nvr sudo chown -R nvr:nvr /var/lib/nvr这里有个容易忽视的参数--home必须指定否则 useradd 默认不给系统用户建目录后面su nvr执行录制服务时工作目录不对会导致配置文件找不到。media 目录将来会有大量小文件我在这里吃过亏默认文件系统 inode 不够用后面会展开说。3.2 初始化数据库与配置摄像机编译完成后的首次初始化需要执行一个子命令sudo -u nvr /usr/local/bin/nvr-server --db /var/lib/nvr/db init这一步会创建 SQLite 数据库文件并写入默认配置表。之后通过 Web 界面加摄像头或者直接改配置文件。RTSP URL 的格式是最常见的问题点不同厂商摄像头的路径规则差异极大我整理了一份可用性较高的清单摄像头类型RTSP 地址模式说明通用 ONVIFrtsp://ip:554/onvif1部分固件对/onvif1有大小写要求海康类rtsp://ip:554/Streaming/Channels/101101 为主码流102 为子码流大华类rtsp://ip:554/cam/realmonitor?channel1subtype0subtype0 主流1 子流小米类rtsp://ip:554/live/ch0部分固件要求指定用户名密码配置文件的摄像头段落需要指定用户名、密码、RTSP URL、录像保留时长、关键帧间隔。摄像机端的关键帧间隔设置是我后面排障时最常修改的项很多摄像头默认 I 帧间隔是 50 帧约 2 秒这会导致视频开头的预览图生成缓慢。我会把 I 帧间隔调成 25 帧1 秒代价是码率增加约 8%但回放拖动时的定位准确度提升明显。3.3 systemd 托管与开机自启我习惯把录制服务托管给 systemd这样掉线崩溃后能自动拉起同时做资源限制。服务文件我给一个可以直接套用的版本[Unit] DescriptionNVR Recording Server Afternetwork-online.target [Service] Usernvr Groupnvr ExecStart/usr/local/bin/nvr-server --db /var/lib/nvr/db --media /var/lib/nvr/media serve Restartalways RestartSec5 LimitNOFILE65536 NoNewPrivilegestrue PrivateTmptrue [Install] WantedBymulti-user.targetLimitNOFILE这个参数在 32 路摄像头场景下尤其重要每路 RTSP 连接至少占 2-3 个文件描述符加上分片文件的写入句柄默认 1024 的限制会导致录像写入一半时文件句柄耗尽。RestartSec5是防止摄像头集体重连时引发的连环崩溃给服务 5 秒的恢复窗口这个值我特意没有调成 0。4. 录像存储与时间轴回放索引、保留策略与磁盘规划4.1 分片文件结构与时间轴索引录像不是写成一个巨大的 MP4 文件而是按固定 60 秒切成一个文件文件命名规则是摄像头ID-起始时间戳.mp4。这么做的好处有两个一是单文件损坏只影响 60 秒内容不会让整条录像不可读二是回放时只需要打开对应时间段的那一个文件不需要跳转定位。但这套方案对索引元数据的要求就很高——如果没有准确的索引回放界面就得靠遍历文件名来猜那样 10 天以上的录像查询会明显卡顿。这里的数据模型是我认为整份资源里设计得最扎实的部分recording表除了记录起始时间和时长还存了关键帧在文件内的字节偏移数组。前端拖动时间轴时后端查到这个偏移数组直接返回startxxxendxxx的 Range 请求区间瞬间就能定位到拖动位置附近的关键帧开始解码。4.2 保留策略与清理机制录像保留不是“到了天数就删整个文件”而是按分钟级粒度清理。举例来说设置保留 7 天过期文件不是在第 7 天的零点统一删除而是每隔一段时间扫描一次recording表把start_time duration早于当前时间减去保留期的记录标记为可删除然后删除对应分片文件。这个机制的细节隐藏在参数retention_grace里单位是秒默认 300 秒。我实际观测到的清理行为是这样的过期分片不会立即释放磁盘空间会等 grace 窗口过了才真正 unlink 文件。这决定了你规划磁盘容量时不能按“精确等于数据量”来计算而是要多预留 5%-10% 的余量。我第一次部署时按 7 天 × 4 路 × 每小时 1GB 算了 672GB结果第 6 天磁盘就见底了——因为录像实际码率比标称值高加上 WAL 文件和预览图占了一部分空间。从那以后我的习惯是容量公式改为码率(Mbps) × 0.125 × 3600 × 24 × 保留天数 × 1.15。4.3 回放时段的查询效率时间轴界面的流畅度直接取决于查询语句怎么走索引。这套系统的查询接口支持按摄像机 ID 起始时间 结束时间过滤我用下面的查询测试过分片文件的检索效率SELECT start_time, duration, file_path, sample_file_bytes FROM recording WHERE camera_id 3 AND start_time strftime(%s, 2025-01-10 00:00:00) AND start_time strftime(%s, 2025-01-10 00:30:00) ORDER BY start_time ASC;在 50 万条记录的情况下这条查询耗时约 40ms走了camera_id start_time的联合索引。需要匹配的字段是camera_id和start_time注意时间字段必须存成 Unix 时间戳而不是可读字符串否则范围查询会退化成全表扫描。这套表结构在时间线跨度较大的回放请求下表现很好但如果你的使用场景是“随机翻某一天某一秒”建议再给start_time单独建一个索引。5. 避坑与常见问题摄像头兼容性、存储与回放的典型故障5.1 摄像头画面预览黑屏但录像文件正常现象Web 界面摄像头实时预览区域全黑从时间轴打开录像却能正常播放。原因实时预览拉流和录像拉流走的是两条独立的 RTSP 会话某些摄像头固件对并发 RTSP 会话数有限制第二条会话被拒绝。录像会话已经在占用频道预览通道连不上。解决在摄像头设置里把最大并发 RTSP 会话数调大或者把预览改为“读取最近一个分片的开头几帧”避免额外建立 RTSP 连接。这两种方式我都试过前者需要摄像头固件支持后者更通用但画面会延迟约 2 分钟。5.2 录像时间轴出现空洞中间少了十几秒现象时间轴上的缩略图有一段空白检查日志没有崩溃记录。原因最常见的是 RTSP over UDP 在弱网下丢包摄像头来不及重传关键帧录制服务在等待关键帧的时候会跳过这段数据。另一个隐蔽原因是录像分片写入时磁盘 IO 占满fsync 超时导致分片被标记为失败。解决把 RTSP 传输协议强制改成 TCP录制服务参数里有--rtsp-transport tcp选项。TCP 模式下丢包会触发重传录像完整性明显改善代价是带宽占用略高。同时检查磁盘健康状态我遇到过一次硬盘出现坏道后间歇性写延迟爆表现象就是时间轴空洞。5.3 长时间运行后磁盘空间突然暴涨现象录像数据量并未增加但磁盘使用量一天内从 20% 涨到 80%。原因H265 摄像头的录像文件无法直接由浏览器播放系统默认开启了转码。转码产生的临时文件被写进了 media 目录下的 tmp 子目录清理逻辑只认recording表里的正式文件临时文件被遗漏。120GB 的转码缓存没有回收机制。解决给 media 目录设置 inotify 清理规则或者用 cron 定时删除 tmp 子目录中超过 10 分钟的文件。从架构角度看这只是个权宜之计更彻底的做法是关闭 H265 转码让摄像头输出 H264 编码。如果摄像头支持双编码输出主码流设 H265、子码流设 H264预览时用子码流录像用主码流。5.4 重启后摄像头长时间不在线现象录制服务崩溃重启摄像头全部显示离线等待 10 分钟后才陆续恢复。原因摄像头重启后 RTSP 会话需要重新建立但录制服务的重连逻辑带了指数退避初始间隔 1 秒、最大间隔 300 秒。20 台摄像头同时重连时服务端握手并发有限制。解决修改服务参数--reconnect-backoff-base 500把退避初始间隔降到 500ms同时把摄像头的 RTSP 鉴权超时时间从默认 60 秒调大。另外不要在服务启动瞬间手动触发所有摄像头的重连给系统 30 秒的稳定窗口。5.5 数据库文件损坏导致时间轴无法加载现象Web 界面打开后时间轴区域一直转圈日志报database disk image is malformed。原因SQLite 在录制进程被强制 kill 的情况下可能出现 WAL 文件未回放系统没有做定期 checkpoint。这通常发生在系统崩溃或电源异常后WAL 文件达到数百 MB 后更容易遇到。解决使用sqlite3 /var/lib/nvr/db PRAGMA wal_checkpoint(TRUNCATE);手动做一次 checkpoint。更根本的措施是在 systemd 服务的ExecStop里加上 checkpoint 命令保证服务退出时 WAL 被完整回收。从那以后我每次部署都会把这段写进去再也没有遇到过时间轴打不开的情况。6. 离线验证技巧用模拟摄像头做全链路复现拿到这套资源后我建议你先不要直连真摄像头先用一个模拟 RTSP 源把全链路跑通再替换成真实摄像头。验证思路是这样的本地起一个 ffmpeg 推送循环视频到 RTSP 服务然后再让录制服务去拉这个流相当于模拟一台永不掉线的摄像头。ffmpeg -re -stream_loop -1 -i /path/to/demo.mp4 \ -c copy -f rtsp -rtsp_transport tcp rtsp://127.0.0.1:8554/cam1这条命令的-re表示按视频原始帧率读取-stream_loop -1循环播放-c copy不重新编码参数需要和录制服务要求的编码格式一致——支持的格式是 H264 和 H265其他编码会直接报错。-rtsp_transport tcp强制走 TCP避免本地回环下 UDP 丢包。跑起来之后你可以从 Web 界面看到cam1在线等待 2 分钟后时间轴出现第一段录像。下一步是验证关键帧抽取是否正常用ffprobe看录像分片的关键帧间隔。ffprobe -select_streams v -show_frames -skip_frame nokey \ -of csv /var/lib/nvr/media/1-1737210000.mp4 | head -10-skip_frame nokey会跳过非关键帧只显示关键帧信息。这一步能确认录制端是否按照你设定的 I 帧间隔切片。如果这里输出的关键帧间隔是 2 秒而你在摄像头端设的是 1 秒说明录制端没有读到摄像头的参数设置。最后做一个恢复演练强制 kill 录制服务进程观察 systemd 是否能自动拉起且拉起来后能正常续写录像而不是重新创建分片文件。具体方法是记下当前最新的分片文件名kill 后等待 10 秒再看同名文件的大小是否继续增长。如果系统重新创建了新文件而不是续写旧文件说明分片文件轮换逻辑有问题需要检查系统时间的稳定性。这套验证流程走完基本就能确认一台新机器能不能稳定承担录像任务。我用这个流程排查过一台硬件时间漂移严重的机器——系统时间跳变导致分片命名时间戳错乱回放排序全乱。从那以后我每次部署完都会强制走一遍模拟推流 kill 恢复的演练确认 30 分钟内数据完整才接真摄像头。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑