hookehuyr

fix(video): 预加载Video.js资源并优化视频播放诊断

- 在HomePage和StudyDetailPage添加Video.js资源预加载函数,解决首次播放黑屏问题
- 为VideoPlayer组件添加加载状态占位符,改善用户体验
- 增强视频播放错误处理和调试日志,便于问题排查
- 修复play()方法调用时的Promise处理,避免静默失败
- 优化跨域视频加载配置和网络超时设置
......@@ -15,6 +15,7 @@
x5-video-player-type="h5"
x5-video-player-fullscreen="true"
preload="metadata"
crossorigin="anonymous"
@play="handleNativePlay"
@pause="handleNativePause"
/>
......@@ -31,6 +32,12 @@
@pause="handlePause"
/>
<!-- Loading 占位符:Video.js 资源加载时显示 -->
<div v-if="!useNativePlayer && !state" class="loading-placeholder">
<div class="loading-spinner"></div>
<div class="loading-text">视频加载中...</div>
</div>
<!-- <div v-if="showNetworkSpeedOverlay && !showErrorOverlay" class="speed-overlay">
<div class="speed-content">
<div
......@@ -45,9 +52,8 @@
</div>
</div> -->
<div v-if="hlsDownloadSpeedText" class="hls-speed-badge">{{ hlsDownloadSpeedText }}</div>
<div v-if="props.debug" class="hls-debug-badge">
{{ hlsSpeedDebugText || "debug:empty" }}
<div v-if="hlsDownloadSpeedText && !props.debug" class="hls-speed-badge">
{{ hlsDownloadSpeedText }}
</div>
<!-- 错误提示覆盖层 -->
......@@ -62,17 +68,17 @@
</template>
<script setup>
import { defineAsyncComponent, ref } from "vue";
import { useVideoPlayer } from "@/composables/useVideoPlayer";
import { defineAsyncComponent, ref, computed, watch } from 'vue'
import { useVideoPlayer } from '@/composables/useVideoPlayer'
const VideoPlayer = defineAsyncComponent(async () => {
await import("video.js/dist/video-js.css");
await import("videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css");
await import("videojs-contrib-quality-levels");
await import("videojs-hls-quality-selector");
const mod = await import("@videojs-player/vue");
return mod.VideoPlayer;
});
await import('video.js/dist/video-js.css')
await import('videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css')
await import('videojs-contrib-quality-levels')
await import('videojs-hls-quality-selector')
const mod = await import('@videojs-player/vue')
return mod.VideoPlayer
})
const props = defineProps({
options: {
......@@ -95,7 +101,7 @@ const props = defineProps({
},
debug: {
type: Boolean,
default: false
default: false,
},
/**
* iOS 环境下是否强制使用原生播放器
......@@ -103,14 +109,14 @@ const props = defineProps({
*/
useNativeOnIos: {
type: Boolean,
default: true
}
});
default: true,
},
})
const emit = defineEmits(["onPlay", "onPause"]);
const emit = defineEmits(['onPlay', 'onPause'])
const videoRef = ref(null);
const nativeVideoRef = ref(null);
const videoRef = ref(null)
const nativeVideoRef = ref(null)
const {
player,
......@@ -127,68 +133,137 @@ const {
hlsSpeedDebugText,
retryLoad,
handleVideoJsMounted,
tryNativePlay
} = useVideoPlayer(props, emit, videoRef, nativeVideoRef);
tryNativePlay,
} = useVideoPlayer(props, emit, videoRef, nativeVideoRef)
// 视频加载诊断(控制台日志)
const logVideoDiagnostics = () => {
if (!props.debug) return
const isSameOrigin = (() => {
if (typeof window === 'undefined' || !props.videoUrl) return false
try {
const videoUrl = new URL(props.videoUrl, window.location.href)
return videoUrl.origin === window.location.origin
} catch {
return false
}
})()
// 检查是否为 HLS 视频
const checkIsM3U8 = url => {
if (!url) return false
return url.toLowerCase().includes('.m3u8')
}
console.group('🎬 [VideoPlayer] 视频加载诊断')
console.log('📌 URL:', videoUrlValue.value)
console.log('🔗 同源:', isSameOrigin)
console.log('🎥 播放器类型:', useNativePlayer.value ? '原生播放器' : 'Video.js')
console.log('📦 挂载状态:', state.value ? '已挂载' : '加载中')
console.log('🌐 HLS支持:', checkIsM3U8(videoUrlValue.value) ? '是' : '否')
console.groupEnd()
}
// 监听视频加载状态变化
watch(
() => [state.value, showErrorOverlay.value, errorMessage.value],
([currentState, showError, errorMsg]) => {
if (props.debug) {
console.group('🎬 [VideoPlayer] 状态变化')
console.log('📊 当前状态:', currentState ? '已挂载' : '加载中')
console.log('❌ 错误状态:', showError ? errorMsg : '无')
console.groupEnd()
}
if (showError && props.debug) {
console.group('❌ [VideoPlayer] 错误诊断')
console.error('错误信息:', errorMsg)
console.log('视频URL:', videoUrlValue.value)
console.log('播放器类型:', useNativePlayer.value ? '原生' : 'Video.js')
console.log('排查建议:')
console.log(' 1. 检查 CDN 服务器是否正常运行')
console.log(' 2. 检查 CORS 跨域配置是否正确')
console.log(' 3. 检查网络连接是否稳定')
console.log(' 4. 检查视频文件格式是否支持')
console.groupEnd()
}
// 每次状态变化都输出诊断信息
if (currentState) {
logVideoDiagnostics()
}
},
{ immediate: true }
)
// 事件处理
/**
* @description 处理播放事件
* @param {Event|Object} payload - 事件对象或数据
*/
const handlePlay = (payload) => emit("onPlay", payload);
const handlePlay = payload => {
if (props.debug) {
console.log('🎬 [VideoPlayer] 视频开始播放', payload)
}
emit('onPlay', payload)
}
/**
* @description 处理暂停事件
* @param {Event|Object} payload - 事件对象或数据
*/
const handlePause = (payload) => emit("onPause", payload);
const handlePause = payload => {
if (props.debug) {
console.log('⏸️ [VideoPlayer] 视频暂停', payload)
}
emit('onPause', payload)
}
/**
* @description 处理原生播放事件
* @param {Event} event - 事件对象
*/
const handleNativePlay = (event) => emit("onPlay", event);
const handleNativePlay = event => emit('onPlay', event)
/**
* @description 处理原生暂停事件
* @param {Event} event - 事件对象
*/
const handleNativePause = (event) => emit("onPause", event);
const handleNativePause = event => emit('onPause', event)
// 暴露方法给父组件
defineExpose({
pause() {
if (useNativePlayer.value) {
try {
nativeVideoRef.value?.pause?.();
emit('onPause', nativeVideoRef.value);
} catch (e) {
}
return;
nativeVideoRef.value?.pause?.()
emit('onPause', nativeVideoRef.value)
} catch (e) {}
return
}
if (player.value && !player.value.isDisposed()) {
try {
player.value.pause();
emit('onPause', player.value);
} catch (e) {
}
player.value.pause()
emit('onPause', player.value)
} catch (e) {}
}
},
play() {
if (useNativePlayer.value) {
tryNativePlay();
return;
tryNativePlay()
return
}
player.value?.play()?.catch(() => {});
player.value?.play()?.catch(() => {})
},
getPlayer() {
return useNativePlayer.value ? nativeVideoRef.value : player.value;
return useNativePlayer.value ? nativeVideoRef.value : player.value
},
getId() {
return props.videoId || "meta_id";
return props.videoId || 'meta_id'
},
});
})
</script>
<style scoped>
......@@ -207,7 +282,43 @@ defineExpose({
}
.video-player.loading {
opacity: 0.6;
opacity: 0;
}
/* Loading 占位符样式 */
.loading-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #4caf50;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
margin-top: 12px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
/* 错误覆盖层样式 */
......
This diff is collapsed. Click to expand it.
......@@ -368,6 +368,7 @@
</template>
<VideoPlayer
v-else
:key="`video_${index}_${item.video_url}`"
:video-url="item.video_url"
:video-id="`home_recommend_video_${index}`"
:use-native-on-ios="false"
......@@ -394,6 +395,16 @@ import LiveStreamCard from '@/components/courses/LiveStreamCard.vue'
import VideoPlayer from '@/components/media/VideoPlayer.vue'
import CheckInList from '@/components/checkin/CheckInList.vue'
// Video.js 资源预加载(解决首次播放黑屏问题)
const preloadVideoJs = () => {
// 在用户可能点击视频前,预先加载 Video.js 资源
import('video.js/dist/video-js.css')
import('videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css')
import('videojs-contrib-quality-levels')
import('videojs-hls-quality-selector')
import('@videojs-player/vue')
}
import FeaturedCoursesSection from '@/components/homePage/FeaturedCoursesSection.vue'
import RecommendationsSection from '@/components/homePage/RecommendationsSection.vue'
import LatestActivitiesSection from '@/components/homePage/LatestActivitiesSection.vue'
......@@ -431,6 +442,9 @@ const activeTab = ref('推荐') // 当前激活的内容标签页
const checkInTypes = ref([])
onMounted(() => {
// 预加载 Video.js 资源,避免首次播放时黑屏
preloadVideoJs()
watch(
() => currentUser.value,
async newVal => {
......
This diff is collapsed. Click to expand it.