|
|
@@ -0,0 +1,258 @@
|
|
|
+import { ref, computed, type Ref } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
+import { addWaypoints, updateWaypoints } from '@/api/waypoint'
|
|
|
+import type { WaypointItem, WaypointPayload, LiveStreamDTO } from '@/types'
|
|
|
+import { ptzControl } from '@/api/camera'
|
|
|
+
|
|
|
+export function useWaypointRecording(currentMediaStream: Ref<LiveStreamDTO | null>) {
|
|
|
+ const { t } = useI18n({ useScope: 'global' })
|
|
|
+
|
|
|
+ const isRecording = ref(false)
|
|
|
+ const recordingStartTime = ref(0)
|
|
|
+ const recordedWaypoints = ref<WaypointItem[]>([])
|
|
|
+ const currentWaypoint = ref<Partial<WaypointItem> | null>(null)
|
|
|
+ const totalDuration = ref(0)
|
|
|
+ const elapsedTime = ref(0)
|
|
|
+ const waypointName = ref('')
|
|
|
+ const waypointDescription = ref('')
|
|
|
+ const isWaypointPlaying = ref(false)
|
|
|
+ const waypointProgress = ref(0)
|
|
|
+ const loopEnabled = ref(false)
|
|
|
+ const loopCount = ref(1)
|
|
|
+ const savedWaypointId = ref<string | null>(null)
|
|
|
+ const showDetailDrawer = ref(false)
|
|
|
+
|
|
|
+ let timerInterval: ReturnType<typeof setInterval> | null = null
|
|
|
+ let playbackTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
+ let playbackAbort = false
|
|
|
+
|
|
|
+ const hasRecordedData = computed(() => recordedWaypoints.value.length > 0 && !isRecording.value)
|
|
|
+
|
|
|
+ function startRecording() {
|
|
|
+ isRecording.value = true
|
|
|
+ recordingStartTime.value = Date.now()
|
|
|
+ recordedWaypoints.value = []
|
|
|
+ currentWaypoint.value = null
|
|
|
+ totalDuration.value = 0
|
|
|
+ elapsedTime.value = 0
|
|
|
+ savedWaypointId.value = null
|
|
|
+
|
|
|
+ timerInterval = setInterval(() => {
|
|
|
+ elapsedTime.value = Date.now() - recordingStartTime.value
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+
|
|
|
+ function stopRecording() {
|
|
|
+ isRecording.value = false
|
|
|
+ totalDuration.value = Date.now() - recordingStartTime.value
|
|
|
+ if (currentWaypoint.value) {
|
|
|
+ completeCurrentWaypoint()
|
|
|
+ }
|
|
|
+ if (timerInterval) {
|
|
|
+ clearInterval(timerInterval)
|
|
|
+ timerInterval = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function captureWaypoint(action: string, speed: number) {
|
|
|
+ if (!isRecording.value) return
|
|
|
+ currentWaypoint.value = {
|
|
|
+ id: recordedWaypoints.value.length + 1,
|
|
|
+ startTime: Date.now() - recordingStartTime.value,
|
|
|
+ action,
|
|
|
+ speed,
|
|
|
+ duration: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function completeCurrentWaypoint() {
|
|
|
+ if (!currentWaypoint.value) return
|
|
|
+ const wp = currentWaypoint.value
|
|
|
+ wp.duration = Date.now() - recordingStartTime.value - (wp.startTime || 0)
|
|
|
+ recordedWaypoints.value.push(wp as WaypointItem)
|
|
|
+ currentWaypoint.value = null
|
|
|
+ }
|
|
|
+
|
|
|
+ function deleteWaypoint(index: number) {
|
|
|
+ recordedWaypoints.value.splice(index, 1)
|
|
|
+ // renumber
|
|
|
+ recordedWaypoints.value.forEach((wp, i) => {
|
|
|
+ wp.id = i + 1
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveWaypointsData(cameraId: string) {
|
|
|
+ const payload: WaypointPayload = {
|
|
|
+ cameraId,
|
|
|
+ name: waypointName.value || `轨迹 ${new Date().toLocaleTimeString()}`,
|
|
|
+ description: waypointDescription.value,
|
|
|
+ loopEnabled: loopEnabled.value,
|
|
|
+ loopCount: loopCount.value,
|
|
|
+ waypoints: recordedWaypoints.value,
|
|
|
+ totalDuration: totalDuration.value
|
|
|
+ }
|
|
|
+
|
|
|
+ if (savedWaypointId.value) {
|
|
|
+ payload.id = savedWaypointId.value
|
|
|
+ const res = await updateWaypoints(payload)
|
|
|
+ if (res.success) {
|
|
|
+ ElMessage.success(t('轨迹更新成功'))
|
|
|
+ }
|
|
|
+ return res
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await addWaypoints(payload)
|
|
|
+ if (res.success && res.data) {
|
|
|
+ savedWaypointId.value = res.data.id!
|
|
|
+ ElMessage.success(t('轨迹保存成功'))
|
|
|
+ }
|
|
|
+ return res
|
|
|
+ }
|
|
|
+
|
|
|
+ async function updateWaypointsData() {
|
|
|
+ if (!savedWaypointId.value) return
|
|
|
+ const cameraId = currentMediaStream.value?.cameraId
|
|
|
+ if (!cameraId) return
|
|
|
+
|
|
|
+ const payload: WaypointPayload = {
|
|
|
+ id: savedWaypointId.value,
|
|
|
+ cameraId,
|
|
|
+ name: waypointName.value,
|
|
|
+ description: waypointDescription.value,
|
|
|
+ loopEnabled: loopEnabled.value,
|
|
|
+ loopCount: loopCount.value,
|
|
|
+ waypoints: recordedWaypoints.value,
|
|
|
+ totalDuration: totalDuration.value
|
|
|
+ }
|
|
|
+ const res = await updateWaypoints(payload)
|
|
|
+ if (res.success) {
|
|
|
+ ElMessage.success(t('轨迹更新成功'))
|
|
|
+ }
|
|
|
+ return res
|
|
|
+ }
|
|
|
+
|
|
|
+ async function playWaypointsPreview() {
|
|
|
+ const cameraId = currentMediaStream.value?.cameraId
|
|
|
+ if (!cameraId || recordedWaypoints.value.length === 0) return
|
|
|
+
|
|
|
+ isWaypointPlaying.value = true
|
|
|
+ waypointProgress.value = 0
|
|
|
+ playbackAbort = false
|
|
|
+
|
|
|
+ const doPlay = async () => {
|
|
|
+ const startTs = Date.now()
|
|
|
+
|
|
|
+ // progress updater
|
|
|
+ const progressInterval = setInterval(() => {
|
|
|
+ if (playbackAbort) {
|
|
|
+ clearInterval(progressInterval)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const elapsed = Date.now() - startTs
|
|
|
+ waypointProgress.value = Math.min((elapsed / totalDuration.value) * 100, 100)
|
|
|
+ }, 50)
|
|
|
+
|
|
|
+ for (const wp of recordedWaypoints.value) {
|
|
|
+ if (playbackAbort) break
|
|
|
+
|
|
|
+ // wait until this waypoint's start time
|
|
|
+ const now = Date.now() - startTs
|
|
|
+ const waitTime = wp.startTime - now
|
|
|
+ if (waitTime > 0) {
|
|
|
+ await new Promise((r) => {
|
|
|
+ playbackTimer = setTimeout(r, waitTime)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (playbackAbort) break
|
|
|
+
|
|
|
+ // execute PTZ action
|
|
|
+ await ptzControl({ cameraId, action: wp.action as any, speed: wp.speed })
|
|
|
+
|
|
|
+ // wait for duration then stop
|
|
|
+ if (wp.duration > 0) {
|
|
|
+ await new Promise((r) => {
|
|
|
+ playbackTimer = setTimeout(r, wp.duration)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (playbackAbort) break
|
|
|
+
|
|
|
+ await ptzControl({ cameraId, action: 'stop' })
|
|
|
+ }
|
|
|
+
|
|
|
+ clearInterval(progressInterval)
|
|
|
+
|
|
|
+ // wait until total duration
|
|
|
+ if (!playbackAbort) {
|
|
|
+ const remaining = totalDuration.value - (Date.now() - startTs)
|
|
|
+ if (remaining > 0) {
|
|
|
+ await new Promise((r) => {
|
|
|
+ playbackTimer = setTimeout(r, remaining)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const loops = loopEnabled.value ? loopCount.value : 1
|
|
|
+ for (let i = 0; i < loops; i++) {
|
|
|
+ if (playbackAbort) break
|
|
|
+ waypointProgress.value = 0
|
|
|
+ await doPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+ waypointProgress.value = 100
|
|
|
+ isWaypointPlaying.value = false
|
|
|
+ }
|
|
|
+
|
|
|
+ function stopWaypointPreview() {
|
|
|
+ playbackAbort = true
|
|
|
+ if (playbackTimer) {
|
|
|
+ clearTimeout(playbackTimer)
|
|
|
+ playbackTimer = null
|
|
|
+ }
|
|
|
+ isWaypointPlaying.value = false
|
|
|
+ waypointProgress.value = 0
|
|
|
+
|
|
|
+ // stop PTZ
|
|
|
+ const cameraId = currentMediaStream.value?.cameraId
|
|
|
+ if (cameraId) {
|
|
|
+ ptzControl({ cameraId, action: 'stop' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatElapsedTime(ms: number): string {
|
|
|
+ const totalSec = Math.floor(ms / 1000)
|
|
|
+ const min = Math.floor(totalSec / 60)
|
|
|
+ const sec = totalSec % 60
|
|
|
+ const tenths = Math.floor((ms % 1000) / 100)
|
|
|
+ return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}.${tenths}`
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ isRecording,
|
|
|
+ recordingStartTime,
|
|
|
+ recordedWaypoints,
|
|
|
+ currentWaypoint,
|
|
|
+ totalDuration,
|
|
|
+ elapsedTime,
|
|
|
+ waypointName,
|
|
|
+ waypointDescription,
|
|
|
+ isWaypointPlaying,
|
|
|
+ waypointProgress,
|
|
|
+ loopEnabled,
|
|
|
+ loopCount,
|
|
|
+ savedWaypointId,
|
|
|
+ showDetailDrawer,
|
|
|
+ hasRecordedData,
|
|
|
+ startRecording,
|
|
|
+ stopRecording,
|
|
|
+ captureWaypoint,
|
|
|
+ completeCurrentWaypoint,
|
|
|
+ deleteWaypoint,
|
|
|
+ saveWaypointsData,
|
|
|
+ updateWaypointsData,
|
|
|
+ playWaypointsPreview,
|
|
|
+ stopWaypointPreview,
|
|
|
+ formatElapsedTime
|
|
|
+ }
|
|
|
+}
|