基于 Vue2 封装大华 RTSP 回放视频组件(PlayerControl.js 实现)

参考链接:基于 Vue3 封装大华 RTSP 回放视频组件(PlayerControl.js 实现)_vue playercontrol大华的使用-CSDN博客

官方教程: WEB无插件开发包使用说明-浙江大华技术股份有限公司

碰到的问题: 1、PlayerControl.js默认是在根目录使用,如果不是在根目录使用需要修改对应文件中的的路径(我的前缀是cockpit)不然会找不到对应的文件

2、我对接的大华的摄像头是H265格式的只能在canvas中展现出来,我这边的功能需要是对视频进行回放和参考链接类似,但是参考链接是能在video中展示因此不需要添加播放、暂停、音量开关、抓图、刷新、全屏功能,canvas中就需要手动添加

js 复制代码
<template>
  <el-dialog :title="'文件预览'" class="prev-file-dialog" append-to-body :visible.sync="DialogVisible" :fullscreen="true">
    <template #title>
      <div class="vn-flex vn-flex-space-between vn-gap-8 vn-flex-y-center vn-fill-width">
        <span>{{ '文件预览' }}</span>
      </div>
    </template>
    <div class="preview-pdf">
      <canvas ref="canvasElement" :style="{ width: '100%', height: '100%' }"></canvas>
      <div class="operation">
        <div class="operation-left vn-flex vn-flex-y-center vn-gap-8">
          <div
            class="play icon vn-pointer"
            :class="canvasOperation.playState ? 'el-icon-video-pause' : 'el-icon-video-play'"
            @click="handlePlay"
          ></div>
          <div class="disconnect icon"></div>
          <button class="control-btn" @click.stop="toggleMute">
            <span v-if="!canvasOperation.muteState" class="icon-volume">🔊</span>
            <span v-else class="icon-muted">🔇</span>
          </button>

          <div class="timestamp vn-flex vn-flex-y-center vn-gap-4">
            <span class="first-time">{{ canvasOperation.firstTime }}</span>
            /
            <span class="total-time">{{ canvasOperation.totalTime }}</span>
            <el-input-number
              v-model="canvasOperation.backTime"
              size="mini"
              clearable
              type="number"
              :min="0"
              :max="canvasOperation.totalTime"
              :controls="false"
              class="number-input"
              :precision="0"
            ></el-input-number>
            <el-button size="mini" type="primary" @click="playerBack">跳转</el-button>
          </div>
        </div>
        <div class="operation-right vn-flex vn-flex-y-center vn-gap-12">
          <!-- 捕获截图 -->
          <div class="el-icon-crop icon" @click="handleCapture"></div>

          <!-- 刷新 -->
          <div class="el-icon-refresh-right icon" @click="handleRefresh"></div>
          <!-- 全屏按钮 -->
          <div class="el-icon-full-screen icon" @click.stop="toggleFullscreen"></div>
        </div>
      </div>
    </div>
  </el-dialog>
</template>

<script lang="ts">
import { Component, Vue, Ref, Prop, PropSync } from 'vue-property-decorator'

@Component({
  name: 'DaHuaVideoPreview',
  components: {}
})
export default class DaHuaVideoPreview extends Vue {
  @Ref() canvasElement!: any
  @PropSync('visible', { default: false }) DialogVisible!: boolean
  // 接收外部参数
  @Prop({
    default: () => {
      return {
        wsURL: 'ws://xxx.xxx.xxx.xxx:9527/rtspoverwebsocket',
        url: '',
        ip: 'xxx.xxx.xxx.xxx',
        port: '9527',
        channel: 1,
        username: 'admin',
        password: 'admin123',
        proto: 'Private3',
        subtype: 0,
        starttime: '2025_11_10_09_10_00',
        endtime: '2025_11_10_10_10_00',
        width: '100%',
        height: '220px'
      }
    }
  })
  props!: any

  player: any = null
  canvasOperation = this.initCanvasData()

  initCanvasData() {
    return {
      playState: false,
      muteState: false,
      isFullscreen: false,
      backTime: 1,
      totalTime: 0,
      firstTime: 0
    }
  }
  playerStop() {
    this.player?.close()
  }

  playerPause() {
    this.player?.pause()
  }

  playerContinue() {
    // this.player?.play()
    this.playerPlay()
  }

  playerCapture() {
    this.player?.capture('test')
  }

  playerPlay() {
    if (this.player) {
      this.player.stop()
      this.player.close()
      this.player = null
    }
    if (!window.PlayerControl) {
      console.error('❌ PlayerControl SDK 未加载,请在 index.html 中引入 /module/playerControl.js')
      return
    }

    this.closePlayer()

    var options = {
      wsURL: `ws://${this.props.ip}:${this.props.port}/rtspoverwebsocket`,
      rtspURL: this.buildRtspUrl(),
      username: this.props.username,
      password: this.props.password,
      h265AccelerationEnabled: true
    }
    this.player = new window.PlayerControl(options)
    let firstTime = 0
    this.player.on('WorkerReady', (rs: any) => {
      console.log('WorkerReady')
      this.player.connect()
    })

    this.player.on('Error', (rs: any) => {
      console.log('error')
      console.log(rs)
    })
    this.player.on('PlayStart', () => {
      console.log('PlayStart')
      this.canvasOperation.playState = true
    })

    this.player.on('UpdateCanvas', (res: any) => {
      if (firstTime === 0) {
        firstTime = res.timestamp //获取录像文件的第一帧的时间戳
      }
      this.canvasOperation.firstTime = res.timestamp - firstTime
    })
    this.player.on('GetTotalTime', (res: any) => {
      console.log(res, 'GetTotalTime')
      this.canvasOperation.totalTime = res || 0
    })

    this.player.on('FileOver', (res: any) => {
      console.log(res, 'FileOver')
      this.handleRefresh()
    })
    this.player.init(this.canvasElement, null)
    window.__player = this.player
  }

  mounted() {
    console.log(this.props, 'props')
    this.$nextTick(() => {
      this.playerContinue()
    })
  }

  beforeDestroy() {
    this.closePlayer()
  }

  playerBack() {
    this.player.playByTime(this.canvasOperation.backTime)
  }

  /** 拼接 RTSP URL 回放 */
  buildRtspUrl() {
    if (this.props?.url) return this.props?.url
    return `rtsp://${this.props.ip}:${this.props.port}/cam/playback?channel=${this.props.channel}&subtype=${this.props.subtype}&starttime=${this.props.starttime}&endtime=${this.props.endtime}`
  }

  closePlayer() {
    if (this.player) {
      try {
        this.player.close()
      } catch (e) {
        console.warn('旧播放器关闭异常:', e)
      }
      this.player = null
    }
  }

  toggleMute() {
    this.canvasOperation.muteState = !this.canvasOperation.muteState
    // 如果要关闭声音,将 val 参数设置为 0 即可。WEB SDK 播放时,默认音量是 0。需要声音时,必须调用该方法,并且参数大于 0
    this.player.setAudioVolume(Number(this.canvasOperation.muteState))
  }

  toggleFullscreen() {
    const videoWrapper = this.$el.querySelector('.preview-pdf') as HTMLElement

    if (!document.fullscreenElement) {
      // 进入全屏
      if (videoWrapper.requestFullscreen) {
        videoWrapper.requestFullscreen()
      }
      this.canvasOperation.isFullscreen = true
    } else {
      // 退出全屏
      if (document.exitFullscreen) {
        document.exitFullscreen()
      }
      this.canvasOperation.isFullscreen = false
    }
  }
  //
  handlePlay() {
    if (!this.canvasOperation.playState) {
      this.player.play()
    } else {
      this.player.pause()
    }
    this.canvasOperation.playState = !this.canvasOperation.playState
  }

  handleCapture() {
    this.player.capture(new Date().getTime())
  }

  handleRefresh() {
    this.canvasOperation = this.initCanvasData()
    this.playerPlay()
  }
}
</script>
<style lang="scss" scoped>
.preview-pdf {
  height: 100%;
  width: 100%;

  position: relative;

  .operation {
    width: 100%;
    height: 40px;
    position: absolute;
    bottom: 0;
    right: 0;
    z-index: 1;
    background-color: rgb(0, 0, 0, 0.5);

    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 0 16px;
    .play {
      color: #fff;
      font-size: 16px;
    }
    .icon {
      font-size: 20px;
      color: #fff;
      cursor: pointer;
    }
    .control-btn {
      background-color: transparent;
      span {
        font-size: 18px;
      }
    }
  }
  .timestamp {
    color: #fff;
    flex-shrink: 0;

    .first-time,
    .total-time {
      flex-shrink: 0;
    }
    .number-input {
      width: 100px;
    }
  }
}

.prev-file-dialog {
  width: 100vw;
  height: 100vh;
  ::v-deep {
    .el-dialog.is-fullscreen {
      height: 100%;
    }
    .el-dialog {
      min-width: 80vw;
      height: calc(70vh);
    }
    .el-dialog__body {
      max-height: 100%;
      min-height: 0;
      height: 100%;
      overflow: hidden;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 12px !important;
      background: #fff;
    }
  }
}
</style>

实现效果:

相关推荐
消失的旧时光-19434 小时前
Kotlinx.serialization 对多态对象(sealed class )支持更好用
java·服务器·前端
少卿4 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技4 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
快起来搬砖了4 小时前
Vue 实现阿里云 OSS 视频分片上传:安全实战与完整方案
vue.js·安全·阿里云
广州华水科技4 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮4 小时前
umi4暗黑模式设置
前端
8***B4 小时前
前端路由权限控制,动态路由生成
前端
军军3605 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1235 小时前
Vue基础知识(一)
前端·javascript·vue.js
terminal0075 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试