自研视频回放系统:当没有开源方案满足需求时

2026-06-27

家里装了监控摄像头,日常拍摄录像自动保存到 NAS 上。日积月累,盘里堆了 67,000 多段视频、4.5TB 数据,横跨 407 天。

然后问题来了:怎么快速找到想看的那段录像?

你可能会想:装个开源的监控系统不就行了?Frigate?Jellyfin?我都试过。结果分别是:太肥、不对、不合适。

这不是一篇教程——是记录我怎么从”找开源方案”到”算了我自己写一个”的过程,以及这中间的权衡和踩坑。

需求到底是什么

先理清楚我到底需要什么。

录像已经有了——摄像头自动推到 NAS,不存在”需要录制”的问题。AI 检测也有了——一个独立的 cam-filter 脚本在跑,识别出有人活动的片段单独归档。

缺的只有一件事:方便地浏览和回放

具体来说:按日期选、拖时间轴找片段、在手机上也行。说白了就是安防 DVR 软件那种体验——日历上显示哪天有录像,点进去是时间线,选一段就播。

这不是 NVR(网络录像机)该干的事,也不是媒体服务器的活。这是一个 surveillance-specific 录像浏览器

试过的开源方案(为什么都不行)

Frigate

Frigate 是目前最流行的开源 NVR,AI 物体检测能力很强。但它强的前提是你有 Coral TPU 或 GPU。

我只有一台纯 CPU 服务器。关掉 AI 检测当纯录像机用?可以,但 Frigate 的设计重心在检测,回放界面做得很基础——只有一个时间线,不支持按日期跳转。而且为了一个”浏览录像”的需求,装一套带 AI 识别能力的 NVR,感觉像买了一辆法拉利用来买买菜。

Shinobi / ZoneMinder / 其他传统 NVR

这些 NVR 都设计成”从摄像头拉流、录制、管理”。但我的录像已经存好了,不需要它们来录。它们的回放界面也偏功能化,手机体验普遍一般。

Jellyfin / Plex / Immich

通用媒体服务器,本质上是”看电影”的。面对 67,000 个按分钟切割的录像文件,它们的索引效率很低。还有一个根本问题:它们不理解”监控录像”这个场景——没有按日期标记覆盖、没有按分钟定位片段的能力。

开源的 gap

一圈看下来就发现:没有一个开源方案专门做”浏览已有监控录像”这一件事。NVR 太重,媒体服务器太偏,都不在点上。这是个挺小众的需求——大部分人要么不需要,要么买了商业方案,要么用 Frigate 跑全套。

但对我来说,这就是明显的 gap。

自研方案:Cam Player

既然没有现成的,就自己写。用 Python 3.9 标准库,零外部依赖,两个文件搞定。

架构

浏览器 (HTML/JS)  ←HTTP→  server.py (Python 标准库)
                                 ↑
                        Index (内存: 日期 → 文件列表)
                                 ↑
                    录像目录 + 人物片段目录

后端一个文件 server.py,前端一个文件 index.html。部署就是 python3 server.py,跑在 8099 端口。

API 设计

接口 返回数据 用途
GET /api/stats 文件数、数据量 首页统计
GET /api/dates 日期列表 哪些天有录像
GET /api/date-types 每日录像类型 原始/人物/都有
GET /api/day/2026-06-25 该天所有片段 某天详细
GET /video/... 视频流 (206) 播放

前端界面

页面分三块区域:日历、时间线、视频播放器。

日历:月度网格,日期下方带彩色圆点——橙色代表有原始录像,绿色代表有人物片段。左右箭头切换月份,固定 250px 高度防止抖动。

时间线:24 小时柱状条,显示当天所有录影片段。两种显示模式:
- 原始 + 人物同时显示:灰色柱状条叠加
- 仅人物片段:绿色针脚标记突出显示

播放器:HTML5 <video>,支持 206 Partial Content 拖进度条。键盘快捷键:空格播放/暂停、左右箭头快进快退、上下箭头切换片段。

索引机制

启动时扫描录像目录,从文件名解析时间和日期。67,000+ 文件(按文件名格式 YYYYMMDD_HHMMSS.mp4 命名),扫描耗时约 60 秒。结果缓存在内存里,重建索引只需重启服务。

部署方式

# 假设录像存在 /mnt/nas/footage/ 和 /mnt/nas/archive/
SOURCE_DIR=/mnt/nas/footage ARCHIVE_DIR=/mnt/nas/archive python3 server.py

资源占用:CPU 约 6%,内存占用极少(索引建立后)。

如果你习惯用 Docker 管理服务,也可以封装成容器:

FROM python:3.11-slim
COPY server.py index.html /app/
EXPOSE 8099
CMD ["python3", "/app/server.py"]

踩坑记录

1. HTML 缓存拖慢调试

server.py 启动时把 index.html 读入内存缓存。改 HTML 后必须重启才能看到效果,前端调试效率很低。

解决:改为每次请求重新读取文件。牺牲了一丁点性能,换来调试时即时刷新。

2. 日历高度抖动

不同月份的天数和起始星期不同,日历高度会变化——30 天 vs 31 天,周日开始 vs 周六开始,页面布局跟着跳。

解决:给日历容器设固定高度,内部内容滚动。

3. 两种录像的显示区分

同时有原始录像(按分钟切割,连续覆盖全天)和人物片段(每段一个事件),在时间线上需要不同的视觉标记。

决策:两种都选时柱状条显示原始录像覆盖范围;仅选人物时用针脚标记突出,不占比例。

4. 手机端适配

桌面是左右分栏(日历 + 视频),手机屏幕太小放不下。

解决:CSS @media 切换——手机端视频全屏、日历做成弹出抽屉、时间线在底部面板。

5. Range 请求

浏览器要拖动进度条,必须支持 206 Partial Content。家用摄像头录的是标准 H.264 MP4,如果服务器只返回完整文件,就拖不动。

实现:解析 HTTP Range header,返回对应字节范围 + 206 状态码。标准做法,关键是要记得做。

效果

这个单文件 Python 服务跑了几个月了,稳定性和响应速度都满意:

  • 60 秒索引 67,000+ 文件
  • CPU 占用约 6%
  • 手机浏览器直接访问,和桌面体验一致
  • 从任意日期到任意片段,点几下就能播放

最近有需求的朋友来家里看到这界面,反应通常是”这个多少钱?”——听说是一个 Python 文件自己写的,都不太信。

总结

这段经历让我重新思考了一个问题:当现有开源方案都不能完全满足需求时,到底是凑合用,还是自己造

开源社区的繁荣容易让人形成惯性思维——遇到问题第一反应是”找个开源方案”。但开源方案的设计目标往往和你不一样。Frigate 的定位是 AI 检测 NVR,Jellyfin 的定位是家庭媒体服务器。它们都很优秀,但不是为”浏览已有监控录像”这个场景设计的。

在这种情况下,自己写一个反而是更务实的做法——没有依赖、没有学习成本、完全按自己的需求来。而且事实证明,很多时候事情没有你想象的那么复杂。一个 Python 文件、零外部依赖、一晚上搞定的原型,已经稳定跑了几个月。

如果你的需求也很”小众”(不在主流开源项目瞄准的方向上),不妨试试这条路。

标签: 监控 自研 Python