
管理一整套智慧园区的视频接入最怕的不是摄像头坏而是几十路视频流没有统一管理手段。早些年我做园区项目时经常是登录服务器手工敲 FFmpeg 命令一台一台拉流遇到摄像头掉线还得靠业务方反馈“画面黑了”才知道出了问题。后来我把这套流程整理成了批量部署脚本用配置文件维护摄像头清单一条命令启动、一条命令停止、一条命令看状态断线还能自动拉起。这个方法不挑环境只要 Linux 服务器上有 FFmpeg 就能跑特别适合园区弱电运维、安防集成商以及自己搭视频中台或数字孪生底座的开发团队。现在把这个方案完整梳理一遍包括为什么要这么设计、脚本里每个模块怎么实现、实际部署会踩到哪些坑一次性讲清楚。1. 场景认知与方案设计1.1 智慧园区视频接入到底难在哪智慧园区的视频接入跟“在监控室看录像”完全不是一回事。园区建设智慧化系统时视频往往要接到自建的流媒体服务器、三维可视化平台、AI 分析盒子或者对接第三方平台。摄像头分布在园区各个角落品牌也不统一海康、大华、宇视、雄迈都可能出现RTSP 地址格式五花八门码率分辨率也各有差异。最直接的办法是用 FFmpeg 把每台摄像头的 RTSP 流拉过来转成 RTMP 推到流媒体服务或者切成 HLS 给网页端播放再或者落盘录制。这里就暴露出三个典型痛点。第一摄像头数量多手动启动进程根本不现实。二十个摄像头你还能一条条复制命令五六十个的时候光是核对 IP、密码、码流路径就够头疼。第二进程挂在后台之后没人知道它什么时候退出。摄像头偶发断网、流媒体服务器重启、磁盘写满任何一个原因都会让 FFmpeg 进程悄无声息地消失。第三摄像头短时无响应时FFmpeg 进程可能一直卡在 TCP 连接上既不退出也不产出数据看起来还活着实际画面已经全黑。所以我需要的不只是一堆启动命令而是一套“配置化管理工具”所有摄像头信息集中在一个配置文件内启动程序负责把所有流全部拉起停止程序负责安全退出监控程序定期检查运行状态发现掉线就自动拉起。这套思路跟 Kubernetes 管理容器有点像摄像头 RTSP 流就是“工作负载”脚本就是控制面负责维持期望状态。只是我不需要那么重的平台Shell 脚本足够。1.2 为什么最终敲定 FFmpeg Shell 的组合市面上有 Zabbix、Prometheus、夜莺、Grafana 这些监控平台也有专门的视频监控软件但把它们用在“管理 FFmpeg 推流进程”这个场景多少有点杀鸡用牛刀。监控平台解决的是资源指标和告警比如 CPU、内存、磁盘、网络流量但摄像头推流场景的核心监控对象是“进程在不在、链路通不通、数据动不动”这些用脚本就能准确判断。而且大部分园区视频服务器都是内网隔离环境装一套 Prometheus 全家桶还要维护抓取配置对弱电运维而言负担不小。FFmpeg 加 Shell 的优势是它足够轻、足够直白。部署时只要拷贝脚本目录、装好 FFmpeg 就行不依赖数据库、不依赖 Web 服务出问题也能直接看脚本逻辑定位。有人可能会问直接用 Python 写不是更优雅在实际生产环境里Python 版本、第三方库、虚拟环境管理反而比 Shell 更容易出状况。Shell 脚本在绝大多数 Linux 发行版上都能直接执行而且和 systemd、cron 等工具衔接自然哪怕换一台服务器也能快速部署。所以这个方案不是我拍脑袋选的是在多个园区项目里迭代验证过的结果。2. 配置设计与脚本架构2.1 用一份 cameras.conf 管住所有摄像头整套脚本的核心不是代码而是那份摄像头配置文件。我把所有接入信息抽出来放在cameras.conf里每行一台摄像头用竖线分隔字段。这样做的好处是后期新增或者减少摄像头运维人员只需要改这一份文件完全不需要碰脚本代码。配置格式定义如下# 摄像头清单 # 格式: ID|RTSP源地址|输出模式|输出目标|录制时长(秒)|目标分辨率 # 输出模式: rtmp / hls / record / rtmprecord east-gate-01|rtsp://admin:Hik12345192.168.10.21:554/Streaming/Channels/101|rtmprecord|rtmp://192.168.30.5:1935/live/east_gate_01|3600|1080p park-west-02|rtsp://admin:dh123456192.168.10.22:554/cam/realmonitor?channel1subtype0|hls|/var/www/html/live/park_west_02|0|720p building-3f-03|rtsp://user:pass192.168.10.23:554/Streaming/Channels/102|record|/data/records/building_3f_03|7200|原码率每行最后一个分辨率字段可以在 1080p、720p 和“原码率”之间切换。原码率意味着 FFmpeg 直接转封装不做转码CPU 开销极低。如果要限制码率或者统一分辨率就在这里指定。这里要注意一个细节RTSP 地址里如果带着这类字符在配置文件中不要做任何转义脚本读取时会用引号包裹不会触发 Shell 解析问题。我在早期版本里给 URL 加过双引号结果读取变量时嵌套出错反而把地址截断了后来统一改成纯地址格式踩坑才结束。每个摄像头还要有独立 ID也就是配置里的第一列。这个 ID 同时用于 PID 文件命名、日志文件命名、录制文件名前缀所以尽量用语义化名字比如east-gate-01、park-west-02比cam1、cam2好维护得多。2.2 FFmpeg 参数按需拼接转发 / 转码 / 录制摄像头接入之后怎么选择 FFmpeg 参数是整个项目里最需要想清楚的环节。很多初次接触的人容易直接套模板把所有摄像头都转码一遍结果 CPU 跑满画面延迟还大。经验法则是能用 copy 就绝不转码。H.264 编码的摄像头如果目标流媒体服务器支持 H.264直接用-c:v copy转封装即可CPU 占用可以忽略不计。比如海康的 H.264 主码流拉到 SRS 再转推到播放端copy 模式完全足够。但下面这几类场景必须转码一是平台强制要求输出分辨率统一比如所有接入的流必须对齐到 1080p 或 720p二是需要在画面上叠加摄像头名称、时间戳水印这必须通过drawtext滤镜三是摄像头输出的是 H.265 而目标平台或者播放器兼容性不好需要转成 H.264。在做分辨率统一时我通常用这样一个参数组合ffmpeg -hide_banner -nostats -loglevel warning \ -rtsp_transport tcp -stimeout 30000000 \ -i rtsp://... \ -vf scale1920:1080 \ -c:v libx264 -preset veryfast -tune zerolatency -b:v 2500k \ -c:a aac -ar 44100 -ac 1 \ -f flv rtmp://...重点解释几个参数。-rtsp_transport tcp强制走 TCP 传输避免 UDP 在跨网段时丢包花屏。-stimeout 30000000是 RTSP 套接字超时时间单位是微秒30 秒没有数据就自动断开这样摄像头断线时 FFmpeg 进程不至于永远挂死。-preset veryfast和-tune zerolatency是 H.264 编码速度与延迟的平衡直播场景下非常关键。录制场景我推荐用分段封装避免单个文件无限增长ffmpeg -rtsp_transport tcp -i rtsp://... \ -c:v copy -c:a copy \ -f segment -segment_time 3600 -strftime 1 \ /data/records/east-gate-01_%Y%m%d_%H%M%S.mp4分段的好处是磁盘清理策略好写只保留最近 N 天超出部分直接按目录时间删除。如果你用的是-c copy MP4 封装注意 FFmpeg 分段 MP4 时可能会因为时间戳问题生成.tmp文件所以我会优先用 MKV 容器或者干脆用-f segment -segment_format mp4实测下来 MP4 分段在异常断电时更容易损坏。3. 一键启停功能的实现细节3.1 启动流程检查 PID、拼命令、后台拉起整个管理脚本我做成一个cam_manager.sh通过传入 start、stop、status、watchdog 等子命令来操作这样运维只需要记住一个入口。启动函数的设计思路是先从配置文件里读一行判断这个摄像头的 PID 文件是否已存在、对应进程是否还活着如果活着就不用重复启动如果没活就按照前面的规则拼接 FFmpeg 命令用nohup放到后台再把 PID 记录下来。核心代码类似下面这样start_one() { local id$1 src$2 mode$3 target$4 seg$5 res$6 local pid_file$PID_DIR/$id.pid if [ -f $pid_file ]; then local old_pid$(cat $pid_file) if kill -0 $old_pid 2/dev/null; then echo [$id] already running, pid$old_pid return 0 fi fi local cmdffmpeg -hide_banner -nostats -loglevel warning cmd -rtsp_transport tcp -stimeout 30000000 -i \$src\ case $res in 1080p) cmd -vf scale1920:1080 -c:v libx264 -preset veryfast -tune zerolatency -b:v 2500k ;; 720p) cmd -vf scale1280:720 -c:v libx264 -preset veryfast -tune zerolatency -b:v 1500k ;; *) cmd -c:v copy ;; esac cmd -c:a aac -ar 44100 -ac 1 case $mode in rtmp) cmd -f flv \$target\ ;; rtmprecord) cmd -f flv \$target\ cmd -c:v copy -c:a copy -f segment -segment_time ${seg:-3600} -strftime 1 \$REC_DIR/${id}_%Y%m%d_%H%M%S.mp4\ ;; hls) cmd -f hls -hls_time 4 -hls_list_size 6 -hls_flags delete_segments \$target/index.m3u8\ ;; record) cmd -c:v copy -c:a copy -f segment -segment_time ${seg:-3600} -strftime 1 \$REC_DIR/${id}_%Y%m%d_%H%M%S.mp4\ ;; esac nohup bash -c $cmd $LOG_DIR/$id.log 21 echo $! $pid_file sleep 1 if kill -0 $(cat $pid_file) 2/dev/null; then echo [$id] started, pid$(cat $pid_file) else echo [$id] start failed, last log: tail -n 20 $LOG_DIR/$id.log fi }启动后延迟 1 秒检查 PID是为了挡掉“命令拼错、密码不对、RTSP 地址根本不可达”这一类低级错误。如果 FFmpeg 启动瞬间就崩溃1 秒内 PID 就已经消失这时候立刻展示日志尾部比用户事后翻日志高效得多。启动所有摄像头就是一个遍历配置文件的过程start_all() { while IFS| read -r id src mode target seg res; do [ -z $id ] continue start_one $id $src $mode $target $seg $res done (grep -vE ^\s*(#|$) $CONF_FILE) }注意我刻意把配置解析和启动逻辑分开将来如果字段增加只需要改 start_one 和配置说明不需要碰循环逻辑。3.2 停止流程优雅退出比粗暴 kill 重要停止函数看似简单但处理不好会把录制文件搞坏或者留下“僵尸进程”的隐患。最基本的停止逻辑是通过 PID 文件找到进程号先发送SIGTERM然后循环等待进程退出超过时间再发SIGKILL。stop_one() { local id$1 local pid_file$PID_DIR/$id.pid [ -f $pid_file ] || { echo [$id] no pid file; return 0; } local pid$(cat $pid_file 2/dev/null) if [ -n $pid ] kill -0 $pid 2/dev/null; then kill $pid # 最多等 5 秒 for i in $(seq 1 10); do if ! kill -0 $pid 2/dev/null; then break fi sleep 0.5 done if kill -0 $pid 2/dev/null; then echo [$id] process stuck, force kill kill -9 $pid else echo [$id] stopped gracefully fi else echo [$id] not running fi rm -f $pid_file }为什么先SIGTERM而不是直接kill -9因为 FFmpeg 收到 SIGTERM 后会正常关闭输出文件、刷新封装格式保证录制文件可播放。如果你直接强杀录制中的 MP4 文件大概率缺 moov box播放器打开会报错需要额外做索引修复。在实际运行中有些摄像头流因为网络异常处于阻塞状态FFmpeg 收到 SIGTERM 后可能迟迟不退出所以 5 秒超时后的强杀是必要的兜底。另外停止所有进程时我建议不要并发 kill而是逐个处理。虽然并发看起来更快但服务器上同时出现几十个 kill 动作磁盘 IO 和日志写入反而会产生抖动不利于排查问题。4. 运行监控与自动恢复4.1 三层巡检逻辑进程、连通性、日志状态状态监控是这套脚本里价值最高的部分。我的巡检思路分为三层先看进程在不在再看摄像头源站通不通最后看 FFmpeg 日志有没有异常输出。第一层进程检测。通过kill -0 $pid判断 PID 是否存在。这个命令不会给进程发信号只做存在性探测安全且高效。如果 PID 文件不存在或者进程不存在直接判定为 down。第二层网络连通性。RTSP 源站的连通性可以用nc或timeout配合ffprobe探测。我一般优先用 nc 检查摄像头 554 端口是否开放timeout 3 nc -z -w3 192.168.10.21 554 /dev/null 21返回成功表示摄像头在线返回失败则可能是断电、网线松动或者 IP 变更。这一步能区分“FFmpeg 进程还在但摄像头挂了”的场景。第三层日志状态。只要 FFmpeg 进程活着不等于推流正常有些情况下进程没有退出但已经陷入重连循环或者输出的日志里全是错误。我会用tail -n 5提取日志最后几行用关键字匹配error、refused、timeout等异常内容有异常就在状态表里标记出来。状态输出做成表格终端下一目了然printf %-24s %-10s %-10s %-20s %s\n 摄像头ID PID 状态 日志最后时间 备注每一行是一个摄像头的完整状态正常是 running异常会显示具体原因。这个表格我后来还做成了定时巡检截图发给运维群比打开监控平台刷指标直观得多。4.2 看门狗自动重启与告警上报有了 status 巡检逻辑自动恢复就顺理成章了。我会在服务器上放一个 crontab 任务每分钟执行一次 watchdog 子命令* * * * * /opt/cam_manager.sh watchdog /opt/cam_manager/watchdog.log 21watchdog 函数内部会对每台摄像头做一遍 status 检查发现进程不存在或者摄像头端口不可达就自动调用 start_one 重新拉流。这里必须引入一个防抖机制。如果摄像头因为断电持续离线FFmpeg 启动会立刻失败如果不做限制每分钟会被反复拉起一次日志会刷得非常难看还可能把流媒体服务器打到过载。所以我会给每台摄像头加一个重启次数的内存状态短时间内比如 5 分钟超过 3 次就暂停重启等下一个周期再说。实现上可以在/tmp下建一个状态文件记录摄像头ID:时间戳:次数。这个做法的思路是持续失败说明不是“进程崩溃”而是“源端故障”这时候重复拉起没有意义不如报警让人去处理。告警上报可以走 Webhook 或者简单邮件脚本。我在园区项目里用的最多的是企业微信群机器人 Webhookwatchdog 发现连续多次重启失败后POST 一条 JSON 消息到群里值守同事马上能收到通知。Webhook 地址不要硬编码在脚本里单独放一个alert.conf文件方便团队各自维护。5. 实战中的问题排查实录5.1 RTSP 频繁断流不只是网络的问题接入海康、大华摄像头最常遇到的怪现象就是FFmpeg 起来之后能跑几小时也可能几分钟就断日志里出现Connection timed out或者Server returned 5xx。一开始我以为是交换机问题后来排查发现是某些摄像头默认限制单路并发连接数如果有多个人同时在浏览器预览同一路流FFmpeg 的连接会被挤掉。后来我在启动参数里统一加上-stimeout 30000000确保断流时进程能主动退出再配合 watchdog 自动拉起断流影响被控制在 1 分钟内。另外RTSP 地址里如果用了subtype0这种参数部分摄像头固件在 UDP 传输下会非常敏感稍微有丢包就断开。强制-rtsp_transport tcp之后问题基本绝迹。网络质量不好的园区不要纠结 UDP 的实时性稳定压倒一切。5.2 转码性能不够CPU 被 FFmpeg 打满有个园区项目在 ARM 架构的边缘服务器上接了几十路 1080p 摄像头我最初按照固定分辨率转码的思路部署结果 CPU 直接 100%摄像头画面全部卡顿。后来把部署方案改成“原码率转发为主、必要转码为辅”CPU 占用立刻降到 20% 以下。如果必须转码建议优先用硬件编码器。NVIDIA 显卡可以用-c:v h264_nvencIntel 核显可以用-c:v h264_qsvRockchip 平台可以用-c:v h264_v4l2m2m。FFmpeg 里怎么确认有没有对应编码器执行ffmpeg -encoders | grep 264就能看到。实际测试中在 RK3588 上使用硬件编码转 HLS单路负载比软编低一半还多。如果实在没有硬件加速那就降低分辨率而不是降低帧率。720p 转码的 CPU 开销不到 1080p 的一半日常安防监控场景 2~4 Mbps 的 720p 码率完全够看。5.3 PID 文件残留导致认为“一直在线”我在状态巡检里漏过一种情况FFmpeg 进程退出后 PID 文件没有清理status 脚本只查 PID 文件存在就不管进程死活结果把一台已经离线的摄像头标记成 running直到业务侧反馈画面丢失才发现。后来所有状态判断都改成“先读 PID 文件再kill -0验证进程”PID 文件只是辅助定位不能作为判断依据。同时在 start_one 里如果发现 PID 文件存在但进程不存在会自动清理旧 PID 文件再启动避免端口冲突或者状态错乱。在实际脚本里处理 PID 残留还有一个好处当摄像头 IP 被别的服务占用时残留在 PID 文件里的旧进程不会干扰新的 RTSP 连接启动逻辑不会因为“感觉进程存在”而拒绝重新拉流。6. 踩坑后留下的一些个人心得这套脚本我迭代了三个园区项目最大的体会是运维工具的价值不在代码多复杂而在“故障发生时能否快速定位问题”。如果摄像头画面丢了运维第一反应不是去查 VLC 能不能播放而是跑一条 status 命令看到网络连通性那列是 DOWN马上就能知道是摄像头掉线还是推流进程退出排查路径瞬间缩短。另一个经验是配置文件一定要配上字段说明。当时我以为只有自己维护脚本注释写得很随意结果项目交接后半个月新同事把分辨率字段写成了1920x1080而脚本匹配的是1080p整批摄像头都启动了原码率模式。后来我在配置头部加了两行备注说明取值范围类似的低级错误几乎绝迹。最后给一个建议如果是正式项目不要满足于 nohup 这种会话级的进程管理可以把脚本生成的命令改造成 systemd 模板服务配合Restartalways和RestartSec5宿主机重启后摄像头流也能自动恢复。我保留这套 Shell 脚本主要是为了日常运维和设备变更灵活真正要保证开机自启的还是 systemd 更稳妥。