feat(视频播放器): 添加网络速度检测和显示功能
添加网络速度检测功能,当视频缓冲或卡顿时显示当前网络速度 新增网络速度覆盖层UI组件,包含网络状态提示和具体速度值 为原生播放器和video.js播放器添加相关事件监听
Showing
2 changed files
with
170 additions
and
1 deletions
| ... | @@ -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 | ... | ... |
-
Please register or login to post a comment