☰
Moonfire NVR:开源网络录像机,用SQLite索引实现轻量视频回放
2026/10/14 7:14:12 网站建设 项目流程

简介:Moonfire NVR 是一套用 Rust 与 JavaScript 实现的开源安全摄像机网络视频录像机,面向有自建监控需求的技术爱好者、嵌入式开发者及轻量级 NVR 运维人员。它把 IP 摄像机输出的 H.264-over-RTSP 流写入磁盘,视频帧以混合格式存储,同时将元数据放 SQLite 数据库,支持任意时间范围动态生成 mp4 文件,且几乎不占用额外 CPU。资源共 116 个文件,体积 5.22MB,包含 48 个 Rust 源文件、24 个 JavaScript 前端脚本、11 个 Markdown 文档、SQL 建库脚本、TOML 配置以及 Dockerfile、示例视频与数据索引等,目录结构清晰,适合直接阅读和二次开发。目前已有 1411 人学习下载。该压缩包可帮助读者理解视频监控存储的工程实现,掌握 Rust 系统编程与 Web 播放器联动的设计思路,也适合作为轻量级嵌入式录像方案的参考。

1. Moonfire NVR:轻量安全摄像机录像方案,先把回放这件事做扎实

手上有 20 台 IP 摄像机要集中录像,不想采购封闭的 NVR 盒子,也不想按月交云存储费,Moonfire NVR 值得认真看。它是一个开源的网络视频录像机,核心职责很纯粹:通过 RTSP 从安全摄像机拉流,按时间片段写入磁盘,用 SQLite 索引,再在浏览器里按时间轴拖动回放。第一次用的人往往意外于它的资源占用,一台普通工控机同时录 8 路主码流,CPU 占用率能一直保持个位数,因为它几乎不转码,录像直接落盘。这方案适合两类人:手里已有一批摄像头、想自建录像回放底座的技术人员,以及做安防集成、想分开存储与分析层的团队。下文从存储原理讲到部署命令、摄像头接入和常见的坑。

2. 先看设计再动手:平面文件、SQLite 索引与浏览器回放的关系

部署之前先把存储模型弄清楚,否则后面遇到“录像缺失”“时间轴跳跃”这类问题,拿到一台机器根本不知道从哪里下手。Moonfire NVR 的设计和其他监控软件最大的不同,是它把“录像文件”和“录像索引”完全分开。

2.1 为什么录像是底座,分析不是它的活

市面上的监控方案很容易陷入一个误区:把录像、移动侦测、人脸识别、车牌识别全塞进同一个进程,机器风扇狂转,业务一多录像先断。Moonfire NVR 的定位很明确,它是一个网络视频录像机,只做三件事:从摄像机拉流、把流写成文件、提供时间轴回放。它不主动做目标检测,不识别车牌,不分析行为。这听起来像功能缺失,其实是选型上的清醒:分析任务是另一层的事,可以先落盘、再做异步分析,而不是在拉流进程里抢占 CPU。我用过一些带 AI 检测的开源监控方案,实时分析一开,CPU 占用直接拉满,录像反而开始丢帧;换成这种“录像归录像、分析归分析”的架构后,录像的可靠性明显提升。

选型时要看对比。封闭式 NVR 盒子配置灵活度低,录像格式私有,想取原始视频文件做分析往往要厂商工具;云录像服务月费会随路数和天数线性上涨。自建开源录像机把数据拿在自己手里,文件格式通用,坏了盘也能用通用工具恢复一部分。缺点也明显:没有商业售后,web 界面功能偏基础,对网络、存储和 Linux 基础有要求。如果你想要的是一个开箱即用、带人形侦测的傻瓜设备,那它不是你的菜;如果你要的是 24 小时不间断的可靠录像底座,这个方向是对的。

对比维度封闭式 NVR 盒子云录像服务Moonfire NVR 自建
录像格式私有格式,难迁移云端私有通用媒体文件,可离线回放
扩展性受硬件通道数限制按路数和天数收费只要磁盘够,通道数几乎不限制
分析能力自带部分规则平台提供默认没有,需自己叠加
运维要求低最低中高,需要懂 Linux

2.2 平面文件加索引,录像不是黑匣子

Moonfire NVR 把录像按时间切成片段,每个片段是一个独立媒体文件,顺序写入磁盘的媒体目录。SQLite 数据库里存的是每个片段的元数据:起始时间、文件位置、时长、大小、编码信息。播放时浏览器先从 SQLite 拿一段时间的片段列表,再按需读取对应文件。索引和文件分离带来的好处,是数据库坏了不等于录像文件丢了,只要媒体目录还在,就能重建索引;反过来,单个文件损坏也只是丢一小段,不会连累整个时间轴。这套设计在排查问题时特别有用,我可以用通用工具直接看媒体文件,而不必依赖某个厂商的私有播放器。

这种做法在“随机访问”上尤其有用。传统录像机如果按一个巨大文件持续写入,回放时要从头拖到尾,非常难受。分段文件配合 SQLite 的时间字段,可以秒级跳到任意时间点。分段时长是可以在初始化或配置里控制的,常见设置为每段 1 小时,也可以缩短到 15 分钟,便于在异常断电时把影响面缩小。分段也不是越短越好,太短会造成小文件太多,文件系统 inode 压力和随机 IO 都会上升。

2.3 先学会用 SQLite 查看录像索引

部署前先熟悉一个排查动作:直接查询 SQLite,确认某个时间范围到底有没有录像。不要一上来就背表名,因为不同版本的表名可能有调整,我一般会先列出库里有哪些表:

SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;

这条 SQL 返回的是一张表名清单。逻辑说明:sqlite_master是 SQLite 的系统元数据表,所有表、索引、视图的定义都记录在里面;type='table'过滤出普通表,ORDER BY name方便阅读。拿到真实表名后再去统计某个时间段内的片段数,就不会因为版本升级导致脚本写错表名。

比如我想确认最近一天有多少录像片段,可以这样查:

SELECT count(*) FROM recording WHERE start_time > strftime('%s','now','-1 day');

这里的recording是我当前版本里实际的表名,你的环境如果不一样,先跑上面那条查表名的 SQL 确认。参数说明:strftime('%s','now','-1 day')是 SQLite 计算 24 小时前 Unix 时间戳的标准写法,直接拿字符串比较start_time是不对的,因为表里存的通常是整数时间戳。这个动作在后续做录像完整性巡检时特别有用。

3. 从零部署 Moonfire NVR:磁盘布局、初始化与首次启动

这一章的目标是在一台干净 Linux 服务器上把 Moonfire NVR 跑起来,能从浏览器打开界面。很多人的第一个坑不是软件本身,而是部署前的存储规划,所以我把磁盘放在最前面。

3.1 存储选型:录像机吃的是顺序 IO,但回放吃随机读

录像写入是持续的顺序写,但时间轴拖动回放时要从不同片段文件里随机读数据,两者对磁盘的要求不一样。常见的做法是优先本地盘,机械盘要选监控级或 NAS 级,容量按“至少能存 15 天”来算;如果路数多、码流高,直接上 SSD。媒体目录放到网络盘是我见过最多的翻车现场,录像能写进去,回放拖动时延迟会明显变大,多路同时回放时直接卡成幻灯片。

我一般这样挂载一块专用数据盘。先格式化,再写入/etc/fstab,目录放在/var/lib/moonfire-nvr:

sudo mkfs.ext4 /dev/sdb1 sudo mkdir -p /var/lib/moonfire-nvr echo '/dev/sdb1 /var/lib/moonfire-nvr ext4 defaults,noatime 0 2' | sudo tee -a /etc/fstab sudo mount /var/lib/moonfire-nvr

逻辑说明:mkfs.ext4把新盘格式化;挂载点单独给录像目录,避免和系统日志抢空间;noatime减少每次读文件时的元数据写盘,对回放场景有帮助。参数说明:defaults是常规挂载选项组合,适合数据盘;末尾的0 2表示允许系统启动时做磁盘检查。如果磁盘上已有数据,不要用这条命令,先备份。

挂载完用lsblk -f看一眼,确认文件系统和挂载点都正确。这里有个细节:如果厂商默认给了你一块已经分区并格式化的盘,重新分区前一定要确认里面没有历史数据,录像机数据盘一旦被覆盖就是真的没有后悔药。

3.2 安装与初始化:db 目录和 media 目录要分开

安装包从官方发布页下载,不同发行版有 deb 或 rpm 包。安装后第一步是初始化数据库目录和媒体目录,先看一下你手上版本的参数:

moonfire-nvr init --help

输出里会有可用的命令行参数。我常用的是这样:

sudo dpkg -i moonfire-nvr_*.deb sudo moonfire-nvr init --db-dir=/var/lib/moonfire-nvr/db --media-dir=/var/lib/moonfire-nvr/media

参数说明:--db-dir放 SQLite 数据库文件,--media-dir放录像片段文件,两者必须分离,否则数据库膨胀后备份不方便。路径要用绝对路径,并且让运行服务的管理用户对这两个目录有读写权限。初始化完成后,会打印监听地址和默认端口,我习惯把监听地址固定成内网地址,后续再加 TLS 加密入口。

源码构建方式也有,需要先准备 Rust 工具链和若干系统依赖,不是必选项。除非你所在的内网环境没法下载预编译包,否则直接用发布页的包更省事。如果你喜欢折腾源码,构建完的产物和你用预编译包得到的运行逻辑没有本质区别,但要注意不同版本对配置文件的读取方式可能有变化,以你装到的版本为准。

3.3 用 systemd 托管进程,并验证端口

预编译包通常会附带 systemd 服务文件,装完就能systemctl start。如果安装包没带,我会手写一个最小 unit,放在/etc/systemd/system/moonfire-nvr.service:

[Unit] Description=Moonfire NVR After=network-online.target [Service] User=moonfire ExecStart=/usr/bin/moonfire-nvr run --config /etc/moonfire-nvr.toml Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

逻辑说明:After=network-online.target保证网络就绪后再启动,避免启动时摄像头域名解析失败;Restart=on-failure让进程崩溃后自动拉起,录像机最好别等人手动去救;RestartSec=5是两次重启的间隔。参数说明:User必须与媒体目录的属主一致,否则进程虽然能启动,但无法写录像文件,表现是“看着在跑、实际没录”。

写入后执行:

sudo systemctl daemon-reload sudo systemctl enable --now moonfire-nvr sudo ss -ltnp | grep 8080

如果输出里有:8080表示服务在监听。这里有个常见误用:enable和start要分开理解,enable是开机自启,start是立即启动,合写enable --now能一气呵成,但如果服务本身配置错误,systemctl会返回失败,需要立刻看日志。查看完整启动日志用:

journalctl -u moonfire-nvr -b

-b是本次开机以来的日志,排错时比-f更适合先看全貌。如果日志里出现权限相关报错,先检查/var/lib/moonfire-nvr下目录的属主。

3.4 首次登录:先控制暴露面,再想加密

浏览器访问http://服务器IP:8080,第一次打开会让你创建管理员账号。这里要强调:8080 端口直接暴露到不可信网络风险很大,特别是对上互联网的摄像头场景。我一般把监听地址绑在内网网卡,前面加一层 TLS 加密入口,或者用 SSH 隧道临时访问。设置完账号后,进入主界面会看到一个空的摄像头列表,下一步就是接入真机。

首次登录后建议把两件事做了:一是确认页面上显示的服务版本,记下来,后续排查问题时“我用的哪个版本”比什么都重要;二是改掉默认端口,如果内网有扫描习惯,把 8080 换成不常用端口能少很多噪音。不要嫌这些动作琐碎,录像服务一旦跑起来,你大概率半年不会再看一眼,前期把基础打牢后面才省心。

4. 接入摄像头与录像计划:RTSP 拉流参数与保留策略

Moonfire NVR 不主动去“发现”摄像头,你要把每个摄像头的 RTSP 地址填进去,它再持续拉流。这一步直接决定录像能不能长期稳定运行。

4.1 RTSP 地址与码流选择:先搞懂主码流和子码流

RTSP 地址的通用格式是rtsp://用户名:密码@摄像头IP:端口/路径,不同厂商路径不同。用户名和密码里有特殊字符时先做 URL 编码,否则地址会解析错。摄像头通常提供两路流:主码流清晰度高、码流大,适合留存证据;子码流分辨率低、省带宽,适合长期连续录像或做预览。常见的做法是录像用主码流,预览和夜间低带宽场景用子码流;如果磁盘空间紧张,就反过来,用子码流全天录,主码流只在事件触发时段录。

接入之前先用 ffmpeg 验证地址是否可直接拉流,把问题挡在配置之前:

ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@192.168.1.64:554/stream0" -t 10 -f null -

逻辑说明:-rtsp_transport tcp强制走 TCP 传输,避免 UDP 丢包后花屏;-t 10只拉 10 秒,能确认通断;-f null -不写输出文件,只做解码测试。参数说明:如果摄像头对 TCP 不友好,可以去掉-rtsp_transport tcp试试 UDP,但生产环境我几乎一律用 TCP。注意测试命令的退出码,返回 0 基本可用;如果报404或401,直接去问厂商拿正确的流路径,不要在 UI 里反复试。

没有真实摄像头时,可以用本地模拟流来验证部署。常见做法是拿一个视频文件循环推成 RTSP 流,但这一步需要额外的流媒体服务,不适合在部署初期引入,所以我的建议是:哪怕用手机摄像头先顶一路,也比用模拟流更接近真实情况。

4.2 在 UI 里添加摄像头:字段与保存后的状态判断

在 Moonfire NVR 界面进入摄像头管理页,新建一条记录,要填的核心字段有:摄像头名称、RTSP 地址、用户名、密码、录像保留天数、录像时间计划。有些字段不是必填,但建议都填上,比如摄像头位置信息,后期回放筛选会方便很多。

保存后不要急着走,观察状态。界面一般会显示拉流状态、最近录像时间、磁盘占用。刚添加的摄像头在 1~2 分钟内应该有录像片段产生,如果没有,进服务端日志看具体报错。日志查看命令:

sudo journalctl -u moonfire-nvr -f

-f是 follow 模式,持续输出新日志。看到 “connection closed” 说明地址不对或端口不通;看到 “401 Unauthorized” 说明用户名密码错误;看到 “no such stream” 说明流路径不对。把日志当作第一排查现场,比在 UI 里反复猜地址高效得多。这里我想强调一个习惯:在添加摄像头之前,先在纸上画出每一路的 IP、端口、主码流路径、子码流路径,接完之后把这张表收好。摄像头一多,靠脑子记 RTSP 地址一定会记混。

4.3 录像保留策略:用容量反推天数

录像保留天数不是一个拍脑袋的数字,而是用容量反推出来的。我常用的容量粗算公式:单路每日大小(GiB)约等于码流(Mbps)乘以 10.8。例如 4Mbps 主码流,每天大约 43GiB;2Mbps 每天约 21.6GiB;再留 10%~15% 余量。给出下表参考:

摄像机码流单路每日录像8 路保存 30 天
2 Mbps约 22 GiB约 5.2 TB
4 Mbps约 43 GiB约 10.1 TB
8 Mbps约 86 GiB约 20.2 TB

注意这是裸流估算,实际还受编码复杂度影响,场景越复杂码流波动越大。录像保留参数设置成“按天过期”,系统会在写入新片段前自动清理最早片段,不需要写 cron 脚本。一个常见的误用是为了“多存一点”把保留天数拉满,结果磁盘一两周就满,满了之后录像缓冲区崩溃,旧的全没了,新的也写不进去。容量规划宁可保守一点,给运维留出手动导出备份的时间。

4.4 从命令行确认录像真的在落盘

UI 上的“正在录像”有时候会给人错觉。我会直接看媒体目录:

watch -n 5 'find /var/lib/moonfire-nvr/media -type f | wc -l' du -sh /var/lib/moonfire-nvr/media

第一行每 5 秒刷新一次片段文件总数,数字持续变大说明拉流和落盘正常;第二行看目录总占用,判断容量增长是否符合预期。如果文件数不变、journalctl也没有新日志,先检查磁盘空间是否满了,再看摄像头的 RTSP 是否已经断开。如果文件一直在涨但 UI 里时间轴没有新片段,那就是索引写入出了问题,通常和数据库所在分区空间不足有关。

5. 踩坑与排查:录像缺失、时间跳变、浏览器转圈的三组现场

运行一段时间后,问题通常集中在三类:时间不对、拉流掉线、回放异常。下面这几条是真实现场里反复出现的,按现象、原因、解决写清楚。

5.1 时间不准与录像断档:日志先于界面

坑 1:现象是录像时间轴整体偏移几个小时,或片段时间与实际对不上。原因多半不是 Moonfire NVR 算错,而是摄像头自身时钟没同步。录像文件打的是 UTC 时间戳,UI 按浏览器时区展示,摄像头时钟一旦漂移,录出来的片段就会记到错误时间窗。解决:给摄像头配置 NTP 服务器,和录像服务器走同一时间源;在服务端对好时区后不要随意改系统时间,否则时间回跳会导致新片段覆盖旧片段。摄像头出厂时间默认是某个固定时区,不配置 NTP 的话,运行一个月漂移几分钟很正常。

坑 2:现象是时间轴上有一段明显的空白,前后录像都正常。最常见的原因是那段时间摄像头或网络掉线,或者有人手动重启了服务。解决:先查journalctl -u moonfire-nvr --since "1 hour ago"看那段时间有没有重连日志,再判断是网络问题还是摄像头端主动断开。如果日志里完全干净,那大概率是录像服务器时间被人为改过,检查系统安全日志。这里想提醒一句:不要靠肉眼在时间轴上看空白段,先用 SQL 查询确认缺失时间范围,再决定要不要深挖。

5.2 RTSP 拉流掉线:UDP 丢包与连接数上限

坑 3:现象是画面每隔十几分钟断一次,断几秒又自己恢复,录像时间轴出现密集小裂缝。原因很典型:RTSP 默认走 UDP,跨交换机或无线环境丢包严重,设备连接数达到上限时也会主动拒绝新连接。解决:第一步把拉流传输方式改成 TCP,大部分小裂缝会消失;第二步检查每台摄像头允许的最大并发连接数,同一路流被预览、录像、手机 App 同时拉取时会超限。生产环境里,预览和录像走不同流地址能显著减少掉线。还有一个容易忽略的点:如果摄像头供电不稳定,也会表现为周期性掉线,先看摄像头侧日志或电源状态再调协议。

5.3 存储 IO 与浏览器播放:两个最容易误判的方向

坑 4:现象是录像能写入但回放拖动特别慢,或多个用户同时回放时界面卡顿。原因常被误判为网络带宽不足,实际是存储随机读能力不够。机械盘在同时承担多路顺序写和多路随机读时,寻道时间会被放大。解决:用iostat -x 1看%util,持续接近 100% 说明磁盘瓶颈;媒体目录迁到本地 SSD,或者限制同时回放路数。另一个常见原因是录像目录所在分区快满,文件系统开始频繁分配块,也会表现为卡顿。如果确认是网络带宽问题,用iftop看实时流量,不要凭感觉。

坑 5:现象是浏览器点击播放一直转圈,换一个浏览器又正常。这多半是编码或浏览器兼容性问题。某些摄像头的默认编码不是 H.264,浏览器无法硬解;或者浏览器扩展干扰了播放请求。解决:把摄像头编码设为 H.264,音频编码尽量用 AAC;先关掉浏览器扩展,实在不行禁用硬件加速试一次。注意别一上来就怀疑 Moonfire NVR 的索引,先用本地播放器打开媒体目录里的一个片段,能放就是文件正常,问题在播放链路。

5.4 数据库与文件不一致:自己手删文件是最大的坑

坑 6:现象是 UI 显示有录像,但播放时提示找不到文件片段。原因通常是有人手动删除过媒体目录里的文件,或磁盘整理时移动了文件,SQLite 里的索引还在,但文件对不上了。解决:把索引记录和磁盘文件对账,确认缺失片段后重建索引或直接忽略这部分。需要提醒的是,以后扩容不要手撕文件,用保留策略控制。这个坑几乎都是运维人员“好心办坏事”造成的,最常见的动作是磁盘快满时直接rm掉最早的录像目录,结果数据库毫不知情,回放界面就会出现大量死索引。我见过有人因此把数据库整个删了重来,白白丢掉几天的录像,事后复盘就一句话:别绕过录像机自己的清理机制。

6. 进阶:录像完整性验证与无人值守运维

做录像机最怕的是出了事要查回放,结果发现“那几天没有录像”。这类事故靠事后翻盘很难救,要改成日常巡检。

6.1 一个容量估算函数,写进你的部署笔记

每次接新项目都要重新算存储,我习惯把它写成一个 bash 函数:

function est_space() { local mbps=$1 days=$2 cams=$3 local gb_per_day=$(echo "scale=2; $mbps * 86400 / 8 / 1024" | bc) echo "$cams cams, ${mbps}Mbps, ${days}days:" echo "$gb_per_day * $days * $cams GiB" | bc } est_space 4 30 8

参数说明:$mbps是单路码流,$days是保留天数,$cams是路数;内部用bc做浮点运算,默认scale=2保留两位小数。这函数算的是裸流,实际配额再乘 1.15 更稳妥。

6.2 SQLite 备份与完整性检查

数据库损坏是录像机最难防的问题,SQLite 的特点是复制文件不等于安全备份,正确做法是用.backup:

DB=$(ls /var/lib/moonfire-nvr/db/*.sqlite3 2>/dev/null | head -n1) sqlite3 "$DB" ".backup '/backup/moonfire-$(date +%F).sqlite3'" sqlite3 "$DB" "PRAGMA integrity_check;"

逻辑说明:第一行找到数据库文件,第二行做在线一致性备份,第三行检查当前库是否损坏,输出ok表示正常。参数说明:.backup会在数据库锁定时也安全复制;不要直接用cp服务运行中的数据库文件,容易拷出一份损坏副本。备份文件要和录像媒体分开存放,否则磁盘整体故障时一起丢。

最后说一个我的习惯:每天给录像服务安排一个 10 分钟级的小巡检,重点看三件事,磁盘余量、数据库完整性、录像文件数量是否持续增长。任何一项异常当天处理,不要拖。录像机这东西,平时默默跑着没人夸你,真到要翻旧录像那天发现没录上,就是血泪教训。希望帮到你。

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

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

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

立即咨询