跳过正文
  1. 文章/

摄像头崩溃级联排查:LLM 每日健康检查如何发现一个隐藏的 Frigate Bug

作者
Yang Hu

每日 LLM 健康检查报告显示 10 台摄像头中有 8 台当天崩溃次数达到数百次。 追查下去,根因是两台婴儿监控摄像头、一个 go2rtc 重连窗口,以及一次 vaapi 级联崩溃——没有一个环节是直接显而易见的。以下是完整的排查与修复过程。

问题是如何发现的
#

我在搭建一个每日家庭健康代理——一个定时脚本,查询所有家庭服务(Frigate、Home Assistant、Paperless、arr 媒体栈),然后将数据交给本地 LLM 分析。核心思路是:不再手动逐个检查仪表盘,而是每天早上收到一份摘要,自动标出异常项。

Frigate 的检查项查询 /api/stats,提取每台摄像头的崩溃次数。某天早上,报告返回了这样的数据:

1
2
3
4
5
6
nanit_adelia:   2228 次崩溃
nanit_leonard:  2228 次崩溃
backyard:        847 次崩溃
front_door:      391 次崩溃
side_a:          203 次崩溃
...

如果没有健康检查,我根本不会注意到——Frigate 容器本身从未重启,Web UI 上各摄像头仍然显示"在线",也没有任何告警弹出。

根因:崩溃级联
#

顺着日志往前追,每天上午 9 点的事件链如下:

  1. Cron 停止 nanit 容器(婴儿监控只需要夜间运行)
  2. nanit RTMP 推流断开,go2rtc 失去数据源
  3. go2rtc 在内部保持 nanit RTSP 流存活约 37 分钟(其重连窗口)
  4. 约 09:37,go2rtc 放弃,对两路 nanit 流返回 404 Not Found
  5. Frigate 的 ffmpeg 在 404 上崩溃;watchdog 立即重启 → 约 10 秒一次的紧密崩溃循环
  6. 两路 nanit ffmpeg 进程同时崩溃循环,耗尽 Intel iGPU vaapi 上下文
  7. 其他所有摄像头——共用同一 vaapi 设备——崩溃,报错 Failed to sync surface
  8. 非 nanit 摄像头在 5–6 小时内逐个自我恢复,随着各自 ffmpeg 进程陆续重启

Frigate 容器本身 0 次重启。所有问题发生在 ffmpeg 子进程层面。崩溃计数是准确的——2228 次大约是 8 小时内每 13 秒一次,与 10 秒的 watchdog 周期完全吻合。

RTMP 架构背景
#

nanit 婴儿监控通过第三方容器(ghcr.io/gregory-m/nanit)与 Nanit 云端认证,并在主机端口 1935 上运行 RTMP 服务端。go2rtc(内嵌于 Frigate)作为客户端从中拉流:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Nanit 摄像头硬件
      │  (专有协议 → 云端认证)
 nanit 容器      ← RTMP 服务端,监听 :1935
   go2rtc        ← 拉取 rtmp://10.0.10.11:1935/local/<uid>
      │  RTSP
Frigate ffmpeg

修复目标:确保 go2rtc 始终有有效的 RTMP 流可以拉取,即使 nanit 容器已停止。

失败的尝试
#

go2rtc ffmpeg: 备用源
#

go2rtc 支持为每个流配置多个源——主源失败时自动切备源。方案:添加一个用 ffmpeg 生成黑屏的备用源。

1
2
3
nanit_adelia:
  - rtmp://10.0.10.11:1935/local/ec06240f
  - "ffmpeg:-re -f lavfi -i color=black:size=640x480:rate=1 -c:v libx264 -preset ultrafast {output}"

问题一: Frigate 会将 {...} 预处理为环境变量模板,{output}Invalid substitution found。用 {{output}} 双花括号转义可绕过。

问题二: go2rtc 的 ffmpeg: 源不会对参数做 shell 分割,整个字符串作为单一 token 传入。结果:Error opening input file -re.

{output}#video 简写方式均告失败。

使用 exec:ffmpegGO2RTC_ALLOW_ARBITRARY_EXEC=true
#

go2rtc 有 exec: 源类型,支持运行任意命令。设置环境变量、配置正确语法后,/dev/shm/go2rtc.yamlexec: 行显示正常——但 ffmpeg 进程从未实际启动。无报错,ps aux 中也看不到进程。原因不明,疑似静默启动失败。

独立占位容器使用不同端口
#

创建 nanit-placeholder 容器,将黑屏帧推送到 go2rtc 的不同路径,再利用 go2rtc 的备用源:主源用 1935,备源用 1936。

致命缺陷: go2rtc 切到备源后,即使主源恢复也不会自动切回,只有当前会话断开才重新尝试。强制切回需要重启 go2rtc 子进程,会导致所有摄像头中断 2–3 秒。不可接受。

占位容器推流到 go2rtc RTMP
#

占位容器尝试通过 frigate_default Docker bridge 网络推流到 rtmp://frigate:1935/...,结果:Connection refused

go2rtc 的 RTMP 端口只能从 network_mode: host 的容器访问(如真实的 nanit 容器),bridge 网络容器无法访问。原因:go2rtc 的 RTMP 端口绑定在主机网卡上,而非 Docker bridge。

修复方案:基于 mediamtx 的同端口切换
#

核心洞察:如果占位服务接管同一个端口(1935),go2rtc 根本察觉不到切换。不需要备用源逻辑,不需要重连触发,不影响其他摄像头。

1
2
3
4
5
6
nanit 容器             ← 19:00–09:00,RTMP 服务端占用主机 :1935
nanit-placeholder      ← 09:00–19:00,mediamtx RTMP 服务端占用主机 :1935
   go2rtc              ← 始终拉 rtmp://10.0.10.11:1935/local/<uid>

占位服务使用 mediamtxbluenviron/mediamtx:latest-ffmpeg),内置 ffmpeg 生成黑屏 H264 帧:

docker-compose.yml

1
2
3
4
5
6
7
8
services:
  nanit-placeholder:
    image: bluenviron/mediamtx:latest-ffmpeg
    container_name: nanit-placeholder
    restart: "no"
    network_mode: host
    volumes:
      - ./mediamtx.yml:/mediamtx.yml

mediamtx.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
rtsp: no
rtmp: yes
rtmpAddress: :1935

paths:
  "local/ec06240f":
    runOnInit: ffmpeg -re -f lavfi -i color=c=black:s=640x480:r=1
      -c:v libx264 -preset ultrafast -profile:v baseline -level 3.0
      -g 1 -pix_fmt yuv420p -f flv rtmp://localhost:1935/local/ec06240f
    runOnInitRestart: yes

  "local/333ea643":
    runOnInit: ffmpeg -re -f lavfi -i color=c=black:s=640x480:r=1
      -c:v libx264 -preset ultrafast -profile:v baseline -level 3.0
      -g 1 -pix_fmt yuv420p -f flv rtmp://localhost:1935/local/333ea643
    runOnInitRestart: yes

几个关键 ffmpeg 参数:

参数原因
-profile:v baseline -level 3.0最大 H264 兼容性,防止 vaapi 解码报错
-g 1每帧都是关键帧,确保流启动干净,RTSP 中继不出现"Invalid data"
不用 -tune stillimage该 tune 产生非标准 H264 结构,约 40 秒后 RTSP 中继断流

yang@debian.lan 的 cron:

1
2
3
4
5
0  9 * * *  docker compose -f /home/yang/docker/nanit/docker-compose.yml stop \
            && docker compose -f /home/yang/docker/nanit-placeholder/docker-compose.yml up -d

0 19 * * *  docker compose -f /home/yang/docker/nanit-placeholder/docker-compose.yml down \
            && docker compose -f /home/yang/docker/nanit/docker-compose.yml up -d

切换过程中有约 2–3 秒断流,go2rtc 重连期间 Frigate 会立刻恢复——这只是短暂的连接中断,而非持续的 404,不会触发 crash loop。

结果
#

修复后,所有摄像头崩溃次数降为零。此前每天上午因 vaapi 级联故障而耗费数小时恢复的非 nanit 摄像头,现在全天稳定运行。

go2rtc 配置保持简洁——每路流只配一个源,无需备用:

1
2
3
4
5
6
go2rtc:
  streams:
    nanit_adelia:
      - rtmp://10.0.10.11:1935/local/ec06240f
    nanit_leonard:
      - rtmp://10.0.10.11:1935/local/333ea643

总结
#

健康检查让这个问题得以曝光。没有每日摘要里的各摄像头崩溃计数,这一切会悄无声息地持续——Frigate 显示绿色,而摄像头的 ffmpeg 进程每天早上循环崩溃数千次,其他摄像头则花半天时间慢慢恢复。

找到根因之后,修复本身并不复杂。难的是走到那一步。