# StreamSnap **Repository Path**: wangwuli/stream-snap ## Basic Information - **Project Name**: StreamSnap - **Description**: 摄像头批量截图管理系统。支持 RTSP / HTTP(ISAPI) 多路摄像头按策略定时抓拍,提供 Web 管理界面、实时预览、协议互转、运行监控。镜像内置 jellyfin-ffmpeg(含 NVDEC 硬解),开箱即用。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-21 - **Last Updated**: 2026-07-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # StreamSnap 摄像头批量截图管理系统。支持 RTSP / HTTP(ISAPI)多路摄像头按策略定时抓拍,提供 Web 管理界面、实时预览、协议互转、运行监控。 ## 界面展示 ![StreamSnap 界面](show/界面.png) ## 功能特性 - **三种抓取模式**,可按路独立配置(混合共存): - `continuous` 连续拉流:常驻 ffmpeg 解码,适合短间隔/需预览 - `on_demand` 按需单帧:每次到点起短命 ffmpeg 抓 1 个 I 帧,适合长间隔/高并发 - `isapi` ISAPI 抓拍:HTTP 单次请求拿 JPEG,不拉流不解码,最轻量(海康等支持 ISAPI 的摄像头) - **协议互转**:RTSP ⇄ HTTPAPI 批量转换(自动提取通道号、保留 IP、统一账密/端口/路径) - **per-stream 策略**:每路流可独立设抓取模式/地址/间隔,支持单路编辑 + 批量修改 - **配置与生效分离**:全局配置保存不中断运行,独立"应用配置"按钮重启生效 - **运行监控**:近 1 分钟成功/空帧计数、最后成功时间、抓取状态,可排序 - **健康检查**:策略与地址协议不匹配时提示(isapi 需 http、continuous/on_demand 需 rtsp) - **自动重连**:连续拉流模式下 ffmpeg 退出/网络瞬断自动退避重连,不丢流 - **去重落盘**:同一 I 帧不重复保存,避免冗余文件和虚高成功计数 - **网络扫描**:批量扫描网段发现摄像头并导入 ## 抓取模式对比 | 模式 | 协议 | 原理 | 适合场景 | 资源占用 | |---|---|---|---|---| | continuous | RTSP | 常驻 ffmpeg 解码填 buffer,定时取帧 | 短间隔(<5s)、需预览 | 中(常驻流) | | on_demand | RTSP | 每次到点起 ffmpeg 抓 1 个 I 帧 | 长间隔、高并发稳定快照 | 低 | | isapi | HTTP | 每次到点发 HTTP 请求拿 JPEG | 超多路、长间隔、支持 ISAPI | 最低 | ## 架构 前端 React(Ant Design)通过 Flask API 与后端交互。Flask 提供流 CRUD、策略配置、协议互转、settings 与 apply 等接口;StreamManager 负责配置持久化、滑动窗口统计与 per-stream 策略调度,每路流各起一个子进程(ProcessGrabber)执行抓取,三种模式分别对应常驻 ffmpeg(continuous)、短命 ffmpeg(on_demand)和 requests HTTP(isapi)。 - **后端**:Python Flask + OpenCV + ffmpeg/ffprobe + requests,多进程(`multiprocessing`)隔离每路流 - **前端**:React + Ant Design + Vite,3 秒轮询刷新状态 - **解码**:ffmpeg 子进程(`-skip_frame nokey` 只解 I 帧,极低 CPU),支持 NVDEC 硬解 - **部署**:Docker(基于 NVIDIA CUDA 镜像 + jellyfin-ffmpeg) ## 快速开始(Docker) 镜像已内置 jellyfin-ffmpeg(含 NVDEC 硬解)与前端构建产物,开箱即用。 ```bash docker run -d --name streamsnap -p 5000:5000 \ -v $(pwd)/data:/app/data \ -v $(pwd)/images:/image_new \ -e APP_USERNAME=admin \ -e APP_PASSWORD=admin \ wangwuli/streamsnap:0.0.1 ``` 访问 `http://localhost:5000`,默认账号 `admin/admin`。 ### 环境变量 | 变量 | 默认值 | 说明 | |---|---|---| | `APP_USERNAME` | `admin` | Web 登录用户名 | | `APP_PASSWORD` | `admin` | Web 登录密码,生产环境务必修改 | | `TZ` | `Asia/Shanghai` | 时区,影响 tracking 模式下截图文件名的时间戳 | | `STREAMS_CONFIG_FILE` | `/app/data/streams_config.json` | 配置文件路径,默认在 /app/data 下;持久化只需挂载 /app/data 卷,无需改本项 | | `FFMPEG_PATH` | `/usr/lib/jellyfin-ffmpeg/ffmpeg` | ffmpeg 可执行文件路径,一般无需改 | | `FFPROBE_PATH` | `/usr/lib/jellyfin-ffmpeg/ffprobe` | ffprobe 可执行文件路径,一般无需改 | ### 端口与卷 - `-p 5000:5000`:容器内 gunicorn 监听 5000,按需改左侧宿主端口 - `-v $(pwd)/data:/app/data`:配置文件(`streams_config.json`)持久化,务必挂载 - `-v $(pwd)/images:/image_new`:截图输出目录(`save_path` 默认 `/image_new`),务必挂载,否则容器重建后图片丢失 - `--gpus all`:启用 NVDEC 硬解时附加(需宿主装 NVIDIA 驱动 + nvidia-container-toolkit);同时需在全局配置里把 `hw_accel` 设为 `true` ### 截图保存目录 截图输出目录由全局配置 `save_path` 决定(**不是环境变量**),默认 `/image_new`。该路径不在 `/app/data` 卷内,启动时必须单独挂载,否则截图写进容器可写层,容器删除即丢失: ```bash -v $(pwd)/images:/image_new ``` 如需指向其他宿主目录(更大磁盘 / 已有采集目录),把宿主目录挂到容器内某路径,再在 Web「设置」里把 `save_path` 改成该路径并「应用配置」生效(等价于 `POST /api/settings` 后再 `POST /api/settings/apply`)。 > 想用内存盘加速可挂 `-v /dev/shm/image_new:/image_new`,但 `/dev/shm` 是 tmpfs,Docker 默认仅 64MB 且容器重启即失。若用它存图务必加 `--shm-size=2g` 扩容,且只适合临时/高速缓存;要长期持久化请用真实宿主路径。 ### 完整示例(自定义账号 + 硬解) ```bash docker run -d --name streamsnap -p 5000:5000 \ --gpus all \ -v $(pwd)/data:/app/data \ -v $(pwd)/images:/image_new \ -e APP_USERNAME=admin \ -e APP_PASSWORD=your-strong-password \ -e TZ=Asia/Shanghai \ wangwuli/streamsnap:0.0.1 ``` ## 配置 配置文件 `backend/streams_config.json`(启动自动创建/补全默认值/迁移旧格式): ```jsonc { "global_settings": { "save_path": "/image_new", "capture_mode": "single", // single=覆盖同名 / tracking=按IP分目录带时间戳 "capture_strategy": "continuous", // 新建流的默认策略 "grab_timeout_seconds": 15, "hw_accel": false, "continuous": { "iframe_only": true, "decode_fps": 1, "frame_buffer_size": 3, "frame_offset_on_save": 0 }, "on_demand": { "max_retries": 2 }, "isapi": { "auth_mode": "digest", "max_retries": 3 } }, "streams": { "": { "url": "rtsp://admin:pass@192.168.1.64:554/Streaming/Channels/101", "interval": 5.0, "capture_strategy": "continuous", // per-stream,可覆盖全局默认 "enabled": true } } } ``` ### URL 格式 | 模式 | url 示例 | |---|---| | continuous / on_demand | `rtsp://admin:pass@192.168.1.64:554/Streaming/Channels/101` | | isapi | `http://admin:pass@192.168.1.64:80/ISAPI/Streaming/channels/101/picture` | > 协议互转按钮可批量在两种格式间转换,IP 保持各路原值。 ## 主要 API | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/streams` | 流列表(含状态/统计) | | POST | `/api/streams` | 添加流(可指定策略) | | PUT | `/api/streams/` | 单路更新(url/interval/strategy) | | DELETE | `/api/streams/` | 删除流 | | POST | `/api/streams//start` `/stop` | 启停单路 | | POST | `/api/streams/batch_interval` `/batch_strategy` | 批量改间隔/策略 | | POST | `/api/streams/convert_isapi` `/convert_rtsp` | 协议互转 | | GET | `/api/streams//preview` | 实时预览帧 | | GET/POST | `/api/settings` | 全局配置(保存不生效) | | POST | `/api/settings/apply` | 应用配置(重启运行中流) | | GET | `/api/scan_stream` | 网段扫描 | ## 目录结构 ``` . ├── backend/ │ ├── app.py # Flask 路由 / API │ ├── manager.py # StreamManager:配置/统计/调度/转换 │ ├── stream_handler.py # RTSPAutoGrabber:三种抓取模式实现 │ ├── stream_worker.py # 子进程入口 │ ├── scanner.py # 网段扫描 │ └── streams_config.json # 配置(运行时生成) ├── frontend/ │ ├── src/App.jsx # 主界面 │ └── vite.config.js ├── data/snapshots/ # 抓拍图片输出 └── Dockerfile ``` ## 技术要点 - **只解 I 帧**:`-skip_frame nokey` + `fps` 滤镜,CPU 极低,画质绝对完美 - **滑动窗口统计**:deque 存近 60s 成功/空帧时间戳,O(1) 读写,不落盘 - **状态三态**:success(存新帧)/ empty(无新帧去重跳过)/ failed(真故障),空帧不误报失败 - **帧去重**:同一 I 帧(timestamp 相同)不重复落盘,`max_frame_age` 超龄则跳过 - **重连退避**:ffmpeg 退出按指数退避(1→30s)重连,网络恢复自愈,不静默卡死 - **配置/生效分离**:保存只写配置,应用才重启,避免改配置中断所有流 ## License MIT