视频播放修改

This commit is contained in:
cwchen 2026-01-08 16:54:26 +08:00
parent 6bb7da638f
commit b081921d0c
1 changed files with 178 additions and 232 deletions

View File

@ -1,6 +1,5 @@
<template>
<div class="video-monitor-container">
<!-- 顶部状态栏 -->
<el-card class="top-bar-card" shadow="never">
<div class="top-bar">
<div class="status-bar">
@ -14,8 +13,12 @@
<i class="el-icon-time"></i>
{{ currentTime }}
</div>
<!-- 画线按钮组 -->
<div class="draw-line-buttons">
<el-button type="default" plain class="fullscreen-btn" @click="toggleFullscreen"
icon="el-icon-full-screen">
{{ isFullscreen ? '退出全屏' : '全屏监控' }}
</el-button>
<el-button v-if="!isDrawingMode" type="primary" class="draw-line-btn" @click="handleDrawLine"
icon="el-icon-edit">
画线
@ -35,25 +38,19 @@
</div>
</el-card>
<!-- 视频播放区域 -->
<el-card class="video-card" shadow="never" :body-style="{ padding: '0', height: '100%' }">
<div class="video-wrapper">
<div class="video-container" ref="videoContainer">
<video ref="videoPlayer" class="video-player" :src="videoUrl" autoplay muted controls
@loadedmetadata="handleVideoLoaded"
@error="handleVideoError"
>
@loadedmetadata="handleVideoLoaded" @error="handleVideoError" controlsList="nofullscreen">
您的浏览器不支持视频播放
</video>
<!-- 画线画布覆盖层 -->
<canvas ref="drawCanvas" class="draw-canvas" :class="{ active: isDrawingMode }"
@mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing"
@mouseleave="stopDrawing"></canvas>
<!-- 已保存的画线显示层只读 -->
<canvas ref="savedLineCanvas" class="saved-line-canvas"></canvas>
<!-- 视频加载提示 -->
<div v-if="!videoUrl" class="video-placeholder">
<i class="el-icon-video-camera"></i>
<p>暂无视频流</p>
@ -72,21 +69,27 @@ export default {
data() {
return {
currentTime: '',
videoUrl: '', //
isDrawingMode: false, // 线
videoUrl: '',
isDrawingMode: false,
isDrawing: false,
startX: 0,
startY: 0,
endX: 0,
endY: 0,
line: null, // 线线
savedLine: null, // 线
initialLine: null, // 线线
currentLine: null, // 线
detectedVehicles: [], //
line: null,
savedLine: null,
initialLine: null,
currentLine: null,
detectedVehicles: [],
timer: null,
saving: false, //
videoErrorShown: false, //
saving: false,
videoErrorShown: false,
//
isFullscreen: false,
//
lastContainerWidth: 0,
lastContainerHeight: 0,
resizeObserver: null,
lineInfo: {
equipmentName: '',
equipmentStatus: '',
@ -114,23 +117,130 @@ export default {
this.updateTime()
}, 1000)
//
// ResizeObserver
this.initResizeObserver()
// ESC 退
document.addEventListener('fullscreenchange', this.onFullscreenChange)
document.addEventListener('webkitfullscreenchange', this.onFullscreenChange)
document.addEventListener('mozfullscreenchange', this.onFullscreenChange)
document.addEventListener('MSFullscreenChange', this.onFullscreenChange)
this.$nextTick(() => {
this.initCanvas()
// 线使
this.initInitialLine()
})
//
this.loadVideoStream()
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
}
//
if (this.resizeObserver) {
this.resizeObserver.disconnect()
}
document.removeEventListener('fullscreenchange', this.onFullscreenChange)
document.removeEventListener('webkitfullscreenchange', this.onFullscreenChange)
document.removeEventListener('mozfullscreenchange', this.onFullscreenChange)
document.removeEventListener('MSFullscreenChange', this.onFullscreenChange)
},
methods: {
/** 更新时间 */
/** 初始化 ResizeObserver */
initResizeObserver() {
const container = this.$refs.videoContainer
if (!container) return
//
this.lastContainerWidth = container.offsetWidth
this.lastContainerHeight = container.offsetHeight
this.resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
// 使 requestAnimationFrame
requestAnimationFrame(() => {
this.handleResize(entry.contentRect)
})
}
})
this.resizeObserver.observe(container)
},
/** 处理容器大小变化(全屏或窗口缩放)核心逻辑 */
handleResize(rect) {
const newWidth = rect.width
const newHeight = rect.height
// 0
if (newWidth === 0 || newHeight === 0) return
// 1.
const scaleX = newWidth / this.lastContainerWidth
const scaleY = newHeight / this.lastContainerHeight
// 2. Canvas CSS
const drawCanvas = this.$refs.drawCanvas
const savedCanvas = this.$refs.savedLineCanvas
if (drawCanvas) {
drawCanvas.width = newWidth
drawCanvas.height = newHeight
}
if (savedCanvas) {
savedCanvas.width = newWidth
savedCanvas.height = newHeight
}
// 3. 线
const scaleLine = (l) => {
if (!l) return null
return {
startX: l.startX * scaleX,
startY: l.startY * scaleY,
endX: l.endX * scaleX,
endY: l.endY * scaleY
}
}
if (this.initialLine) this.initialLine = scaleLine(this.initialLine)
if (this.savedLine) this.savedLine = scaleLine(this.savedLine)
if (this.line) this.line = scaleLine(this.line)
if (this.currentLine) this.currentLine = scaleLine(this.currentLine)
// 4.
this.lastContainerWidth = newWidth
this.lastContainerHeight = newHeight
// 5.
this.redrawCanvas()
},
/** 全屏切换 */
toggleFullscreen() {
const container = this.$refs.videoContainer
if (!document.fullscreenElement) {
//
if (container.requestFullscreen) {
container.requestFullscreen()
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen()
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen()
}
} else {
// 退
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
},
/** 监听全屏状态变化 */
onFullscreenChange() {
this.isFullscreen = !!document.fullscreenElement
},
updateTime() {
const now = new Date()
const year = now.getFullYear()
@ -142,12 +252,15 @@ export default {
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/** 初始化画布 */
initCanvas() {
const canvas = this.$refs.drawCanvas
const savedCanvas = this.$refs.savedLineCanvas
const container = this.$refs.videoContainer
if (container) {
// lastContainerWidth
this.lastContainerWidth = container.offsetWidth
this.lastContainerHeight = container.offsetHeight
if (canvas) {
canvas.width = container.offsetWidth
canvas.height = container.offsetHeight
@ -155,7 +268,6 @@ export default {
if (savedCanvas) {
savedCanvas.width = container.offsetWidth
savedCanvas.height = container.offsetHeight
// 线线
if (this.savedLine || this.line || this.initialLine) {
this.redrawCanvas()
}
@ -163,7 +275,6 @@ export default {
}
},
/** 初始化默认线 */
async initInitialLine() {
try {
const container = this.$refs.videoContainer
@ -172,14 +283,11 @@ export default {
const containerWidth = container.offsetWidth || 1920
const containerHeight = container.offsetHeight || 1080
// 线
const res = await getLineDetailAPI({})
if (res.code === 200 && res.data) {
const data = res.data
this.lineInfo = data
// 线
if (data.startX && data.startY && data.endX && data.endY) {
this.initialLine = {
startX: (parseFloat(data.startX) / 100) * containerWidth,
@ -187,8 +295,6 @@ export default {
endX: (parseFloat(data.endX) / 100) * containerWidth,
endY: (parseFloat(data.endY) / 100) * containerHeight
}
// 线
this.savedLine = { ...this.initialLine }
this.redrawCanvas()
}
@ -198,95 +304,71 @@ export default {
}
},
/** 处理画线按钮点击 */
handleDrawLine() {
this.isDrawingMode = true
// 线线
this.line = null // 线
this.clearCanvas() // 线
// 线
this.line = null
this.clearCanvas()
if (this.savedLine) {
this.redrawCanvas()
}
this.$message.success('画线模式已开启,在视频上按住鼠标左键拖动即可画线(只能画一条线)')
this.$message.success('画线模式已开启,在视频上按住鼠标左键拖动即可画线')
},
/** 保存画线 */
async handleSaveLines() {
// 线
// 线线
const lineToSave = this.line || this.savedLine
if (!lineToSave) {
this.$message.warning('请先画线后再保存')
return
}
this.saving = true
try {
//
const container = this.$refs.videoContainer
const containerWidth = container ? container.offsetWidth : 1920
const containerHeight = container ? container.offsetHeight : 1080
//
const params = {
startX: String(((lineToSave.startX / containerWidth) * 100).toFixed(2)),
startY: String(((lineToSave.startY / containerHeight) * 100).toFixed(2)),
endX: String(((lineToSave.endX / containerWidth) * 100).toFixed(2)),
endY: String(((lineToSave.endY / containerHeight) * 100).toFixed(2))
}
const res = await updateLineAPI(params)
if (res.code === 200) {
this.$message.success('画线数据保存成功')
// 线线线
if (this.line) {
const newLine = { ...this.line }
this.savedLine = newLine
// 线线
this.initialLine = { ...newLine }
this.line = null
}
// 线线
this.isDrawingMode = false
//
this.clearCanvas()
// 线线
this.redrawCanvas()
} else {
this.$message.error(res.msg || '保存失败')
}
} catch (error) {
console.error('保存画线数据失败:', error)
// this.$message.error('')
} finally {
this.saving = false
}
},
/** 取消画线 */
handleCancelLines() {
// 线使线
if (this.initialLine) {
this.isDrawingMode = false
this.line = null // 线
this.clearCanvas() //
// 使线
this.line = null
this.clearCanvas()
this.savedLine = { ...this.initialLine }
this.redrawCanvas()
this.$message.info('已取消画线模式,恢复初始线')
} else if (this.savedLine) {
// 线线线线
this.isDrawingMode = false
this.line = null // 线
this.clearCanvas() //
// 线
this.line = null
this.clearCanvas()
this.redrawCanvas()
this.$message.info('已取消画线模式')
} else {
// 线线
this.$confirm('确定要取消画线吗?画线将被清除', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@ -295,20 +377,16 @@ export default {
this.isDrawingMode = false
this.line = null
this.clearCanvas()
// 线
const savedCanvas = this.$refs.savedLineCanvas
if (savedCanvas) {
const ctx = savedCanvas.getContext('2d')
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
}
this.$message.info('已取消画线')
}).catch(() => {
//
})
}).catch(() => { })
}
},
/** 清除画布 */
clearCanvas() {
const canvas = this.$refs.drawCanvas
if (canvas) {
@ -317,20 +395,16 @@ export default {
}
},
/** 开始画线 */
startDrawing(e) {
if (!this.isDrawingMode) return
// 线使线
if (this.savedLine) {
this.savedLine = null
// 线
const savedCanvas = this.$refs.savedLineCanvas
if (savedCanvas) {
const ctx = savedCanvas.getContext('2d')
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
}
}
// 线线线
if (this.line) {
this.line = null
}
@ -347,7 +421,6 @@ export default {
}
},
/** 画线 */
draw(e) {
if (!this.isDrawing) return
const canvas = this.$refs.drawCanvas
@ -356,18 +429,15 @@ export default {
const currentX = e.clientX - rect.left
const currentY = e.clientY - rect.top
// 线
if (this.currentLine) {
this.currentLine.endX = currentX
this.currentLine.endY = currentY
}
// 线线
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = '#ff0000'
ctx.lineWidth = 3
// 线
if (this.currentLine) {
ctx.beginPath()
ctx.moveTo(this.currentLine.startX, this.currentLine.startY)
@ -376,32 +446,24 @@ export default {
}
},
/** 停止画线 */
stopDrawing() {
if (this.isDrawing && this.currentLine) {
//
if (this.currentLine.startX !== this.currentLine.endX ||
this.currentLine.startY !== this.currentLine.endY) {
// 线使线
if (this.savedLine) {
this.savedLine = null
// 线
const savedCanvas = this.$refs.savedLineCanvas
if (savedCanvas) {
const ctx = savedCanvas.getContext('2d')
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
}
}
// 线线
this.line = {
startX: this.currentLine.startX,
startY: this.currentLine.startY,
endX: this.currentLine.endX,
endY: this.currentLine.endY
}
// 线
this.redrawCanvas()
}
this.currentLine = null
@ -409,21 +471,13 @@ export default {
this.isDrawing = false
},
/** 重绘画布 */
redrawCanvas() {
// 线
const savedCanvas = this.$refs.savedLineCanvas
if (savedCanvas) {
const container = this.$refs.videoContainer
if (container) {
savedCanvas.width = container.offsetWidth
savedCanvas.height = container.offsetHeight
}
const ctx = savedCanvas.getContext('2d')
//
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
// 线线线线
const lineToDraw = this.savedLine || this.line || this.initialLine
if (lineToDraw) {
ctx.strokeStyle = '#ff0000'
@ -436,49 +490,30 @@ export default {
}
},
/** 视频加载完成 */
handleVideoLoaded() {
console.log('视频加载完成')
//
this.$nextTick(() => {
this.initCanvas()
// 线线
if (this.savedLine || this.line || this.initialLine) {
this.redrawCanvas()
}
})
},
/** 视频加载错误 */
handleVideoError(e) {
// URL
if (!this.videoUrl) {
return
}
//
if (this.videoErrorShown) {
return
}
if (!this.videoUrl) return
if (this.videoErrorShown) return
console.error('视频加载失败:', e)
this.videoErrorShown = true
this.$message.error('视频加载失败,请检查视频源地址')
},
/** 加载视频流 */
async loadVideoStream() {
try {
const res = await getVideoStreamAPI()
if (res.code === 200 && res.data && res.data.streamUrl) {
//
this.videoErrorShown = false
this.videoUrl = res.data.streamUrl
} else {
//
console.log('未获取到视频流地址')
}
} catch (error) {
console.error('获取视频流失败:', error)
//
}
}
}
@ -486,6 +521,7 @@ export default {
</script>
<style scoped lang="scss">
/* 保持原有样式不变,仅为 video-container 增加全屏时的样式处理 */
.video-monitor-container {
width: 100%;
height: calc(100vh - 84px);
@ -581,57 +617,6 @@ export default {
align-items: center;
gap: 12px;
}
.draw-line-btn {
background: #1f72ea;
border: none;
box-shadow: 0px 4px 8px 0px rgba(51, 135, 255, 0.5);
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
&:hover {
background: #4a8bff;
box-shadow: 0px 6px 12px 0px rgba(51, 135, 255, 0.6);
transform: translateY(-1px);
}
}
.save-line-btn {
background: #67c23a;
border: none;
box-shadow: 0px 4px 8px 0px rgba(103, 194, 58, 0.5);
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
&:hover {
background: #85ce61;
box-shadow: 0px 6px 12px 0px rgba(103, 194, 58, 0.6);
transform: translateY(-1px);
}
}
.cancel-line-btn {
background: #909399;
border: none;
box-shadow: 0px 4px 8px 0px rgba(144, 147, 153, 0.5);
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
&:hover {
background: #a6a9ad;
box-shadow: 0px 6px 12px 0px rgba(144, 147, 153, 0.6);
transform: translateY(-1px);
}
}
}
}
@ -666,11 +651,25 @@ export default {
display: flex;
align-items: center;
justify-content: center;
/* 关键:确保全屏时背景为黑色 */
background-color: #000;
/* 兼容浏览器的全屏伪类 */
&:fullscreen {
width: 100vw;
height: 100vh;
}
&:-webkit-full-screen {
width: 100vw;
height: 100vh;
}
}
.video-player {
width: 100%;
height: 100%;
/* 保持宽高比,或者使用 fill 铺满 */
object-fit: fill;
background: #000;
}
@ -723,74 +722,6 @@ export default {
z-index: 1.5;
}
.detection-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.vehicle-box {
position: absolute;
border: 2px solid #00ff00;
box-sizing: border-box;
pointer-events: none;
animation: pulse 2s infinite;
.box-corner {
position: absolute;
width: 12px;
height: 12px;
background: #00ff00;
border: 2px solid #000;
box-sizing: border-box;
&.corner-tl {
top: -6px;
left: -6px;
border-right: none;
border-bottom: none;
}
&.corner-tr {
top: -6px;
right: -6px;
border-left: none;
border-bottom: none;
}
&.corner-bl {
bottom: -6px;
left: -6px;
border-right: none;
border-top: none;
}
&.corner-br {
bottom: -6px;
right: -6px;
border-left: none;
border-top: none;
}
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
//
@media (max-width: 768px) {
.video-monitor-container {
padding: 8px;
@ -815,4 +746,19 @@ export default {
}
}
}
</style>
/* 隐藏 Chrome, Edge, Safari 的全屏按钮 */
video::-webkit-media-controls-fullscreen-button {
display: none !important;
}
/* 隐藏部分旧版浏览器的全屏功能(如果存在) */
video::-webkit-media-controls-enclosure {
overflow: hidden !important;
}
/* 针对特定移动端(如某些安卓浏览器) */
video::-internal-media-controls-download-button {
display: none;
}
</style>