Viseron 3.7.0 API 使用详解

Viseron 3.7.0 REST API 与 MQTT 集成实测详解:15 个处理器模块路由全景,XSRF/JWT/PAT 三种凭据链路,摄像头、事件、录像、HLS 回放、配置调参与日志端点逐条 curl 实测,负样本回执佐证路由规则。

ViseronViseronNVR摄像头视频监控RTSP2026/10/30

Viseron 3.7.0 API 使用详解

目标:把 Viseron 3.7.0 的 REST API 与 MQTT 事件输出讲透——15 个处理器模块的路由全景、三种凭据(XSRF+Cookie 会话 / JWT / 个人访问令牌 PAT)的获取与调用链路、摄像头 / 事件 / 录像 / 流媒体 / 配置调参 / 日志系统六大域端点的逐条实测,全部命令输入与回显在 Ubuntu 22.04 服务器实测跑出(2026-10-30),可照抄复现。环境部署见《Viseron 3.7.0 Docker Compose 部署指南》,配置参数语义见《Viseron 3.7.0 配置参数详解》。

一、环境信息

本篇全部接口验证在下列环境中完成,所有回显均为真实输出(2026-10-30):

项目值
操作系统Ubuntu 22.04(Linux x86_64,6 核 CPU / 19GB 内存)
Viseron3.7.0(内置 PostgreSQL 16.15 / FFmpeg 8.1 / go2rtc / nginx,Tornado Web 框架)
Web/API 端口8888(部署时映射为宿主 8151,本篇命令统一用 http://127.0.0.1:8151)
API 前缀/api/v1(全部 REST 路由挂在该前缀下)
管理账号admin / q1w2e3r4(首次启动引导页创建,角色 admin)
摄像头fake_camera(768x432@12fps,MediaMTX 模拟 RTSP 源)
检测器mog2 运动检测 + darknet(YOLOv7-tiny)物体检测
PAT 前缀vpat_(个人访问令牌,128 位十六进制)
JWT 有效期1800 秒(expiration 字段,登录回执实测)

Viseron 的 API 与 Web UI 同源同端口:浏览器访问 http://<服务器>:8888 打开前端页面,前端数据全部来自 /api/v1/*。命令行集成只依赖 curl(或任意 HTTP 客户端)与一个可用凭据,无需额外 SDK。

二、API 总览

2.1 路由体系:15 个处理器模块

Viseron 3.7.0 的 REST API 由 webserver 组件下 15 个处理器模块注册而成,全部挂在 /api/v1 前缀下。按模块汇总(✓ = 本篇实测,— = 源码注册、未逐一实测):

模块路由方法权限实测
auth/auth/enabledGET免认证✓
auth/auth/loginPOST免认证(login 限流)✓
auth/auth/tokenPOST免认证(token 限流)✓
auth/auth/logoutPOST登录用户✓
auth/auth/requestGET登录用户✓
auth/auth/usersGETADMIN✓
auth/auth/createPOSTADMIN—
auth/auth/user/{user_id}GET/PUT/DELETEGET 登录用户;PUT/DELETE ADMIN—
auth/auth/users/{user_id}PUTADMIN—
auth(需摄像头令牌的图像端点)GET免认证 + camera token—
camera/camera/{id}/snapshotGET免认证 + camera token✓
camera/camera/{id}GET登录用户✓
camera/camera/{id}/startPOSTADMIN✓
camera/camera/{id}/stopPOSTADMIN✓
camera/camera/{id}/manual_recordingPOSTADMIN/WRITE✓
cameras/camerasGET登录用户✓
cameras/cameras/failedGET登录用户✓
events/events/{camera}GET登录用户✓
events/events/{camera}/amountGET登录用户✓
events/events/amountPOST登录用户✓
events/events/dates_of_interestPOST登录用户✓
recordings/recordingsGET登录用户✓
recordings/recordings/{camera}GET登录用户✓
recordings/recordings/{camera}/{recording_id}DELETEADMIN✓
recordings/recordings/{camera}/available_timespansGET登录用户—
recordings/recordings/{camera}DELETEADMIN(清空该摄像头录像)—
hls/hls/{camera}/available_timespansGET登录用户(可 token 参数)✓
hls/hls/{camera}/{id}/index.m3u8GET登录用户(可 token 参数)✓
config/configGETADMIN✓
tune/tuneGETADMIN✓
tune/tune/{camera}GETADMIN✓
tune/tune/{camera}PUTADMIN✓
logger/logger/logsGETADMIN✓
logger/logger/configGETADMIN✓
system/system/dispatched_eventsGETADMIN✓
profile/profile/available_timezonesGET登录用户✓
profile/profile/preferencesPUT登录用户—
profile/profile/display_namePUT登录用户—
profile/profile/access_tokensGET/POST登录用户✓
profile/profile/access_tokens/{token_id}DELETE登录用户✓
profile/profile/revoke_allPOST登录用户—
publicimage/publicimageGET免认证(需 image token)✓
download/downloadGET登录用户(需 token)✓
onboarding/onboardingPOST免认证(onboarding 限流)—
compreface/compreface/update_subjectsGET登录用户✓

解读:

  • 权限四层模型:每条路由在源码注册时声明四类属性——requires_auth(默认 true,唯一例外是 enabled/login/token/onboarding/snapshot/publicimage 这几条设计上就要在无凭据时可达的路由)、requires_role(ADMIN / READ / WRITE 三级角色)、requires_camera_token(用摄像头专属 access_token 而非用户凭据)、rate_limit(login / token / onboarding 三个敏感端点单独限流)。
  • 角色只分三级:admin(全部权限)、write(读写)、read(只读)。用户管理、启停摄像头、删录像、调参等写操作普遍要求 ADMIN 或 ADMIN/WRITE;查询类端点三级角色均可。
  • 两个「token」不要混淆:access_token(每摄像头一枚,GET /cameras 回执里返回,用于 snapshot / publicimage / download 这类面向设备或第三方的免登录取图端点)与 vpat_ 令牌(用户级个人访问令牌,用于模拟用户身份调任意 API)。后文分别演示。

2.2 无凭据访问的默认回执

先看反面教材——不带任何凭据直接调接口,是后面所有实测的对照基线:

curl -s http://127.0.0.1:8151/api/v1/cameras -w '\n[HTTP %{http_code}]\n'
{"status": 401, "error": "Authentication required"}
[HTTP 401]

凡是 401,正文都是同一个结构:status(HTTP 同码数字)+ error(一句话原因)。部署时启用了 webserver.auth 的环境,所有未带凭据的请求都会得到这一句,包括写操作(不会半执行)。

三、认证链路实测

3.1 认证状态探测:GET /auth/enabled

唯一不需要凭据的「元端点」,客户端启动时先调它判断要不要走登录流程:

curl -s http://127.0.0.1:8151/api/v1/auth/enabled
{"enabled": true, "onboarding_complete": true}

两个字段:enabled 表示是否启用了认证(对应 config.yaml 的 webserver.auth 段);onboarding_complete 表示首次引导(创建管理员账号)是否已完成。enabled: true 时其余接口一律要求凭据;onboarding_complete: false 时前端会强制进入 POST /onboarding 初始化流程。

3.2 JWT 登录:XSRF 防护 → POST /auth/login

浏览器场景的完整登录分三步:先拿 XSRF Cookie,再带 XSRF 头提交账密,最后组装 JWT。这是 Web UI 前端每次打开页面都在做的事,脚本场景可以完整复现:

# 第 1 步:请求任意免认证端点,让服务端种下 _xsrf cookie(-c 保存 cookie jar)
curl -s -c /tmp/jar.txt -o /dev/null http://127.0.0.1:8151/api/v1/auth/enabled
XSRF=$(awk '$6=="_xsrf"{print $7}' /tmp/jar.txt)

# 第 2 步:POST /auth/login,必须带 X-Xsrftoken 与 X-Requested-With 两个头
curl -s -b /tmp/jar.txt -c /tmp/jar.txt -X POST http://127.0.0.1:8151/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -H "X-Xsrftoken: $XSRF" \
  -H "X-Requested-With: XMLHttpRequest" \
  -d '{"username":"admin","password":"q1w2e3r4","client_id":"d5f3a9c8-1111-4b2a-9c33-viseron0001"}'

实测回显:

{"header": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", "payload": "eyJpc3MiOiI4MTNhN2UyYmVhNzI0MzBlOTJkMjc5MWIzOTE2MzBlMCIsImlhdCI6MTc5MzMzMzM2NSwiZXhwIjoxNzkzMzM1MTY1fQ", "expiration": 1800, "expires_at": "2026-10-30T04:39:25.077722+00:00", "expires_at_timestamp": 1793335165, "session_expires_at": "2026-11-06T04:09:25.039988+00:00", "session_expires_at_timestamp": 1793938165}

逐字段解读:

字段值含义
headereyJhbGciOiJIUzI1NiIs...JWT 的第一段,base64 解码是 {"alg":"HS256","typ":"JWT"}
payloadeyJpc3MiOiI4MTNhN2UyYmVh...JWT 的第二段,解码见下
expiration1800JWT 有效期秒数,30 分钟
expires_at / expires_at_timestamp…/1793335165JWT 过期时刻(ISO 与 Unix 双格式)
session_expires_at / …_timestamp2026-11-06T04:09:25…会话过期时刻 = 配置的 session_expiry.days(本环境 7 天)

把 payload 段 base64 解码,能看到 JWT 的实际内容:

{"iss": "813a7e2bea72430e92d2791b391630e0", "iat": 1793333365, "exp": 1793335165}

iss(签发者标识)每次登录都不同(本环境四轮登录实测得到四个不同值),是签发时生成的会话标识而非固定用户 ID;iat/exp 是签发与过期时间戳。

关键坑:JWT 是「两段 + Cookie 签名」结构。 login 回执只给了 header 和 payload 两段,第三段签名不在回执里,而在 cookie jar 的 signature_cookie 中。缺了签名直接当 Bearer 用会被 401(本篇实测):

curl -s -H "Authorization: Bearer <header>.<payload>" \
  http://127.0.0.1:8151/api/v1/cameras -w '\n[HTTP %{http_code}]\n'
{"status": 401, "error": "Authentication required"}
[HTTP 401]

正确拼法是从 cookie jar 取 signature_cookie,按 | 切分后第 5 段的 2:<base64> 就是签名:

JWT=$(python3 - <<'EOF'
import base64, json, urllib.parse
raw = None
for line in open('/tmp/jar.txt'):
    if 'signature_cookie' in line:
        raw = urllib.parse.unquote(line.rstrip('\n').split('\t')[-1].strip().strip('"'))
parts = raw.split('|')
sig = base64.b64decode(parts[4].split(':', 1)[1]).decode()
login = json.load(open('/tmp/login_resp.json'))   # login 回执存盘
print(f"{login['header']}.{login['payload']}.{sig}")
EOF
)
echo "$JWT"

某次实测拼出的完整 JWT(三段齐全):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjOGRhZDFmNmIwYmM0ZWMwYTdhMWU2YzU3N2JmYjM0MiIsImlhdCI6MTc5MzMyNzEzMywiZXhwIjoxNzkzMzI4OTMzfQ.QWpYFsguxV6Jswdq8U2t92vVoL4J3sFvc4kpKllbLPE

拼好的 JWT 用法是「浏览器形态」:JWT 放 Authorization: Bearer 头、同时把 cookie jar 一起带上(签名 cookie 要参与校验):

curl -s -b /tmp/jar.txt -H "Authorization: Bearer $JWT" \
  http://127.0.0.1:8151/api/v1/cameras -w '\n[HTTP %{http_code}]\n' | tail -1
[HTTP 200]

💡 技术拓展:为什么「JWT + cookie 签名」而不是标准单头 JWT?Viseron 的 webserver 基于 Tornado,XSRF 防护与签名 Cookie 是 Tornado 的内建安全机制——把 JWT 签名托管在 HttpOnly Cookie 里,前端 JS 无法读取,天然防 XSS 窃取令牌;代价是纯脚本调用必须维护 cookie 状态。脚本场景更省事的路径是下面的 PAT(无 Cookie 依赖),JWT 链路主要是给浏览器前端和自己写代理层用的。

3.3 JWT 刷新:POST /auth/token

JWT 30 分钟过期,浏览器前端到期前会调 POST /auth/token 静默换新。该端点免认证但单独限流(rate_limit: "token"),请求体要求 OAuth2 风格的 grant_type。用错误 grant_type 探测出的校验规则(回执即文档):

curl -s -X POST -H "Content-Type: application/json" \
  -d '{"grant_type":"access_token"}' \
  http://127.0.0.1:8151/api/v1/auth/token -w '\n[HTTP %{http_code}]\n'
{"status": 400, "error": "Invalid body: {\"grant_type\":\"access_token\"}. required key not provided @ data['client_id']. Got None\nvalue must be one of ['refresh_token'] for dictionary value @ data['grant_type']. Got 'access_token'"}

可见 schema 是 {grant_type: "refresh_token", client_id: <字符串>},且刷新凭据需随请求携带(把刚过期的 JWT 当凭据传入会得到明确拒绝):

curl -s -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \
  -d '{"grant_type":"refresh_token","client_id":"d5f3a9c8-1111-4b2a-9c33-viseron0001"}' \
  http://127.0.0.1:8151/api/v1/auth/token -w '\n[HTTP %{http_code}]\n'
{"status": 400, "error": "Invalid refresh token"}
[HTTP 400]

Invalid refresh token 说明 refresh token 是与 JWT 独立的另一份凭据(由浏览器端随会话持有),脚本场景拿不到它,也不需要它——脚本集成的正解是 PAT。该端点的价值在浏览器静默续期与自研代理层复用,参数规则本节已用实测探明。

3.4 创建个人访问令牌(PAT):POST /profile/access_tokens

PAT 是脚本集成的首选凭据:一次创建、长期有效、随 Authorization: Bearer 头走、无 Cookie/XSRF 依赖。创建需要已登录身份(JWT+Cookie 或已有一枚 PAT),body 只要一个 name:

curl -s -X POST http://127.0.0.1:8151/api/v1/profile/access_tokens \
  -H "Authorization: Bearer $JWT" -b /tmp/jar.txt \
  -H "Content-Type: application/json" \
  -d '{"name":"api-doc-demo"}' -w '\n[HTTP %{http_code}]\n'
{"id": "6c738dac0b6944ce85e6e5ee9e37508f", "name": "api-doc-demo", "created_at": 1793327392.88088, "expires_at": null, "last_used_at": null, "last_used_by": null, "token": "vpat_349eaef4874966c72a33893935aed8c559d3caafdfe93a809723d0d9565300623c8daf0db165fef17862b5d39304be5e973c0e8dfd9ab9844c163e9fb55e1283"}
[HTTP 201]

回执解读:

字段值含义
id6c738dac0b6944ce85e6e5ee9e37508f令牌 ID(32 位十六进制),撤销时用它
tokenvpat_349eaef4…(共 133 字符)令牌明文只在创建回执出现这一次,立即妥善保存
expires_atnull永不过期(创建时可传有效期)
last_used_at / last_used_bynull使用追踪,首次调用后回填

同一入口也能在 Web UI 创建:登录后右上角 Profile 页 → Access Tokens → New Token,与本接口等价。

一枚 PAT 等价于创建者本人的身份与角色(admin 的 PAT = 全权限)。不带任何 Cookie,一个头走天下:

PAT=vpat_349eaef4874966c72a33893935aed8c559d3caafdfe93a809723d0d9565300623c8daf0db165fef17862b5d39304be5e973c0e8dfd9ab9844c163e9fb55e1283
curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/cameras -w '\n[HTTP %{http_code}]\n' | tail -1
[HTTP 200]

本篇后续所有命令都基于这一形态。PAT 会让服务端记录使用轨迹,GET /profile/access_tokens 可见(last_used_by 是来源 IP):

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/profile/access_tokens
{"access_tokens": [{"id": "6c738dac0b6944ce85e6e5ee9e37508f", "name": "api-doc-demo", "created_at": 1793327392.88088, "expires_at": null, "last_used_at": 1793327393.794733, "last_used_by": "10.89.61.1"}]}

撤销单枚令牌(DELETE 按 id,幂等):

curl -s -X DELETE -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/profile/access_tokens/6c738dac0b6944ce85e6e5ee9e37508f -w '\n[HTTP %{http_code}]\n'
{"success": true}
[HTTP 200]

登出(浏览器会话场景)用 POST /auth/logout,需要登录态 Cookie,实测回执 {"success": true};脚本场景没有会话可登出,撤销 PAT 即可。另有一条轻量端点 GET /auth/request 用于校验当前凭据是否有效(有效性探针),PAT 调用返回:

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/auth/request -w '\n[HTTP %{http_code}]\n'
{"success": true}
[HTTP 200]

3.6 登录限流:429 实测

POST /auth/login 挂了 rate_limit: "login"。连续用错误密码轰它,实测第 11 次起被限流(默认 10 次 / 60 秒,对应 webserver.auth.rate_limits 配置):

attempt 1: 401
attempt 2: 401
...
attempt 10: 401
attempt 11: 429
attempt 12: 429

正确密码在限流窗口内也会被 429 拒绝,生产环境不要放宽这条——它是暴力破解的第一道闸。若误触限流,等 60 秒窗口自然过去即可。

3.7 角色与用户管理:GET /auth/users

ADMIN 专用,返回全部用户。本环境只有一个引导页创建的管理员:

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/auth/users
{"users": [{"id": "11a9ee5b86994827acd8050674387884", "name": "Administrator", "username": "admin", "role": "admin", "enabled": true, "assigned_cameras": null, "preferences": null}]}

id 是该用户的固定标识(与 JWT payload 里每次都变的 iss 会话标识是两回事);assigned_cameras 支持把用户限定到部分摄像头(多用户分权场景);role 取值 admin/read/write。源码还注册了 POST /auth/create(ADMIN 建用户)与 /auth/user/{user_id} 的 GET/PUT/DELETE(查、改、删单个用户),本篇未逐一实测,语义按 REST 约定。

四、摄像头 API

4.1 摄像头清单:GET /cameras

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/cameras
{"fake_camera": {"identifier": "fake_camera", "name": "Fake Camera", "width": 768, "height": 432, "access_token": "b4d22889c4061c2cabf4895cfd0761428a154a27f9636ae741c0536a5c99e1c4378cd815ca9f906b6a35496bb2d42ffad4fd05470b059b4660e7b2ca2dd15da0", "mainstream": {"width": 768, "height": 432}, "still_image": {"refresh_interval": 10, "available": true, "width": 768, "height": 432}, "is_on": true, "connected": true, "live_stream_available": false, "is_recording": true, "ptz_support": null}}

逐字段解读(外层键是摄像头标识符):

字段值含义
identifier / namefake_camera / Fake Camera标识符(config.yaml 的摄像头键名)与显示名
width / height768 / 432当前取流分辨率
access_tokenb4d22889…(128 hex)摄像头专属令牌,供 snapshot/publicimage/download 免登录取图用
mainstream768x432主码流信息
still_imagerefresh_interval 10, available true静态图每 10 秒刷新、当前可取
is_on / connectedtrue / true摄像头已启用且已连上 RTSP 源
is_recordingtrue此刻正在录像
live_stream_availablefalseWeb 端实时流可用性(取决于流配置,本环境未开 live)
ptz_supportnullPTZ 云台能力(无则为 null)

排查摄像头接入故障用 GET /cameras/failed,正常时是空对象:

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/cameras/failed
{}

4.2 单摄像头详情与状态翻转:GET / POST /camera/{id}

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/camera/fake_camera
{"identifier": "fake_camera", "name": "Fake Camera", "width": 768, "height": 432, "access_token": "b4d22889c4061c2cabf4895cfd0761428a154a27f9636ae741c0536a5c99e1c4378cd815ca9f906b6a35496bb2d42ffad4fd05470b059b4660e7b2ca2dd15da0", "mainstream": {"width": 768, "height": 432}, "still_image": {"refresh_interval": 10, "available": true, "width": 768, "height": 432}, "is_on": true, "connected": true, "live_stream_available": false, "is_recording": true, "ptz_support": null}

POST /camera/{id}/stop 停用摄像头(断流、暂停检测,ADMIN):

curl -s -X POST -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/camera/fake_camera/stop -w '\n[HTTP %{http_code}]\n'
{"success": true}
[HTTP 200]

停用后立刻回查,is_on 与 connected 双双翻转:

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/camera/fake_camera \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print({'is_on': d['is_on'], 'connected': d['connected']})"
{'is_on': False, 'connected': False}

POST /camera/{id}/start 恢复(ADMIN):

curl -s -X POST -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/camera/fake_camera/start -w '\n[HTTP %{http_code}]\n'
{"success": true}
[HTTP 200]

这对接口等价于 Web UI 里摄像头卡片的暂停/恢复按钮,适合在维护摄像头网络时脚本化摘流。

4.3 抓快照:GET /camera/{id}/snapshot

该端点设计为「摄像头令牌」免登录访问(requires_camera_token),但用用户凭据(PAT)同样可调,支持 width 参数缩放:

curl -s -o /tmp/snap.jpg -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/camera/fake_camera/snapshot?width=640" \
  -w '[HTTP %{http_code}] size=%{size_download} type=%{content_type}\n'
file /tmp/snap.jpg
[HTTP 200] size=84976 type=image/jpeg
/tmp/snap.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 640x360, components 3

拿到的就是当前帧 JPEG(width=640 等比缩到 640x360)。第三方看板/传感器要拉画面又不能交出用户凭据时,改用摄像头令牌:GET /camera/fake_camera/snapshot?token=<access_token>,与下面的 publicimage 同一套 token 机制。

4.4 手动录像:POST /camera/{id}/manual_recording

不走检测触发、人工立即开录(ADMIN/WRITE),body 用 action 表达开停:

curl -s -X POST -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
  -d '{"action":"start","duration":5}' \
  http://127.0.0.1:8151/api/v1/camera/fake_camera/manual_recording -w '\n[HTTP %{http_code}]\n'
{"success": true}
[HTTP 200]

duration 单位秒。录像 5 秒后自动收尾,到第六节录像列表里能看到这条 trigger_type: "manual" 的新录像(id 87)。停录用 {"action":"stop"},同样回 {"success": true}。

4.5 公开图像与文件下载:GET /publicimage、GET /download

两个「带 token 免登录取文件」的端点。先看参数错误的实测回执(校验器把正确 schema 打在错误信息里):

curl -s "http://127.0.0.1:8151/api/v1/publicimage?camera_identifier=fake_camera" -w '\n[HTTP %{http_code}]\n'
{"status": 400, "error": "Invalid request arguments: {'camera_identifier': 'fake_camera'}. extra keys not allowed @ data['camera_identifier']. Got 'fake_camera'\nrequired key not provided @ data['token']. Got None"}

/publicimage 只接受 token 参数。但该 token 不是摄像头的 access_token,而是 config.yaml 里 public_images 段为特定图像配置的发布令牌(本环境未配置,实测 404):

CT=b4d22889c4061c2cabf4895cfd0761428a154a27f9636ae741c0536a5c99e1c4378cd815ca9f906b6a35496bb2d42ffad4fd05470b059b4660e7b2ca2dd15da0
curl -s "http://127.0.0.1:8151/api/v1/publicimage?token=$CT" -w '\n[HTTP %{http_code}]\n'
{"status": 404, "error": "Token not found"}
[HTTP 404]

/download 同理——传任意 path 会被拒(要求 token):

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/download?path=/files/thumbnails/fake_camera/86.jpg" -w '\n[HTTP %{http_code}]\n'
{"status": 400, "error": "Invalid request arguments: {'path': '/files/thumbnails/fake_camera/86.jpg'}. extra keys not allowed @ data['path']. Got '/files/thumbnails/fake_camera/86.jpg'\nrequired key not provided @ data['token']. Got None"}

两者的正确姿势:thumbnails/snapshots 等文件路径在事件与录像回执里都以 /files/... 形式给出,Web UI 的下载按钮走的就是 /files/ 静态路由(浏览器直接打开即可);/download、/publicimage 是给「不能带用户凭据的外部系统」准备的 token 化出口,token 在对应配置段(public_images 等)中生成。脚本场景直接用 PAT 请求 /files/... 静态路径或 snapshot 端点即可。

五、事件 API

5.1 事件列表:GET /events/{camera}

事件是 Viseron 的核心数据流,三类事件(motion / object / recording)共用一个列表端点,date 参数按天过滤:

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/events/fake_camera?date=2026-10-30" | head -c 1600
{"events": [{"camera_identifier": "fake_camera", "type": "recording", "id": 86, "trigger_type": "object", "start_time": "2026-10-30T02:29:48.239233+00:00", "start_timestamp": 1793327388.239233, "end_time": null, "end_timestamp": null, "duration": null, "hls_url": "/api/v1/hls/fake_camera/86/index.m3u8", "thumbnail_path": "/files/thumbnails/fake_camera/86.jpg", "created_at": "2026-10-30T02:29:48.239548+00:00", "created_at_timestamp": 1793327388.239548, "lookback": 5}, {"camera_identifier": "fake_camera", "type": "object", "id": 45, "time": "2026-10-30T02:29:48.237221+00:00", "timestamp": 1793327388.237221, "label": "bicycle", "confidence": 0.785, "created_at": "2026-10-30T02:29:48.237221+00:00", "created_at_timestamp": 1793327388.237221, "snapshot_path": "/files/snapshots/object_detector/fake_camera/2026-10-30-02-29-48-b2a7a5e5-54ac-407f-b72b-5d077320efd8.jpg", "lookback": 5}, {"camera_identifier": "fake_camera", "type": "motion", "id": 34, "start_time": "2026-10-30T02:29:38.842583+00:00", "start_timestamp": 1793327378.842583, "end_time": "2026-10-30T02:29:39.666644+00:00", "end_timestamp": 1793327379.666644, "duration": 0.824061, "snapshot_path": "/files/snapshots/motion_detector/fake_camera/2026-10-30-02-29-38-e2d2843d-03d7-4b45-a90d-dca344b13ff7.jpg", "created_at": "2026-10-30T02:29:38.843261+00:00", "created_at_timestamp": 1793327378.843261, "lookback": 5}, …

(输出过长,节选前 3 条)三类事件的结构差异一目了然:

type关键字段含义
recordingtrigger_type / hls_url / thumbnail_path / duration一次录像,trigger_type 说明触发源(object / motion / manual),hls_url 直接可播
objectlabel / confidence / snapshot_path一次物体检测命中,含标签、置信度与目标快照
motionstart_time / end_time / duration / snapshot_path一次运动事件,含起止与运动快照

所有时间字段都是 ISO 字符串 + Unix 时间戳双份,snapshot_path / thumbnail_path 是可直接下载的静态路径(拼在 http://<服务器>:8888 后即可访问)。

5.2 事件计数:按天与按摄像头

单摄像头按天计数(GET 形态,date 参数指定日期):

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/events/fake_camera/amount?date=2026-10-30" -w '\n[HTTP %{http_code}]\n'
{"events_amount": {"2026-10-30": {"motion": 124, "recording": 365, "object": 208}}}
[HTTP 200]

多摄像头批量计数用 POST 形态(body 传 camera_identifiers 数组),顺带一提 URL 写错的负样本——把 amount 当摄像头名会得到 404,回执顺带确认了路由匹配规则:

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/events/amount" -w '\n[HTTP %{http_code}]\n'
{"status": 404, "error": "Camera amount not found"}
[HTTP 404]

正确的批量形态:

curl -s -X POST -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
  -d '{"camera_identifiers":["fake_camera"]}' \
  http://127.0.0.1:8151/api/v1/events/amount -w '\n[HTTP %{http_code}]\n'
{"events_amount": {"2026-10-30": {"motion": 25, "recording": 65, "object": 34}}}
[HTTP 200]

5.3 活跃日期:POST /events/dates_of_interest

做时间轴/日历视图时先问「哪些天有事件」,避免逐天空查:

curl -s -X POST -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
  -d '{"camera_identifiers":["fake_camera"]}' \
  http://127.0.0.1:8151/api/v1/events/dates_of_interest -w '\n[HTTP %{http_code}]\n'
{"dates_of_interest": {"2026-10-30": {"events": 125, "timespanAvailable": true}}}
[HTTP 200]

timespanAvailable: true 表示该日还有可回放的时间段(对应录像/HLS 的 available_timespans)。

六、录像 API

6.1 录像列表:GET /recordings、GET /recordings/{camera}

录像按「摄像头 → 日期 → 录像 ID」两级分组。单摄像头形态:

curl -s -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/recordings/fake_camera | head -c 1000
{"2026-10-30": {"87": {"id": 87, "camera_identifier": "fake_camera", "start_time": "2026-10-30T02:29:53.985351+00:00", "start_timestamp": 1793327393.985351, "end_time": "2026-10-30T02:29:59.584679+00:00", "end_timestamp": 1793327399.584679, "trigger_type": "manual", "trigger_id": null, "thumbnail_path": "/files/thumbnails/fake_camera/87.jpg", "hls_url": "/api/v1/hls/fake_camera/87/index.m3u8"}, "86": {"id": 86, "camera_identifier": "fake_camera", "start_time": "2026-10-30T02:29:48.239233+00:00", "start_timestamp": 1793327388.239233, "end_time": "2026-10-30T02:29:53.983004+00:00", "end_timestamp": 1793327393.983004, "trigger_type": "object", "trigger_id": null, "thumbnail_path": "/files/thumbnails/fake_camera/86.jpg", "hls_url": "/api/v1/hls/fake_camera/86/index.m3u8"}, …

(节选)4.4 节手动触发的录像 87 出现了,trigger_type: "manual"、时长 5.6 秒(比 duration 5 略长是收尾对齐分片的正常现象)。多摄像头清单 GET /recordings 结构相同,外层多套一层摄像头标识符。

6.2 删除录像:DELETE /recordings/{camera}/{id}

按录像 ID 删(ADMIN)。先查列表挑一条最老的,再删,再核对总数:

# 删除 id=2 的录像
curl -s -X DELETE -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/recordings/fake_camera/2 -w '\n[HTTP %{http_code}]\n'
{"success": true}
[HTTP 200]

删除前列表总数 460、删除后回查 459,删掉的就是 id=2 那条。两个实测负样本帮我们钉死路由形态:

# 带日期段删:404(该形态不存在)
curl -s -X DELETE -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/recordings/fake_camera/2026-10-30/2 -w '\n[HTTP %{http_code}]\n'
{"status": 404, "error": "Endpoint not found"}
[HTTP 404]

即删除按录像 ID寻址(/recordings/{camera}/{recording_id}),不是「日期 + ID」。源码还注册了 GET /recordings/{camera}/available_timespans(可用时间段)与 DELETE /recordings/{camera}(清空整摄像头录像,慎用)两条,前者误用时回执同样说明路由结构:

curl -s -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/recordings/available_timespans -w '\n[HTTP %{http_code}]\n'
{"status": 404, "error": "Camera available_timespans not found"}
[HTTP 404]

available_timespans 被当成了摄像头标识符——路由是 /recordings/{camera}/... 形态,必须先给摄像头名。日常批量清理优先依赖 storage 组件的保留策略(tiers 的 max_age/max_size,见《Viseron 3.7.0 配置参数详解》5.5 节),DELETE 接口适合单条纠错。

七、流媒体回放:HLS API

7.1 可回放时间段:GET /hls/{camera}/available_timespans

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/hls/fake_camera/available_timespans?date=2026-10-30" -w '\n[HTTP %{http_code}]\n'
{"timespans": [{"start": 1793326220, "end": 1793326451, "duration": 230}, {"start": 1793326482, "end": 1793327387, "duration": 904}]}
[HTTP 200]

返回该日期有连续录像的时间段(Unix 时间戳,duration 秒)——持续录像 + 事件录像拼接成的可回放区间,前端时间轴就靠它渲染。

7.2 播放列表:GET /hls/{camera}/{id}/index.m3u8

单条录像的 HLS 播放列表,id 即录像 ID:

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/hls/fake_camera/86/index.m3u8" -w '\n[HTTP %{http_code}]\n'
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="/files/segments/fake_camera/init.mp4"
#EXT-X-PROGRAM-DATE-TIME:2026-10-30T02:29:42.179+00:00
#EXTINF:4.992067,
/files/segments/fake_camera/1793327383.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-10-30T02:29:47.171+00:00
#EXTINF:4.993,
/files/segments/fake_camera/1793327389.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-10-30T02:29:52.164+00:00
#EXTINF:5.012189,
/files/segments/fake_camera/1793327394.m4s
#EXT-X-ENDLIST
[HTTP 200]

标准 fMP4-HLS:#EXT-X-MAP 指向初始化段 init.mp4,正文是 5 秒左右的 m4s 分片,分片路径挂在 /files/segments/ 静态路由下。拿 m3u8 地址喂给任意播放器即可回放:

ffplay -autoexit \
  'http://127.0.0.1:8151/api/v1/hls/fake_camera/86/index.m3u8'

播放器直接出画面即回放链路打通。事件回执里的 hls_url 字段(/api/v1/hls/{camera}/{id}/index.m3u8)与本节端点一致,拿到事件就能播。

八、配置、调参与运维 API

8.1 查看生效配置:GET /config

ADMIN 专用,返回当前生效的完整 config.yaml(纯 YAML 文本,不是 JSON):

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/config | head -16
ffmpeg:
  camera:
    fake_camera:
      name: Fake Camera
      host: mediamtx
      port: 8554
      path: /cam
      stream_format: rtsp
      fps: 12
      recorder:
        idle_timeout: 5

mog2:
  motion_detector:
    cameras:
      fake_camera:
        fps: 2

(节选前 16 行)它与磁盘上 config.yaml 的区别:这里返回的是「服务运行时真正生效的值」——排「改了文件没生效」时以它为准。

8.2 运行时调参:GET /tune 与 PUT /tune/{camera}

/tune 是 3.7.0 的调参利器:GET /tune 汇总所有摄像头各域实际生效的配置,GET /tune/{camera} 看单摄像头(附检测器支持的完整标签清单):

curl -s -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/tune/fake_camera | head -c 700
{"camera": {"ffmpeg": {"name": "Fake Camera", "host": "mediamtx", "port": 8554, "path": "/cam", "stream_format": "rtsp", "fps": 12, "recorder": {"idle_timeout": 5}}}, "motion_detector": {"mog2": {"fps": 2}}, "object_detector": {"darknet": {"fps": 2, "scan_on_motion_only": false, "labels": [{"label": "person", "confidence": 0.7, "trigger_event_recording": true}, {"label": "bicycle", "confidence": 0.7, "trigger_event_recording": true}, {"label": "car", "confidence": 0.7, "trigger_event_recording": true}], "available_labels": ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat", "traffic light", …

(节选)available_labels 列出 YOLOv7 权重认识的全部 80 个 COCO 标签——labels 白名单只能从这个集合里挑,调参前先查它。运行时热调参用 PUT,body 指明 domain + component + 参数键值(ADMIN):

curl -s -X PUT -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
  -d '{"domain":"motion_detector","component":"mog2","fps":2}' \
  http://127.0.0.1:8151/api/v1/tune/fake_camera -w '\n[HTTP %{http_code}]\n'
{"message": "Configuration updated successfully"}
[HTTP 200]

PUT 完立即 GET /tune/fake_camera 可见 mog2.fps: 2 生效。缺 domain/component 的 body 会被拒(回执即文档):

{"status": 400, "error": "Missing 'domain' or 'component' in request"}

/tune 的热更新不落盘——重启后仍以 config.yaml 为准,适合「先在线试参数、试好了再写回文件」的工作流。

8.3 日志:GET /logger/logs、GET /logger/config

curl -s -H "Authorization: Bearer $PAT" \
  "http://127.0.0.1:8151/api/v1/logger/logs?lines=12" | head -c 700
{"logs": [{"id": "2026-10-30 10:29:45.579_info_viseron.components.nvr.nvr.fake_camera_11", "timestamp": "2026-10-30 10:29:45.579", "timestamp_unix_ms": 1793327385579, "level": "info", "name": "viseron.components.nvr.nvr.fake_camera", "message": "Pausing motion detector", "raw": "2026-10-30 10:29:45.579 [INFO    ] [viseron.components.nvr.nvr.fake_camera] - Pausing motion detector"}, {"id": "2026-10-30 10:29:45.580_info_viseron.components.ffmpeg.recorder.fake_camera_10", "timestamp": "2026-10-30 10:29:45.580", "timestamp_unix_ms": 1793327385580, "level": "info", "name": "viseron.components.ffmpeg.recorder.fake_camera", "message": "Stopping recorder", "raw": "2026-10-30 10:29:45.580 [INFO    ] [viseron.components.ffmpeg.recorder.fake_camera] - Stopping recorder"}, …

(节选)lines 控制条数,每条含结构化字段(timestamp/level/name/message)与 raw 原始行,接日志告警时用结构化字段,人看用 raw。当前日志配置回执:

curl -s -H "Authorization: Bearer $PAT" http://127.0.0.1:8151/api/v1/logger/config
{"default_level": "info", "logs": {}, "cameras": {}}

8.4 事件总线快照:GET /system/dispatched_events

ADMIN 专用,返回系统事件总线派发过的全部事件名,组件与域的装载状态一眼可查:

curl -s -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/system/dispatched_events | head -c 900
{"events": ["component/setup/loaded/darknet", "component/setup/loaded/data_stream", "component/setup/loaded/ffmpeg", "component/setup/loaded/logger", "component/setup/loaded/mog2", "component/setup/loaded/nvr", "component/setup/loaded/storage", "component/setup/loaded/webserver", "domain/registered/camera", "domain/registered/motion_detector", "domain/registered/nvr", "domain/registered/object_detector", "domain/setup/loaded/camera/fake_camera", "domain/setup/loaded/motion_detector/fake_camera", "domain/setup/loaded/nvr/fake_camera", "domain/setup/loaded/object_detector/fake_camera", "domain/setup/loading/camera/fake_camera", "domain/setup/loading/motion_detector/fake_camera", "domain/setup/loading/nvr/fake_camera", "domain/setup/loading/object_detector/fake_camera", "entity_added", "fake_camera/camera/frame_bytes", "fake_camera/camera/started", "fake_camera/camera/status", …

(节选)事件名三段式 对象/动作/目标:component/setup/loaded/<组件> 是组件装载成功,domain/... 是域注册与逐摄像头装载,fake_camera/... 是摄像头级运行事件。排障时比对这份清单能立刻发现哪个组件/域没有 loaded。

8.5 杂项端点:时区、初始化、人脸识别

GET /profile/available_timezones 返回全部合法时区(配置 preferences 时用,实测列表含 Africa/Abidjan…Asia/Shanghai… 数百项,节选):

{"timezones": ["Africa/Abidjan", "Africa/Accra", …, "Asia/Shanghai", …]}

POST /onboarding 是首次启动创建管理员的入口(免认证 + onboarding 限流);本环境 onboarding_complete: true,重复调用无意义。GET /compreface/update_subjects 服务于 compreface 人脸识别组件的注册表刷新,未部署该组件时的实测回执:

curl -s -H "Authorization: Bearer $PAT" \
  http://127.0.0.1:8151/api/v1/compreface/update_subjects -w '\n[HTTP %{http_code}]\n'
{"status": 400, "error": "Compreface Recognition not initialized."}
[HTTP 400]

九、MQTT 集成

除 REST 外,Viseron 可启用 mqtt 组件把状态实时推给外部系统(Home Assistant 等)。本环境部署时已配置 mqtt 指向同网络的 Mosquitto,用 mosquitto_sub 抓取主题树(-v 显示主题名,image 主题是二进制会刷屏,过滤掉):

mosquitto_sub -h <MQTT服务器> -t 'viseron/#' -v -C 12 \
  | grep -v '^viseron/image/'

实测抓包(消息 = 主题 + 载荷原文):

viseron/lwt alive
viseron/state online
viseron/binary_sensor/fake_camera_connected/state {"state": "on", "attributes": {"name": "Fake Camera Connected", "domain": "binary_sensor"}}
viseron/binary_sensor/fake_camera_still_image_available/state {"state": "on", "attributes": {"name": "Fake Camera Still Image Available", "domain": "binary_sensor"}}
viseron/binary_sensor/fake_camera_recorder/state {"state": "off", "attributes": {"name": "Fake Camera Recorder", "domain": "binary_sensor", "id": 301, "start_time": "2026-10-30T03:27:07.109407+00:00", "start_timestamp": 1793330827.109407, "end_time": "2026-10-30T03:27:19.979850+00:00", "end_timestamp": 1793330839.97985, "date": "2026-10-30", "thumbnail_path": "/thumbnails/fake_camera/301.jpg", "objects": [], "trigger_type": "motion"}}
viseron/binary_sensor/fake_camera_motion_detected/state {"state": "off", "attributes": {"name": "Fake Camera Motion Detected", "domain": "binary_sensor"}}
viseron/binary_sensor/fake_camera_object_detected_person/state {"state": "off", "attributes": {"name": "Fake Camera Object Detected Person", "domain": "binary_sensor", "count": 0, "objects": []}}
viseron/binary_sensor/fake_camera_object_detected_bicycle/state {"state": "off", "attributes": {"name": "Fake Camera Object Detected Bicycle", "domain": "binary_sensor", "count": 0, "objects": []}}
viseron/binary_sensor/fake_camera_object_detected_car/state {"state": "off", "attributes": {"name": "Fake Camera Object Detected Car", "domain": "binary_sensor", "count": 0, "objects": []}}
viseron/binary_sensor/fake_camera_object_detected/state {"state": "off", "attributes": {"name": "Fake Camera Object Detected", "domain": "binary_sensor", "count": 0, "objects": []}}
viseron/toggle/fake_camera_connection/state {"state": "on", "attributes": {"name": "Fake Camera Connection", "domain": "toggle"}}
viseron/toggle/fake_camera_manual_recording/state {"state": "off", "attributes": {"name": "Fake Camera Manual Recording", "domain": "toggle"}}

主题树与用途:

主题载荷用途
viseron/lwtalive遗嘱主题,掉线时变 offline,用于存活监测
viseron/stateonline / offline服务级状态
viseron/binary_sensor/{camera}_connected/stateJSON:state + attributes摄像头连接状态
viseron/binary_sensor/{camera}_motion_detected/stateJSON运动检测布尔量
viseron/binary_sensor/{camera}_object_detected[_label]/stateJSON,attributes 含 count / objects物体检测布尔量(总开关 + 每标签一枚)
viseron/binary_sensor/{camera}_recorder/stateJSON,attributes 含 id / start_time / trigger_type / thumbnail_path录像机状态,一次录像起止都推
viseron/toggle/{camera}_connection/stateJSON远程启停摄像头(写回该主题即控制)
viseron/toggle/{camera}_manual_recording/stateJSON远程手动录像开关
viseron/image/{camera}_latest_thumbnail/imageJPEG 二进制最新缩略图原图推送

载荷统一是 {state, attributes} JSON,attributes 把 REST 里的事件字段(objects、trigger_type、thumbnail_path)一并带上,Home Assistant 的 MQTT discovery 可直接消化这些 entity。对接 HA 时把 home_assistant.discovery_prefix 配置好,实体自动出现(配置见《Viseron 3.7.0 配置参数详解》5.6 节)。注意 image 主题是原始 JPEG 二进制,文本终端订阅会显示乱码——订阅时避开(如 mosquitto_sub -t 'viseron/binary_sensor/#' -v)或用支持二进制的客户端落盘查看。

十、常见问题

Q1:脚本调 API 总是 401? 按序检查:① 是否带了 Authorization: Bearer <token> 头;② token 是不是完整的 vpat_ 开头(创建回执里的整串);③ 如果用的是 JWT——纯 Bearer JWT 不带 Cookie 必 401(3.2 节实测),脚本场景改用 PAT;④ 凭据被撤销或过期也会 401,用 GET /auth/request 快速探活。

Q2:登录返回 429? 登录限流 10 次 / 60 秒(3.6 节实测第 11 次起 429)。等待窗口过去,且排查为什么在连续登录失败——错误密码风暴本身往往说明上游有自动化在试。

Q3:POST 报 Invalid body: … required key not provided? Viseron 的参数校验器(voluptuous)会把期望 schema 打进错误信息,照着补字段即可。如 /auth/token 要求 grant_type(且只接受 refresh_token)+ client_id,/tune/{camera} 的 PUT 要求 domain + component。这类 400 回执是最可靠的接口文档。

Q4:/events/amount、/recordings/available_timespans 返回 404 "Camera amount not found"? 路由是 /events/{camera_identifier}/amount、/recordings/{camera_identifier}/... 形态——你把固定词写进了摄像头位(5.2、6.2 节实测负样本)。补上真实摄像头标识符即可。

Q5:删除录像 404 "Endpoint not found"? 删除按录像 ID 寻址:DELETE /recordings/{camera}/{id}。带日期段(/recordings/{camera}/{date}/{id})不是合法路由(6.2 节实测)。

Q6:改了 config.yaml,GET /config 里没变化? GET /config 运行时生效值。配置只在启动时加载,重启服务后 GET /config 才会变;想不重启临时调参用 PUT /tune/{camera}(8.2 节实测,不落盘)。

Q7:MQTT 抓包 image 主题全是乱码? 那是原始 JPEG 二进制(第九节),不是故障。订阅时排除 image 主题或用二进制客户端。

Q8:Web UI 上传的文件(快照/缩略图/录像分片)怎么直接下载? 事件与录像回执里的 /files/... 路径是静态路由,拼在 http://<服务器>:8888 后浏览器直接打开;脚本用 PAT 请求同一路径即可。/download 与 /publicimage 是给无凭据外部系统的 token 化出口(4.5 节)。

十一、参考链接