hookehuyr

feat(视频播放器): 添加网络速度检测和显示功能

添加网络速度检测功能,当视频缓冲或卡顿时显示当前网络速度
新增网络速度覆盖层UI组件,包含网络状态提示和具体速度值
为原生播放器和video.js播放器添加相关事件监听
...@@ -31,6 +31,13 @@ ...@@ -31,6 +31,13 @@
31 @pause="handlePause" 31 @pause="handlePause"
32 /> 32 />
33 33
34 + <div v-if="showNetworkSpeedOverlay && !showErrorOverlay" class="speed-overlay">
35 + <div class="speed-content">
36 + <div class="speed-title">网络较慢</div>
37 + <div class="speed-value">{{ `当前网速:${networkSpeedText}` }}</div>
38 + </div>
39 + </div>
40 +
34 <!-- 错误提示覆盖层 --> 41 <!-- 错误提示覆盖层 -->
35 <div v-if="showErrorOverlay" class="error-overlay"> 42 <div v-if="showErrorOverlay" class="error-overlay">
36 <div class="error-content"> 43 <div class="error-content">
...@@ -90,6 +97,8 @@ const { ...@@ -90,6 +97,8 @@ const {
90 videoOptions, 97 videoOptions,
91 showErrorOverlay, 98 showErrorOverlay,
92 errorMessage, 99 errorMessage,
100 + showNetworkSpeedOverlay,
101 + networkSpeedText,
93 retryLoad, 102 retryLoad,
94 handleVideoJsMounted, 103 handleVideoJsMounted,
95 tryNativePlay 104 tryNativePlay
...@@ -188,6 +197,42 @@ defineExpose({ ...@@ -188,6 +197,42 @@ defineExpose({
188 line-height: 1.5; 197 line-height: 1.5;
189 } 198 }
190 199
200 +.speed-overlay {
201 + position: absolute;
202 + top: 0;
203 + left: 0;
204 + right: 0;
205 + bottom: 0;
206 + display: flex;
207 + align-items: center;
208 + justify-content: center;
209 + z-index: 900;
210 + pointer-events: none;
211 +}
212 +
213 +.speed-content {
214 + padding: 14px 18px;
215 + border-radius: 12px;
216 + background: rgba(0, 0, 0, 0.65);
217 + backdrop-filter: blur(10px);
218 + color: #fff;
219 + text-align: center;
220 + max-width: 80%;
221 +}
222 +
223 +.speed-title {
224 + font-size: 16px;
225 + font-weight: 600;
226 + line-height: 1.2;
227 + margin-bottom: 8px;
228 +}
229 +
230 +.speed-value {
231 + font-size: 14px;
232 + line-height: 1.4;
233 + opacity: 0.95;
234 +}
235 +
191 .retry-button { 236 .retry-button {
192 background: #007bff; 237 background: #007bff;
193 color: white; 238 color: white;
......
...@@ -29,6 +29,11 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -29,6 +29,11 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
29 const retryCount = ref(0); 29 const retryCount = ref(0);
30 const maxRetries = 3; 30 const maxRetries = 3;
31 31
32 + const showNetworkSpeedOverlay = ref(false);
33 + const networkSpeedText = ref('');
34 + const hasEverPlayed = ref(false);
35 + let networkSpeedTimer = null;
36 +
32 // 原生播放器状态 37 // 原生播放器状态
33 const nativeReady = ref(false); 38 const nativeReady = ref(false);
34 let nativeListeners = null; 39 let nativeListeners = null;
...@@ -176,6 +181,11 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -176,6 +181,11 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
176 // 7. 错误处理逻辑 181 // 7. 错误处理逻辑
177 const handleError = (code, message = '') => { 182 const handleError = (code, message = '') => {
178 showErrorOverlay.value = true; 183 showErrorOverlay.value = true;
184 + showNetworkSpeedOverlay.value = false;
185 + if (networkSpeedTimer) {
186 + clearInterval(networkSpeedTimer);
187 + networkSpeedTimer = null;
188 + }
179 switch (code) { 189 switch (code) {
180 case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED 190 case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
181 errorMessage.value = '视频格式不支持或无法加载,请检查网络连接' + getErrorHint(); 191 errorMessage.value = '视频格式不支持或无法加载,请检查网络连接' + getErrorHint();
...@@ -200,6 +210,45 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -200,6 +210,45 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
200 } 210 }
201 }; 211 };
202 212
213 + const updateNetworkSpeed = () => {
214 + if (typeof navigator === 'undefined') {
215 + networkSpeedText.value = '未知';
216 + return;
217 + }
218 +
219 + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
220 + const downlink = connection && typeof connection.downlink === 'number' ? connection.downlink : null;
221 + if (downlink && downlink > 0) {
222 + networkSpeedText.value = `${downlink.toFixed(1)} Mbps`;
223 + return;
224 + }
225 +
226 + const effectiveType = connection && typeof connection.effectiveType === 'string' ? connection.effectiveType : '';
227 + networkSpeedText.value = effectiveType ? `${effectiveType}` : '未知';
228 + };
229 +
230 + const showNetworkSpeed = () => {
231 + if (!hasEverPlayed.value) return;
232 + if (showErrorOverlay.value) return;
233 + if (showNetworkSpeedOverlay.value) return;
234 +
235 + showNetworkSpeedOverlay.value = true;
236 + updateNetworkSpeed();
237 +
238 + if (networkSpeedTimer) clearInterval(networkSpeedTimer);
239 + networkSpeedTimer = setInterval(() => {
240 + updateNetworkSpeed();
241 + }, 800);
242 + };
243 +
244 + const hideNetworkSpeed = () => {
245 + showNetworkSpeedOverlay.value = false;
246 + if (networkSpeedTimer) {
247 + clearInterval(networkSpeedTimer);
248 + networkSpeedTimer = null;
249 + }
250 + };
251 +
203 // 4. 原生播放器逻辑 (iOS微信) 252 // 4. 原生播放器逻辑 (iOS微信)
204 const initNativePlayer = () => { 253 const initNativePlayer = () => {
205 const videoEl = nativeVideoRef.value; 254 const videoEl = nativeVideoRef.value;
...@@ -228,10 +277,49 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -228,10 +277,49 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
228 handleError(videoEl.error?.code); 277 handleError(videoEl.error?.code);
229 }; 278 };
230 279
280 + const onPlay = () => {
281 + hasEverPlayed.value = true;
282 + hideNetworkSpeed();
283 + };
284 +
285 + const onPause = () => {
286 + hideNetworkSpeed();
287 + };
288 +
289 + const onWaiting = () => {
290 + if (videoEl.paused) return;
291 + showNetworkSpeed();
292 + };
293 +
294 + const onStalled = () => {
295 + if (videoEl.paused) return;
296 + showNetworkSpeed();
297 + };
298 +
299 + const onPlaying = () => {
300 + hideNetworkSpeed();
301 + };
302 +
231 videoEl.addEventListener("loadstart", onLoadStart); 303 videoEl.addEventListener("loadstart", onLoadStart);
232 videoEl.addEventListener("canplay", onCanPlay); 304 videoEl.addEventListener("canplay", onCanPlay);
233 videoEl.addEventListener("error", onError); 305 videoEl.addEventListener("error", onError);
234 - nativeListeners = { videoEl, onLoadStart, onCanPlay, onError }; 306 + videoEl.addEventListener("play", onPlay);
307 + videoEl.addEventListener("pause", onPause);
308 + videoEl.addEventListener("waiting", onWaiting);
309 + videoEl.addEventListener("stalled", onStalled);
310 + videoEl.addEventListener("playing", onPlaying);
311 +
312 + nativeListeners = {
313 + videoEl,
314 + onLoadStart,
315 + onCanPlay,
316 + onError,
317 + onPlay,
318 + onPause,
319 + onWaiting,
320 + onStalled,
321 + onPlaying,
322 + };
235 323
236 if (props.autoplay) { 324 if (props.autoplay) {
237 tryNativePlay(); 325 tryNativePlay();
...@@ -326,6 +414,31 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -326,6 +414,31 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
326 retryCount.value = 0; 414 retryCount.value = 0;
327 }); 415 });
328 416
417 + player.value.on('play', () => {
418 + hasEverPlayed.value = true;
419 + hideNetworkSpeed();
420 + });
421 +
422 + player.value.on('pause', () => {
423 + hideNetworkSpeed();
424 + });
425 +
426 + player.value.on('waiting', () => {
427 + if (!hasEverPlayed.value) return;
428 + if (player.value?.paused?.()) return;
429 + showNetworkSpeed();
430 + });
431 +
432 + player.value.on('stalled', () => {
433 + if (!hasEverPlayed.value) return;
434 + if (player.value?.paused?.()) return;
435 + showNetworkSpeed();
436 + });
437 +
438 + player.value.on('playing', () => {
439 + hideNetworkSpeed();
440 + });
441 +
329 if (props.autoplay) { 442 if (props.autoplay) {
330 player.value.play().catch(console.warn); 443 player.value.play().catch(console.warn);
331 } 444 }
...@@ -341,6 +454,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -341,6 +454,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
341 454
342 retryCount.value++; 455 retryCount.value++;
343 showErrorOverlay.value = false; 456 showErrorOverlay.value = false;
457 + hideNetworkSpeed();
344 458
345 if (useNativePlayer.value) { 459 if (useNativePlayer.value) {
346 const videoEl = nativeVideoRef.value; 460 const videoEl = nativeVideoRef.value;
...@@ -365,6 +479,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -365,6 +479,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
365 watch(() => videoUrlValue.value, () => { 479 watch(() => videoUrlValue.value, () => {
366 retryCount.value = 0; 480 retryCount.value = 0;
367 showErrorOverlay.value = false; 481 showErrorOverlay.value = false;
482 + hideNetworkSpeed();
483 + hasEverPlayed.value = false;
368 void probeVideo(); 484 void probeVideo();
369 485
370 // 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境) 486 // 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境)
...@@ -386,12 +502,18 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -386,12 +502,18 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
386 nativeListeners.videoEl.removeEventListener("loadstart", nativeListeners.onLoadStart); 502 nativeListeners.videoEl.removeEventListener("loadstart", nativeListeners.onLoadStart);
387 nativeListeners.videoEl.removeEventListener("canplay", nativeListeners.onCanPlay); 503 nativeListeners.videoEl.removeEventListener("canplay", nativeListeners.onCanPlay);
388 nativeListeners.videoEl.removeEventListener("error", nativeListeners.onError); 504 nativeListeners.videoEl.removeEventListener("error", nativeListeners.onError);
505 + nativeListeners.videoEl.removeEventListener("play", nativeListeners.onPlay);
506 + nativeListeners.videoEl.removeEventListener("pause", nativeListeners.onPause);
507 + nativeListeners.videoEl.removeEventListener("waiting", nativeListeners.onWaiting);
508 + nativeListeners.videoEl.removeEventListener("stalled", nativeListeners.onStalled);
509 + nativeListeners.videoEl.removeEventListener("playing", nativeListeners.onPlaying);
389 } 510 }
390 511
391 if (hlsInstance.value) { 512 if (hlsInstance.value) {
392 hlsInstance.value.destroy(); 513 hlsInstance.value.destroy();
393 } 514 }
394 515
516 + hideNetworkSpeed();
395 if (videoRef.value?.$player) { 517 if (videoRef.value?.$player) {
396 videoRef.value.$player.dispose(); 518 videoRef.value.$player.dispose();
397 } 519 }
...@@ -405,6 +527,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { ...@@ -405,6 +527,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
405 videoOptions, 527 videoOptions,
406 showErrorOverlay, 528 showErrorOverlay,
407 errorMessage, 529 errorMessage,
530 + showNetworkSpeedOverlay,
531 + networkSpeedText,
408 retryLoad, 532 retryLoad,
409 handleVideoJsMounted, 533 handleVideoJsMounted,
410 tryNativePlay 534 tryNativePlay
......