Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7962a7e50a | |||
| 0674f260a4 |
@@ -0,0 +1,58 @@
|
||||
# 赵府智家系统架构
|
||||
|
||||
## 部署与职责
|
||||
|
||||
系统运行于自己的 Ubuntu 小电脑,提供局域网和 Tailscale 网页入口。第一阶段是视觉模块。管理程序、SQLite 和 MediaMTX 在同一主机运行,使用独立服务用户;网页只访问管理程序,设备凭据与媒体管理接口留在服务端。
|
||||
|
||||
摄像头与录像机继续在其原有网络工作。实时画面可直接从摄像头取流,也可由录像机提供;读取哪个位置的历史录像必须显式标明。录像机原有录像回放需要设备型号对应的检索、回放适配器。
|
||||
|
||||
## 对象关系
|
||||
|
||||
```text
|
||||
空间档案(家庭 / 公司 / 其他)
|
||||
├─ 地区:省、市、区县、街道
|
||||
├─ 地址:小区或园区、楼栋、单元、楼层、门牌
|
||||
├─ 所属:家庭或单位
|
||||
├─ 摄像头对象(稳定 ID、品牌、型号、序列号、安装位置)
|
||||
│ ├─ 镜头通道 1 → 主码流 / 子码流
|
||||
│ └─ 镜头通道 2 → 主码流 / 子码流
|
||||
└─ 录像机对象(地址、接入驱动、账号、空间引用)
|
||||
|
||||
每个镜头通道分别关联:
|
||||
实时来源:直接摄像头 / 录像机
|
||||
录像来源:本机 / 录像机(可独立于实时来源)
|
||||
```
|
||||
|
||||
## 接入层
|
||||
|
||||
当前 RTSP 负责实时视频,ONVIF 负责读取设备身份、媒体配置、独立视频源及主子码流地址。不同品牌使用相同对象结构,路径和能力由设备返回值或对应驱动确定;不按品牌名字保证所有型号兼容。当前品牌驱动只提供路径模板,自动摄像头配置读取支持 ONVIF Media v1。
|
||||
|
||||
主码流用于清晰预览和可选本机录像,子码流用于多画面预览。系统目前不修改摄像头或录像机设置,不自动导入不存在的通道,不把服务可达当成画面接通。
|
||||
|
||||
## 管理 API
|
||||
|
||||
所有数据接口(除会话状态与健康检查)需要管理员 Cookie 会话。写接口接受 JSON,检查同源请求。
|
||||
|
||||
| 接口 | 用途 |
|
||||
| --- | --- |
|
||||
| GET /api/session | 初始化、登录状态 |
|
||||
| POST /api/setup、/api/login、/api/logout | 管理员会话 |
|
||||
| GET /api/state | 对象、镜头实际码流状态、存储状态 |
|
||||
| POST /api/sites | 保存结构化空间档案 |
|
||||
| POST /api/assets | 保存实体摄像头对象 |
|
||||
| POST /api/recorders | 保存录像机与连接凭据 |
|
||||
| POST /api/cameras | 保存镜头通道(早期接口名保留) |
|
||||
| POST /api/onvif | 只读设备身份、视频源与码流配置 |
|
||||
| POST /api/discover | 有界局域网 RTSP 探测 |
|
||||
| POST /api/storage | 本机录像容量与保留策略 |
|
||||
| GET /api/recordings | 本机录像时段 |
|
||||
| GET /media/live/... | 登录鉴权后的 HLS 代理 |
|
||||
| GET /media/playback | 本机录像回放、下载代理 |
|
||||
|
||||
## 当前交付与后续
|
||||
|
||||
本阶段实现独立运行、空间建模、三类视觉对象、实时视频接入和本机可选录像。摄像头真实接入结果由部署记录和主机运行状态说明。
|
||||
|
||||
下一阶段可加入具体录像机的历史回放适配器;随后扩展其他设备类型和联动规则。PTZ、语音对讲、AI 检测、多用户细分权限目前没有实现,不作为已具备的能力展示。
|
||||
|
||||
配置写入 SQLite 并生成运行时媒体配置,录像数据独立存储。当前为单机模式;升级前备份整个数据目录。凭据和录像不包含在公开源码中。
|
||||
@@ -10,7 +10,7 @@
|
||||
- **录像机**:独立的设备与凭据档案,通道可以引用录像机获取画面。也可以直接连接摄像头。
|
||||
- **录像来源**:明确区分录像机原有录像与小电脑新保存的录像。
|
||||
|
||||
## v0.1.0 功能与边界
|
||||
## 当前功能与边界
|
||||
|
||||
源码包括空间 / 摄像头 / 录像机 / 镜头档案,局域网 RTSP 探测、ONVIF 设备信息与独立镜头媒体配置读取、同源 HLS 播放、本机可选持续录像、日期查询、录像时间轴、定位回放和片段下载、存储保护、管理员初始化与登录。
|
||||
|
||||
@@ -28,7 +28,7 @@ HLS 会有数秒延迟。此版本不转码,推荐 H.264;H.265 和不同音
|
||||
sudo VISION_BIND=<本机的Tailscale-IPv4> bash deploy/install.sh
|
||||
```
|
||||
|
||||
安装脚本校验并安装固定版本 MediaMTX v1.21.1,创建无登录权限的服务用户,数据写入 `/var/lib/zhaovision`,应用入口为 `http://<Tailscale-IP>:8790/`。视频服务的 HTTP、RTSP 与管理 API 全部仅监听回环地址,由应用进行登录鉴权并代理。
|
||||
安装脚本校验并安装固定版本 MediaMTX v1.21.1,创建无登录权限的服务用户,数据写入 `/var/lib/zhaovision`,应用入口为 `http://<Tailscale-IP>:8790/`。可用逗号分隔的 `VISION_BIND` 同时指定本机局域网与 Tailscale 地址;已安装系统在 `/etc/zhaovision.env` 中修改并重启服务。视频服务的 HTTP、RTSP 与管理 API 全部仅监听回环地址,由应用进行登录鉴权并代理。
|
||||
|
||||
首次打开页面,读取服务器上的 `/var/lib/zhaovision/setup-code.txt`,输入初始化码,设置至少 10 位管理密码。初始化后码文件删除。摄像头与录像机密码在自己的界面填写。
|
||||
|
||||
@@ -61,4 +61,6 @@ mediamtx ./data/mediamtx.yml
|
||||
- [hls.js](https://github.com/video-dev/hls.js) v1.7.3,Apache-2.0,浏览器构建与许可证位于 `web/vendor/`。
|
||||
- 官方接口参考:[录像](https://mediamtx.org/docs/features/record)、[回放](https://mediamtx.org/docs/features/playback)、[配置](https://mediamtx.org/docs/references/configuration-file)。
|
||||
|
||||
本项目源码以 MIT 许可证发布。v0.1.0 为初始版本,真实录像机的历史回放兼容性需完成型号适配后另行验证。
|
||||
[系统架构与对象关系](ARCHITECTURE.md)。
|
||||
|
||||
本项目源码以 MIT 许可证发布。v0.1.x 为初始版本,真实录像机的历史回放兼容性需完成型号适配后另行验证。
|
||||
|
||||
@@ -302,6 +302,30 @@ def media_json(path, playback=False):
|
||||
raise Problem('视频服务暂不可用,或该时段尚无可回放录像', 503)
|
||||
|
||||
|
||||
def media_config_watch():
|
||||
# Reconcile API configuration after atomic file replacements. Multiple rapid
|
||||
# writes can be coalesced by a filesystem watcher; the file remains authority.
|
||||
applied = None
|
||||
while True:
|
||||
try:
|
||||
raw = CONFIG.read_bytes()
|
||||
digest = hashlib.sha256(raw).digest()
|
||||
if digest != applied:
|
||||
desired = json.loads(raw)['paths']
|
||||
existing = {p['name'] for p in media_json('/v3/config/paths/list?itemsPerPage=200').get('items', [])}
|
||||
for name, body in desired.items():
|
||||
action, method = ('patch', 'PATCH') if name in existing else ('add', 'POST')
|
||||
req = urllib.request.Request('http://127.0.0.1:' + str(API_PORT) + '/v3/config/paths/' + action + '/' + name,
|
||||
data=json.dumps(body).encode(), method=method,
|
||||
headers={'Authorization': MEDIA_AUTH, 'Content-Type': 'application/json'})
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
response.read()
|
||||
applied = digest
|
||||
except Exception:
|
||||
pass # Retry on the next pass, including after a media process restart.
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def status():
|
||||
try:
|
||||
data = media_json('/v3/paths/list?itemsPerPage=200')
|
||||
@@ -682,9 +706,15 @@ def initialize():
|
||||
if __name__ == '__main__':
|
||||
initialize()
|
||||
threading.Thread(target=storage_watch, daemon=True).start()
|
||||
bind = os.environ.get('VISION_BIND', '127.0.0.1')
|
||||
threading.Thread(target=media_config_watch, daemon=True).start()
|
||||
bindings = [h.strip() for h in os.environ.get('VISION_BIND', '127.0.0.1').split(',') if h.strip()]
|
||||
port = int(os.environ.get('VISION_PORT', '8790'))
|
||||
server = ThreadingHTTPServer((bind, port), Handler)
|
||||
server.daemon_threads = True
|
||||
print('ZhaoVision listening on ' + bind + ':' + str(port), flush=True)
|
||||
server.serve_forever()
|
||||
servers = []
|
||||
for bind in dict.fromkeys(bindings):
|
||||
server = ThreadingHTTPServer((bind, port), Handler)
|
||||
server.daemon_threads = True
|
||||
servers.append(server)
|
||||
print('ZhaoVision listening on ' + bind + ':' + str(port), flush=True)
|
||||
for server in servers[1:]:
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
servers[0].serve_forever()
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Run from a committed, published release checkout on an x86_64 Ubuntu host.
|
||||
# VISION_BIND is the host's Tailscale IPv4; defaults to loopback.
|
||||
# VISION_BIND is a comma-separated list of explicit private/Tailscale IPv4s.
|
||||
# MEDIA_ARCHIVE optionally points to an already downloaded pinned release archive.
|
||||
[[ $(id -u) == 0 ]] || { echo 'Run as root'; exit 1; }
|
||||
[[ $(uname -m) == x86_64 ]] || { echo 'This installer targets x86_64'; exit 1; }
|
||||
@@ -10,8 +10,9 @@ release_dir=$(cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
bind_address=${VISION_BIND:-127.0.0.1}
|
||||
python3 - "$bind_address" <<'PY'
|
||||
import ipaddress,sys
|
||||
ip=ipaddress.ip_address(sys.argv[1])
|
||||
assert ip.version==4 and (ip.is_loopback or ip in ipaddress.ip_network('100.64.0.0/10')), 'Use a Tailscale or loopback address'
|
||||
for value in sys.argv[1].split(','):
|
||||
ip=ipaddress.ip_address(value.strip())
|
||||
assert ip.version==4 and not ip.is_unspecified and not ip.is_multicast and (ip.is_private or ip in ipaddress.ip_network('100.64.0.0/10')), 'Use explicit private or Tailscale addresses'
|
||||
PY
|
||||
getent passwd zhaovision >/dev/null || useradd --system --home-dir /var/lib/zhaovision --shell /usr/sbin/nologin zhaovision
|
||||
install -d -o zhaovision -g zhaovision -m 700 /var/lib/zhaovision /var/lib/zhaovision/recordings
|
||||
|
||||
Reference in New Issue
Block a user