|
|
@@ -6,110 +6,23 @@
|
|
|
<span class="title">{{ isPlayback ? '录像回放' : '实时播放' }} - {{ channelId }}</span>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 录像日期选择 -->
|
|
|
- <div v-if="isPlayback" class="date-picker">
|
|
|
- <span>选择录像日期:</span>
|
|
|
- <el-date-picker
|
|
|
- v-model="queryDate"
|
|
|
- type="date"
|
|
|
- placeholder="选择日期"
|
|
|
- value-format="YYYY-MM-DD"
|
|
|
- @change="loadDevRecord"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 播放器区域 -->
|
|
|
- <div class="player-wrapper" v-loading="playerLoading">
|
|
|
- <div ref="playerContainer" class="player-container"></div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 控制按钮 -->
|
|
|
- <div class="control-bar">
|
|
|
- <div class="control-left">
|
|
|
- <el-button v-if="!playing" type="primary" :icon="VideoPlay" @click="handlePlay">播放</el-button>
|
|
|
- <el-button v-else type="danger" :icon="VideoPause" @click="handleStop">停止</el-button>
|
|
|
-
|
|
|
- <el-button v-if="!muted" type="info" :icon="Mute" @click="handleMute">静音</el-button>
|
|
|
- <el-button v-else type="warning" :icon="Microphone" @click="handleUnmute">放音</el-button>
|
|
|
-
|
|
|
- <el-slider
|
|
|
- v-model="volume"
|
|
|
- :disabled="muted"
|
|
|
- :max="100"
|
|
|
- :format-tooltip="(val: number) => `音量: ${val}%`"
|
|
|
- class="volume-slider"
|
|
|
- @change="handleVolumeChange"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="control-right">
|
|
|
- <el-button :icon="Camera" @click="handleScreenshot">截图</el-button>
|
|
|
- <el-button :icon="FullScreen" @click="handleFullscreen">全屏</el-button>
|
|
|
-
|
|
|
- <template v-if="isPlayback && playing">
|
|
|
- <el-button v-if="!paused" type="primary" :icon="VideoPause" @click="handlePause">暂停</el-button>
|
|
|
- <el-button v-else type="success" :icon="VideoPlay" @click="handleResume">恢复</el-button>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 云台控制(仅实时播放) -->
|
|
|
- <div v-if="!isPlayback" class="ptz-control">
|
|
|
- <h4>云台控制</h4>
|
|
|
- <div class="ptz-grid">
|
|
|
- <div class="ptz-row">
|
|
|
- <el-button @click="ptzControl('upleft')">↖</el-button>
|
|
|
- <el-button @click="ptzControl('up')">↑</el-button>
|
|
|
- <el-button @click="ptzControl('upright')">↗</el-button>
|
|
|
- </div>
|
|
|
- <div class="ptz-row">
|
|
|
- <el-button @click="ptzControl('left')">←</el-button>
|
|
|
- <el-button type="danger" @click="ptzControl('stop')">停</el-button>
|
|
|
- <el-button @click="ptzControl('right')">→</el-button>
|
|
|
- </div>
|
|
|
- <div class="ptz-row">
|
|
|
- <el-button @click="ptzControl('downleft')">↙</el-button>
|
|
|
- <el-button @click="ptzControl('down')">↓</el-button>
|
|
|
- <el-button @click="ptzControl('downright')">↘</el-button>
|
|
|
- </div>
|
|
|
- <div class="ptz-row ptz-zoom">
|
|
|
- <el-button @click="ptzControl('zoomin')">放大</el-button>
|
|
|
- <el-button @click="ptzControl('zoomout')">缩小</el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <!-- 功能不可用提示 -->
|
|
|
+ <el-result
|
|
|
+ icon="warning"
|
|
|
+ title="功能暂不可用"
|
|
|
+ sub-title="GB28181 设备播放功能需要 Jessibuca 播放器支持,该播放器已停止维护。"
|
|
|
+ >
|
|
|
+ <template #extra>
|
|
|
+ <el-button type="primary" @click="goBack">返回通道列表</el-button>
|
|
|
+ </template>
|
|
|
+ </el-result>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
|
+import { computed } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-import {
|
|
|
- ArrowLeft,
|
|
|
- VideoPlay,
|
|
|
- VideoPause,
|
|
|
- Mute,
|
|
|
- Microphone,
|
|
|
- Camera,
|
|
|
- FullScreen
|
|
|
-} from '@element-plus/icons-vue'
|
|
|
-import {
|
|
|
- play,
|
|
|
- stopPlay,
|
|
|
- playback,
|
|
|
- playbackStop,
|
|
|
- playbackPause,
|
|
|
- playbackReplay,
|
|
|
- getDevRecord,
|
|
|
- ptzControl as ptzControlApi
|
|
|
-} from '@/api/camera'
|
|
|
-
|
|
|
-declare global {
|
|
|
- interface Window {
|
|
|
- Jessibuca: any
|
|
|
- }
|
|
|
-}
|
|
|
+import { ArrowLeft } from '@element-plus/icons-vue'
|
|
|
|
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
|
@@ -118,242 +31,9 @@ const deviceId = route.params.deviceId as string
|
|
|
const channelId = route.params.channelId as string
|
|
|
const isPlayback = computed(() => route.query.mode === 'playback')
|
|
|
|
|
|
-const playerContainer = ref<HTMLElement>()
|
|
|
-const playerLoading = ref(false)
|
|
|
-const playing = ref(false)
|
|
|
-const muted = ref(true)
|
|
|
-const paused = ref(false)
|
|
|
-const volume = ref(100)
|
|
|
-const queryDate = ref('')
|
|
|
-
|
|
|
-let jessibuca: any = null
|
|
|
-let streamInfo = {
|
|
|
- ssrc: '',
|
|
|
- flv: ''
|
|
|
-}
|
|
|
-
|
|
|
-function initPlayer() {
|
|
|
- if (!playerContainer.value) return
|
|
|
-
|
|
|
- jessibuca = new window.Jessibuca({
|
|
|
- container: playerContainer.value,
|
|
|
- videoBuffer: 0.2,
|
|
|
- decoder: '/js/jessibuca/decoder.js',
|
|
|
- timeout: 20,
|
|
|
- debug: false,
|
|
|
- isResize: false,
|
|
|
- loadingText: '加载中...',
|
|
|
- isFlv: true,
|
|
|
- showBandwidth: true,
|
|
|
- supportDblclickFullscreen: true,
|
|
|
- operateBtns: {
|
|
|
- fullscreen: true,
|
|
|
- screenshot: false,
|
|
|
- play: false,
|
|
|
- audio: false
|
|
|
- },
|
|
|
- forceNoOffscreen: true,
|
|
|
- isNotMute: false
|
|
|
- })
|
|
|
-
|
|
|
- jessibuca.on('error', (error: any) => {
|
|
|
- console.error('Player error:', error)
|
|
|
- destroyPlayer()
|
|
|
- })
|
|
|
-
|
|
|
- jessibuca.on('timeout', () => {
|
|
|
- console.log('Player timeout')
|
|
|
- destroyPlayer()
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function destroyPlayer() {
|
|
|
- if (jessibuca) {
|
|
|
- jessibuca.destroy()
|
|
|
- jessibuca = null
|
|
|
- }
|
|
|
- initPlayer()
|
|
|
-}
|
|
|
-
|
|
|
-async function handlePlay() {
|
|
|
- if (isPlayback.value) {
|
|
|
- loadDevRecord()
|
|
|
- } else {
|
|
|
- playLive()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function playLive() {
|
|
|
- playerLoading.value = true
|
|
|
- try {
|
|
|
- const res = await play(deviceId, channelId)
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- streamInfo.ssrc = res.data.streamId
|
|
|
- streamInfo.flv = res.data.flv
|
|
|
- playing.value = true
|
|
|
-
|
|
|
- if (jessibuca && streamInfo.flv) {
|
|
|
- jessibuca.play(streamInfo.flv)
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('播放失败', error)
|
|
|
- } finally {
|
|
|
- playerLoading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function loadDevRecord() {
|
|
|
- if (!queryDate.value) {
|
|
|
- const today = new Date()
|
|
|
- queryDate.value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
|
|
- }
|
|
|
-
|
|
|
- playerLoading.value = true
|
|
|
- try {
|
|
|
- const date = new Date(queryDate.value).getTime()
|
|
|
- const start = Math.floor(date / 1000)
|
|
|
- const end = Math.floor((date + 24 * 60 * 60 * 1000 - 1) / 1000)
|
|
|
-
|
|
|
- const res = await getDevRecord(deviceId, channelId, { start, end })
|
|
|
-
|
|
|
- if (res.code === 200 && res.data?.recordItems && res.data.recordItems.length > 0) {
|
|
|
- const records = res.data.recordItems
|
|
|
- const firstRecord = records[0]
|
|
|
- if (firstRecord) {
|
|
|
- await playRecordback(firstRecord.start, end)
|
|
|
- }
|
|
|
- } else {
|
|
|
- ElMessage.warning('当前通道没有录像')
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('加载录像失败', error)
|
|
|
- } finally {
|
|
|
- playerLoading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function playRecordback(start: number, end: number) {
|
|
|
- try {
|
|
|
- const res = await playback(deviceId, channelId, { start, end })
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- streamInfo.ssrc = res.data.streamId
|
|
|
- streamInfo.flv = res.data.flv
|
|
|
- playing.value = true
|
|
|
-
|
|
|
- if (jessibuca && streamInfo.flv) {
|
|
|
- jessibuca.play(streamInfo.flv)
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('回放失败', error)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function handleStop() {
|
|
|
- playerLoading.value = true
|
|
|
- try {
|
|
|
- if (isPlayback.value) {
|
|
|
- await playbackStop(deviceId, channelId)
|
|
|
- } else {
|
|
|
- await stopPlay(deviceId, channelId)
|
|
|
- }
|
|
|
- playing.value = false
|
|
|
- paused.value = false
|
|
|
- streamInfo.ssrc = ''
|
|
|
- streamInfo.flv = ''
|
|
|
- destroyPlayer()
|
|
|
- } finally {
|
|
|
- playerLoading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function handlePause() {
|
|
|
- if (!playing.value || !isPlayback.value) return
|
|
|
-
|
|
|
- try {
|
|
|
- const res = await playbackPause(deviceId, channelId)
|
|
|
- if (res.code === 200) {
|
|
|
- paused.value = true
|
|
|
- jessibuca?.pause()
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('暂停失败', error)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function handleResume() {
|
|
|
- if (!paused.value || !isPlayback.value) return
|
|
|
-
|
|
|
- try {
|
|
|
- const res = await playbackReplay(deviceId, channelId)
|
|
|
- if (res.code === 200) {
|
|
|
- paused.value = false
|
|
|
- jessibuca?.play()
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('恢复失败', error)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function handleMute() {
|
|
|
- jessibuca?.mute()
|
|
|
- muted.value = true
|
|
|
-}
|
|
|
-
|
|
|
-function handleUnmute() {
|
|
|
- jessibuca?.cancelMute()
|
|
|
- muted.value = false
|
|
|
-}
|
|
|
-
|
|
|
-function handleVolumeChange(val: number) {
|
|
|
- jessibuca?.setVolume(val / 100)
|
|
|
-}
|
|
|
-
|
|
|
-function handleScreenshot() {
|
|
|
- if (playing.value) {
|
|
|
- jessibuca?.screenshot()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function handleFullscreen() {
|
|
|
- if (playing.value) {
|
|
|
- jessibuca?.setFullscreen(true)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function ptzControl(command: string) {
|
|
|
- try {
|
|
|
- await ptzControlApi(deviceId, channelId, command)
|
|
|
- } catch (error) {
|
|
|
- console.error('云台控制失败', error)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
function goBack() {
|
|
|
router.push(`/camera/channel/${deviceId}`)
|
|
|
}
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- // 等待 Jessibuca 脚本加载
|
|
|
- const checkJessibuca = () => {
|
|
|
- if (window.Jessibuca) {
|
|
|
- initPlayer()
|
|
|
- } else {
|
|
|
- setTimeout(checkJessibuca, 100)
|
|
|
- }
|
|
|
- }
|
|
|
- checkJessibuca()
|
|
|
-})
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- if (playing.value) {
|
|
|
- handleStop()
|
|
|
- }
|
|
|
- if (jessibuca) {
|
|
|
- jessibuca.destroy()
|
|
|
- }
|
|
|
-})
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@@ -375,89 +55,4 @@ onBeforeUnmount(() => {
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-.date-picker {
|
|
|
- margin-bottom: 20px;
|
|
|
- padding: 15px 20px;
|
|
|
- background-color: #fff;
|
|
|
- border-radius: 4px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
-
|
|
|
- span {
|
|
|
- margin-right: 10px;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.player-wrapper {
|
|
|
- background-color: #000;
|
|
|
- border-radius: 4px;
|
|
|
- overflow: hidden;
|
|
|
-
|
|
|
- .player-container {
|
|
|
- width: 100%;
|
|
|
- height: 500px;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.control-bar {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- margin-top: 20px;
|
|
|
- padding: 15px 20px;
|
|
|
- background-color: #fff;
|
|
|
- border-radius: 4px;
|
|
|
-
|
|
|
- .control-left,
|
|
|
- .control-right {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- }
|
|
|
-
|
|
|
- .volume-slider {
|
|
|
- width: 100px;
|
|
|
- margin-left: 10px;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.ptz-control {
|
|
|
- margin-top: 20px;
|
|
|
- padding: 20px;
|
|
|
- background-color: #fff;
|
|
|
- border-radius: 4px;
|
|
|
-
|
|
|
- h4 {
|
|
|
- margin-bottom: 15px;
|
|
|
- font-size: 14px;
|
|
|
- color: #303133;
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-grid {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- gap: 5px;
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-row {
|
|
|
- display: flex;
|
|
|
- gap: 5px;
|
|
|
-
|
|
|
- .el-button {
|
|
|
- width: 50px;
|
|
|
- height: 40px;
|
|
|
- padding: 0;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-zoom {
|
|
|
- margin-top: 10px;
|
|
|
-
|
|
|
- .el-button {
|
|
|
- width: 80px;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
</style>
|