去年一个做仓储的朋友找我,说他那两间库房想装监控,问了几家集成商,报价单上的数字让他直接把手机扣在桌上。我看了下需求:4 个点位、本地存 30 天、手机上能看、断电重启后要自己爬起来。这套需求放到成品方案里,报价能被硬件和年度服务费撑得很高;但如果自己动手,一台旧主机加几块监控盘就能跑起来。后来我用开源组件给他搭了一套低成本视频监控系统,推流走内网 RTSP 转 RTMP,播放端网页和手机都能开,录像按 5 分钟切段写本地盘,整套系统的软件成本是零,硬件成本主要是硬盘。这套东西跑了小半年,中间踩过的坑不少,今天把从取流、推流、播放到录像落盘的完整链路拆开讲一遍,包括参数怎么算、码率怎么配、断电后录像文件为什么打不开、回放怎么快速定位。适合想自己动手省一笔、又不愿意在稳定性上妥协的人看,稍微懂点命令行就能跟着做。
1. 整体方案设计与选型思路
1.1 先把需求落到纸面:三个问题决定架构
动手之前,我习惯先逼着自己回答三个问题,这三个答案基本就把架构框死了。第一是延迟要求:你需要的是"事后能查",还是"实时盯着看"?如果只是事后查录像,那推流和播放都无所谓延迟,HLS 这种 10 秒起步的方案完全够用,实现成本最低;但如果要在门口实时看着人进出,延迟超过 3 秒就没法用了,那就必须上 HTTP-FLV 或者 WebRTC。第二是点位数和留存天数:这直接决定硬盘容量和写入压力,4 路和 16 路是两个完全不同的工程量。第三是观看位置:只在内网看,还是要在外面用手机看?内网的话一切好办,出了内网就要考虑端口暴露、鉴权和带宽上行,风险和成本都会跳一档。
我见过不少人上来就问"用什么流媒体服务器好",其实这个问题没有标准答案,因为选型的依据不在服务器本身,而在上面这三个约束条件。同样一套软件,配 HLS 就是低延迟要求不高的低成本方案,配 WebRTC 就变成实时对讲级别的方案,中间差的不是软件,是需求边界。
所以我的建议是拿张纸,把"延迟上限、点位数、留存天数、观看范围、预算上限"这五行写下来,再去看技术方案。写不清楚这五行,后面选什么东西都是拍脑袋,返工概率极高。
1.2 硬件与软件选型:把每一分钱花在刀刃上
低成本不等于将就,钱要花在真正影响稳定性的地方。我的排序是:硬盘 > 网络 > 主机 > 摄像头。硬盘排第一是因为录像写入是 7×24 小时的持续压力,一块普通的家用盘在这种场景下寿命会明显打折,用监控级硬盘(支持长时间连续写入、带写入缓存管理)能少掉很多麻烦。主机的算力需求其实很低,因为我们是"转封装"而不是"转码",-c copy 模式下 CPU 占用基本可以忽略,一台淘汰的办公小主机、甚至一块低功耗开发板都能扛住 4 到 8 路的转发。
软件层面我的组合是:取流用 RTSP,转发用轻量开源流媒体服务,录像用 ffmpeg 分段写盘,播放端网页用 flv 或 hls 播放器。选轻量流媒体服务的理由很直接——它不需要数据库、不需要复杂配置文件,一个 yaml 就能起来,出问题的时候日志一眼看得懂。相比那些功能大而全的方案,轻量方案少了转码、少了集群、少了管理后台,但也少了 90% 的故障面。这个取舍在低成本场景下非常划算,因为我们本来就不需要那些功能。
摄像头这边有个省钱技巧:不需要买带云台、带智能分析的型号,只要它支持标准 RTSP 输出就行,很多入门型号都支持。买回来之后把主码流分辨率设成 1080P 或者 720P,子码流设成 640×360,主码流用来录像,子码流用来做低带宽预览,同一台设备出两路流,一分钱不多花。
1.3 为什么不用成品套装:成本与可控性对比
成品方案的优势是开箱即用,劣势是每一个扩展需求都要额外付费。我列了个对比,把几个关键维度摆在一起看会更清楚。
| 维度 | 成品套装 | 自建方案 |
|---|---|---|
| 初始成本 | 硬件+授权费,点位越多越贵 | 主要是硬盘和主机,软件零成本 |
| 扩容一路 | 通常要加授权或换主机 | 加一个摄像头,改一行配置 |
| 录像格式 | 私有格式,导出要专用工具 | 标准 mp4/mkv,随便拷 |
| 故障定位 | 黑盒,只能等售后 | 日志、磁盘、进程全可见 |
| 远程观看 | 依赖厂商云服务 | 自己决定,可内网也可自建入口 |
| 学习成本 | 低 | 需要一点命令行基础 |
"录像格式是私有格式"这一条是我最在意的。之前帮人从一套成品设备里导出一段事故录像,专用播放器装了三台电脑才装成功,导出还限速。自建方案用标准容器格式,录像文件拷到任何一台电脑上都能播放,这个自由度在真正需要用的时候价值极大。
可控性还有一层意思是升级节奏。成品设备的固件更新由厂商决定,有时候一次更新把某个功能改掉了你也没办法。自建方案里每个组件都是你引入的,升不升级、升到哪个版本,你说了算。当然这也意味着你要自己对升级负责,所以我在第 6 章专门写了守护和自恢复的做法。
2. 推流链路:摄像头怎么把画面送出去
2.1 取流方式与协议选择:RTSP 是起点
绝大多数网络摄像头对外提供的标准接口就是 RTSP,地址形如rtsp://用户名:密码@192.168.1.64:554/路径。路径各家不一样,有的叫/stream1,有的是一长串带通道号的路径,最省事的办法是去看摄像头自己的配置页面,那里一般会直接给出主码流和子码流的完整地址。如果你手上是一台不知道地址的老设备,可以用 ONVIF 发现工具扫一遍内网,它会返回设备的服务地址,再逐个试。
RTSP 有两个传输模式,TCP 和 UDP。这里有个特别容易踩的坑:默认走 UDP 的时候,只要网络有一点丢包,画面就会花屏、马赛克,甚至整路卡死。因为 UDP 不重传,丢一个包就是一块花屏。我在第一次搭的时候就吃了这个亏,白天看着好好的,晚上库房那边网络一波动,画面就满屏绿块。后来在 ffmpeg 里加上-rtsp_transport tcp,问题立刻消失。代价是延迟稍微高一点点、带宽占用略大,但在内网环境下完全可以接受。
所以我的默认配置永远是 TCP 取流,除非你的摄像头和服务器之间隔了很差的无线链路,那才需要回头去优化网络本身。取流失败的时候先别怀疑软件,先拿ffplay直接播一下 RTSP 地址,能播说明取流没问题,问题在后面的环节;播不了就是网络、账号密码或者编码格式的问题,一步步排就行。
2.2 服务端搭建:流媒体服务的部署与关键参数
服务端的角色是"接收推流、对外分发"。我选的是一类轻量开源流媒体服务,它同时支持 RTSP、RTMP、HLS、WebRTC 几种输出,配置文件是一个 yaml。核心配置大概长这样:
# 服务监听端口 rtspAddress: :8554 rtmpAddress: :1935 hlsAddress: :8888 webrtcAddress: :8889 paths: cam1: source: rtsp://user:pass@192.168.1.64:554/stream1 sourceOnDemand: yes sourceOnDemandCloseAfter: 30s cam2: source: rtsp://user:pass@192.168.1.65:554/stream1 sourceOnDemand: yes sourceOnDemandCloseAfter: 30s这里面最关键的两个参数是sourceOnDemand和sourceOnDemandCloseAfter。前者表示"有人看的时候才去拉摄像头的流",后者表示"没人看超过 30 秒就断开"。为什么这么配?因为如果服务端一直保持和摄像头的连接,那 4 路摄像头就是 4 条常连接,一旦某条连接因为网络抖动断了,服务端要自己重连,重连失败的窗口期里这路就是黑的。而按需拉流模式下,观众一来就重新建连,反而更容易拿到一条干净的流。
但这里有个冲突要提醒:如果你同时要 24 小时录像,就不能完全靠按需拉流。因为没人看的时候流就断了,录像也会跟着断。我的做法是把"转发"和"录像"拆成两条独立链路——转发服务按需拉流给播放端用,录像则由一个独立的 ffmpeg 进程常驻拉流写盘。两条链路互不影响,播放端卡了不影响录像,录像进程崩了也不影响看。这个"链路隔离"的思路是我踩了几次坑之后定下来的,后面第 4 章还会展开。
部署方式上,直接下二进制文件跑起来就行,不需要容器。用 systemd 托管,配置开机自启,日志走 journald,出问题journalctl -u 服务名 -f就能实时看。这套东西的资源占用很低,4 路 1080P 转发,内存 200MB 以内,CPU 个位数百分比。
2.3 带宽与参数计算:码率、帧率、GOP 怎么定
这是最容易被跳过、但最影响体验的一节。所有数字都建立在码率上,码率定错,后面全错。先给一个经验参考表,都是我实际跑过的组合:
| 分辨率 | 帧率 | 建议码率 | 每小时占用 | 每路每天 | 4 路 30 天 |
|---|---|---|---|---|---|
| 640×360 | 15 | 512 kbps | 约 0.23 GB | 约 5.5 GB | 约 660 GB |
| 1280×720 | 25 | 2 Mbps | 约 0.9 GB | 约 21.6 GB | 约 2.6 TB |
| 1920×1080 | 25 | 4 Mbps | 约 1.8 GB | 约 43.2 GB | 约 5.2 TB |
| 2560×1440 | 25 | 6 Mbps | 约 2.7 GB | 约 64.8 GB | 约 7.8 TB |
换算过程写清楚,方便你自己改数字:码率(Mbps)÷ 8 = 每秒兆字节数(MB/s)。2 Mbps 就是 0.25 MB/s,乘以 3600 秒得到每小时 900 MB,也就是 0.9 GB。再乘以 24 得到每天 21.6 GB。这里的 GB 按 1000 进制算,跟硬盘厂商标称一致,跟系统里df显示的 GiB 会有大约 7% 的差异,做容量规划时留 10% 余量就够了。
上行带宽也要算。如果摄像头是往服务端推流,那服务端所在网络的上行要能承载路数 × 码率 × 1.2。4 路 1080P 就是 4×4×1.2 ≈ 19 Mbps 的上行余量,家用宽带的上行通常是短板,这一点一定要提前确认,不然内网好好的,一出门看就卡成 PPT。
GOP 长度是我第二个要强调的参数。GOP 是一组画面里关键帧的间隔,用秒或者帧数表示。GOP 越大,同样画质下码率越省,但播放端起播越慢、拖进度条定位越粗。监控场景我一般设 2 秒(25 帧率下就是 50 帧一个关键帧)。设成 2 秒的好处是:从任意位置开始录像,最多丢 2 秒的画面就能接上一个关键帧;播放器起播也能在 2 秒内出画面。设成 10 秒的话,录像切片和拖动定位都会变得很难受。这个参数在摄像头自己的编码设置里改,改完记得重启那一路流。
3. 播放端:多端低延迟播放的实现
3.1 延迟对比:HLS、HTTP-FLV、WebRTC 到底差多少
播放协议的选择本质上是在"兼容性"和"延迟"之间做权衡。我把实测数据整理了一下,测试条件是内网千兆、1080P 25 帧、GOP 2 秒:
| 协议 | 典型延迟 | 起播速度 | 浏览器兼容 | 实现复杂度 |
|---|---|---|---|---|
| HLS | 6~30 秒 | 慢,要等切片 | 全兼容,iOS 原生支持 | 最低 |
| HTTP-FLV | 1~3 秒 | 快,秒开 | 需 JS 解码库 | 低 |
| WebRTC | 0.2~0.8 秒 | 快 | 现代浏览器支持 | 中 |
| RTSP 直连 | 0.5~1 秒 | 快 | 浏览器不支持,需插件 | 高 |
HLS 延迟大的原因不是编码慢,而是它的工作方式:服务端把流切成一个个 ts 文件(通常 2 到 6 秒一个),播放器要先下载到几个片段才能开始播,还要在缓冲区里垫上几个片段防止卡顿,两者相加就是十几秒。这个延迟是机制决定的,不是你调参数能调下来的。所以 HLS 我只用在一种场景:不需要实时性、但要最大兼容性的地方,比如给家里人用手机看回放。
HTTP-FLV 是我最常用的方案。它把流封装成一个不断增长的长连接,播放器收到就能解,延迟天然就在 1 到 3 秒。浏览器不原生支持 flv,需要一个 JS 解码库把它解成 MSE 可用的数据喂给 video 标签,这个库在开源社区很成熟,一个文件引进来就能用。
WebRTC 延迟最低,但坑也最多:需要额外配置候选地址、处理 NAT 穿透、部分网络环境下建连失败。如果你对延迟的要求是"能看清门口是谁",HTTP-FLV 完全够;如果是"我要跟门口的人说话",那才值得上 WebRTC。
3.2 网页播放器落地:一份能直接跑的播放页
播放页我尽量做得简单,一个 video 标签加一个播放器初始化,不要框架、不要打包,一个 html 文件丢到静态目录就能访问。下面这份是 HTTP-FLV 版本,把播放库的文件放在本地 static 目录下,不依赖外网:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>监控预览</title> <style> body { margin: 16px; background: #111; } .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } video { width: 100%; background: #000; } </style> </head> <body> <div class="grid"> <video id="v1" muted autoplay playsinline></video> <video id="v2" muted autoplay playsinline></video> </div> <script src="/static/mpegts.js"></script> <script> function play(id, url) { var el = document.getElementById(id); if (!mpegts.isSupported()) { console.warn('当前浏览器不支持 MSE'); return; } var player = mpegts.createPlayer({ type: 'flv', isLive: true, url: url }, { enableStashBuffer: false, // 关闭缓冲区,降低延迟 stashInitialSize: 128, liveBufferLatencyChasing: true, // 追帧,防止越看越慢 liveBufferLatencyMaxLatency: 3.0, liveBufferLatencyMinRemain: 0.8 }); player.attachMediaElement(el); player.load(); player.play(); // 断流自动重连 player.on(mpegts.Events.ERROR, function () { setTimeout(function () { play(id, url); }, 3000); }); } play('v1', 'http://192.168.1.10:8888/cam1/index.m3u8'.replace('index.m3u8', '') + 'cam1.flv'); play('v2', 'http://192.168.1.10:8080/live/cam2.flv'); </script> </body> </html>几个参数值得单独说。enableStashBuffer: false是降延迟的关键,它关掉了播放器的"囤货"行为,不囤就意味着延迟不会越积越多,代价是网络一抖可能会轻微卡顿。liveBufferLatencyChasing是追帧开关,长时间播放之后播放器会慢慢落后于直播点,这个开关会在延迟超过阈值时快进追上。liveBufferLatencyMaxLatency: 3.0意思是最多容忍 3 秒落后,超过就追。这三个参数配合起来,实测能稳定在 1.2 到 2 秒之间。
注意:
muted和playsinline这两个属性在移动端非常关键。不加muted,浏览器会因为自动播放策略直接拒绝播放;不加playsinline,iOS 上会强制全屏。这两个坑我都踩过,页面一片黑以为是流的问题,其实是被浏览器拦了。
3.3 移动端与外网访问的现实约束
手机上观看要解决两个问题:一是解码兼容性,二是网络可达性。兼容性方面,上面这套 flv 方案在安卓的现代浏览器里没问题,iOS 上因为不支持 MSE,得退回 HLS。所以我的做法是同一路流同时开 FLV 和 HLS 两个输出,网页端做一次能力检测,支持 MSE 就用 FLV,不支持就用 HLS,用户无感切换。
网络可达性就麻烦一些。我不推荐把内网端口直接暴露到公网,主要是两个原因:一是监控流被陌生地址扫到就是纯粹的隐私泄露,二是家用宽带的上行本来就小,多人同时看会互相拖垮。比较稳妥的做法是在内网做一层访问入口,用身份校验挡住未授权请求,再通过一个受控通道进去;或者干脆做成"事件触发"模式——手机上收到告警推送,点开再看那一段录像,而不是一直挂着实时流。后者对带宽和安全性都更友好,也是我现在给朋友配的默认方式。
还有一个细节:移动端观看时把分辨率切到子码流。主码流 1080P 在 6 寸屏上完全看不出差别,但流量和延迟差好几倍。子码流 640×360、512 kbps,手机 4G 下也能流畅看。这个"主码流录像、子码流预览"的分工,是低成本方案里性价比最高的一条实践。
4. 录像与存储:24 小时不间断写入怎么不炸
4.1 录像策略与存储容量计算
录像这块最容易出的问题不是"录不下来",而是"录了几天磁盘满了,然后整条链路全崩"。所以第一步是算清楚容量,第二步是设计好满载之后的行为。
容量公式其实就一行:单路每天容量(GB)= 码率(Mbps)× 3600 × 24 ÷ 8 ÷ 1000。以 2 Mbps 算,2×3600×24÷8÷1000 = 21.6 GB。4 路 30 天,就是 21.6×4×30 = 2592 GB,约 2.6 TB。再留 15% 的空间给文件系统开销、索引、系统本身,实际配盘建议 3 TB 起步。这个余量不能省,因为写盘速度会随磁盘占用率上升而下降,盘越满,写入越慢,越容易丢帧。
我要强调一个很多人忽略的点:录像不要用转码。ffmpeg 拉流之后加-c copy,直接把 H.264 裸流拷进容器,CPU 占用接近于零,画质无损。如果加了转码参数(比如为了统一成某个分辨率),CPU 会瞬间打满,一台小主机最多扛一两路,而且画质还要再损失一次。监控场景下原始画质已经很一般了,二次压缩基本等于看不清人脸。所以除非源流是 H.265 而你的播放器不支持,否则永远用-c copy。
策略上我分三层:主码流全时录像用来事后查证;移动侦测片段由摄像头自己产生,用来快速定位"什么时候有人来过";关键帧定时抓图每 5 分钟存一张,用来做时间轴缩略图,回放时一眼就能看到哪一段有变化。这三层加起来,检索效率比单纯翻录像高一个数量级。
4.2 分段、循环覆盖与文件索引
录像文件千万不能录成一个巨大的文件。原因很简单:一个 24 小时的 mp4,一旦录制过程中断电,整个文件的索引(moov box)没写进去,这个文件就彻底报废了,前面录的全白费。所以必须分段。
我用的命令是这样的:
ffmpeg -hide_banner -loglevel warning \ -rtsp_transport tcp \ -i "rtsp://user:pass@192.168.1.64:554/stream1" \ -c copy \ -f segment \ -segment_time 300 \ -segment_format matroska \ -reset_timestamps 1 \ -strftime 1 \ "/data/rec/cam1/%Y%m%d_%H%M%S.mkv"逐个参数解释一下为什么这么配。-segment_time 300是每 5 分钟切一个文件,这个粒度是我试出来的平衡点:切太碎(比如 30 秒)会产生大量小文件,目录里几十万个文件,检索和备份都会变慢;切太大(比如 1 小时)断电损失就大,而且定位精度差。5 分钟一个文件,4 路一天是 1152 个文件,数量和精度都合适。
-segment_format matroska是重点。mkv 格式对断电的容忍度远高于 mp4,因为它不依赖一个集中的索引结构,即使写入过程被打断,已经写进去的数据仍然可以播放。我最早用 mp4,一次意外断电之后发现最后那个文件打不开,用修复工具也救不回来;换成 mkv 之后再遇到同样情况,最多丢最后几秒。如果必须用 mp4(比如某些播放器只认 mp4),那一定要加-movflags +frag_keyframe+empty_moov做分片 mp4,让索引分散写入。
-reset_timestamps 1是让每个分段文件的时间戳从零开始。不加这个参数的话,每个文件的起始时间戳是连续的(第二个文件从 300 秒开始),大多数播放器打开会显示"时长异常"或者直接拖不动进度条。
-strftime 1让文件名按时间格式化,%Y%m%d_%H%M%S就是20240512_143000.mkv这种。文件名带时间戳的好处是不依赖任何数据库就能按时间找文件,出问题时直接ls一下就知道哪一段缺了。
循环覆盖我不用 ffmpeg 自带的-segment_wrap,因为它是环形覆盖,文件名会重复使用,可能导致新旧文件混在一起。我的做法是单独跑一个清理脚本,每 10 分钟扫一次,删掉超过保留天数的文件:
#!/bin/bash # 保留 30 天,删除更早的录像 find /data/rec -type f -name "*.mkv" -mtime +30 -delete # 顺手清掉空目录 find /data/rec -type d -empty -delete这个脚本要配合 cron 或者 systemd timer 跑,而且必须加-delete之前在测试目录里验证一遍,我自己就写错过路径前缀,差点把整个挂载点下的东西删干净。所以脚本里我强制写死绝对路径,不加任何变量。
索引这块,我用一个轻量的 SQLite 表记录每个文件的开始时间、结束时间、路数、大小,回放页面查表就能列出来:
CREATE TABLE rec ( id INTEGER PRIMARY KEY, cam TEXT NOT NULL, path TEXT NOT NULL, start_ts INTEGER NOT NULL, end_ts INTEGER NOT NULL, size INTEGER ); CREATE INDEX idx_cam_ts ON rec(cam, start_ts);入库的动作放在文件写完之后由一个小脚本做,扫目录比对,没入库的补进去。这个表很小,几百万条也就百来兆,查询基本是毫秒级。
4.3 回放与检索:怎么在几个小时里找到那 30 秒
回放的体验决定了这套系统有没有被真正用起来。我见过很多自建方案,录像存得好好的,但是要找"昨天下午三点左右门口那个人",只能在文件夹里一层层翻,翻到崩溃。所以检索一定要做。
我的回放页给三个入口。按时间轴:页面上一条横条,有录像的时段标成绿色,缺录像的时段标红,点击跳到对应时间。这个"缺录像标红"特别有用,一眼就能看出哪段时间链路出过问题。按事件:摄像头的移动侦测会推事件,事件时间和录像对上,点一下直接播那 30 秒前后。按关键帧缩略图:每 5 分钟一张图,铺成一个网格,看到有变化的再点进去。
播放端用 video 标签直接播 mkv 就够了吗?不一定,浏览器对 mkv 的支持并不统一。稳妥做法是回放时用 ffmpeg 做一次实时的按需转封装,把 mkv 转成浏览器能吃的 fmp4 或者直接转成 HLS,边转边播,不落地:
ffmpeg -ss 100 -i input.mkv -t 60 -c copy -movflags frag_keyframe+empty_moov -f mp4 pipe:1-ss放在-i前面是快速定位(seek 到关键帧),比放在后面快几十倍,代价是定位点会往前对齐到最近的关键帧,最多差 2 秒。这个精度对监控回放完全够,但速度体验差得非常明显——同样定位到一小时后的某个点,前置 seek 是毫秒级,后置 seek 要把前面一小时全解一遍。
5. 常见问题与排查实录
5.1 卡顿、花屏、延迟高:按这个顺序查
画面出问题的时候,最容易犯的错是直接去改播放器参数,改了半天发现是网线松了。我总结了一个固定的排查顺序,从下往上查,每一步都能明确排除一块。
第一步查取流。在服务器上直接ffplay -rtsp_transport tcp "rtsp://..."播一下。如果这里就花屏,问题在摄像头到服务器这段网络,或者摄像头编码本身有问题,跟后面的推流、播放没关系。花屏十次里有八次是 UDP 丢包,加-rtsp_transport tcp能解决大半。
第二步查转发。用ffplay播服务端的 flv 或者 hls 地址。如果源流正常但转发出来卡,去服务端日志里看有没有"连接超时""读取错误"的记录,通常是服务端到摄像头的连接不稳定,或者服务端的出口带宽被打满了。
第三步查播放。前两步都正常,播放端还是卡,那就看浏览器。按 F12 打开开发者工具,看 Network 里那条 flv 请求的下载速度,如果持续低于码率,就是带宽不够;如果速度正常但画面卡,那是解码性能问题,把硬件加速打开或者改用子码流。
延迟越看越大是另一个常见症状。原因是播放器的缓冲区在慢慢囤货,网络稍微慢一点它就多囤一点,永远不会主动吐出来。解决办法就是前面说的liveBufferLatencyChasing加enableStashBuffer: false,让播放器主动追帧。这里有个反直觉的点:关掉缓冲反而更流畅,因为延迟低了,缓冲区不会累积到触发播放器的保护性暂停。
5.2 录像丢失与磁盘写满:隐蔽的坑
录像最常见的三个故障,我按出现频率排一下。
磁盘写满导致整条链路崩掉。这是最致命的,因为一旦磁盘 100%,ffmpeg 写不进去会直接退出,服务端转发还在跑,你看着画面一切正常,其实录像早就停了。所以一定要做两件事:一是清理脚本按天数删,不能等满了再删;二是加一个磁盘水位监控,超过 85% 就告警,超过 95% 就主动停掉录像并记录日志。监控脚本很简单:
#!/bin/bash THRESHOLD=85 USED=$(df /data | awk 'NR==2 {gsub("%","",$5); print $5}') if [ "$USED" -ge "$THRESHOLD" ]; then echo "磁盘使用率 ${USED}%,超过阈值" | logger -t rec-monitor fi录像文件时长对不上。明明设了 300 秒一段,结果有的文件只有 20 秒,有的有 900 秒。这个现象通常是因为源流的 GOP 太长——分段只能在关键帧处切,如果 GOP 是 10 秒,那分段边界就会在 10 秒的整数倍上浮动,噪点大的时候画质波动会让关键帧间隔变得不均匀。把摄像头的 GOP 固定成 2 秒(I 帧间隔 50),分段时长立刻就稳定了。
断电重启后录像进程没起来。手动跑没问题,重启就没了,八成是没配 systemd 自启,或者配了但启动顺序不对(网络还没就绪就去连摄像头,连不上就退出了)。解决办法是在 unit 里加Restart=always和RestartSec=10,再加一个After=network-online.target。这样即使第一次连不上,10 秒后也会重试,重试几次总能连上。
5.3 常见问题速查表
把上面这些和另外几个高频问题整理成表,方便对着查:
| 现象 | 最可能原因 | 快速验证 | 处理方式 |
|---|---|---|---|
| 画面花屏、绿块 | RTSP 走 UDP 丢包 | 换 TCP 再播 | 加-rtsp_transport tcp |
| 延迟越看越大 | 播放器缓冲区累积 | 看延迟是否单调上升 | 关 stash、开追帧 |
| 起播要等十几秒 | 用了 HLS | 换 FLV 地址试 | 切 HTTP-FLV |
| 单路录像突然中断 | 磁盘写满或进程退出 | df -h、systemctl status | 清理脚本+自动重启 |
| 分段文件时长乱跳 | 源流 GOP 过长 | 看关键帧间隔 | 摄像头设 GOP 2 秒 |
| 分段文件打不开 | 容器用 mp4 且写入中断 | 换 mkv 复现 | 改用 matroska |
| 手机上看不了 | iOS 不支持 MSE | 换 HLS 地址 | 双协议输出+能力检测 |
| 回放拖动很慢 | seek 位置放在-i后 | 对比前置 seek | -ss放到-i前 |
| CPU 占用异常高 | 误加了转码参数 | 看命令里有没有-c copy | 强制转封装 |
| 外网看卡顿 | 上行带宽不足 | 测上行 | 用子码流预览 |
这张表里我标红的其实就三条:TCP 取流、mkv 分段、-c copy转封装。这三条守住了,80% 的坑就绕过去了。
6. 稳定性与运维:让系统自己扛住
6.1 进程守护与自恢复:把"手动重启"变成历史
自建系统最大的心理负担是"我不在家的时候它挂了怎么办"。解决办法不是天天盯着,而是让每个组件都能自己活过来。这套系统里需要守护的进程有三个:流媒体服务、每路一个的录像 ffmpeg、清理脚本的定时任务。前两个用 systemd 托管,最后一个用 timer。
录像进程的 unit 大概这样:
[Unit] Description=Camera 1 Recorder After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/bin/ffmpeg -hide_banner -loglevel warning \ -rtsp_transport tcp \ -i "rtsp://user:pass@192.168.1.64:554/stream1" \ -c copy -f segment -segment_time 300 \ -segment_format matroska -reset_timestamps 1 -strftime 1 \ /data/rec/cam1/%Y%m%d_%H%M%S.mkv Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target关键在Restart=always和RestartSec=10。前者保证无论什么原因退出都会重拉,后者给摄像头一点恢复时间,避免疯狂重连把设备打挂。我一开始设成 1 秒,结果摄像头那边连接数被刷满,反而要重启摄像头才能恢复,这个教训很值。
光有自动重启还不够,有一种情况是"进程活着但没在干活"——ffmpeg 卡在某个状态,没退出也没写文件。针对这种僵尸状态,我加了个文件活跃度检查:每 15 分钟看一次最新的录像文件修改时间,如果超过 15 分钟没更新,就强制重启这个 unit。这个检查用一个简短的脚本配合 timer 就行:
#!/bin/bash CAM=$1 DIR="/data/rec/$CAM" LATEST=$(find "$DIR" -type f -name "*.mkv" -printf '%T@\n' | sort -n | tail -1 | cut -d. -f1) NOW=$(date +%s) if [ -z "$LATEST" ] || [ $((NOW - LATEST)) -gt 900 ]; then systemctl restart "rec-$CAM.service" logger -t rec-watchdog "重启 $CAM 录像进程" fi这套"自动重启 + 活跃度看门狗"的组合,把我从每天查一遍的状态里解放出来了。跑了半年,实际需要人工介入的次数是个位数。
6.2 账号、权限与访问控制:低成本不等于不设防
监控流里全是家里的画面,权限这块不能省。最基础的三件事:摄像头改默认密码、流媒体服务开启鉴权、录像目录权限收紧。第一件事最容易被忽略,很多摄像头的出厂密码是弱口令,在同一网段里非常容易被扫到。第二件事在流媒体服务的配置里开用户名密码,播放地址带上凭据,虽然简单但能挡住绝大多数误访问。第三件事是把录像目录的属主设成运行服务的那个专用账号,权限 750,别的用户读不到。
再往上一层是网络隔离。我的做法是把摄像头全部放在一个单独的网段,这个网段不允许访问外网,只允许服务端去访问它们。这样即使某个摄像头有漏洞,它能做的也有限。这个隔离在普通路由器上就能配,不需要专门设备,成本为零但收益很大。
还有一点是日志留存。谁在什么时间看了哪一路,这个记录下来,出问题时能追溯。不用搞多复杂,服务端本身就打印访问日志,落盘保留 30 天就行。
6.3 成本清单与后续扩展方向
最后把整套东西的成本摊开算一下,给个真实参考。硬件部分:4 路入门级网络摄像头(支持 RTSP 即可)、一台低功耗小主机(二手的就行)、4TB 监控级硬盘一块、一个千兆交换机、若干网线和电源。软件部分全部是开源的,零成本。整体下来硬件支出可控,比成品方案低不少,而且没有后续的授权费用。
| 项目 | 数量 | 说明 |
|---|---|---|
| 网络摄像头 | 4 | 只需支持标准 RTSP 输出 |
| 低功耗主机 | 1 | 4 核以上、内存 4GB 足够 |
| 监控级硬盘 | 1 | 4TB,按 2.6TB 需求留余量 |
| 千兆交换机 | 1 | 内网转发带宽要够 |
| 软件 | 全部 | 开源组件,零授权费用 |
扩展方向有三个我认为值得做。一是加一路音频,很多摄像头支持音频输入,加进来之后门口有人说话也能录到,对讲场景很有用。二是做告警联动,把移动侦测事件推到手机,比一直挂着实时流省心得多。三是异地备份关键片段,把每天的事件片段同步到另一块盘或者另一个位置,防止整机故障导致数据全丢。这三个都不需要换硬件,现有机型上改配置就行。
我个人在实际操作中的体会是,自建监控系统最容易高估的是"功能需求",最容易低估的是"存储和网络"。真正让人夜里睡不着的从来不是少了什么花哨功能,而是某天要用录像的时候发现那一段是空的。所以我的建议是:先把推流、录像、自动清理这三条链路跑通,让它们稳定跑上一周,再回头去做播放页的美化和远程访问。基础链路稳了,上面加什么都是锦上添花;基础链路不稳,加再多功能也是空中楼阁。