usePTZ.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. import { ref, type Ref } from 'vue'
  2. import { ElMessage, ElMessageBox } from 'element-plus'
  3. import { useI18n } from 'vue-i18n'
  4. import {
  5. type PresetInfo,
  6. presetList,
  7. presetGoto,
  8. presetSet,
  9. presetRemove,
  10. getPTZCapabilities,
  11. ptzControl
  12. } from '@/api/camera'
  13. import type { LiveStreamDTO, PTZAction } from '@/types'
  14. import type { PTZCapabilities, LocalPreset, TimelinePoint } from '../types'
  15. const directionToAction: Record<string, PTZAction> = {
  16. UP: 'up',
  17. DOWN: 'down',
  18. LEFT: 'left',
  19. RIGHT: 'right',
  20. STOP: 'stop'
  21. }
  22. export function usePTZ(
  23. currentMediaStream: Ref<LiveStreamDTO | null>,
  24. timelinePoints: Ref<TimelinePoint[]>,
  25. callbacks: {
  26. sortAndRenumberPoints: () => void
  27. saveTimelineConfig: () => void
  28. selectedPoint: Ref<TimelinePoint | null>
  29. }
  30. ) {
  31. const { t } = useI18n({ useScope: 'global' })
  32. const ptzSpeed = ref(50)
  33. const zoomValue = ref(0)
  34. const ptzPresetList = ref<PresetInfo[]>([])
  35. const activePresetId = ref<string | null>(null)
  36. const editingPresetId = ref<string | null>(null)
  37. const editingPresetName = ref('')
  38. const cameraCapabilities = ref<PTZCapabilities | null>(null)
  39. const capabilitiesLoading = ref(false)
  40. const presetsLoading = ref(false)
  41. const activePanels = ref(['ptz', 'preset', 'camera'])
  42. function hasCameraConnection(): boolean {
  43. return !!currentMediaStream.value?.cameraId
  44. }
  45. async function handlePTZ(direction: string) {
  46. const cameraId = currentMediaStream.value?.cameraId
  47. if (!cameraId) {
  48. ElMessage.warning(t('请先选择直播流'))
  49. return
  50. }
  51. try {
  52. const command = directionToAction[direction] || 'stop'
  53. const res = await ptzControl({ cameraId, command, speed: ptzSpeed.value })
  54. if (!res.success) {
  55. console.error('PTZ 控制失败', res.errMsg)
  56. }
  57. } catch (error) {
  58. console.error('PTZ 控制失败', error)
  59. }
  60. }
  61. async function handlePTZStop() {
  62. const cameraId = currentMediaStream.value?.cameraId
  63. if (!cameraId) return
  64. try {
  65. await ptzControl({ cameraId, command: 'stop' })
  66. } catch (error) {
  67. console.error('PTZ 停止失败', error)
  68. }
  69. }
  70. function formatZoomTooltip(val: number) {
  71. if (val === 0) return t('停止')
  72. return val > 0 ? `${t('放大')} ${val}` : `${t('缩小')} ${Math.abs(val)}`
  73. }
  74. async function handleZoomChange(val: number) {
  75. const cameraId = currentMediaStream.value?.cameraId
  76. if (!cameraId) return
  77. if (val === 0) {
  78. await ptzControl({ cameraId, command: 'stop' })
  79. return
  80. }
  81. const command = val > 0 ? 'zoom_in' : 'zoom_out'
  82. await ptzControl({ cameraId, command, speed: Math.abs(val) })
  83. }
  84. async function handleZoomRelease() {
  85. zoomValue.value = 0
  86. const cameraId = currentMediaStream.value?.cameraId
  87. if (!cameraId) return
  88. await ptzControl({ cameraId, command: 'stop' })
  89. }
  90. async function handleZoomIn() {
  91. const cameraId = currentMediaStream.value?.cameraId
  92. if (!cameraId) {
  93. ElMessage.warning(t('请先选择直播流'))
  94. return
  95. }
  96. try {
  97. const res = await ptzControl({ cameraId, command: 'zoom_in', speed: ptzSpeed.value })
  98. if (!res.success) console.error('Zoom in 失败', res.errMsg)
  99. } catch (error) {
  100. console.error('Zoom in 失败', error)
  101. }
  102. }
  103. async function handleZoomOut() {
  104. const cameraId = currentMediaStream.value?.cameraId
  105. if (!cameraId) {
  106. ElMessage.warning(t('请先选择直播流'))
  107. return
  108. }
  109. try {
  110. const res = await ptzControl({ cameraId, command: 'zoom_out', speed: ptzSpeed.value })
  111. if (!res.success) console.error('Zoom out 失败', res.errMsg)
  112. } catch (error) {
  113. console.error('Zoom out 失败', error)
  114. }
  115. }
  116. async function loadPTZPresets() {
  117. const cameraId = currentMediaStream.value?.cameraId
  118. if (!cameraId) {
  119. ElMessage.warning(t('请先选择直播流'))
  120. return
  121. }
  122. presetsLoading.value = true
  123. try {
  124. const res = await presetList({ cameraId })
  125. if (res.code === 200 && res.data) {
  126. ptzPresetList.value = res.data as PresetInfo[]
  127. } else {
  128. ptzPresetList.value = []
  129. if (!res.success) ElMessage.error(res.errMsg || t('加载预置位失败'))
  130. }
  131. } catch (error) {
  132. console.error('加载 PTZ 预置位失败', error)
  133. ptzPresetList.value = []
  134. } finally {
  135. presetsLoading.value = false
  136. }
  137. }
  138. async function handleGotoPTZPreset(preset: PresetInfo) {
  139. const cameraId = currentMediaStream.value?.cameraId
  140. if (!cameraId) {
  141. ElMessage.warning(t('请先配置摄像头连接'))
  142. return
  143. }
  144. try {
  145. activePresetId.value = preset.id
  146. const res = await presetGoto({ cameraId, presetId: parseInt(preset.id) })
  147. if (res.code === 200) {
  148. ElMessage.success(`${t('已跳转到预置位')}: ${preset.name || preset.id}`)
  149. } else {
  150. ElMessage.error(res.errMsg || t('跳转失败'))
  151. }
  152. } catch (error) {
  153. console.error('跳转 PTZ 预置位失败', error)
  154. ElMessage.error(t('跳转失败'))
  155. }
  156. }
  157. async function handleEditPreset(preset: { id: string; name: string }) {
  158. const cameraId = currentMediaStream.value?.cameraId
  159. if (!cameraId) {
  160. ElMessage.warning(t('请先配置摄像头连接'))
  161. return
  162. }
  163. try {
  164. await ElMessageBox.confirm(
  165. `${t('将当前摄像头位置保存到预置位')} "${preset.name || `Preset ${preset.id}`}"?`,
  166. t('设置预置位'),
  167. { type: 'info' }
  168. )
  169. const res = await presetSet({
  170. cameraId,
  171. presetId: parseInt(preset.id),
  172. presetName: preset.name || `Preset ${preset.id}`
  173. })
  174. if (res.code === 200) {
  175. ElMessage.success(`${t('预置位设置成功')}: ${preset.name || `Preset ${preset.id}`}`)
  176. } else {
  177. ElMessage.error(res.errMsg || t('设置失败'))
  178. }
  179. } catch (error) {
  180. if ((error as Error).toString().includes('cancel')) return
  181. console.error('设置预置位失败', error)
  182. ElMessage.error(t('设置失败'))
  183. }
  184. }
  185. async function handleDeletePreset(preset: { id: string; name: string }) {
  186. const cameraId = currentMediaStream.value?.cameraId
  187. if (!cameraId) {
  188. ElMessage.warning(t('请先配置摄像头连接'))
  189. return
  190. }
  191. try {
  192. await ElMessageBox.confirm(`${t('确定删除预置位')} "${preset.name || `Preset ${preset.id}`}"?`, t('删除确认'), {
  193. type: 'warning'
  194. })
  195. const res = await presetRemove({ cameraId, presetId: parseInt(preset.id) })
  196. if (res.success) {
  197. ElMessage.success(t('删除成功'))
  198. loadPTZPresets()
  199. } else {
  200. ElMessage.error(res.errMsg || t('删除失败'))
  201. }
  202. } catch (error) {
  203. if (error !== 'cancel') {
  204. console.error('删除预置位失败', error)
  205. ElMessage.error(t('删除失败'))
  206. }
  207. }
  208. }
  209. // Local preset operations (based on timelinePoints)
  210. function startEditPresetName(preset: LocalPreset) {
  211. editingPresetId.value = preset.id
  212. editingPresetName.value = preset.name
  213. }
  214. function cancelEditPresetName() {
  215. editingPresetId.value = null
  216. editingPresetName.value = ''
  217. }
  218. function savePresetName(preset: LocalPreset) {
  219. const newName = editingPresetName.value.trim()
  220. if (!newName || newName === preset.name) {
  221. cancelEditPresetName()
  222. return
  223. }
  224. const point = timelinePoints.value.find((p) => p.id === preset.pointId)
  225. if (!point) {
  226. cancelEditPresetName()
  227. return
  228. }
  229. point.presetName = newName
  230. callbacks.saveTimelineConfig()
  231. cancelEditPresetName()
  232. }
  233. async function handleGotoLocalPreset(preset: LocalPreset) {
  234. const cameraId = currentMediaStream.value?.cameraId
  235. if (!cameraId) {
  236. ElMessage.warning(t('请先配置摄像头连接'))
  237. return
  238. }
  239. const point = timelinePoints.value.find((p) => p.id === preset.pointId)
  240. if (!point?.presetId) {
  241. ElMessage.warning(t('未找到对应的预置位'))
  242. return
  243. }
  244. try {
  245. activePresetId.value = preset.id
  246. const res = await presetGoto({ cameraId, presetId: point.presetId })
  247. if (res.code === 200) {
  248. ElMessage.success(`${t('已跳转到')}: ${preset.name}`)
  249. } else {
  250. ElMessage.error(res.errMsg || t('跳转失败'))
  251. }
  252. } catch (error) {
  253. console.error('跳转预置位失败', error)
  254. ElMessage.error(t('跳转失败'))
  255. }
  256. }
  257. async function handleUpdateLocalPreset(preset: LocalPreset) {
  258. const cameraId = currentMediaStream.value?.cameraId
  259. if (!cameraId) {
  260. ElMessage.warning(t('请先配置摄像头连接'))
  261. return
  262. }
  263. const point = timelinePoints.value.find((p) => p.id === preset.pointId)
  264. if (!point?.presetId) {
  265. ElMessage.warning(t('未找到对应的预置位'))
  266. return
  267. }
  268. try {
  269. await ElMessageBox.confirm(`${t('将当前摄像头位置保存到预置位')} "${preset.name}"?`, t('设置预置位'), {
  270. type: 'info'
  271. })
  272. const res = await presetSet({
  273. cameraId,
  274. presetId: point.presetId,
  275. presetName: preset.name
  276. })
  277. if (res.code === 200) {
  278. ElMessage.success(`${t('预置位设置成功')}: ${preset.name}`)
  279. } else {
  280. ElMessage.error(res.errMsg || t('设置失败'))
  281. }
  282. } catch (error) {
  283. if ((error as Error).toString().includes('cancel')) return
  284. console.error('设置预置位失败', error)
  285. ElMessage.error(t('设置失败'))
  286. }
  287. }
  288. async function handleDeleteLocalPreset(preset: LocalPreset) {
  289. const cameraId = currentMediaStream.value?.cameraId
  290. if (!cameraId) {
  291. ElMessage.warning(t('请先配置摄像头连接'))
  292. return
  293. }
  294. const point = timelinePoints.value.find((p) => p.id === preset.pointId)
  295. if (!point?.presetId) {
  296. ElMessage.warning(t('未找到对应的预置位'))
  297. return
  298. }
  299. try {
  300. await ElMessageBox.confirm(`${t('确定删除预置位')} "${preset.name}"?`, t('删除确认'), {
  301. type: 'warning'
  302. })
  303. const res = await presetRemove({ cameraId, presetId: point.presetId })
  304. if (res.success) {
  305. const index = timelinePoints.value.findIndex((p) => p.id === preset.pointId)
  306. if (index !== -1) {
  307. timelinePoints.value.splice(index, 1)
  308. callbacks.sortAndRenumberPoints()
  309. callbacks.saveTimelineConfig()
  310. if (callbacks.selectedPoint.value?.id === preset.pointId) {
  311. callbacks.selectedPoint.value = null
  312. }
  313. }
  314. ElMessage.success(t('删除成功'))
  315. } else {
  316. ElMessage.error(res.errMsg || t('删除失败'))
  317. }
  318. } catch (error) {
  319. if (error !== 'cancel') {
  320. console.error('删除预置位失败', error)
  321. ElMessage.error(t('删除失败'))
  322. }
  323. }
  324. }
  325. async function loadCameraCapabilities() {
  326. const cameraId = currentMediaStream.value?.cameraId
  327. if (!cameraId) return
  328. capabilitiesLoading.value = true
  329. try {
  330. const res = await getPTZCapabilities({ cameraId })
  331. if (res.success && res.data) {
  332. cameraCapabilities.value = res.data as PTZCapabilities
  333. } else {
  334. cameraCapabilities.value = null
  335. }
  336. } catch (error) {
  337. console.error('加载摄像头能力失败', error)
  338. cameraCapabilities.value = null
  339. } finally {
  340. capabilitiesLoading.value = false
  341. }
  342. }
  343. return {
  344. ptzSpeed,
  345. zoomValue,
  346. ptzPresetList,
  347. activePresetId,
  348. editingPresetId,
  349. editingPresetName,
  350. cameraCapabilities,
  351. capabilitiesLoading,
  352. presetsLoading,
  353. activePanels,
  354. hasCameraConnection,
  355. handlePTZ,
  356. handlePTZStop,
  357. formatZoomTooltip,
  358. handleZoomChange,
  359. handleZoomRelease,
  360. handleZoomIn,
  361. handleZoomOut,
  362. loadPTZPresets,
  363. handleGotoPTZPreset,
  364. handleEditPreset,
  365. handleDeletePreset,
  366. startEditPresetName,
  367. cancelEditPresetName,
  368. savePresetName,
  369. handleGotoLocalPreset,
  370. handleUpdateLocalPreset,
  371. handleDeleteLocalPreset,
  372. loadCameraCapabilities
  373. }
  374. }