videoStore.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import { shallowRef } from 'vue'
  2. import { defineStore } from 'pinia'
  3. import { createVideoApi, VideoRequestError } from '../services/videoApi.js'
  4. import { createVideoLiveSession } from '../services/videoLiveSession.js'
  5. import { createVideoPlaybackSession, defaultHistoryWindow } from '../services/videoPlaybackSession.js'
  6. import { useDetailStore } from './detailStore.js'
  7. import { useEnhancementDevicesStore } from './enhancementDevicesStore.js'
  8. export const useVideoStore = defineStore('cockpitVideo', () => {
  9. const state = shallowRef({ open: false, device: null, ownerSource: 'detail', phase: 'idle', media: null, error: '', cleanupError: '',
  10. playbackMedia: null, historyPhase: '', historyError: '', records: [], empty: false })
  11. const playerOptions = shallowRef({})
  12. let options = {}, controller, history
  13. const publish = (patch) => { state.value = { ...state.value, ...patch } }
  14. // History is a separate viewer: its media/error/phase live under their own fields so the
  15. // live player never consumes a playback stream or vice versa.
  16. const publishHistory = ({ media, error, phase, controls, ...rest }) => {
  17. const next = { ...rest }
  18. if (media !== undefined) next.playbackMedia = media
  19. if (error !== undefined) next.historyError = error
  20. if (phase !== undefined) next.historyPhase = phase
  21. if (controls !== undefined) next.playbackControls = controls
  22. publish(next)
  23. }
  24. function configure(next = {}) {
  25. if (controller) throw new Error('请在建立视频会话前配置传输')
  26. options = next
  27. playerOptions.value = next.playerOptions || {}
  28. }
  29. const session = () => controller || (controller = createVideoLiveSession(createVideoApi(options), publish, options.timers))
  30. async function loadDevice(deviceCode) {
  31. const data = await createVideoApi(options).device(deviceCode)
  32. if (data.deviceCode !== deviceCode || typeof data.typeCode !== 'string' || typeof data.capabilities?.live !== 'boolean') {
  33. throw new VideoRequestError('INVALID_RESPONSE', '视频能力载荷与当前设备不匹配')
  34. }
  35. return data
  36. }
  37. async function open(device, ownerSource = 'detail') {
  38. if (!['detail', 'enhancement'].includes(ownerSource)) throw new Error('监控入口来源不合法')
  39. const selection = useDetailStore().selection
  40. const selectedCode = ownerSource === 'enhancement' ? useEnhancementDevicesStore().selected?.code
  41. : selection.level === 'equipment' ? selection.deviceCode : ''
  42. // A detached/late entry cannot take ownership from a still-valid viewer.
  43. if (!selectedCode || selectedCode !== device?.deviceCode) return
  44. if (state.value.open && state.value.device?.deviceCode === device?.deviceCode) { publish({ ownerSource }); return }
  45. publish({ open: true, device, ownerSource, error: '' })
  46. if (device?.typeCode !== '20' || device.bound !== true || device.capabilities?.live !== true) {
  47. publish({ phase: 'failed', error: '当前设备未确认实时视频能力' }); return
  48. }
  49. await session().start(device.deviceCode)
  50. }
  51. async function close() { publish({ open: false, playbackMedia: null, playbackControls: {} }); if (history) await history.dispose(); history = undefined; if (controller) await controller.stop() }
  52. /** Ends the live stream but keeps the monitor open; tab switches use this instead of close(). */
  53. async function stopLive() { if (controller) await controller.stop() }
  54. async function startLive() {
  55. const device = state.value.device
  56. if (!device?.deviceCode) return
  57. if (device.typeCode !== '20' || device.bound !== true || device.capabilities?.live !== true) {
  58. publish({ phase: 'failed', error: '当前设备未确认实时视频能力' }); return
  59. }
  60. await session().start(device.deviceCode)
  61. }
  62. const playback = () => history || (history = createVideoPlaybackSession(createVideoApi(options), publishHistory, options.timers))
  63. /** History only lists what the device/NVR returns; it never starts a stream by itself. */
  64. async function queryHistory(startTime, endTime) {
  65. const device = state.value.device
  66. if (!device?.deviceCode) return []
  67. const records = await playback().query(device.deviceCode, startTime, endTime)
  68. return records
  69. }
  70. async function play(startTime, endTime) {
  71. const device = state.value.device
  72. if (!device?.deviceCode) return false
  73. return playback().play(device.deviceCode, startTime, endTime)
  74. }
  75. async function stopPlayback() { if (history) await history.stop(); publish({ playbackMedia: null, playbackControls: {} }) }
  76. /** Only a confirmed control may be sent; the panel greys the rest out. */
  77. async function controlPlayback(key) { return history ? history.control(key) : false }
  78. const historyWindow = (now) => defaultHistoryWindow(now)
  79. async function retry() { if (state.value.device) await session().start(state.value.device.deviceCode) }
  80. async function retryCleanup() { if (controller) await controller.stop() }
  81. const ready = () => controller?.ready()
  82. // Keep the player's own failure code: without it a permanent browser/media error looks retryable.
  83. const fail = (error, code) => {
  84. if (code) controller?.fail(Object.assign(new Error(typeof error === 'string' ? error : error?.message || '视频播放失败'), { code }))
  85. else controller?.fail(error instanceof Error ? error : error?.message || '视频播放失败')
  86. }
  87. return { state, playerOptions, configure, loadDevice, open, close, retry, retryCleanup, ready, fail,
  88. stopLive, startLive, queryHistory, play, stopPlayback, controlPlayback, historyWindow }
  89. })