视频同步播放功能组件
This commit is contained in:
parent
315739f22d
commit
03277ce88f
|
|
@ -6,7 +6,7 @@ VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
|
|
||||||
# 跨域代理,您可以配置多个 ,请注意,没有换行符
|
# 跨域代理,您可以配置多个 ,请注意,没有换行符
|
||||||
VITE_PROXY = [["/nursing-unit_101","http://localhost:8091/nursing-unit_101"],["/upload","http://localhost:3300/upload"],["/opeexup","http://localhost:8081/opeapi/"]]
|
VITE_PROXY = [["/nursing-unit_101","http://localhost:8091/nursing-unit_101"],["/upload","http://localhost:3300/upload"],["/opeexup","http://localhost:8081/opeapi/"],["/opemedia","https://www.focusnu.com/media101/upFiles101"]]
|
||||||
|
|
||||||
#后台接口全路径地址(必填)
|
#后台接口全路径地址(必填)
|
||||||
VITE_GLOB_DOMAIN_URL=http://localhost:8091/nursing-unit_101
|
VITE_GLOB_DOMAIN_URL=http://localhost:8091/nursing-unit_101
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ VITE_USE_MOCK = false
|
||||||
# 发布路径
|
# 发布路径
|
||||||
VITE_PUBLIC_PATH = /biz101
|
VITE_PUBLIC_PATH = /biz101
|
||||||
|
|
||||||
|
VITE_PROXY = [["/opemedia","https://www.focusnu.com/media101/upFiles101"]]
|
||||||
|
|
||||||
# 是否启用gzip或brotli压缩
|
# 是否启用gzip或brotli压缩
|
||||||
# 选项值: gzip | brotli | none
|
# 选项值: gzip | brotli | none
|
||||||
# 如果需要多个可以使用“,”分隔
|
# 如果需要多个可以使用“,”分隔
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<!-- 公共控制器 -->
|
||||||
|
<VideoSyncController ref="controller" />
|
||||||
|
|
||||||
|
<!-- 两个视频播放器 -->
|
||||||
|
<div class="videos">
|
||||||
|
<div class="video-item">
|
||||||
|
<h3>视频1:</h3>
|
||||||
|
<video ref="video1" controls preload="auto" :src="video1Url" crossorigin="anonymous" class="video-player"
|
||||||
|
@loadeddata="onVideo1Loaded" @error="onVideo1Error">
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-item">
|
||||||
|
<h3>视频2</h3>
|
||||||
|
<video ref="video2" controls preload="auto" :src="video2Url" crossorigin="anonymous" class="video-player"
|
||||||
|
@loadeddata="onVideo2Loaded" @error="onVideo2Error">
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import VideoSyncController from './tongshibofangCom.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
// 在线视频地址
|
||||||
|
// const video1Url = ref('/opemedia/temp/将进酒_1754615544253.mp4')
|
||||||
|
// const video2Url = ref('/opemedia/temp/18.6m_1754636202719.mp4')
|
||||||
|
const video1Url = ref('https://www.focusnu.com/media101/upFiles101/temp/testvideo1.mp4')
|
||||||
|
const video2Url = ref('https://www.focusnu.com/media101/upFiles101/temp/testvideo2.mp4')
|
||||||
|
|
||||||
|
|
||||||
|
// 引用
|
||||||
|
const controller = ref()
|
||||||
|
const video1 = ref<HTMLVideoElement>()
|
||||||
|
const video2 = ref<HTMLVideoElement>()
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const video1Loaded = ref(false)
|
||||||
|
const video2Loaded = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
// 组件挂载时注册视频
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
registerVideos()
|
||||||
|
}, 100) // 等DOM渲染完成
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册视频
|
||||||
|
const registerVideos = () => {
|
||||||
|
if (video1.value && controller.value) {
|
||||||
|
controller.value.registerVideo1(video1.value)
|
||||||
|
}
|
||||||
|
if (video2.value && controller.value) {
|
||||||
|
controller.value.registerVideo2(video2.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频加载完成事件
|
||||||
|
const onVideo1Loaded = () => {
|
||||||
|
video1Loaded.value = true
|
||||||
|
console.log('视频1加载完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVideo2Loaded = () => {
|
||||||
|
video2Loaded.value = true
|
||||||
|
console.log('视频2加载完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频加载错误
|
||||||
|
const onVideo1Error = (e: Event) => {
|
||||||
|
video1Loaded.value = false
|
||||||
|
error.value = '视频1加载失败,可能是网络或跨域问题'
|
||||||
|
console.error('视频1错误:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVideo2Error = (e: Event) => {
|
||||||
|
video2Loaded.value = false
|
||||||
|
error.value = '视频2加载失败,可能是网络或跨域问题'
|
||||||
|
console.error('视频2错误:', e)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.error-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videos {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #000;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.videos {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
<template>
|
||||||
|
<div class="video-controller">
|
||||||
|
<a-space>
|
||||||
|
<!-- 播放控制 -->
|
||||||
|
<a-button @click="playAll" type="primary" :icon="h(PlayCircleOutlined)">播放</a-button>
|
||||||
|
<a-button @click="pauseAll" :icon="h(PauseCircleOutlined)">暂停</a-button>
|
||||||
|
<a-button @click="replayAll" :icon="h(RedoOutlined)">重播</a-button>
|
||||||
|
<a-button @click="stopAll" :icon="h(StopOutlined)">停止</a-button>
|
||||||
|
|
||||||
|
<!-- 快进快退控制 -->
|
||||||
|
<a-button-group>
|
||||||
|
<a-button @mousedown="startSeek(-3)" @mouseup="stopSeek" @mouseleave="stopSeek" @touchstart="startSeek(-3)"
|
||||||
|
@touchend="stopSeek">
|
||||||
|
-3s
|
||||||
|
</a-button>
|
||||||
|
<a-button @mousedown="startSeek(-9)" @mouseup="stopSeek" @mouseleave="stopSeek" @touchstart="startSeek(-9)"
|
||||||
|
@touchend="stopSeek">
|
||||||
|
-9s
|
||||||
|
</a-button>
|
||||||
|
<a-button @mousedown="startSeek(-15)" @mouseup="stopSeek" @mouseleave="stopSeek" @touchstart="startSeek(-15)"
|
||||||
|
@touchend="stopSeek">
|
||||||
|
-15s
|
||||||
|
</a-button>
|
||||||
|
<a-button @mousedown="startSeek(3)" @mouseup="stopSeek" @mouseleave="stopSeek" @touchstart="startSeek(3)"
|
||||||
|
@touchend="stopSeek">
|
||||||
|
+3s
|
||||||
|
</a-button>
|
||||||
|
<a-button @mousedown="startSeek(9)" @mouseup="stopSeek" @mouseleave="stopSeek" @touchstart="startSeek(9)"
|
||||||
|
@touchend="stopSeek">
|
||||||
|
+9s
|
||||||
|
</a-button>
|
||||||
|
<a-button @mousedown="startSeek(15)" @mouseup="stopSeek" @mouseleave="stopSeek" @touchstart="startSeek(15)"
|
||||||
|
@touchend="stopSeek">
|
||||||
|
+15s
|
||||||
|
</a-button>
|
||||||
|
</a-button-group>
|
||||||
|
|
||||||
|
<!-- 倍速控制 -->
|
||||||
|
<a-select v-model:value="playbackRate" style="width: 120px" @change="changeSpeed">
|
||||||
|
<a-select-option value="0.5">0.5x 慢速</a-select-option>
|
||||||
|
<a-select-option value="0.75">0.75x 较慢</a-select-option>
|
||||||
|
<a-select-option value="1.0">1x 正常</a-select-option>
|
||||||
|
<a-select-option value="1.25">1.25x 较快</a-select-option>
|
||||||
|
<a-select-option value="1.5">1.5x 快速</a-select-option>
|
||||||
|
<a-select-option value="2.0">2x 极快</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
|
||||||
|
<a-button @click="resetSpeed" :icon="h(UndoOutlined)">正常速度</a-button>
|
||||||
|
|
||||||
|
<!-- 按住倍速按钮 -->
|
||||||
|
<a-button @mousedown="fastForwardStart" @mouseup="fastForwardStop" @mouseleave="fastForwardStop"
|
||||||
|
@touchstart="fastForwardStart" @touchend="fastForwardStop" type="primary">
|
||||||
|
按住加速(松开恢复)
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<!-- 音量控制 -->
|
||||||
|
<a-button-group>
|
||||||
|
<a-tooltip title="音量+">
|
||||||
|
<a-button @click="volumeUp" :icon="h(PlusOutlined)" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip title="音量-">
|
||||||
|
<a-button @click="volumeDown" :icon="h(MinusOutlined)" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip title="静音">
|
||||||
|
<a-button @click="toggleMute" :icon="h(muted ? SoundOutlined : SoundFilled)" />
|
||||||
|
</a-tooltip>
|
||||||
|
</a-button-group>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, h, onUnmounted } from 'vue'
|
||||||
|
import {
|
||||||
|
PlayCircleOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
|
RedoOutlined,
|
||||||
|
StopOutlined,
|
||||||
|
UndoOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
MinusOutlined,
|
||||||
|
SoundOutlined,
|
||||||
|
SoundFilled
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
// 存储两个视频的引用
|
||||||
|
const video1 = ref<HTMLVideoElement>()
|
||||||
|
const video2 = ref<HTMLVideoElement>()
|
||||||
|
const playbackRate = ref('1.0')
|
||||||
|
const muted = ref(false)
|
||||||
|
const volume = ref(0.7)
|
||||||
|
|
||||||
|
// 新增变量
|
||||||
|
const seekInterval = ref<number | null>(null)
|
||||||
|
const fastForwardInterval = ref<number | null>(null)
|
||||||
|
const originalSpeed = ref(1.0)
|
||||||
|
|
||||||
|
// 注册视频元素
|
||||||
|
const registerVideo1 = (el: HTMLVideoElement) => {
|
||||||
|
video1.value = el
|
||||||
|
if (video1.value) {
|
||||||
|
video1.value.volume = volume.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const registerVideo2 = (el: HTMLVideoElement) => {
|
||||||
|
video2.value = el
|
||||||
|
if (video2.value) {
|
||||||
|
video2.value.volume = volume.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 快进快退功能
|
||||||
|
const startSeek = (seconds: number) => {
|
||||||
|
if (seekInterval.value) {
|
||||||
|
clearInterval(seekInterval.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
seekVideos(seconds)
|
||||||
|
|
||||||
|
// 然后每200ms执行一次
|
||||||
|
seekInterval.value = window.setInterval(() => {
|
||||||
|
seekVideos(seconds)
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopSeek = () => {
|
||||||
|
if (seekInterval.value) {
|
||||||
|
clearInterval(seekInterval.value)
|
||||||
|
seekInterval.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekVideos = (seconds: number) => {
|
||||||
|
if (video1.value && !isNaN(video1.value.duration)) {
|
||||||
|
const newTime = video1.value.currentTime + seconds
|
||||||
|
video1.value.currentTime = Math.max(0, Math.min(newTime, video1.value.duration))
|
||||||
|
}
|
||||||
|
if (video2.value && !isNaN(video2.value.duration)) {
|
||||||
|
const newTime = video2.value.currentTime + seconds
|
||||||
|
video2.value.currentTime = Math.max(0, Math.min(newTime, video2.value.duration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 按住倍速播放功能
|
||||||
|
const fastForwardStart = () => {
|
||||||
|
if (video1.value) {
|
||||||
|
originalSpeed.value = video1.value.playbackRate
|
||||||
|
video1.value.playbackRate = 2.0
|
||||||
|
}
|
||||||
|
if (video2.value) {
|
||||||
|
video2.value.playbackRate = 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fastForwardStop = () => {
|
||||||
|
if (video1.value) {
|
||||||
|
video1.value.playbackRate = originalSpeed.value
|
||||||
|
}
|
||||||
|
if (video2.value) {
|
||||||
|
video2.value.playbackRate = originalSpeed.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原有播放控制方法
|
||||||
|
const playAll = () => {
|
||||||
|
const playVideo1 = video1.value?.play()
|
||||||
|
const playVideo2 = video2.value?.play()
|
||||||
|
|
||||||
|
Promise.all([playVideo1, playVideo2].filter(Boolean))
|
||||||
|
.then(() => {
|
||||||
|
console.log('两个视频都已开始播放')
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('播放失败:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseAll = () => {
|
||||||
|
video1.value?.pause()
|
||||||
|
video2.value?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
const replayAll = () => {
|
||||||
|
if (video1.value) {
|
||||||
|
video1.value.currentTime = 0
|
||||||
|
}
|
||||||
|
if (video2.value) {
|
||||||
|
video2.value.currentTime = 0
|
||||||
|
}
|
||||||
|
playAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAll = () => {
|
||||||
|
pauseAll()
|
||||||
|
if (video1.value) video1.value.currentTime = 0
|
||||||
|
if (video2.value) video2.value.currentTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeSpeed = (speed: string) => {
|
||||||
|
playbackRate.value = speed
|
||||||
|
originalSpeed.value = parseFloat(speed)
|
||||||
|
if (video1.value) video1.value.playbackRate = originalSpeed.value
|
||||||
|
if (video2.value) video2.value.playbackRate = originalSpeed.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSpeed = () => {
|
||||||
|
playbackRate.value = '1.0'
|
||||||
|
originalSpeed.value = 1.0
|
||||||
|
if (video1.value) video1.value.playbackRate = 1.0
|
||||||
|
if (video2.value) video2.value.playbackRate = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音量控制
|
||||||
|
const volumeUp = () => {
|
||||||
|
volume.value = Math.min(1, volume.value + 0.1)
|
||||||
|
updateVolume()
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeDown = () => {
|
||||||
|
volume.value = Math.max(0, volume.value - 0.1)
|
||||||
|
updateVolume()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
muted.value = !muted.value
|
||||||
|
updateVolume()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVolume = () => {
|
||||||
|
const vol = muted.value ? 0 : volume.value
|
||||||
|
if (video1.value) video1.value.volume = vol
|
||||||
|
if (video2.value) video2.value.volume = vol
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清除定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (seekInterval.value) clearInterval(seekInterval.value)
|
||||||
|
if (fastForwardInterval.value) clearInterval(fastForwardInterval.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({ registerVideo1, registerVideo2 })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-controller {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue