RTSP 多摄像头视觉接入
让 agent 按区域调用摄像头,并把采集到的照片 / 视频转成能直接阅读的内容。
前置认知:图片不需要「上传」
照片与视频帧都用 Read 工具直接读,不需要上传到任何在线模型。 Read 会把 jpg 作为多模态输入放进上下文,没有额外的上传或视觉 API 调用步骤。唯一不能直接读的是 mp4——因此视频必须先抽帧成 jpg 再读。
目录约定
scripts/capture.ps1 逻辑,不含任何设备信息
cameras.json 谁:区域 → RTSP 地址(含凭据,不入库)
config.json 怎么管:时长上限 / 保留天数 / 容量配额 / 抽帧策略
config.example.json 策略模板(首次使用复制为 config.json)
cameras.example.json 清单模板(首次使用复制为 cameras.json)
captures/<区域>/ 产出:照片与视频
_frames/<视频名>/ 产出:抽帧图片(中间产物,自动回收)
工作流
拍照(两步)
1. capture.ps1 -Mode snap -Area 大门 → stdout 单行 JSON,取 file 字段
2. Read <file> → 直接看到画面内容
看视频(推荐 peek,一次拿到视频 + 帧)
1. capture.ps1 -Mode peek -Area 客厅 -Seconds 8
2. 取返回的 frames[].file,Read 其中若干帧
拆分路径(已有视频、或要单独控制时长):
-Mode record 得到 mp4 → -Mode frames -VideoPath <mp4> 得到帧图 → Read。
读帧策略(直接影响成本与效果)
- 先少后多:默认 4~6 帧足以判断「有没有人 / 有没有变化」。需要细节(人脸、车牌、门是否关严)再提高
-Frames重抽。 - 自动帧数:约每 2 秒 1 帧,下限 4、上限 12。20 秒视频 → 10 帧。
- 每帧带
at_sec:据此可说清「第 4.5 秒有人经过」,而非只描述一个画面。 - 多帧无变化 = 静止场景:这本身就是有效结论(「无人员活动」)。
- 抽帧只有画面,没有声音:需要判断人声或异响时,本 skill 不覆盖,需另接语音转写。
命令参考
$S = "<skill-dir>\scripts\capture.ps1"
# 发现:列出所有摄像头及其覆盖范围(agent 决策入口)
powershell -ExecutionPolicy Bypass -File $S -Mode list
powershell -ExecutionPolicy Bypass -File $S -Mode list -Probe # 附带连通性探测
# 拍照
powershell -ExecutionPolicy Bypass -File $S -Mode snap -Area 大门
powershell -ExecutionPolicy Bypass -File $S -Mode snap -Area 门口 # 别名同样命中
powershell -ExecutionPolicy Bypass -File $S -Mode snap -Area all # 全部启用区域各拍一张
# 录像 / 看一眼
powershell -ExecutionPolicy Bypass -File $S -Mode record -Area 客厅 -Seconds 10
powershell -ExecutionPolicy Bypass -File $S -Mode peek -Area 客厅 -Seconds 8 -Frames 6
# 对已有视频抽帧
powershell -ExecutionPolicy Bypass -File $S -Mode frames -VideoPath "D:\clip.mp4"
powershell -ExecutionPolicy Bypass -File $S -Mode frames -VideoPath "D:\clip.mp4" -Frames 10 -MaxWidth 800
# 清理
powershell -ExecutionPolicy Bypass -File $S -Mode prune
| 参数 | 默认 | 说明 |
|---|---|---|
-Mode |
snap |
list 清单 / snap 拍照 / record 录像 / peek 录像+抽帧 / frames 对已有视频抽帧 / prune 仅清理 |
-Area |
cameras.json 的 default |
区域名或别名;all = 全部启用区域(仅 snap) |
-Seconds |
15(peek 未指定时 6) |
录像时长,超 max_clip_seconds 自动截断 |
-OutFile |
自动 | 指定输出路径(该文件不纳入自动清理) |
-ConfigPath |
skill 内 config.json |
指定其他策略文件 |
-CamerasPath |
skill 内 cameras.json |
指定其他摄像头清单 |
-Probe |
关 | 仅 list 有效,附加 TCP 连通性探测 |
-VideoPath |
— | 仅 frames:输入视频绝对路径 |
-Frames |
0 |
期望抽帧数;0 = 按时长自动(约每 2 秒 1 帧,4~12) |
-MaxWidth |
0 |
帧图宽度上限;0 = 用 config.json 的 frame_max_width |
-FramesOut |
_frames/<视频名>/ |
帧图输出目录 |
区域解析顺序:区域名精确匹配 → 别名精确匹配 → 双向模糊包含。匹配不到会报错并列出所有可用区域。
配置字段
完整样例见 cameras.example.json / config.example.json,两者均可直接复制使用。
cameras.json —— 摄像头清单
| 字段 | 说明 |
|---|---|
key(区域名,如 大门) |
同时作为输出子目录名。agent 用这个名字点名调用 |
url |
RTSP 连接串 |
note |
该摄像头覆盖的物理范围。这是 agent 判断「该调哪只眼睛」的依据,务必写具体可见物 |
aliases |
别名 / 俗称。用户说「看看门口」要能命中「大门」 |
enabled |
false = 暂时停用(设备维修)。列表会显示,采集会跳过 |
default(顶层) |
未指定 -Area 时用哪台 |
> note 写得越具体,路由判断越准。「客厅全景,可见沙发、电视柜」远好于「客厅摄像头」。
config.json —— 采集与治理策略
| 字段 | 默认 | 说明 |
|---|---|---|
transport |
tcp |
tcp(丢包少)或 udp |
connect_timeout_us |
8000000 |
ffmpeg 连接超时(微秒) |
output_dir |
空 | 留空 = skill 目录下 captures/ |
subdir_per_area |
true |
按区域分子目录存放 |
ffmpeg_path |
空 | 留空自动探测 |
max_clip_seconds |
20 |
单次录像时长硬上限,超出强制截断 |
retention_days |
7 |
超过该天数的文件优先清理 |
max_total_mb |
2048 |
容量上限(所有区域汇总,不含帧图) |
max_files |
0 |
文件数上限(合计),0 = 不限 |
keep_newest |
2 |
无论如何保留最新 N 个。防误删保护栏,不建议设为 0 |
cleanup_on_capture |
true |
每次采集后自动清理 |
frame_max_width |
1280 |
抽帧后最大宽度;原图更宽时等比缩小,更窄时不放大;0 = 不缩放 |
frames_ttl_hours |
24 |
帧图保留小时数,超时目录在下次抽帧时回收;0 = 不回收 |
> ⚠️ cameras.json 与 config.json 含摄像头凭据,已由 .gitignore 拦截,不要提交。
输出契约(下游业务依赖此结构)
脚本向 stdout 输出单行 JSON,所有文件路径均为绝对路径。
照片(snap)
{ "ok": true, "mode": "snap", "area": "大门",
"file": "C:\\...\\captures\\大门\\snap_20260912_153644.jpg",
"size_mb": 0.31, "managed": true }
录像 / peek
{ "ok": true, "mode": "peek", "area": "客厅",
"file": "C:\\...\\captures\\客厅\\record_20260912_153644.mp4",
"duration_s": 5, "size_mb": 1.71,
"frames": [ { "index": 0, "at_sec": 0, "file": "C:\\...\\_frames\\record_20260912_153644\\f001.jpg", "size_kb": 108.4 },
{ "index": 1, "at_sec": 1.2, "file": "C:\\...\\_frames\\record_20260912_153644\\f002.jpg", "size_kb": 90.4 } ],
"frame_count": 4,
"frame_dir": "C:\\...\\_frames\\record_20260912_153644" }
抽帧(frames)
{ "ok": true, "mode": "frames",
"video": "C:\\...\\record_20260912_153321.mp4",
"duration_s": 6, "interval_s": 1.5, "frame_count": 4,
"frames": [ ... ], "frame_dir": "C:\\...", "scaled": true, "source_width": 2304 }
取用要点
- 交付物路径:照片 / 视频 =
file;帧图 =frames[].file;帧目录 =frame_dir。 managed: true表示纳入自动清理;-OutFile指定的文件为false,永不被自动删除。-Area all返回批量结构,含succeeded/failed/results[];单台失败不中断其余采集,错误记在该台的error。-Mode list返回区域清单,每区域附note/aliases/reachable/files_count/files_mb,URL 自动脱敏(admin:***@)。- 失败时
ok: false并带error(frames模式以退出码 1 结束)。
存储治理
三层递进清理(作用于 captures/,含各区域子目录)
| 顺序 | 触发条件 | 删除顺序 |
|---|---|---|
| 1 | 过期(retention_days) |
最旧优先 |
| 2 | 容量超限(max_total_mb,跨区域汇总) |
最旧优先,删到低于配额 |
| 3 | 数量超限(max_files) |
最旧优先 |
帧图独立回收(作用于 _frames/)
帧图是「阅读」用的中间产物,不参与 max_total_mb 统计,否则配额会被帧图挤占。由 frames_ttl_hours 单独控制,每次抽帧时顺手删除超时目录。
防误删四重保护
- 双重保护栏:
keep_newest保留最新 N 个;本次刚写入的文件另被显式加入保护名单。 - 时长硬上限:
max_clip_seconds在调用 ffmpeg 前截断,单文件不可能无限大。 - 只管自己的产出:仅清理命名符合
^(snap|record)_\d{8}_\d{6}\.(jpg|mp4)$的文件。-OutFile自定义命名的文件不受管理。 - 敏感路径安全阀:
output_dir指向驱动器根、用户主目录或系统目录时,清理跳过并返回skipped: true。
配额超限时的预期行为
当受保护文件本身已超出配额(如配额 2 MB 但单次录像 5 MB),脚本会删光不受保护的旧文件、保留受保护的最近文件,并返回 over_capacity: true。这是刻意取舍:宁可超限报警,也不丢刚写入的数据。此时应调大 max_total_mb 或调小 max_clip_seconds。
> 多区域配额是全局的:某区域高频采集会挤占其他区域的留存空间。这是磁盘总量下的必然取舍。
已知行为与注意事项
- 录像实时阻塞:录 20 秒耗时约 20 秒。命令超时应设为
Seconds + 60秒以上;peek同理(录像 + 抽帧,再留几秒余量)。 -Area all仅支持snap:录像与peek是串行实时操作,批量会长时间占用。多区域录像请逐个调用。- 并发拉流限制:多数 IP 摄像头仅允许 1~2 路同时拉流。若同一台已被 Home Assistant 等占用,可能连不上——改用子码流(如
ch1/sub/av_stream)即可。 - 编码差异:主码流常为 HEVC/H.265。脚本默认转码 H.264+AAC 保证通用播放,录制期 CPU 占用升高。
output_dir务必固定:留空落在 skill 目录captures/。若指向随调用变化的工作目录,清理作用域会漂移失控。- 帧图不长期保存:默认 24 小时后回收。某帧要长期留档,用
-FramesOut指到持久位置,或自行复制。
修改脚本时必读的踩坑
.ps1必须存为 UTF-8 with BOM。PowerShell 5.1 会把无 BOM 的 UTF-8 当 GBK 读,中文乱码后报的却是字符串缺少终止符、意外的标记这类语法错误——根因是编码,不是语法。OrderedDictionary的PSObject.Properties只返回字典类型自身成员(Count/Keys/Values…),不含业务键。不能用$r.PSObject.Properties.Name -contains $key判断字段存在,否则 JSON 字段会成片丢失。一律用索引器$r["key"]。- 不要用
$args作变量名(PowerShell 自动变量)。参数数组用$ffArgs。 - native 程序 stderr 会触发
NativeCommandError。ffmpeg 的无害警告(HEVCCould not find ref with POC 0)在$ErrorActionPreference = "Stop"下会中断脚本,看起来失败但文件已录好。务必走封装的Invoke-FFmpeg。 Test-NetConnection在本机可能不回显,测端口用System.Net.Sockets.TcpClient(脚本内Test-RtspReachable已封装)。cmd.exe在部分 PowerShell 环境中不可用,用原生语法替代。- 均匀抽帧用
fps=滤镜:-vf "fps=<1/间隔>",间隔 = 时长 ÷ 期望帧数。短视频(≤20 秒)下比场景检测更可预期。 - 缩放只在原图更宽时才做:先 ffprobe 取宽度,
宽度 > MaxWidth才追加scale=$MaxWidth:-2,避免把小图放大成糊图。
改动后的回归清单
- 动清理逻辑 → 回归四种场景:安全阀拦截、时长截断、容量超限且保护最新文件、过期删除(含子目录递归)。
- 动抽帧 → 回归:
frames抽帧、peek组合、-Frames/-MaxWidth参数覆盖、非法视频路径、帧图 TTL 回收。 - 动多区域路由 → 回归:
list清单、-Probe、单区域点名、别名匹配、非法区域报错、-Area all批量、按区域分目录。 - 任何改动后先确认脚本仍为 UTF-8 with BOM。
典型排查顺序
按数据流逐段验证,不要跳步:
ping <摄像头IP>- 测 554 端口(
Test-RtspReachable或 TcpClient 写法) ffprobe -rtsp_transport tcp -timeout 8000000 -i <url>— 流能否解析、分辨率与编码-Mode list -Probe— 确认清单与连通性- 依次执行
snap→record→peek - 抽帧异常时单跑
-Mode frames -VideoPath <已有 mp4>,隔离是采集问题还是抽帧问题 - 磁盘异常时
-Mode prune,观察统计字段