import { shallowRef } from 'vue' import { defineStore } from 'pinia' import { createVideoApi, VideoRequestError } from '../services/videoApi.js' import { createVideoLiveSession } from '../services/videoLiveSession.js' import { createVideoPlaybackSession, defaultHistoryWindow } from '../services/videoPlaybackSession.js' import { useDetailStore } from './detailStore.js' import { useEnhancementDevicesStore } from './enhancementDevicesStore.js' export const useVideoStore = defineStore('cockpitVideo', () => { const state = shallowRef({ open: false, device: null, ownerSource: 'detail', phase: 'idle', media: null, error: '', cleanupError: '', playbackMedia: null, historyPhase: '', historyError: '', records: [], empty: false }) const playerOptions = shallowRef({}) let options = {}, controller, history const publish = (patch) => { state.value = { ...state.value, ...patch } } // History is a separate viewer: its media/error/phase live under their own fields so the // live player never consumes a playback stream or vice versa. const publishHistory = ({ media, error, phase, controls, ...rest }) => { const next = { ...rest } if (media !== undefined) next.playbackMedia = media if (error !== undefined) next.historyError = error if (phase !== undefined) next.historyPhase = phase if (controls !== undefined) next.playbackControls = controls publish(next) } function configure(next = {}) { if (controller) throw new Error('请在建立视频会话前配置传输') options = next playerOptions.value = next.playerOptions || {} } const session = () => controller || (controller = createVideoLiveSession(createVideoApi(options), publish, options.timers)) async function loadDevice(deviceCode) { const data = await createVideoApi(options).device(deviceCode) if (data.deviceCode !== deviceCode || typeof data.typeCode !== 'string' || typeof data.capabilities?.live !== 'boolean') { throw new VideoRequestError('INVALID_RESPONSE', '视频能力载荷与当前设备不匹配') } return data } async function open(device, ownerSource = 'detail') { if (!['detail', 'enhancement'].includes(ownerSource)) throw new Error('监控入口来源不合法') const selection = useDetailStore().selection const selectedCode = ownerSource === 'enhancement' ? useEnhancementDevicesStore().selected?.code : selection.level === 'equipment' ? selection.deviceCode : '' // A detached/late entry cannot take ownership from a still-valid viewer. if (!selectedCode || selectedCode !== device?.deviceCode) return if (state.value.open && state.value.device?.deviceCode === device?.deviceCode) { publish({ ownerSource }); return } publish({ open: true, device, ownerSource, error: '' }) if (device?.typeCode !== '20' || device.bound !== true || device.capabilities?.live !== true) { publish({ phase: 'failed', error: '当前设备未确认实时视频能力' }); return } await session().start(device.deviceCode) } async function close() { publish({ open: false, playbackMedia: null, playbackControls: {} }); if (history) await history.dispose(); history = undefined; if (controller) await controller.stop() } /** Ends the live stream but keeps the monitor open; tab switches use this instead of close(). */ async function stopLive() { if (controller) await controller.stop() } async function startLive() { const device = state.value.device if (!device?.deviceCode) return if (device.typeCode !== '20' || device.bound !== true || device.capabilities?.live !== true) { publish({ phase: 'failed', error: '当前设备未确认实时视频能力' }); return } await session().start(device.deviceCode) } const playback = () => history || (history = createVideoPlaybackSession(createVideoApi(options), publishHistory, options.timers)) /** History only lists what the device/NVR returns; it never starts a stream by itself. */ async function queryHistory(startTime, endTime) { const device = state.value.device if (!device?.deviceCode) return [] const records = await playback().query(device.deviceCode, startTime, endTime) return records } async function play(startTime, endTime) { const device = state.value.device if (!device?.deviceCode) return false return playback().play(device.deviceCode, startTime, endTime) } async function stopPlayback() { if (history) await history.stop(); publish({ playbackMedia: null, playbackControls: {} }) } /** Only a confirmed control may be sent; the panel greys the rest out. */ async function controlPlayback(key) { return history ? history.control(key) : false } const historyWindow = (now) => defaultHistoryWindow(now) async function retry() { if (state.value.device) await session().start(state.value.device.deviceCode) } async function retryCleanup() { if (controller) await controller.stop() } const ready = () => controller?.ready() // Keep the player's own failure code: without it a permanent browser/media error looks retryable. const fail = (error, code) => { if (code) controller?.fail(Object.assign(new Error(typeof error === 'string' ? error : error?.message || '视频播放失败'), { code })) else controller?.fail(error instanceof Error ? error : error?.message || '视频播放失败') } return { state, playerOptions, configure, loadDevice, open, close, retry, retryCleanup, ready, fail, stopLive, startLive, queryHistory, play, stopPlayback, controlPlayback, historyWindow } })