| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- 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 }
- })
|