Vue3 视频打点业务解决方案详解
一、核心业务场景
- 教育视频关键帧标记
- 用户UGC内容精彩片段标注
- 视频审核问题点位记录
- 影视制作关键帧注释
二、技术方案架构
核心依赖:
- @vueuse/core(推荐)
- video.js(可选)
- 原生HTML5 Video
三、基础实现方案
<template>
<div class="video-container">
<video ref="videoRef" controls class="video-player">
<source src="/demo.mp4" type="video/mp4">
</video>
<div class="controls">
<button @click="addMarker">添加标记</button>
<button @click="saveMarkers">保存标记</button>
</div>
<div class="marker-list">
<div v-for="(marker, index) in markers" :key="index"
class="marker-item" @click="jumpTo(marker.time)">
{{ formatTime(marker.time) }} - {{ marker.comment }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { useMediaControls } from '@vueuse/core'
// 视频元素引用
const videoRef = ref(null)
const markers = reactive([])
// 使用VueUse的媒体控制
const { currentTime, duration } = useMediaControls(videoRef)
// 添加标记点
const addMarker = () => {
if (!videoRef.value) return
const marker = {
time: currentTime.value,
comment: prompt('请输入标记备注') || '未命名标记'
}
markers.push(marker)
markers.sort((a, b) => a.time - b.time)
}
// 跳转到指定时间
const jumpTo = (time) => {
if (videoRef.value) {
videoRef.value.currentTime = time
videoRef.value.play()
}
}
// 时间格式化
const formatTime = (seconds) => {
const date = new Date(0)
date.setSeconds(seconds)
return date.toISOString().substr(11, 8)
}
// 保存标记点
const saveMarkers = () => {
localStorage.setItem('video_markers', JSON.stringify(markers))
}
// 初始化加载
onMounted(() => {
const saved = localStorage.getItem('video_markers')
if (saved) markers.push(...JSON.parse(saved))
})
// 清理事件监听
onUnmounted(() => {
if (videoRef.value) {
videoRef.value.pause()
videoRef.value.removeAttribute('src')
videoRef.value.load()
}
})
</script>
<style scoped>
.video-container {
max-width: 800px;
margin: 0 auto;
}
.video-player {
width: 100%;
height: 450px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
}
.marker-list {
border-top: 1px solid #ddd;
padding-top: 15px;
}
.marker-item {
padding: 8px;
margin: 5px 0;
cursor: pointer;
background: #f5f5f5;
transition: background 0.3s;
}
.marker-item:hover {
background: #e0e0e0;
}
</style>
四、高级功能实现
- 可视化进度条标记
<template>
<div class="progress-bar" @click="seek">
<div class="progress" :style="{ width: progressPercent }">
<div v-for="marker in markers"
:key="marker.time"
class="marker"
:style="{ left: getMarkerPosition(marker.time) }"
@click.stop="jumpTo(marker.time)">
</div>
</div>
</div>
</template>
<script setup>
// 进度计算
const progressPercent = computed(() =>
`${(currentTime.value / duration.value * 100) || 0}%`
)
const getMarkerPosition = (time) =>
`${(time / duration.value * 100)}%`
const seek = (e) => {
const rect = e.target.getBoundingClientRect()
const percent = (e.clientX - rect.left) / rect.width
videoRef.value.currentTime = percent * duration.value
}
</script>
<style>
.progress-bar {
height: 5px;
background: #ddd;
cursor: pointer;
position: relative;
}
.progress {
height: 100%;
background: #42b983;
position: relative;
}
.marker {
position: absolute;
width: 8px;
height: 8px;
background: #ff4757;
border-radius: 50%;
top: -2px;
transform: translateX(-50%);
}
</style>
- 标记点协同编辑(WebSocket集成)
// 添加WebSocket支持
import { useWebSocket } from '@vueuse/core'
const { data, send } = useWebSocket('ws://your-websocket-url')
watch(data, (newData) => {
try {
const msg = JSON.parse(newData)
if (msg.type === 'MARKER_UPDATE') {
markers.splice(0, markers.length, ...msg.data)
}
} catch(e) {
console.error('WS数据解析失败', e)
}
})
const broadcastMarkers = () => {
send(JSON.stringify({
type: 'MARKER_UPDATE',
data: markers
}))
}
五、常见问题解决方案
- 时间精度问题
// 使用高精度时间处理
const getCurrentTime = () => {
return videoRef.value ? Math.floor(videoRef.value.currentTime * 100) / 100 : 0
}
- 大数量标记性能优化
// 虚拟滚动优化
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(
markers,
{
itemHeight: 40,
overscan: 10
}
)
- 跨浏览器兼容性
// 全屏处理兼容
const requestFullscreen = () => {
if (videoRef.value.requestFullscreen) {
videoRef.value.requestFullscreen()
} else if (videoRef.value.webkitRequestFullscreen) {
videoRef.value.webkitRequestFullscreen()
}
}
- 视频预加载优化
<video preload="metadata">
<source src="/video.mp4#t=0.5" type="video/mp4">
</video>
六、最佳实践建议
- 数据持久化策略
- 使用IndexedDB存储大量标记数据
- 配合LocalStorage实现自动保存
- 重要数据增加服务端同步
- 性能优化方案
// 防抖处理标记更新
const updateMarkers = useDebounceFn((newMarkers) => {
sendUpdateToServer(newMarkers)
}, 1000)
- 安全增强
// XSS防护
const sanitizeComment = (text) => {
return text.replace(/</g, '<').replace(/>/g, '>')
}
- 可访问性增强
<button aria-label="添加视频标记" @click="addMarker">
<svg><!-- 图标 --></svg>
</button>
七、扩展功能建议
- 标记分类系统
const markerTypes = {
IMPORTANT: { color: '#ff4757' },
COMMENT: { color: '#2ed573' },
QUESTION: { color: '#ffa502' }
}
- AI自动标记
const detectScenes = async () => {
const response = await fetch('/api/detect-scenes', {
method: 'POST',
body: videoFile
})
const scenes = await response.json()
markers.push(...scenes)
}
- 视频帧精确预览
const generateThumbnail = async (time) => {
const canvas = document.createElement('canvas')
canvas.width = 160
canvas.height = 90
videoRef.value.currentTime = time
await new Promise(r => videoRef.value.onseeked = r)
canvas.getContext('2d').drawImage(videoRef.value, 0, 0)
return canvas.toDataURL()
}
该方案完整实现了视频打点的核心功能,并提供了扩展性良好的架构设计,可根据具体业务需求进行模块化扩展。建议在实际项目中根据具体需求选择合适的第三方库和优化策略。