提示

返回博客列表

30 路摄像头怎么录:RTSP 拉流、断线重连、时间戳修正与存储策略

客户那边有 30 路网络摄像头,要求 7×24 录像存档,保留 30 天。

我一开始的想法很简单:30 条 ffmpeg 命令,每条拉一路 RTSP 存文件,完事。

上线第一周就出问题:

  1. 有 4 路画面频繁花屏(UDP 丢包);
  2. 有一路摄像头重启后,ffmpeg 进程挂了就再也没起来(ffmpeg 自己不会重连);
  3. 录出来的文件时长不对(30 分钟的片段,元数据说 2 小时)——摄像头的时间戳乱跳;
  4. 30 路同时跑,把接入交换机的带宽打满,其他业务受影响。

这篇写完整方案:拉流参数、重连机制、时间戳修正、存储策略,以及最后稳定运行半年多的架构。

TL;DR:四个必须做对的点——用 -rtsp_transport tcp(避免 UDP 丢包花屏);ffmpeg 不会自己重连,要靠外层守护(systemd Restart=always 或者 while 循环);摄像头的时间戳经常不可靠,用 -fflags +genpts 和 -use_wallclock_as_timestamps 1 修正;录制用 -c copy 不转码(CPU 几乎为零,代价是文件大,事后转码)。另外:一定要有"流是不是还活着"的监控,否则断了一路你可能一周后才发现。

目录

一、先搞清楚 RTSP 地址怎么拼

不同厂商的 RTSP URL 格式都不一样,这是第一个要花时间的事:

厂商 主码流 子码流
海康 rtsp://user:pass@IP:554/Streaming/Channels/101 .../102
大华 rtsp://user:pass@IP:554/cam/realmonitor?channel=1&subtype=0 subtype=1
宇视 rtsp://user:pass@IP:554/video1 video2
TP-Link rtsp://user:pass@IP:554/stream1 stream2
ONVIF 通用 用 ONVIF 探测 GetStreamUri

建议:如果摄像头支持 ONVIF,用 ONVIF 的 GetStreamUri 自动获取地址,比手工拼可靠:

# python-onvif 或者 onvif-cli
onvif-cli --host 192.168.1.64 --user admin --pass xxx GetStreamUri

先用 ffprobe 验证地址对不对:

ffprobe -v error -rtsp_transport tcp -show_entries stream=codec_name,width,height,r_frame_rate \
  -of default=noprint_wrappers=1 "rtsp://user:pass@192.168.1.64:554/Streaming/Channels/101"
# codec_name=h265
# width=1920
# height=1080
# r_frame_rate=25/1

这一步很重要:它同时告诉你编码格式(决定后面能不能 -c copy)、分辨率、帧率。我见过不少"地址能连上但取到的是子码流(640×360)"的情况——录了半天发现分辨率不对。

二、拉流参数:TCP 还是 UDP

RTSP 的音视频数据走 RTP,可以用 UDP 或 TCP:

方式 优点 缺点
UDP(默认) 延迟低、开销小 丢包就花屏(UDP 不重传)
TCP 不丢包(TCP 重传) 延迟略高、网络差时会卡顿

监控录制场景一律用 TCP——我们要的是"录得完整",不是"延迟低"。那点延迟(几十毫秒)对存档毫无意义。

-rtsp_transport tcp

这就是第一个坑的解法:那 4 路花屏的摄像头,改成 TCP 之后完全正常(因为接入网络有丢包,UDP 丢了关键帧的数据就花屏)。

还要加超时:

-stimeout 5000000        # 单位微秒,这里是 5 秒;连不上就报错退出

(老版本 ffmpeg 用 -stimeout,新版本也可以用 -timeout,注意两者单位不同——-stimeout 是微秒。)

三、ffmpeg 不会重连:外层守护

这是第二个坑,也是最常见的误解:ffmpeg 没有可靠的 RTSP 重连机制。

网上经常能看到这几个参数:

-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 30

它们只对 HTTP/HTTPS 类的协议有效,对 RTSP 基本没用(RTSP 的重连涉及重新 DESCRIBE/SETUP/PLAY,不是简单的"重发请求")。

所以正确的做法是外层守护——让 ffmpeg 挂掉后被重新拉起。

方案 A:systemd(推荐)

# /etc/systemd/system/cam-01.service
[Unit]
Description=Camera 01 recorder
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=camrec
ExecStart=/usr/local/bin/cam_record.sh 01
Restart=always
RestartSec=5
# ffmpeg 收到 SIGTERM 后会优雅退出,给它时间
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

关键三行:

  • Restart=always:进程退出就重启(不管是正常退出还是崩了);
  • RestartSec=5:等 5 秒再重启(避免摄像头还没恢复时疯狂重试);
  • TimeoutStopSec=30:给 ffmpeg 时间优雅退出,保证最后一个片段被正确封装。

30 路就写 30 个 service 文件? 用 systemd 的模板单元:

# /etc/systemd/system/cam@.service
[Unit]
Description=Camera %i recorder
After=network-online.target

[Service]
Type=simple
User=camrec
ExecStart=/usr/local/bin/cam_record.sh %i
Restart=always
RestartSec=5
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target

启动:

systemctl enable --now cam@01 cam@02 cam@03 ... cam@30
# 或者
for i in $(seq -w 1 30); do systemctl enable --now cam@$i; done

方案 B:脚本内循环(更简单)

#!/bin/bash
# cam_record.sh
CAM_ID="$1"
URL=$(get_camera_url "$CAM_ID")
OUTDIR="/data/record/$CAM_ID"
mkdir -p "$OUTDIR"

while true; do
    ffmpeg -hide_banner -loglevel warning \
      -rtsp_transport tcp \
      -stimeout 5000000 \
      -i "$URL" \
      -c copy \
      -f segment -segment_time 1800 -segment_atclocktime 1 \
      -strftime 1 "$OUTDIR/%Y%m%d_%H%M%S.mp4"
    echo "[$(date)] camera $CAM_ID ffmpeg exited ($?), retry in 5s" >&2
    sleep 5
done

while true 循环 + sleep 是最朴素也最可靠的。systemd 的方案更规范(有日志、有状态、能统一管理),脚本方案更简单。

我们最后用的是 systemd 模板 + 脚本(脚本里只跑一次 ffmpeg,重启由 systemd 负责)——职责分离,日志也干净。

四、时间戳:录出来时长不对的元凶

第三个坑,也是最隐蔽的。

现象:录了一个小时的文件,ffprobe 报告时长 3 小时;或者播放器拖动进度条时跳来跳去;或者多个片段拼接后时间戳跳变。

原因:摄像头给的时间戳(PTS/DTS)不可靠。常见问题:

  1. DTS 乱序或不递增(某些摄像头的固件 bug);
  2. 断流重连后时间戳回退(重新从 0 开始或者跳变);
  3. 时间戳单位(time_base)不对;
  4. 摄像头时钟不准,导致时间戳和实际时间差很多。

解决办法(组合使用):

ffmpeg -rtsp_transport tcp -i "$URL" \
  -fflags +genpts+igndts \              # 忽略输入的 DTS,重新生成 PTS
  -use_wallclock_as_timestamps 1 \      # 用"收到数据的墙钟时间"当时间戳
  -avoid_negative_ts make_zero \        # 负时间戳归零
  -c copy -f segment ...
参数 作用 什么时候用
+genpts 重新生成 PTS 时间戳缺失/乱序
+igndts 忽略输入 DTS DTS 不可靠(摄像头常见)
-use_wallclock_as_timestamps 1 用到达时间当时间戳 时间戳跳变/回退严重时
-avoid_negative_ts make_zero 负时间戳归零 录制起点为负

-use_wallclock_as_timestamps 1 是终极手段:它完全抛弃摄像头给的时间戳,用"ffmpeg 收到这一帧的系统时间"。代价是帧率不再精确(因为网络抖动会让到达时间不均匀),但时长一定是准的——对录制存档来说,这个交易很划算。

判断摄像头的时间戳是否可靠:

ffprobe -v error -rtsp_transport tcp -select_streams v:0 -show_frames \
  -show_entries frame=pts_time -of csv=p=0 -read_intervals "%+#20" "$URL" | head -20
# 看 pts_time 是否单调递增、间隔是否均匀

我现在的做法是:先用默认参数录 5 分钟,检查时长对不对;不对再逐步加上面的参数。不同品牌摄像头的表现差异很大,没有通用答案。

五、录制与切片

不要录成一个无限大的文件。 原因:

  • 一个文件坏了,整段丢失;
  • 检索困难(要找某天下午 3 点的内容,得先定位到文件内的位置);
  • 文件太大,后续转码/上传都很麻烦。

用 segment muxer 按时间切片:

-f segment \
-segment_time 1800 \              # 每 30 分钟一片
-segment_atclocktime 1 \          # 对齐到整点/半点(而不是从启动时刻算)
-strftime 1 \                     # 文件名用时间格式
-reset_timestamps 1 \             # 每片时间戳从 0 开始(避免累积误差)
"$OUTDIR/%Y%m%d_%H%M%S.mp4"
  • -segment_atclocktime 1:让切片边界落在整点/半点(配合 -segment_time 1800),而不是"进程启动后 30 分钟"——这样文件命名有规律,检索方便;
  • -reset_timestamps 1:每片重置时间戳,避免长时间录制后的浮点累积误差;
  • -strftime 1:文件名用 20260311_143000.mp4 这种格式。

关键帧对齐:-f segment 默认在关键帧处切。如果摄像头的关键帧间隔很大(比如 10 秒以上),切片时间会有偏差(30 分钟 ± 10 秒)。可以加:

-segment_frames 或 -force_key_frames

但对 -c copy(不重新编码)的模式,无法强制关键帧——只能接受误差,或者用 -segment_time 配合 -break_non_keyframes(会在非关键帧处切,但那一片的开头会花屏一小段)。录制场景我建议接受误差,不要为了精确切片牺牲画面。

六、转不转码:不要实时转码

这是最重要的性能建议。

摄像头输出的已经是 H.264/H.265 码流,直接 -c copy 存下来就行,CPU 占用接近 0。

方案 CPU(30 路) 体积 说明
-c copy ~3% 100% 推荐:原样存
实时转 H.265 ~600%(8 核不够) 60% 不现实
实时降分辨率 ~300% 40% 一般没必要

如果最终要省空间,做法是:"先原样存 → 事后批量转码 → 删除原始文件":

实时录制(-c copy,CPU 几乎为 0)
      ↓
定时任务(凌晨低峰)转码前一天的文件
      ↓
转码成功 → 删原始文件;失败 → 保留原始文件并告警

这样做的好处:

  1. 录制端压力极小(30 路只要很少的 CPU);
  2. 转码可以挑业务低峰、可以用慢 preset(画质好);
  3. 转码失败还能重来(原始文件还在)。

转码阈值:保留 30 天的原始文件不现实(太大),所以我们是当天保留原始、次日转码归档,原始文件保留 3 天作为缓冲。

七、多路并发与带宽

30 路 1080p 摄像头,每路 4 Mbps:

30 × 4 Mbps = 120 Mbps 持续入站带宽

千兆网卡(实际 ~940 Mbps)够用,但要注意:

  1. 摄像头和录制服务器最好在同一个二层网络(跨公网/跨 VPN 录制是另一回事,延迟和丢包风险大很多);
  2. 交换机端口带宽:如果 30 路都走同一个上行口,那个口就是瓶颈;
  3. 磁盘写入带宽:120 Mbps = 15 MB/s 持续写入,机械盘也扛得住,但加上后续的转码读写就吃紧了。

磁盘 IO 规划:

录制写入:15 MB/s(持续)
转码读取 + 写入:峰值可能 200 MB/s(凌晨批量跑)

所以录制盘和转码输出盘最好分开,或者至少用 SSD。

进程数:30 个 ffmpeg 进程(每路一个),每个进程 CPU 占用很低(-c copy 时主要是 IO)。8 核机器绰绰有余。但内存要注意:每个 ffmpeg 进程大约 30~80 MB,30 路就是 1~2.4 GB,加上系统和其他服务,8GB 内存是起步。

八、健康监控:断流怎么发现

这是最容易被忽略、也最容易出事的一环。

ffmpeg 挂了 → systemd 会重启 → 但如果摄像头本身断了(断电、网络故障),ffmpeg 会一直等在那里或者不断重连,从外面看进程还在,实际上什么都没录。

三个必须有的检查:

1. 文件新鲜度检查(最重要)

每个摄像头目录里最新文件的修改时间,如果超过 N 分钟没更新,就是出问题了:

#!/bin/bash
# check_camera.sh
for d in /data/record/*/; do
    cam=$(basename "$d")
    latest=$(find "$d" -name '*.mp4' -printf '%T@\n' 2>/dev/null | sort -n | tail -1)
    if [ -z "$latest" ]; then
        echo "ALERT: $cam 没有任何录像文件"
        continue
    fi
    now=$(date +%s)
    age=$(( now - ${latest%.*} ))
    if [ "$age" -gt 600 ]; then
        echo "ALERT: $cam 最新录像已 $((age/60)) 分钟未更新"
    fi
done

每 5 分钟跑一次,有输出就告警。

2. 服务状态检查

systemctl list-units 'cam@*' --state=failed --no-legend

3. 定期探测流是否可用(更主动)

#!/bin/bash
# probe_camera.sh —— 每 30 分钟探测一次所有摄像头
while read -r id url; do
    if ! ffprobe -v error -rtsp_transport tcp -stimeout 5000000 \
         -show_entries stream=codec_name -of csv=p=0 "$url" >/dev/null 2>&1; then
        echo "ALERT: camera $id 探测失败"
    fi
done < /etc/cameras.list

注意:探测会占用摄像头的一个连接(很多摄像头有并发连接数限制,常见的是 2~5 路)。如果已经有一路在录制,再探测可能超过限制。所以:

  • 探测频率别太高(30 分钟一次够了);
  • 或者用 RTSP OPTIONS/DESCRIBE(ffmpeg 的 -rtsp_flags? 简单办法是 ffprobe -f rtsp 只取头信息就退出)。

我们的最终方案:文件新鲜度检查为主(它最真实——没文件就是没录到),服务状态检查为辅,流探测每天一次(用于发现"进程在但流已断"的僵尸状态)。

九、存储策略与保留期

30 路 × 4 Mbps × 30 天:

4 Mbps × 30 路 = 120 Mbps
120 Mbps × 86400 秒 = 1.24 TB/天
1.24 TB × 30 天 = 37 TB

37 TB 不是小数目,所以存储策略要算清楚:

策略 说明
原始保留期 3 天(缓冲用)
转码后保留期 30 天(合规要求)
转码压缩率 转 H.265 后约 60% → 22 TB
降帧率 监控场景 15fps 足够(很多摄像头本来就设 15fps)

降码率的正确做法(不是压画质,而是从源头):

  1. 用子码流录制(很多场景子码流 640×360 就够用)——存储能省 80%;
  2. 降低帧率(15fps 甚至 10fps)——省 40~50%;
  3. 只在有运动时录制(用 ffmpeg 的 select='gt(scene,0.01)' 或者摄像头的移动侦测)——省得最多,但实现复杂。

我们的实际配置:主码流 1080p@15fps 录制 7 天(高清),子码流 640×360 录制 30 天(长期)。这个组合在合规和成本之间取得了平衡。

清理任务:

# 每天凌晨清理超过保留期的文件
find /data/record -name '*.mp4' -mtime +30 -delete

注意 delete 的性能:一次性删几万个文件会很慢(而且 IO 打满)。用分批 + ionice:

find /data/record -name '*.mp4' -mtime +30 -print0 | \
  ionice -c 3 xargs -0 -n 500 rm -f

十、安全与合规

这块必须说,因为监控录像涉及隐私。

技术安全:

  1. 摄像头不要暴露在公网——RTSP 的默认凭据(admin/12345)是扫描器的重点目标;
  2. 改默认密码,并且用强密码;
  3. 网络隔离:摄像头单独一个 VLAN,只有录制服务器能访问;
  4. 录制服务器的存储目录权限最小化(只有录制用户和授权人员能读);
  5. 录像文件加密(如果存储介质可能丢失)。

合规(不是法律建议,是我的处理原则):

  • 告知义务:公共场所安装监控要有明示标识;
  • 最小必要:不要录不需要的区域(比如对着更衣室、宿舍内部);
  • 保留期:按业务需要和当地规定设定,到期自动删除("能删"和"删了"是两回事,要有自动任务);
  • 访问控制:谁能看录像要有明确授权和审计日志;
  • 不对外传播:录像不能随便转发、不能发到公开平台。

我在交付文档里会明确写一句:本方案仅用于客户自有场所的安全管理,录像数据的访问和保留由客户负责,并要求客户确认已履行告知义务。 这不是推卸责任,是提醒——技术上能做到的事,不代表可以做。

十一、坑清单

  1. 用默认的 UDP 拉流 → 丢包花屏。用 -rtsp_transport tcp。
  2. 以为 -reconnect 对 RTSP 有效 → 无效。靠外层守护(systemd Restart=always)。
  3. Restart=always 但没设 RestartSec → 摄像头没恢复时疯狂重启,日志爆炸。设 5~30 秒。
  4. 不设 TimeoutStopSec → systemd 默认 90 秒后 SIGKILL,最后一个片段可能没封装完。
  5. 时间戳不加修正 → 录出来的时长不对、拼接跳变。用 +genpts+igndts,严重时 -use_wallclock_as_timestamps 1。
  6. 实时转码 → CPU 打满、路数上不去。先 -c copy,事后批量转码。
  7. 录成一个大文件 → 坏了全丢、检索困难。用 segment 切片。
  8. 切片不对齐整点 → 文件名乱、检索麻烦。加 -segment_atclocktime 1。
  9. 没有文件新鲜度监控 → 断了一路一周后才发现。这是最常见的事故。
  10. 探测太频繁 → 超过摄像头的并发连接数限制,导致录制被挤掉。
  11. 摄像头并发连接数限制 → 多路访问同一摄像头会失败。了解设备的限制。
  12. 默认凭据 → 摄像头被入侵。改密码 + 网络隔离。
  13. 存储没算够 → 跑一周发现盘满了,然后录制全部失败。上线前先算:码率 × 路数 × 保留天数。
  14. 清理任务一次性删几万文件 → IO 打满。分批 + ionice -c 3。
  15. 忽略摄像头本身的时钟 → 时间戳错乱。给摄像头配 NTP(很多设备支持)。
  16. 录制服务器单点 → 服务器挂了全部中断。重要场景要有第二台。
  17. 没验证过恢复流程 → 需要调阅录像时才发现文件格式有问题。定期做一次"调阅演练"。

最后说说这个系统的整体体会。

录制系统看起来是"起几个进程"的事,但它的难点全在"长期稳定运行"上:

  • 网络会抖动 → TCP + 重连;
  • 摄像头会重启 → 守护进程;
  • 时间戳会乱 → 参数修正;
  • 磁盘会满 → 容量规划 + 清理;
  • 服务会静默失败 → 新鲜度监控。

这五件事每一件在测试时都不会发生(测试环境网络稳定、摄像头不重启、数据量小、跑几分钟)。所以录制系统的可靠性完全取决于"你有没有主动去想那些不会发生的事"。

我的习惯是:上线前把"如果 X 断了会怎样"列一遍,然后给每一条配一个检测手段。这个清单后来救过我们几次——比如有一次某路摄像头被施工碰断了电源,告警在 11 分钟后就发了,我们当天就恢复了。如果没有新鲜度监控,这可能要等客户需要调阅录像时才发现,那时候已经晚了。

最后一句:监控系统的价值在"需要调阅的那一刻"才体现。平时跑得好好的不代表系统可靠,只有"随时能调出任意一路任意时段的完整录像"才算真的可靠。所以定期做一次调阅演练(随机抽一路、抽一个时间段,看能不能顺利导出),比任何技术指标都更能说明问题。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部