- api/video/request.ts: 删除自实现的 WVP access-token 登录/缓存/401 续期逻辑,
直接 re-export requestClient(yudao Authorization: Bearer),
wvpRequestClient 名字仅作过渡期别名,TODO 标记后续统一重命名。
- api/video/device/index.ts: 路径从 /video/device/{proxy,user,server,...} 迁移到
后端 @RequestMapping 对齐的 /video/{proxy,ai/*,server/media_server};
删除 getAlertImageUrl(老 WVP 时代产物),删除截图 URL 里的 ?access-token。
- api/video/edge/index.ts: /video/device/device/* -> /video/ai/device/*(对齐 AiEdgeDeviceController)。
- views/video/device/camera: HEAD 探活 URL 同步,补注释说明后端需对
/video/ai/roi/snap/image permitAll 或依赖 session cookie。
- vite.config: 删除 /admin-api/video/device/* 按子路径 rewrite 到 WVP:18080 的规则,
统一走 /admin-api 到芋道网关 48080;去掉 VITE_WVP_USERNAME/PASSWORD_MD5 依赖
(安全改进:凭据不再打进客户端 bundle)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
314 lines
8.6 KiB
TypeScript
314 lines
8.6 KiB
TypeScript
/**
|
||
* Video 模块 - 设备管理 API
|
||
*
|
||
* 请求走统一的 requestClient(yudao 框架鉴权:Authorization: Bearer)。
|
||
* video 微服务已接入 gateway,路径对齐后端 @RequestMapping:
|
||
* /video/proxy/* 拉流代理(StreamProxyController)
|
||
* /video/server/* 媒体服务器(ServerController)
|
||
* /video/ai/camera/* 摄像头
|
||
* /video/ai/roi/* ROI 及算法绑定
|
||
* /video/ai/algorithm/* 算法
|
||
* /video/ai/config/* 配置推送
|
||
*/
|
||
import { useAppConfig } from '@vben/hooks';
|
||
|
||
import { wvpRequestClient } from '#/api/video/request';
|
||
|
||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
export namespace VideoDeviceApi {
|
||
/** 分页响应结构 */
|
||
export interface PageResult<T> {
|
||
list: T[];
|
||
total: number;
|
||
}
|
||
|
||
/** 摄像头(拉流代理) */
|
||
export interface Camera {
|
||
id?: number;
|
||
type?: string; // 'default' | 'ffmpeg'
|
||
app?: string;
|
||
stream?: string;
|
||
cameraCode?: string; // 摄像头唯一编码
|
||
cameraName?: string; // 摄像头名称(用户自定义)
|
||
edgeDeviceId?: string; // 绑定的边缘设备ID
|
||
srcUrl?: string;
|
||
timeout?: number;
|
||
rtspType?: string; // '0'=TCP, '1'=UDP, '2'=Multicast
|
||
enable?: boolean;
|
||
enableAudio?: boolean;
|
||
enableMp4?: boolean;
|
||
enableDisableNoneReader?: boolean;
|
||
relatesMediaServerId?: string;
|
||
ffmpegCmdKey?: string;
|
||
pulling?: boolean;
|
||
mediaServerId?: string;
|
||
streamKey?: string;
|
||
createTime?: string;
|
||
}
|
||
|
||
/** ROI 区域 */
|
||
export interface Roi {
|
||
id?: number;
|
||
roiId?: string;
|
||
cameraId?: string;
|
||
deviceId?: string;
|
||
name?: string;
|
||
roiType?: string; // 'rectangle' | 'polygon'
|
||
coordinates?: string;
|
||
color?: string;
|
||
priority?: number;
|
||
enabled?: number; // 0 | 1
|
||
description?: string;
|
||
algorithms?: RoiAlgoBinding[];
|
||
}
|
||
|
||
/** ROI 算法绑定(详情嵌套结构) */
|
||
export interface RoiAlgoBinding {
|
||
bind: AlgoBind;
|
||
algorithm?: Algorithm;
|
||
}
|
||
|
||
/** 算法绑定记录 */
|
||
export interface AlgoBind {
|
||
bindId?: string;
|
||
roiId?: string;
|
||
algoCode?: string;
|
||
enabled?: number; // 0 | 1
|
||
params?: string;
|
||
}
|
||
|
||
/** 算法定义 */
|
||
export interface Algorithm {
|
||
id?: number;
|
||
algoCode?: string;
|
||
algoName?: string;
|
||
description?: string;
|
||
isActive?: boolean;
|
||
paramSchema?: string;
|
||
globalParams?: string;
|
||
}
|
||
|
||
/** 媒体服务器 */
|
||
export interface MediaServer {
|
||
id: string;
|
||
ip: string;
|
||
}
|
||
}
|
||
|
||
// ==================== 摄像头管理 ====================
|
||
|
||
/** 摄像头列表(分页) */
|
||
export function getCameraList(params: {
|
||
page: number;
|
||
count: number;
|
||
query?: string;
|
||
pulling?: boolean;
|
||
}) {
|
||
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Camera>>(
|
||
'/video/proxy/list',
|
||
{ params },
|
||
);
|
||
}
|
||
|
||
/** 新增摄像头 */
|
||
export function addCamera(data: Partial<VideoDeviceApi.Camera>) {
|
||
return wvpRequestClient.post('/video/proxy/add', data);
|
||
}
|
||
|
||
/** 编辑摄像头 */
|
||
export function updateCamera(data: Partial<VideoDeviceApi.Camera>) {
|
||
return wvpRequestClient.post('/video/proxy/update', data);
|
||
}
|
||
|
||
/** 保存摄像头(新增/编辑,有 id 为编辑) */
|
||
export function saveCamera(data: Partial<VideoDeviceApi.Camera>) {
|
||
return data.id ? updateCamera(data) : addCamera(data);
|
||
}
|
||
|
||
/** 删除摄像头 */
|
||
export function deleteCamera(id: number) {
|
||
return wvpRequestClient.delete('/video/proxy/delete', {
|
||
params: { id },
|
||
});
|
||
}
|
||
|
||
/** 开始拉流 */
|
||
export function startCamera(id: number) {
|
||
return wvpRequestClient.get('/video/proxy/start', { params: { id } });
|
||
}
|
||
|
||
/** 停止拉流 */
|
||
export function stopCamera(id: number) {
|
||
return wvpRequestClient.get('/video/proxy/stop', { params: { id } });
|
||
}
|
||
|
||
/** 在线媒体服务器列表 */
|
||
export function getMediaServerList() {
|
||
return wvpRequestClient.get<VideoDeviceApi.MediaServer[]>(
|
||
'/video/server/media_server/online/list',
|
||
);
|
||
}
|
||
|
||
/** 摄像头选项列表(用于下拉搜索选择) */
|
||
export function getCameraOptions() {
|
||
return wvpRequestClient.get<
|
||
{ cameraCode: string; cameraName: string }[]
|
||
>('/video/ai/camera/options');
|
||
}
|
||
|
||
// ==================== ROI 区域管理 ====================
|
||
|
||
/** ROI 列表(分页) */
|
||
export function getRoiList(params: {
|
||
page: number;
|
||
count: number;
|
||
cameraId?: string;
|
||
deviceId?: string;
|
||
query?: string;
|
||
}) {
|
||
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Roi>>(
|
||
'/video/ai/roi/list',
|
||
{ params },
|
||
);
|
||
}
|
||
|
||
/** ROI 详情(含算法绑定) */
|
||
export function getRoiDetail(id: number) {
|
||
return wvpRequestClient.get<VideoDeviceApi.Roi>(`/video/ai/roi/${id}`);
|
||
}
|
||
|
||
/** 某摄像头的所有 ROI */
|
||
export function getRoiByCameraId(cameraId: string) {
|
||
return wvpRequestClient.get<VideoDeviceApi.Roi[]>(
|
||
'/video/ai/roi/channel',
|
||
{ params: { cameraId } },
|
||
);
|
||
}
|
||
|
||
/** 保存 ROI(新增/编辑) */
|
||
export function saveRoi(data: Partial<VideoDeviceApi.Roi>) {
|
||
return wvpRequestClient.post('/video/ai/roi/save', data);
|
||
}
|
||
|
||
/** 删除 ROI */
|
||
export function deleteRoi(roiId: string) {
|
||
return wvpRequestClient.delete(`/video/ai/roi/delete/${roiId}`);
|
||
}
|
||
|
||
/**
|
||
* 获取摄像头截图 URL
|
||
*
|
||
* 非 force:返回代理 URL,浏览器按 URL 缓存。
|
||
* force:先触发边缘端截图,再返回加时间戳的代理 URL 破缓存。
|
||
*
|
||
* 图片鉴权假设(重要):
|
||
* - 老 WVP 的 ?access-token=xxx 已移除。
|
||
* - 返回的 URL 会被 <img src> 使用,浏览器不会带上 Authorization 头;
|
||
* 该端点必须在后端满足以下任一条件之一才能正常显示:
|
||
* (a) SecurityConfig 里对 /video/ai/roi/snap/image 放行(permitAll);
|
||
* (b) 依赖 yudao session cookie(非 JWT)做鉴权;
|
||
* (c) 后端返回预签名的 OSS URL 并由调用方改造成 302 重定向。
|
||
* - 任何 401/403 不会在这里抛出,调用方需要处理 <img onerror> 以显示占位图。
|
||
*/
|
||
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
|
||
if (force) {
|
||
// 强制刷新:先触发边缘端截图(等待完成)
|
||
try {
|
||
await wvpRequestClient.get('/video/ai/roi/snap', {
|
||
params: { cameraCode, force: true },
|
||
});
|
||
} catch (err) {
|
||
// 截图请求可能超时,但 OSS 上可能已有图片,继续返回代理 URL
|
||
if (import.meta.env.DEV) {
|
||
console.debug('[video] force snap 触发失败,继续返回已有代理 URL', err);
|
||
}
|
||
}
|
||
return (
|
||
`${apiURL}/video/ai/roi/snap/image` +
|
||
`?cameraCode=${encodeURIComponent(cameraCode)}` +
|
||
`&t=${Date.now()}`
|
||
);
|
||
}
|
||
return (
|
||
`${apiURL}/video/ai/roi/snap/image` +
|
||
`?cameraCode=${encodeURIComponent(cameraCode)}`
|
||
);
|
||
}
|
||
|
||
// ==================== 算法管理 ====================
|
||
|
||
/** 算法列表 */
|
||
export function getAlgorithmList(deviceId?: string) {
|
||
return wvpRequestClient.get<VideoDeviceApi.Algorithm[]>(
|
||
'/video/ai/algorithm/list',
|
||
{ params: deviceId ? { deviceId } : {} },
|
||
);
|
||
}
|
||
|
||
/** 保存算法全局参数 */
|
||
export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
|
||
return wvpRequestClient.post(
|
||
`/video/ai/algorithm/global-params/${algoCode}`,
|
||
{ globalParams },
|
||
);
|
||
}
|
||
|
||
/** 批量更新设备算法参数 */
|
||
export function updateDeviceAlgoParams(deviceId: string, algoCode: string, params: string) {
|
||
return wvpRequestClient.post(
|
||
`/video/ai/algorithm/device-binds/${deviceId}/${algoCode}`,
|
||
{ params },
|
||
);
|
||
}
|
||
|
||
// ==================== 算法绑定 ====================
|
||
|
||
/** 绑定算法到 ROI */
|
||
export function bindAlgo(data: { roiId: string; algoCode: string }) {
|
||
return wvpRequestClient.post('/video/ai/roi/bindAlgo', data);
|
||
}
|
||
|
||
/** 解绑算法 */
|
||
export function unbindAlgo(bindId: string) {
|
||
return wvpRequestClient.delete('/video/ai/roi/unbindAlgo', {
|
||
params: { bindId },
|
||
});
|
||
}
|
||
|
||
/** 更新算法绑定参数 */
|
||
export function updateAlgoParams(data: {
|
||
bindId: string;
|
||
params?: string;
|
||
enabled?: number;
|
||
}) {
|
||
return wvpRequestClient.post('/video/ai/roi/updateAlgoParams', data);
|
||
}
|
||
|
||
// ==================== 配置推送 ====================
|
||
|
||
/** 推送配置到边缘端 */
|
||
export function pushConfig(cameraId: string) {
|
||
return wvpRequestClient.post('/video/ai/config/push', null, {
|
||
params: { cameraId },
|
||
});
|
||
}
|
||
|
||
/** 一次性推送全部配置到本地Edge */
|
||
export function pushAllConfig() {
|
||
return wvpRequestClient.post<Record<string, any>>(
|
||
'/video/ai/config/push-all',
|
||
);
|
||
}
|
||
|
||
/** 导出摄像头配置 JSON */
|
||
export function exportConfig(cameraId: string) {
|
||
return wvpRequestClient.get<Record<string, any>>(
|
||
'/video/ai/config/export',
|
||
{ params: { cameraId } },
|
||
);
|
||
}
|
||
|