feat:切换dplayer播放器
This commit is contained in:
parent
b37cdd3ccc
commit
5b82a9b044
43
package-lock.json
generated
43
package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.11.0",
|
||||
"ckplayer": "^3.1.2",
|
||||
"dplayer": "^1.27.1",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pinia": "^3.0.3",
|
||||
"quill": "^2.0.3",
|
||||
@ -20,6 +21,7 @@
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dplayer": "^1.25.5",
|
||||
"@types/node": "^24.0.15",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
@ -1398,6 +1400,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dplayer": {
|
||||
"version": "1.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dplayer/-/dplayer-1.25.5.tgz",
|
||||
"integrity": "sha512-p/7O94dHDo0Irn2KWIqFE+fBCA4DS7QL3jfCOjCUPBAOgppyyTjmNZjKEfiJa1M3n1oVQqG7xnPwhiIuCqOzkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||
@ -1777,6 +1786,12 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balloon-css": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/balloon-css/-/balloon-css-1.2.0.tgz",
|
||||
"integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
|
||||
@ -2139,6 +2154,28 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dplayer": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/dplayer/-/dplayer-1.27.1.tgz",
|
||||
"integrity": "sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "1.2.3",
|
||||
"balloon-css": "^1.0.3",
|
||||
"promise-polyfill": "8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dplayer/node_modules/axios": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz",
|
||||
"integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -3207,6 +3244,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-polyfill": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
|
||||
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.11.0",
|
||||
"ckplayer": "^3.1.2",
|
||||
"dplayer": "^1.27.1",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pinia": "^3.0.3",
|
||||
"quill": "^2.0.3",
|
||||
@ -25,6 +26,7 @@
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dplayer": "^1.25.5",
|
||||
"@types/node": "^24.0.15",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
|
456
src/components/DPlayerVideo.vue
Normal file
456
src/components/DPlayerVideo.vue
Normal file
@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div class="dplayer-wrapper">
|
||||
<div :id="playerId" class="dplayer-container"></div>
|
||||
|
||||
<!-- 原生视频播放器回退 -->
|
||||
<video
|
||||
v-if="error && videoUrl"
|
||||
class="fallback-video"
|
||||
:src="videoUrl"
|
||||
:poster="poster"
|
||||
controls
|
||||
preload="auto"
|
||||
@play="emit('play')"
|
||||
@pause="emit('pause')"
|
||||
@ended="emit('ended')"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载视频...</p>
|
||||
</div>
|
||||
<div v-if="error && !videoUrl" class="error-overlay">
|
||||
<p>视频加载失败</p>
|
||||
<button @click="retryLoad" class="retry-btn">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import DPlayer from 'dplayer'
|
||||
|
||||
interface Props {
|
||||
videoUrl?: string
|
||||
poster?: string
|
||||
title?: string
|
||||
description?: string
|
||||
autoplay?: boolean
|
||||
showControls?: boolean
|
||||
qualities?: Array<{
|
||||
label: string
|
||||
value: string
|
||||
url: string
|
||||
}>
|
||||
currentQuality?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
videoUrl: '',
|
||||
poster: '',
|
||||
title: '视频播放',
|
||||
description: '',
|
||||
autoplay: false,
|
||||
showControls: true,
|
||||
qualities: () => [],
|
||||
currentQuality: '360p'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
play: []
|
||||
pause: []
|
||||
ended: []
|
||||
error: [error: Event]
|
||||
}>()
|
||||
|
||||
const playerId = ref(`dplayer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`)
|
||||
const dplayer = ref<DPlayer | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
|
||||
// 监听URL变化
|
||||
watch(() => props.videoUrl, (newUrl) => {
|
||||
if (newUrl) {
|
||||
// 减少日志输出,避免F12卡顿
|
||||
// console.log('🎬 DPlayer URL changed:', newUrl)
|
||||
nextTick(() => {
|
||||
initDPlayer(newUrl)
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 加载HLS.js
|
||||
const loadHLSScript = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if ((window as any).Hls) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1.4.14/dist/hls.min.js'
|
||||
script.onload = () => {
|
||||
console.log('✅ HLS.js loaded')
|
||||
resolve()
|
||||
}
|
||||
script.onerror = () => {
|
||||
console.error('❌ Failed to load HLS.js')
|
||||
reject(new Error('Failed to load HLS.js'))
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化DPlayer
|
||||
const initDPlayer = async (url: string) => {
|
||||
if (!url) return
|
||||
|
||||
loading.value = true
|
||||
error.value = false
|
||||
|
||||
try {
|
||||
// 销毁之前的播放器
|
||||
if (dplayer.value) {
|
||||
try {
|
||||
dplayer.value.destroy()
|
||||
} catch (e) {
|
||||
console.warn('销毁播放器失败:', e)
|
||||
}
|
||||
dplayer.value = null
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
const container = document.getElementById(playerId.value)
|
||||
if (!container) {
|
||||
console.error('❌ 容器未找到:', playerId.value)
|
||||
error.value = true
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const isHLS = url.includes('.m3u8')
|
||||
// console.log('🎬 初始化DPlayer:', { url, isHLS })
|
||||
|
||||
// 如果是HLS,加载HLS.js
|
||||
if (isHLS) {
|
||||
await loadHLSScript()
|
||||
}
|
||||
|
||||
// DPlayer配置 - 先使用简单配置确保能播放
|
||||
const options: any = {
|
||||
container: container,
|
||||
autoplay: props.autoplay,
|
||||
theme: '#1890ff',
|
||||
loop: false,
|
||||
lang: 'zh-cn',
|
||||
screenshot: true,
|
||||
hotkey: true,
|
||||
preload: 'auto',
|
||||
volume: 0.8,
|
||||
mutex: true,
|
||||
video: {
|
||||
url: url,
|
||||
pic: props.poster || '',
|
||||
type: 'normal'
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('🎬 DPlayer配置:', options)
|
||||
|
||||
// HLS特殊处理
|
||||
if (isHLS && (window as any).Hls && (window as any).Hls.isSupported()) {
|
||||
// console.log('🔧 使用HLS.js')
|
||||
options.video.type = 'hls'
|
||||
options.video.customType = {
|
||||
hls: function(video: HTMLVideoElement, _player: DPlayer) {
|
||||
// console.log('🎬 配置HLS.js')
|
||||
|
||||
const hls = new (window as any).Hls({
|
||||
debug: false,
|
||||
enableWorker: false, // 禁用Worker避免兼容性问题
|
||||
lowLatencyMode: false,
|
||||
// 缓冲区配置 - 减少bufferAppendError
|
||||
maxBufferLength: 20, // 减少最大缓冲长度
|
||||
maxMaxBufferLength: 40, // 减少绝对最大缓冲长度
|
||||
maxBufferSize: 30 * 1000 * 1000, // 30MB缓冲区大小
|
||||
maxBufferHole: 0.3, // 缓冲区空洞容忍度
|
||||
highBufferWatchdogPeriod: 1, // 高缓冲区监控周期
|
||||
nudgeOffset: 0.05, // 微调偏移
|
||||
nudgeMaxRetry: 2, // 最大微调重试次数
|
||||
// 网络配置
|
||||
fragLoadingTimeOut: 20000,
|
||||
fragLoadingMaxRetry: 3,
|
||||
manifestLoadingTimeOut: 10000,
|
||||
manifestLoadingMaxRetry: 2,
|
||||
// 启用渐进式加载
|
||||
progressive: false,
|
||||
// 减少并发请求
|
||||
maxLoadingDelay: 2
|
||||
})
|
||||
|
||||
hls.on((window as any).Hls.Events.MANIFEST_PARSED, () => {
|
||||
// console.log('✅ HLS manifest解析成功')
|
||||
})
|
||||
|
||||
// 错误恢复计数器
|
||||
let bufferErrorCount = 0
|
||||
let networkErrorCount = 0
|
||||
let lastErrorTime = 0
|
||||
const maxRetries = 3
|
||||
const errorCooldown = 5000 // 5秒内不重复显示相同错误
|
||||
|
||||
hls.on((window as any).Hls.Events.ERROR, (_event: any, data: any) => {
|
||||
const now = Date.now()
|
||||
|
||||
// 特殊处理bufferAppendError - 静默处理
|
||||
if (data.details === 'bufferAppendError') {
|
||||
bufferErrorCount++
|
||||
if (data.fatal && bufferErrorCount <= maxRetries) {
|
||||
// 静默恢复,不输出日志避免卡顿
|
||||
setTimeout(() => {
|
||||
if (hls && !hls.destroyed) {
|
||||
hls.recoverMediaError()
|
||||
}
|
||||
}, 50 * bufferErrorCount) // 快速恢复
|
||||
}
|
||||
return // 完全静默处理
|
||||
}
|
||||
|
||||
// 防止频繁日志输出
|
||||
if (now - lastErrorTime < errorCooldown) {
|
||||
return // 跳过重复错误
|
||||
}
|
||||
lastErrorTime = now
|
||||
|
||||
// 只处理真正重要的错误
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case (window as any).Hls.ErrorTypes.NETWORK_ERROR:
|
||||
networkErrorCount++
|
||||
if (networkErrorCount <= maxRetries) {
|
||||
console.log(`🔄 网络错误,重试 ${networkErrorCount}/${maxRetries}`)
|
||||
setTimeout(() => {
|
||||
if (hls && !hls.destroyed) {
|
||||
hls.startLoad()
|
||||
}
|
||||
}, 1000 * networkErrorCount)
|
||||
} else {
|
||||
console.error('❌ 网络连接失败')
|
||||
error.value = true
|
||||
loading.value = false
|
||||
}
|
||||
break
|
||||
case (window as any).Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.log('🔄 媒体错误,尝试恢复')
|
||||
setTimeout(() => {
|
||||
if (hls && !hls.destroyed) {
|
||||
hls.recoverMediaError()
|
||||
}
|
||||
}, 500)
|
||||
break
|
||||
default:
|
||||
console.error('❌ 播放器错误:', data.details)
|
||||
error.value = true
|
||||
loading.value = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// 完全忽略非致命错误的日志输出
|
||||
})
|
||||
|
||||
hls.loadSource(video.src)
|
||||
hls.attachMedia(video)
|
||||
|
||||
// 保存引用用于清理
|
||||
;(video as any).hlsInstance = hls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建播放器
|
||||
// console.log('🔨 创建DPlayer实例...')
|
||||
dplayer.value = new DPlayer(options)
|
||||
|
||||
// 绑定事件
|
||||
bindEvents()
|
||||
|
||||
// console.log('✅ DPlayer初始化成功')
|
||||
loading.value = false
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ DPlayer初始化失败:', err)
|
||||
error.value = true
|
||||
loading.value = false
|
||||
emit('error', new Event('error'))
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
const bindEvents = () => {
|
||||
if (!dplayer.value) return
|
||||
|
||||
;(dplayer.value as any).on('play', () => {
|
||||
// console.log('▶️ 播放')
|
||||
emit('play')
|
||||
})
|
||||
|
||||
;(dplayer.value as any).on('pause', () => {
|
||||
// console.log('⏸️ 暂停')
|
||||
emit('pause')
|
||||
})
|
||||
|
||||
;(dplayer.value as any).on('ended', () => {
|
||||
// console.log('⏹️ 结束')
|
||||
emit('ended')
|
||||
})
|
||||
|
||||
;(dplayer.value as any).on('error', (error: Event) => {
|
||||
// 获取详细错误信息
|
||||
if (dplayer.value && dplayer.value.video) {
|
||||
const video = dplayer.value.video
|
||||
|
||||
// 过滤掉HLS初始化过程中的无害错误
|
||||
const isHLSInitError = video.error === null &&
|
||||
video.networkState === 2 &&
|
||||
video.readyState === 0 &&
|
||||
video.currentSrc.includes('blob:')
|
||||
|
||||
if (isHLSInitError) {
|
||||
console.log('ℹ️ HLS初始化过程中的正常状态转换,忽略此错误')
|
||||
return // 不触发错误事件
|
||||
}
|
||||
|
||||
console.error('🚨 DPlayer错误:', error)
|
||||
console.error('📹 视频错误详情:', {
|
||||
error: video.error,
|
||||
networkState: video.networkState,
|
||||
readyState: video.readyState,
|
||||
currentSrc: video.currentSrc
|
||||
})
|
||||
|
||||
if (video.error) {
|
||||
const errorMessages = {
|
||||
1: 'MEDIA_ERR_ABORTED - 加载中止',
|
||||
2: 'MEDIA_ERR_NETWORK - 网络错误',
|
||||
3: 'MEDIA_ERR_DECODE - 解码错误',
|
||||
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED - 格式不支持'
|
||||
}
|
||||
console.error('❌ 错误代码:', video.error.code, '-', errorMessages[video.error.code as keyof typeof errorMessages])
|
||||
|
||||
// 只有真正的错误才触发错误事件
|
||||
emit('error', error)
|
||||
}
|
||||
} else {
|
||||
console.error('🚨 DPlayer错误:', error)
|
||||
emit('error', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重试加载
|
||||
const retryLoad = () => {
|
||||
if (props.videoUrl) {
|
||||
initDPlayer(props.videoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// DPlayer内置功能,不需要自定义清晰度和截图函数
|
||||
|
||||
// 暴露方法
|
||||
const play = () => dplayer.value?.play()
|
||||
const pause = () => dplayer.value?.pause()
|
||||
const seek = (time: number) => dplayer.value?.seek(time)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
if (props.videoUrl) {
|
||||
initDPlayer(props.videoUrl)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (dplayer.value) {
|
||||
try {
|
||||
dplayer.value.destroy()
|
||||
} catch (e) {
|
||||
console.warn('销毁DPlayer失败:', e)
|
||||
}
|
||||
dplayer.value = null
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ play, pause, seek, retry: retryLoad, player: dplayer })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dplayer-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dplayer-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.loading-overlay,
|
||||
.error-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
/* 回退视频播放器 */
|
||||
.fallback-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
@ -34,9 +34,23 @@
|
||||
<div class="video-player-section">
|
||||
<div class="video-player enrolled">
|
||||
<div class="video-container">
|
||||
<!-- CKPlayer 容器 -->
|
||||
<div v-if="currentVideoUrl" id="ckplayer_container" class="ckplayer-container">
|
||||
</div>
|
||||
<!-- DPlayer 视频播放器 -->
|
||||
<DPlayerVideo
|
||||
v-if="currentVideoUrl"
|
||||
ref="dplayerRef"
|
||||
:video-url="currentVideoUrl"
|
||||
:poster="course?.coverImage || course?.thumbnail || ''"
|
||||
:title="currentSection?.name || '课程视频'"
|
||||
:description="currentSection?.name || ''"
|
||||
:autoplay="false"
|
||||
:show-controls="true"
|
||||
:qualities="videoQualities"
|
||||
:current-quality="currentQuality"
|
||||
@play="onVideoPlay"
|
||||
@pause="onVideoPause"
|
||||
@ended="onVideoEnded"
|
||||
@error="onVideoError"
|
||||
/>
|
||||
<div v-else class="video-placeholder"
|
||||
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
||||
<div class="placeholder-content">
|
||||
@ -454,6 +468,7 @@ import type { Course, CourseSection, SectionVideo, VideoQuality, CourseComment,
|
||||
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
||||
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
||||
import NotesModal from '@/components/common/NotesModal.vue'
|
||||
import DPlayerVideo from '@/components/DPlayerVideo.vue'
|
||||
|
||||
// 声明全局CKPlayer类型
|
||||
declare global {
|
||||
@ -477,6 +492,7 @@ const FORCE_LOCAL_VIDEO = true
|
||||
const currentSection = ref<CourseSection | null>(null)
|
||||
const currentVideoUrl = ref<string>('')
|
||||
const ckplayer = ref<any>(null)
|
||||
const dplayerRef = ref<InstanceType<typeof DPlayerVideo>>()
|
||||
|
||||
// 视频相关状态
|
||||
const currentVideo = ref<SectionVideo | null>(null)
|
||||
@ -691,8 +707,9 @@ const loadCourseSections = async () => {
|
||||
if (firstVideo) {
|
||||
currentSection.value = firstVideo
|
||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
// DPlayer组件会自动处理视频URL变化
|
||||
// await nextTick()
|
||||
// initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -735,7 +752,8 @@ const loadMockData = () => {
|
||||
if (firstVideo) {
|
||||
currentSection.value = firstVideo
|
||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||
setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
|
||||
// DPlayer组件会自动处理视频URL变化
|
||||
// setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1048,8 +1066,8 @@ const handleVideoPlay = async (section: CourseSection) => {
|
||||
// 等待DOM更新
|
||||
await nextTick()
|
||||
|
||||
// 初始化CKPlayer播放器
|
||||
initCKPlayer(videoUrl)
|
||||
// DPlayer组件会自动处理视频URL变化
|
||||
// initCKPlayer(videoUrl)
|
||||
|
||||
// 标记为已完成
|
||||
if (!section.completed) {
|
||||
@ -1061,8 +1079,11 @@ const handleVideoPlay = async (section: CourseSection) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化CKPlayer播放器
|
||||
const initCKPlayer = (url: string) => {
|
||||
// 初始化CKPlayer播放器 - 已替换为DPlayer,注释掉避免冲突
|
||||
const initCKPlayer = (_url: string) => {
|
||||
console.log('⚠️ CKPlayer已被DPlayer替换,跳过初始化')
|
||||
return
|
||||
/*
|
||||
// 清理之前的播放器实例
|
||||
if (ckplayer.value) {
|
||||
try {
|
||||
@ -1126,6 +1147,7 @@ const initCKPlayer = (url: string) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize CKPlayer:', error)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// CKPlayer回调函数
|
||||
@ -1203,18 +1225,22 @@ const handleExam = (section: CourseSection) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 视频事件处理
|
||||
// const handleVideoLoadStart = () => {
|
||||
// console.log('视频开始加载')
|
||||
// }
|
||||
// DPlayer视频事件处理
|
||||
const onVideoPlay = () => {
|
||||
console.log('▶️ 视频开始播放')
|
||||
}
|
||||
|
||||
// const handleVideoCanPlay = () => {
|
||||
// console.log('视频可以播放')
|
||||
// }
|
||||
const onVideoPause = () => {
|
||||
console.log('⏸️ 视频暂停')
|
||||
}
|
||||
|
||||
// const handleVideoError = (event: Event) => {
|
||||
// console.error('视频播放错误:', event)
|
||||
// }
|
||||
const onVideoEnded = () => {
|
||||
console.log('⏹️ 视频播放结束')
|
||||
}
|
||||
|
||||
const onVideoError = (error: Event) => {
|
||||
console.error('🚨 视频播放错误:', error)
|
||||
}
|
||||
|
||||
// 初始化模拟状态(已报名状态)
|
||||
const initializeEnrolledState = () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user