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",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"ckplayer": "^3.1.2",
|
"ckplayer": "^3.1.2",
|
||||||
|
"dplayer": "^1.27.1",
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
@ -20,6 +21,7 @@
|
|||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dplayer": "^1.25.5",
|
||||||
"@types/node": "^24.0.15",
|
"@types/node": "^24.0.15",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
@ -1398,6 +1400,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@ -1777,6 +1786,12 @@
|
|||||||
"proxy-from-env": "^1.1.0"
|
"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": {
|
"node_modules/birpc": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
|
||||||
@ -2139,6 +2154,28 @@
|
|||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@ -3207,6 +3244,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"ckplayer": "^3.1.2",
|
"ckplayer": "^3.1.2",
|
||||||
|
"dplayer": "^1.27.1",
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dplayer": "^1.25.5",
|
||||||
"@types/node": "^24.0.15",
|
"@types/node": "^24.0.15",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"typescript": "^5.8.3",
|
"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-section">
|
||||||
<div class="video-player enrolled">
|
<div class="video-player enrolled">
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<!-- CKPlayer 容器 -->
|
<!-- DPlayer 视频播放器 -->
|
||||||
<div v-if="currentVideoUrl" id="ckplayer_container" class="ckplayer-container">
|
<DPlayerVideo
|
||||||
</div>
|
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"
|
<div v-else class="video-placeholder"
|
||||||
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
||||||
<div class="placeholder-content">
|
<div class="placeholder-content">
|
||||||
@ -454,6 +468,7 @@ import type { Course, CourseSection, SectionVideo, VideoQuality, CourseComment,
|
|||||||
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
||||||
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
||||||
import NotesModal from '@/components/common/NotesModal.vue'
|
import NotesModal from '@/components/common/NotesModal.vue'
|
||||||
|
import DPlayerVideo from '@/components/DPlayerVideo.vue'
|
||||||
|
|
||||||
// 声明全局CKPlayer类型
|
// 声明全局CKPlayer类型
|
||||||
declare global {
|
declare global {
|
||||||
@ -477,6 +492,7 @@ const FORCE_LOCAL_VIDEO = true
|
|||||||
const currentSection = ref<CourseSection | null>(null)
|
const currentSection = ref<CourseSection | null>(null)
|
||||||
const currentVideoUrl = ref<string>('')
|
const currentVideoUrl = ref<string>('')
|
||||||
const ckplayer = ref<any>(null)
|
const ckplayer = ref<any>(null)
|
||||||
|
const dplayerRef = ref<InstanceType<typeof DPlayerVideo>>()
|
||||||
|
|
||||||
// 视频相关状态
|
// 视频相关状态
|
||||||
const currentVideo = ref<SectionVideo | null>(null)
|
const currentVideo = ref<SectionVideo | null>(null)
|
||||||
@ -691,8 +707,9 @@ const loadCourseSections = async () => {
|
|||||||
if (firstVideo) {
|
if (firstVideo) {
|
||||||
currentSection.value = firstVideo
|
currentSection.value = firstVideo
|
||||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||||
await nextTick()
|
// DPlayer组件会自动处理视频URL变化
|
||||||
initCKPlayer(currentVideoUrl.value)
|
// await nextTick()
|
||||||
|
// initCKPlayer(currentVideoUrl.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -735,7 +752,8 @@ const loadMockData = () => {
|
|||||||
if (firstVideo) {
|
if (firstVideo) {
|
||||||
currentSection.value = firstVideo
|
currentSection.value = firstVideo
|
||||||
currentVideoUrl.value = getVideoUrl(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更新
|
// 等待DOM更新
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// 初始化CKPlayer播放器
|
// DPlayer组件会自动处理视频URL变化
|
||||||
initCKPlayer(videoUrl)
|
// initCKPlayer(videoUrl)
|
||||||
|
|
||||||
// 标记为已完成
|
// 标记为已完成
|
||||||
if (!section.completed) {
|
if (!section.completed) {
|
||||||
@ -1061,8 +1079,11 @@ const handleVideoPlay = async (section: CourseSection) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化CKPlayer播放器
|
// 初始化CKPlayer播放器 - 已替换为DPlayer,注释掉避免冲突
|
||||||
const initCKPlayer = (url: string) => {
|
const initCKPlayer = (_url: string) => {
|
||||||
|
console.log('⚠️ CKPlayer已被DPlayer替换,跳过初始化')
|
||||||
|
return
|
||||||
|
/*
|
||||||
// 清理之前的播放器实例
|
// 清理之前的播放器实例
|
||||||
if (ckplayer.value) {
|
if (ckplayer.value) {
|
||||||
try {
|
try {
|
||||||
@ -1126,6 +1147,7 @@ const initCKPlayer = (url: string) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize CKPlayer:', error)
|
console.error('Failed to initialize CKPlayer:', error)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// CKPlayer回调函数
|
// CKPlayer回调函数
|
||||||
@ -1203,18 +1225,22 @@ const handleExam = (section: CourseSection) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频事件处理
|
// DPlayer视频事件处理
|
||||||
// const handleVideoLoadStart = () => {
|
const onVideoPlay = () => {
|
||||||
// console.log('视频开始加载')
|
console.log('▶️ 视频开始播放')
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const handleVideoCanPlay = () => {
|
const onVideoPause = () => {
|
||||||
// console.log('视频可以播放')
|
console.log('⏸️ 视频暂停')
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const handleVideoError = (event: Event) => {
|
const onVideoEnded = () => {
|
||||||
// console.error('视频播放错误:', event)
|
console.log('⏹️ 视频播放结束')
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
const onVideoError = (error: Event) => {
|
||||||
|
console.error('🚨 视频播放错误:', error)
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化模拟状态(已报名状态)
|
// 初始化模拟状态(已报名状态)
|
||||||
const initializeEnrolledState = () => {
|
const initializeEnrolledState = () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user