hookehuyr

feat(课程页面): 添加视频播放错误处理和媒体文件预览功能

为视频播放器添加错误处理覆盖层和自动重试机制,优化视频加载配置
在课程详情页增加音频和视频播放弹窗,改进文件预览功能
根据设备类型显示不同的文件操作按钮,支持多种媒体格式预览
...@@ -10,6 +10,14 @@ ...@@ -10,6 +10,14 @@
10 @play="handlePlay" 10 @play="handlePlay"
11 @pause="handlePause" 11 @pause="handlePause"
12 /> 12 />
13 + <!-- 错误提示覆盖层 -->
14 + <div v-if="showErrorOverlay" class="error-overlay">
15 + <div class="error-content">
16 + <div class="error-icon">⚠️</div>
17 + <div class="error-message">{{ errorMessage }}</div>
18 + <button @click="retryLoad" class="retry-button">重试</button>
19 + </div>
20 + </div>
13 </div> 21 </div>
14 </template> 22 </template>
15 23
...@@ -45,20 +53,47 @@ const emit = defineEmits(["onPlay", "onPause"]); ...@@ -45,20 +53,47 @@ const emit = defineEmits(["onPlay", "onPause"]);
45 const videoRef = ref(null); 53 const videoRef = ref(null);
46 const player = ref(null); 54 const player = ref(null);
47 const state = ref(null); 55 const state = ref(null);
56 +const showErrorOverlay = ref(false);
57 +const errorMessage = ref('');
58 +const retryCount = ref(0);
59 +const maxRetries = 3;
48 60
49 const videoOptions = computed(() => ({ 61 const videoOptions = computed(() => ({
50 controls: true, 62 controls: true,
51 - preload: "auto", 63 + preload: "metadata", // 改为metadata以减少初始加载
52 responsive: true, 64 responsive: true,
53 autoplay: props.autoplay, 65 autoplay: props.autoplay,
54 // 启用倍速播放功能 66 // 启用倍速播放功能
55 playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2], 67 playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2],
68 + // 添加多种格式支持
56 sources: [ 69 sources: [
57 { 70 {
58 src: props.videoUrl, 71 src: props.videoUrl,
59 type: "video/mp4", 72 type: "video/mp4",
60 }, 73 },
74 + // 备用源,如果主源失败则尝试其他格式
75 + {
76 + src: props.videoUrl,
77 + type: "video/webm",
78 + },
79 + {
80 + src: props.videoUrl,
81 + type: "video/ogg",
82 + },
61 ], 83 ],
84 + // HTML5配置优化
85 + html5: {
86 + vhs: {
87 + overrideNative: !videojs.browser.IS_SAFARI,
88 + },
89 + nativeVideoTracks: false,
90 + nativeAudioTracks: false,
91 + nativeTextTracks: false,
92 + },
93 + // 错误处理配置
94 + errorDisplay: true,
95 + // 网络和加载配置
96 + techOrder: ['html5'],
62 // onPlay: () => emit("onPlay"), 97 // onPlay: () => emit("onPlay"),
63 // onPause: () => emit("onPause"), 98 // onPause: () => emit("onPause"),
64 userActions: { 99 userActions: {
...@@ -81,9 +116,72 @@ const handleMounted = (payload) => { ...@@ -81,9 +116,72 @@ const handleMounted = (payload) => {
81 state.value = payload.state; 116 state.value = payload.state;
82 player.value = payload.player; 117 player.value = payload.player;
83 if (player.value) { 118 if (player.value) {
119 + // 添加错误处理监听器
120 + player.value.on('error', (error) => {
121 + console.error('VideoJS播放错误:', error);
122 + const errorCode = player.value.error();
123 + if (errorCode) {
124 + console.error('错误代码:', errorCode.code, '错误信息:', errorCode.message);
125 +
126 + // 显示用户友好的错误信息
127 + showErrorOverlay.value = true;
128 +
129 + // 根据错误类型进行处理
130 + switch (errorCode.code) {
131 + case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
132 + errorMessage.value = '视频格式不支持或无法加载,请检查网络连接';
133 + console.warn('视频格式不支持,尝试重新加载...');
134 + // 自动重试(如果重试次数未超限)
135 + if (retryCount.value < maxRetries) {
136 + setTimeout(() => {
137 + retryLoad();
138 + }, 1000);
139 + }
140 + break;
141 + case 3: // MEDIA_ERR_DECODE
142 + errorMessage.value = '视频解码失败,可能是文件损坏';
143 + console.warn('视频解码错误');
144 + break;
145 + case 2: // MEDIA_ERR_NETWORK
146 + errorMessage.value = '网络连接错误,请检查网络后重试';
147 + console.warn('网络错误,尝试重新加载...');
148 + if (retryCount.value < maxRetries) {
149 + setTimeout(() => {
150 + retryLoad();
151 + }, 2000);
152 + }
153 + break;
154 + case 1: // MEDIA_ERR_ABORTED
155 + errorMessage.value = '视频加载被中止';
156 + console.warn('视频加载被中止');
157 + break;
158 + default:
159 + errorMessage.value = '视频播放出现未知错误';
160 + }
161 + }
162 + });
163 +
164 + // 添加加载状态监听
165 + player.value.on('loadstart', () => {
166 + console.log('开始加载视频');
167 + showErrorOverlay.value = false; // 隐藏错误提示
168 + });
169 +
170 + player.value.on('canplay', () => {
171 + console.log('视频可以播放');
172 + showErrorOverlay.value = false; // 隐藏错误提示
173 + retryCount.value = 0; // 重置重试计数
174 + });
175 +
176 + player.value.on('loadedmetadata', () => {
177 + console.log('视频元数据加载完成');
178 + });
179 +
84 // TAG: 自动播放 180 // TAG: 自动播放
85 if (props.autoplay) { 181 if (props.autoplay) {
86 - player.value.play(); 182 + player.value.play().catch(error => {
183 + console.warn('自动播放失败:', error);
184 + });
87 } 185 }
88 186
89 // if (!wxInfo().isPc && !wxInfo().isWeiXinDesktop) { // 非PC端,且非微信PC端 187 // if (!wxInfo().isPc && !wxInfo().isWeiXinDesktop) { // 非PC端,且非微信PC端
...@@ -125,6 +223,24 @@ const handlePause = (payload) => { ...@@ -125,6 +223,24 @@ const handlePause = (payload) => {
125 emit("onPause", payload) 223 emit("onPause", payload)
126 } 224 }
127 225
226 +/**
227 + * 重试加载视频
228 + */
229 +const retryLoad = () => {
230 + if (retryCount.value >= maxRetries) {
231 + errorMessage.value = '重试次数已达上限,请稍后再试';
232 + return;
233 + }
234 +
235 + retryCount.value++;
236 + showErrorOverlay.value = false;
237 +
238 + if (player.value && !player.value.isDisposed()) {
239 + console.log(`第${retryCount.value}次重试加载视频`);
240 + player.value.load();
241 + }
242 +};
243 +
128 onBeforeUnmount(() => { 244 onBeforeUnmount(() => {
129 if (videoRef.value?.$player) { 245 if (videoRef.value?.$player) {
130 videoRef.value.$player.dispose(); 246 videoRef.value.$player.dispose();
...@@ -133,8 +249,8 @@ onBeforeUnmount(() => { ...@@ -133,8 +249,8 @@ onBeforeUnmount(() => {
133 249
134 defineExpose({ 250 defineExpose({
135 pause() { 251 pause() {
136 - if (player && typeof player?.pause === 'function') { 252 + if (player.value && typeof player.value.pause === 'function') {
137 - player?.pause(); 253 + player.value.pause();
138 emit('onPause', player.value); 254 emit('onPause', player.value);
139 } 255 }
140 }, 256 },
...@@ -171,6 +287,52 @@ defineExpose({ ...@@ -171,6 +287,52 @@ defineExpose({
171 opacity: 0.6; 287 opacity: 0.6;
172 } 288 }
173 289
290 +/* 错误覆盖层样式 */
291 +.error-overlay {
292 + position: absolute;
293 + top: 0;
294 + left: 0;
295 + right: 0;
296 + bottom: 0;
297 + background: rgba(0, 0, 0, 0.8);
298 + display: flex;
299 + align-items: center;
300 + justify-content: center;
301 + z-index: 1000;
302 +}
303 +
304 +.error-content {
305 + text-align: center;
306 + color: white;
307 + padding: 20px;
308 +}
309 +
310 +.error-icon {
311 + font-size: 48px;
312 + margin-bottom: 16px;
313 +}
314 +
315 +.error-message {
316 + font-size: 16px;
317 + margin-bottom: 20px;
318 + line-height: 1.5;
319 +}
320 +
321 +.retry-button {
322 + background: #007bff;
323 + color: white;
324 + border: none;
325 + padding: 10px 20px;
326 + border-radius: 4px;
327 + cursor: pointer;
328 + font-size: 14px;
329 + transition: background-color 0.3s;
330 +}
331 +
332 +.retry-button:hover {
333 + background: #0056b3;
334 +}
335 +
174 :deep(.vjs-big-play-button) { 336 :deep(.vjs-big-play-button) {
175 display: none !important; 337 display: none !important;
176 } 338 }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 </div> 7 </div>
8 8
9 <!-- Featured Course Banner --> 9 <!-- Featured Course Banner -->
10 - <div class="px-4 mb-5"> 10 + <div class="px-4 mb-5" v-if="bannerList.length">
11 <van-swipe 11 <van-swipe
12 class="rounded-xl overflow-hidden shadow-lg h-40" 12 class="rounded-xl overflow-hidden shadow-lg h-40"
13 :autoplay="3000" 13 :autoplay="3000"
......
This diff is collapsed. Click to expand it.