Parcourir la source

Remove Jessibuca player integration from the video player component and related files. Update Vite configuration for improved chunk management. Replace player functionality with a warning message indicating the unavailability of the GB28181 device playback feature due to the discontinuation of the Jessibuca player.

yb il y a 3 semaines
Parent
commit
acd6691d78
5 fichiers modifiés avec 30 ajouts et 528 suppressions
  1. 0 12
      index.html
  2. 0 16
      public/js/jessibuca/README.md
  3. 9 80
      src/components/VideoPlayer.vue
  4. 12 417
      src/views/camera/video.vue
  5. 9 3
      vite.config.ts

+ 0 - 12
index.html

@@ -5,18 +5,6 @@
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>摄像头管理系统</title>
-    <!-- Jessibuca 播放器(用于 FLV/GB28181 流,可选) -->
-    <script>
-      // 动态加载 Jessibuca,避免文件不存在时报错
-      (function() {
-        var script = document.createElement('script');
-        script.src = '/js/jessibuca/jessibuca.js';
-        script.onerror = function() {
-          console.warn('Jessibuca player not found. FLV playback will be unavailable.');
-        };
-        document.head.appendChild(script);
-      })();
-    </script>
   </head>
   <body>
     <div id="app"></div>

+ 0 - 16
public/js/jessibuca/README.md

@@ -1,16 +0,0 @@
-# Jessibuca 播放器
-
-请从以下地址下载 Jessibuca 播放器文件,并放置在此目录下:
-
-- 下载地址: https://github.com/nicoxiang/jessibuca/releases
-
-需要的文件:
-- jessibuca.js
-- decoder.js
-- decoder.wasm
-
-或者使用 npm 安装后复制:
-```bash
-npm install jessibuca-pro
-# 然后从 node_modules/jessibuca-pro/dist 复制文件
-```

+ 9 - 80
src/components/VideoPlayer.vue

@@ -24,11 +24,6 @@
       />
     </template>
 
-    <!-- Jessibuca Player (FLV/GB28181) -->
-    <template v-else-if="playerType === 'jessibuca'">
-      <div ref="jessibucaContainer" class="jessibuca-container" />
-    </template>
-
     <!-- Native Video (MP4/WebM) -->
     <template v-else>
       <video
@@ -53,7 +48,7 @@ interface Props {
   src?: string                    // 视频源地址
   videoId?: string                // Cloudflare Stream video ID
   customerDomain?: string         // Cloudflare 自定义域名
-  playerType?: 'cloudflare' | 'hls' | 'jessibuca' | 'native'
+  playerType?: 'cloudflare' | 'hls' | 'native'
   useIframe?: boolean             // Cloudflare 是否使用 iframe
   controls?: boolean
   autoplay?: boolean
@@ -83,10 +78,8 @@ const emit = defineEmits<{
 
 const wrapperRef = ref<HTMLElement>()
 const videoRef = ref<HTMLVideoElement>()
-const jessibucaContainer = ref<HTMLElement>()
 
 let hlsInstance: any = null
-let jessibucaInstance: any = null
 
 // Cloudflare Stream iframe URL
 const cloudflareIframeSrc = computed(() => {
@@ -152,44 +145,6 @@ async function initHls() {
   bindVideoEvents()
 }
 
-// 初始化 Jessibuca
-function initJessibuca() {
-  if (!jessibucaContainer.value || !window.Jessibuca) return
-
-  jessibucaInstance = new window.Jessibuca({
-    container: jessibucaContainer.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: true,
-      play: true,
-      audio: true
-    },
-    forceNoOffscreen: true,
-    isNotMute: !props.muted
-  })
-
-  jessibucaInstance.on('error', (error: any) => {
-    emit('error', error)
-  })
-
-  jessibucaInstance.on('play', () => {
-    emit('play')
-  })
-
-  jessibucaInstance.on('pause', () => {
-    emit('pause')
-  })
-}
-
 function bindVideoEvents() {
   if (!videoRef.value) return
 
@@ -209,60 +164,40 @@ function destroyHls() {
   }
 }
 
-function destroyJessibuca() {
-  if (jessibucaInstance) {
-    jessibucaInstance.destroy()
-    jessibucaInstance = null
-  }
-}
-
 // 公开方法
-function play(url?: string) {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    jessibucaInstance.play(url || props.src)
-  } else if (videoRef.value) {
+function play() {
+  if (videoRef.value) {
     videoRef.value.play()
   }
 }
 
 function pause() {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    jessibucaInstance.pause()
-  } else if (videoRef.value) {
+  if (videoRef.value) {
     videoRef.value.pause()
   }
 }
 
 function stop() {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    jessibucaInstance.destroy()
-    initJessibuca()
-  } else if (videoRef.value) {
+  if (videoRef.value) {
     videoRef.value.pause()
     videoRef.value.currentTime = 0
   }
 }
 
 function setVolume(volume: number) {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    jessibucaInstance.setVolume(volume)
-  } else if (videoRef.value) {
+  if (videoRef.value) {
     videoRef.value.volume = volume
   }
 }
 
 function setMuted(muted: boolean) {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    muted ? jessibucaInstance.mute() : jessibucaInstance.cancelMute()
-  } else if (videoRef.value) {
+  if (videoRef.value) {
     videoRef.value.muted = muted
   }
 }
 
 function screenshot() {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    jessibucaInstance.screenshot()
-  } else if (videoRef.value) {
+  if (videoRef.value) {
     const canvas = document.createElement('canvas')
     canvas.width = videoRef.value.videoWidth
     canvas.height = videoRef.value.videoHeight
@@ -276,9 +211,7 @@ function screenshot() {
 }
 
 function fullscreen() {
-  if (props.playerType === 'jessibuca' && jessibucaInstance) {
-    jessibucaInstance.setFullscreen(true)
-  } else if (wrapperRef.value) {
+  if (wrapperRef.value) {
     wrapperRef.value.requestFullscreen?.()
   }
 }
@@ -303,8 +236,6 @@ watch(() => props.src, (newSrc) => {
 onMounted(() => {
   if (props.playerType === 'hls') {
     initHls()
-  } else if (props.playerType === 'jessibuca') {
-    initJessibuca()
   } else if (props.playerType === 'native') {
     bindVideoEvents()
   }
@@ -312,7 +243,6 @@ onMounted(() => {
 
 onBeforeUnmount(() => {
   destroyHls()
-  destroyJessibuca()
 })
 </script>
 
@@ -327,7 +257,6 @@ onBeforeUnmount(() => {
 
 .cloudflare-iframe,
 .video-element,
-.jessibuca-container,
 .cloudflare-container {
   width: 100%;
   height: 100%;

+ 12 - 417
src/views/camera/video.vue

@@ -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>

+ 9 - 3
vite.config.ts

@@ -33,9 +33,15 @@ export default defineConfig({
     chunkSizeWarningLimit: 1500,
     rollupOptions: {
       output: {
-        manualChunks: {
-          'element-plus': ['element-plus', '@element-plus/icons-vue'],
-          'vue-vendor': ['vue', 'vue-router', 'pinia']
+        manualChunks(id) {
+          if (id.includes('node_modules')) {
+            // Vue 核心和 Element Plus 放在一起,避免循环依赖
+            if (id.includes('vue') || id.includes('@vue') ||
+                id.includes('element-plus') || id.includes('@element-plus') ||
+                id.includes('pinia') || id.includes('vue-router')) {
+              return 'vendor'
+            }
+          }
         }
       }
     }