|
@@ -0,0 +1,411 @@
|
|
|
+<template>
|
|
|
+ <div class="video-player-container">
|
|
|
+ <div class="video-container" ref="videoContainer">
|
|
|
+ <video
|
|
|
+ ref="videoPlayer"
|
|
|
+ class="video-js vjs-big-play-centered"
|
|
|
+ preload="auto"
|
|
|
+ crossOrigin="anonymous"
|
|
|
+ ></video>
|
|
|
+ <!-- :poster="poster" -->
|
|
|
+
|
|
|
+ <!-- 自定义控件 -->
|
|
|
+ <div class="lcfbox">
|
|
|
+ <div class="lcbox">
|
|
|
+ <div class="tit">下一次可能出现的设备:</div>
|
|
|
+ <div class="txt">1#旁 2号摄像头</div>
|
|
|
+ </div>
|
|
|
+ <div class="flexcj">
|
|
|
+ <div class="bigbox flexc">
|
|
|
+ <img src="@/assets/images/search/biga.png" @click="zoomOut" style="margin-left: 0;"/>
|
|
|
+ <div class="custom-dot-steps">
|
|
|
+ <div
|
|
|
+ v-for="i in 5"
|
|
|
+ :key="i"
|
|
|
+ class="step-dot"
|
|
|
+ :class="{ 'active': i == stepsactive}"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+ <img src="@/assets/images/search/bigb.png" @click="zoomIn"/>
|
|
|
+ </div>
|
|
|
+ <div class="bigbox flexc" style="padding-left: 7px;">
|
|
|
+ <el-select v-model="playbackRate" @change="changeSpeed" placeholder="1.0x" popper-class="slectbox">
|
|
|
+ <el-option
|
|
|
+ v-for="item in speeds"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.text"
|
|
|
+ :value="item.value">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ <img src="@/assets/images/search/bficoa.png" @click="takeScreenshot" />
|
|
|
+ <img src="@/assets/images/search/bficob.png" @click="replay"/>
|
|
|
+ <img src="@/assets/images/search/bficoc.png" @click="toggleFullscreen"/>
|
|
|
+
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 进度条 -->
|
|
|
+ <div class="vjs-progress-container" @click="seek">
|
|
|
+ <div class="vjs-progress-bar">
|
|
|
+ <div class="vjs-progress-filled" :style="{ width: progressPercentage + '%' }">
|
|
|
+ <img src="@/assets/images/search/cir.png" v-if="progressPercentage"/>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="playbox">
|
|
|
+ <div class="pltit">
|
|
|
+ <div class="tit">10:32:18</div>
|
|
|
+ <div class="txt">2025.06.13</div>
|
|
|
+ </div>
|
|
|
+ <div class="playbtn flexcc flex1">
|
|
|
+ <img src="@/assets/images/search/pre.png"/>
|
|
|
+ <div @click="paly=!paly" class="playb">
|
|
|
+ <img src="@/assets/images/search/videob.png" @click="handlePlay" v-if="paly"/>
|
|
|
+ <img src="@/assets/images/search/videoa.png" @click="handlePause" v-else/>
|
|
|
+ </div>
|
|
|
+ <img src="@/assets/images/search/next.png"/>
|
|
|
+ </div>
|
|
|
+ <div class="pltit txr">
|
|
|
+ <div class="tit">10:47:56</div>
|
|
|
+ <div class="txt">2025.06.13</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- <div v-if="!playerLoading" class="vjs-custom-controls">
|
|
|
+ <button class="vjs-custom-button" @click="replay" title="重播">↻</button>
|
|
|
+
|
|
|
+ <button class="vjs-custom-button" @click="zoomOut" title="缩小">-</button>
|
|
|
+ <span class="vjs-zoom-level">{{ zoomPercentage }}%</span>
|
|
|
+ <button class="vjs-custom-button" @click="zoomIn" title="放大">+</button>
|
|
|
+
|
|
|
+ <div class="vjs-progress-container" @click="seek">
|
|
|
+ <div class="vjs-progress-bar">
|
|
|
+ <div class="vjs-progress-filled" :style="{ width: progressPercentage + '%' }"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <select class="vjs-speed-selector" v-model="playbackRate" @change="changeSpeed" title="播放速度">
|
|
|
+ <option v-for="speed in speeds" :key="speed.value" :value="speed.value">{{ speed.text }}</option>
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <button class="vjs-custom-button" @click="takeScreenshot" title="截图">📷</button>
|
|
|
+ <button class="vjs-custom-button" @click="toggleFullscreen" title="全屏">⛶</button>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import videojs from 'video.js'
|
|
|
+import 'video.js/dist/video-js.css'
|
|
|
+import html2canvas from 'html2canvas'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'VideoPlayer',
|
|
|
+ props: {
|
|
|
+ src: {
|
|
|
+ type: String,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+ poster: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ player: null,
|
|
|
+ paly:false,
|
|
|
+ playerLoading: true,
|
|
|
+ zoomLevel: 1,
|
|
|
+ minZoom: 0.5,
|
|
|
+ maxZoom: 1.5,
|
|
|
+ zoomStep: 0.25,
|
|
|
+ playbackRate: 1,
|
|
|
+ stepsactive:3,
|
|
|
+ speeds: [
|
|
|
+ { value: 0.5, text: '0.5x' },
|
|
|
+ { value: 0.75, text: '0.75x' },
|
|
|
+ { value: 1, text: '1x' },
|
|
|
+ { value: 1.25, text: '1.25x' },
|
|
|
+ { value: 1.5, text: '1.5x' },
|
|
|
+ { value: 2, text: '2x' }
|
|
|
+ ],
|
|
|
+ progressPercentage: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ zoomPercentage() {
|
|
|
+ return Math.round(this.zoomLevel * 100)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.initPlayer()
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.player) {
|
|
|
+ this.player.dispose()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ togglePlay() {
|
|
|
+ if (this.player.paused()) {
|
|
|
+ this.player.play()
|
|
|
+ } else {
|
|
|
+ this.player.pause()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handlePlay(){
|
|
|
+ this.player.pause(); // 会触发videoPlay()函数
|
|
|
+ },
|
|
|
+ handlePause(){
|
|
|
+ this.player.play(); // 会触发videoPlay()函数
|
|
|
+ },
|
|
|
+ initPlayer() {
|
|
|
+ const videoElement = this.$refs.videoPlayer
|
|
|
+
|
|
|
+ // 合并默认选项和传入的选项
|
|
|
+ const defaultOptions = {
|
|
|
+ controls: false,
|
|
|
+ autoplay: false,
|
|
|
+ fluid: true,
|
|
|
+ sources: [{
|
|
|
+ src: this.src,
|
|
|
+ type: 'video/mp4'
|
|
|
+ }]
|
|
|
+ }
|
|
|
+
|
|
|
+ const finalOptions = Object.assign({}, defaultOptions, this.options)
|
|
|
+
|
|
|
+ // 初始化播放器
|
|
|
+ this.player = videojs(videoElement, finalOptions, () => {
|
|
|
+ this.playerLoading = false
|
|
|
+
|
|
|
+ // 监听时间更新事件来更新进度条
|
|
|
+ this.player.on('timeupdate', this.updateProgress)
|
|
|
+
|
|
|
+ // 监听全屏变化
|
|
|
+ this.player.on('fullscreenchange', () => {
|
|
|
+ if (!this.player.isFullscreen()) {
|
|
|
+ this.resetZoom()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ updateProgress() {
|
|
|
+ if (this.player.duration()) {
|
|
|
+ this.progressPercentage = (this.player.currentTime() / this.player.duration()) * 100
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ seek(event) {
|
|
|
+ if (!this.player) return
|
|
|
+
|
|
|
+ const progressBar = event.currentTarget
|
|
|
+ const percent = event.offsetX / progressBar.offsetWidth
|
|
|
+ this.player.currentTime(this.player.duration() * percent)
|
|
|
+ },
|
|
|
+
|
|
|
+ replay() {
|
|
|
+ this.player.currentTime(0)
|
|
|
+ this.player.play()
|
|
|
+ },
|
|
|
+
|
|
|
+ zoomIn() {
|
|
|
+ if (this.zoomLevel < this.maxZoom) {
|
|
|
+ this.zoomLevel += this.zoomStep
|
|
|
+ this.stepsactive++
|
|
|
+ this.applyZoom()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ zoomOut() {
|
|
|
+ if (this.zoomLevel > this.minZoom) {
|
|
|
+ this.zoomLevel -= this.zoomStep
|
|
|
+ this.stepsactive--
|
|
|
+ this.applyZoom()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ applyZoom() {
|
|
|
+ const videoElement = this.$refs.videoPlayer
|
|
|
+ if (videoElement) {
|
|
|
+ videoElement.style.transform = `scale(${this.zoomLevel})`
|
|
|
+ videoElement.style.transformOrigin = 'center center'
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ resetZoom() {
|
|
|
+ this.zoomLevel = 1
|
|
|
+ const videoElement = this.$refs.videoPlayer
|
|
|
+ if (videoElement) {
|
|
|
+ videoElement.style.transform = 'none'
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ changeSpeed() {
|
|
|
+ this.player.playbackRate(this.playbackRate)
|
|
|
+ },
|
|
|
+ takeScreenshot() {
|
|
|
+ const videoElement = this.$refs.videoPlayer
|
|
|
+ // videoElement.setAttribute("crossOrigin", "anonymous"); // 处理跨域
|
|
|
+ // video.setAttribute("src", url);
|
|
|
+ // // 静音操作,防止播放失败
|
|
|
+ // video.setAttribute("muted", "muted");
|
|
|
+ // 创建一个canvas来绘制视频帧
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.width = videoElement.videoWidth
|
|
|
+ canvas.height = videoElement.videoHeight
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height)
|
|
|
+ // 创建下载链接
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.download = `screenshot-${new Date().getTime()}.png`
|
|
|
+ link.href = canvas.toDataURL('image/png')
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+
|
|
|
+ // 或者使用html2canvas捕获整个播放器
|
|
|
+ // html2canvas(this.$refs.videoContainer).then(canvas => {
|
|
|
+ // const link = document.createElement('a')
|
|
|
+ // link.download = `screenshot-${new Date().getTime()}.png`
|
|
|
+ // link.href = canvas.toDataURL('image/png')
|
|
|
+ // link.click()
|
|
|
+ // })
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleFullscreen() {
|
|
|
+ if (this.player.isFullscreen()) {
|
|
|
+ this.player.exitFullscreen()
|
|
|
+ } else {
|
|
|
+ this.player.requestFullscreen()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.flex{display: flex;}
|
|
|
+.flex0{flex: 0 0 auto;}
|
|
|
+.flex1{flex: 1;}
|
|
|
+.flexc{display: flex;align-items: center;}
|
|
|
+.flexcc{display: flex;align-items: center;justify-content: center;}
|
|
|
+.flexcj{display: flex;justify-content: space-between;}
|
|
|
+.lcfbox{position: absolute;left: 17px;right: 17px;bottom: 15px;
|
|
|
+ .lcbox{background: rgba(0, 0, 0, 0.5);border-radius: 10px;padding: 10px 12px;display: inline-block;margin-bottom: 13px;
|
|
|
+ .tit{font-size: 14px;color: #FFFFFF;margin-bottom: 6px;}
|
|
|
+ .txt{font-weight: bold;font-size: 14px;color: #FFFFFF;}
|
|
|
+ }
|
|
|
+ .bigbox{background: rgba(0, 0, 0, 0.5);border-radius: 10px;padding: 6px 19px;min-height: 30px;margin-bottom: 12px;
|
|
|
+ img{width: 17px;height: 17px;margin-left: 11px;}
|
|
|
+ }
|
|
|
+ .playbox{min-height: 60px;padding: 9px 18px 9px 28px;
|
|
|
+ background: rgba(255, 255, 255, 0.8);display: flex;align-items: center;
|
|
|
+ .pltit{
|
|
|
+ .tit{font-size: 18px;color: #3D455B;margin-bottom: 3px;}
|
|
|
+ .txt{font-size: 12px;color: #666666;}
|
|
|
+ }
|
|
|
+ .playbtn{
|
|
|
+ .pre{width: 18px;height: 20px;}
|
|
|
+ .playb{width: 36px;height: 36px;margin: 0 29px;
|
|
|
+ img{width: 100%;height: 100%;}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+.custom-dot-steps {margin-left: 13px;width: 82px;position: relative;display: flex;align-items: center;justify-content: space-between;}
|
|
|
+
|
|
|
+.custom-dot-steps::before {content: '';position: absolute;top: 50%;left: 0;right: 0;height: 2px;background: #FFFFFF;transform: translateY(-50%);z-index: 1;}
|
|
|
+
|
|
|
+.step-dot {width: 4px;height: 4px;border-radius: 50%;background: #FFFFFF;position: relative;
|
|
|
+ &.active{width: 8px;height: 8px;}
|
|
|
+}
|
|
|
+.video-player-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;height: 100%;
|
|
|
+ /* max-width: 800px;
|
|
|
+ margin: 0 auto; */
|
|
|
+}
|
|
|
+
|
|
|
+.video-container {
|
|
|
+ width: 100%;height: 100%;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.video-js {
|
|
|
+ width: 100%;
|
|
|
+ height: 100% !important;
|
|
|
+ padding-top: 50% !important;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background-color: #000;
|
|
|
+ transition: transform 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-custom-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 10px;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ position: absolute;
|
|
|
+ bottom: 150px;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-custom-button {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: white;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 16px;
|
|
|
+ margin: 0 5px;
|
|
|
+ padding: 5px 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-custom-button:hover {
|
|
|
+ color: #00a8ff;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-progress-container {
|
|
|
+ flex-grow: 1;
|
|
|
+ margin-bottom:14px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-progress-bar {
|
|
|
+ height: 8px;
|
|
|
+ border-radius:4px;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-progress-filled {
|
|
|
+ height: 100%;border-radius: 4px;
|
|
|
+ background: red;position: relative;
|
|
|
+ img{width: 24px;height: 24px;position: absolute;right: -6px;top: -8px;}
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-speed-selector {
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 5px;
|
|
|
+ margin: 0 5px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.vjs-zoom-level {
|
|
|
+ color: white;
|
|
|
+ margin: 0 5px;
|
|
|
+ min-width: 40px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+</style>
|