/** * 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 { 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>( '/video/proxy/list', { params }, ); } /** 新增摄像头 */ export function addCamera(data: Partial) { return wvpRequestClient.post('/video/proxy/add', data); } /** 编辑摄像头 */ export function updateCamera(data: Partial) { return wvpRequestClient.post('/video/proxy/update', data); } /** 保存摄像头(新增/编辑,有 id 为编辑) */ export function saveCamera(data: Partial) { 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( '/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>( '/video/ai/roi/list', { params }, ); } /** ROI 详情(含算法绑定) */ export function getRoiDetail(id: number) { return wvpRequestClient.get(`/video/ai/roi/${id}`); } /** 某摄像头的所有 ROI */ export function getRoiByCameraId(cameraId: string) { return wvpRequestClient.get( '/video/ai/roi/channel', { params: { cameraId } }, ); } /** 保存 ROI(新增/编辑) */ export function saveRoi(data: Partial) { 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 会被 使用,浏览器不会带上 Authorization 头; * 该端点必须在后端满足以下任一条件之一才能正常显示: * (a) SecurityConfig 里对 /video/ai/roi/snap/image 放行(permitAll); * (b) 依赖 yudao session cookie(非 JWT)做鉴权; * (c) 后端返回预签名的 OSS URL 并由调用方改造成 302 重定向。 * - 任何 401/403 不会在这里抛出,调用方需要处理 以显示占位图。 */ export async function getSnapUrl(cameraCode: string, force = false): Promise { 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( '/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>( '/video/ai/config/push-all', ); } /** 导出摄像头配置 JSON */ export function exportConfig(cameraId: string) { return wvpRequestClient.get>( '/video/ai/config/export', { params: { cameraId } }, ); }