hookehuyr

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

添加网络速度检测功能,当视频缓冲或卡顿时显示当前网络速度
新增网络速度覆盖层UI组件,包含网络状态提示和具体速度值
为原生播放器和video.js播放器添加相关事件监听
......@@ -31,6 +31,13 @@
@pause="handlePause"
/>
<div v-if="showNetworkSpeedOverlay && !showErrorOverlay" class="speed-overlay">
<div class="speed-content">
<div class="speed-title">网络较慢</div>
<div class="speed-value">{{ `当前网速:${networkSpeedText}` }}</div>
</div>
</div>
<!-- 错误提示覆盖层 -->
<div v-if="showErrorOverlay" class="error-overlay">
<div class="error-content">
......@@ -90,6 +97,8 @@ const {
videoOptions,
showErrorOverlay,
errorMessage,
showNetworkSpeedOverlay,
networkSpeedText,
retryLoad,
handleVideoJsMounted,
tryNativePlay
......@@ -188,6 +197,42 @@ defineExpose({
line-height: 1.5;
}
.speed-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 900;
pointer-events: none;
}
.speed-content {
padding: 14px 18px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(10px);
color: #fff;
text-align: center;
max-width: 80%;
}
.speed-title {
font-size: 16px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 8px;
}
.speed-value {
font-size: 14px;
line-height: 1.4;
opacity: 0.95;
}
.retry-button {
background: #007bff;
color: white;
......
......@@ -29,6 +29,11 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
const retryCount = ref(0);
const maxRetries = 3;
const showNetworkSpeedOverlay = ref(false);
const networkSpeedText = ref('');
const hasEverPlayed = ref(false);
let networkSpeedTimer = null;
// 原生播放器状态
const nativeReady = ref(false);
let nativeListeners = null;
......@@ -176,6 +181,11 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
// 7. 错误处理逻辑
const handleError = (code, message = '') => {
showErrorOverlay.value = true;
showNetworkSpeedOverlay.value = false;
if (networkSpeedTimer) {
clearInterval(networkSpeedTimer);
networkSpeedTimer = null;
}
switch (code) {
case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
errorMessage.value = '视频格式不支持或无法加载,请检查网络连接' + getErrorHint();
......@@ -200,6 +210,45 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
}
};
const updateNetworkSpeed = () => {
if (typeof navigator === 'undefined') {
networkSpeedText.value = '未知';
return;
}
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
const downlink = connection && typeof connection.downlink === 'number' ? connection.downlink : null;
if (downlink && downlink > 0) {
networkSpeedText.value = `${downlink.toFixed(1)} Mbps`;
return;
}
const effectiveType = connection && typeof connection.effectiveType === 'string' ? connection.effectiveType : '';
networkSpeedText.value = effectiveType ? `${effectiveType}` : '未知';
};
const showNetworkSpeed = () => {
if (!hasEverPlayed.value) return;
if (showErrorOverlay.value) return;
if (showNetworkSpeedOverlay.value) return;
showNetworkSpeedOverlay.value = true;
updateNetworkSpeed();
if (networkSpeedTimer) clearInterval(networkSpeedTimer);
networkSpeedTimer = setInterval(() => {
updateNetworkSpeed();
}, 800);
};
const hideNetworkSpeed = () => {
showNetworkSpeedOverlay.value = false;
if (networkSpeedTimer) {
clearInterval(networkSpeedTimer);
networkSpeedTimer = null;
}
};
// 4. 原生播放器逻辑 (iOS微信)
const initNativePlayer = () => {
const videoEl = nativeVideoRef.value;
......@@ -228,10 +277,49 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
handleError(videoEl.error?.code);
};
const onPlay = () => {
hasEverPlayed.value = true;
hideNetworkSpeed();
};
const onPause = () => {
hideNetworkSpeed();
};
const onWaiting = () => {
if (videoEl.paused) return;
showNetworkSpeed();
};
const onStalled = () => {
if (videoEl.paused) return;
showNetworkSpeed();
};
const onPlaying = () => {
hideNetworkSpeed();
};
videoEl.addEventListener("loadstart", onLoadStart);
videoEl.addEventListener("canplay", onCanPlay);
videoEl.addEventListener("error", onError);
nativeListeners = { videoEl, onLoadStart, onCanPlay, onError };
videoEl.addEventListener("play", onPlay);
videoEl.addEventListener("pause", onPause);
videoEl.addEventListener("waiting", onWaiting);
videoEl.addEventListener("stalled", onStalled);
videoEl.addEventListener("playing", onPlaying);
nativeListeners = {
videoEl,
onLoadStart,
onCanPlay,
onError,
onPlay,
onPause,
onWaiting,
onStalled,
onPlaying,
};
if (props.autoplay) {
tryNativePlay();
......@@ -326,6 +414,31 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
retryCount.value = 0;
});
player.value.on('play', () => {
hasEverPlayed.value = true;
hideNetworkSpeed();
});
player.value.on('pause', () => {
hideNetworkSpeed();
});
player.value.on('waiting', () => {
if (!hasEverPlayed.value) return;
if (player.value?.paused?.()) return;
showNetworkSpeed();
});
player.value.on('stalled', () => {
if (!hasEverPlayed.value) return;
if (player.value?.paused?.()) return;
showNetworkSpeed();
});
player.value.on('playing', () => {
hideNetworkSpeed();
});
if (props.autoplay) {
player.value.play().catch(console.warn);
}
......@@ -341,6 +454,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
retryCount.value++;
showErrorOverlay.value = false;
hideNetworkSpeed();
if (useNativePlayer.value) {
const videoEl = nativeVideoRef.value;
......@@ -365,6 +479,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
watch(() => videoUrlValue.value, () => {
retryCount.value = 0;
showErrorOverlay.value = false;
hideNetworkSpeed();
hasEverPlayed.value = false;
void probeVideo();
// 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境)
......@@ -386,12 +502,18 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
nativeListeners.videoEl.removeEventListener("loadstart", nativeListeners.onLoadStart);
nativeListeners.videoEl.removeEventListener("canplay", nativeListeners.onCanPlay);
nativeListeners.videoEl.removeEventListener("error", nativeListeners.onError);
nativeListeners.videoEl.removeEventListener("play", nativeListeners.onPlay);
nativeListeners.videoEl.removeEventListener("pause", nativeListeners.onPause);
nativeListeners.videoEl.removeEventListener("waiting", nativeListeners.onWaiting);
nativeListeners.videoEl.removeEventListener("stalled", nativeListeners.onStalled);
nativeListeners.videoEl.removeEventListener("playing", nativeListeners.onPlaying);
}
if (hlsInstance.value) {
hlsInstance.value.destroy();
}
hideNetworkSpeed();
if (videoRef.value?.$player) {
videoRef.value.$player.dispose();
}
......@@ -405,6 +527,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) {
videoOptions,
showErrorOverlay,
errorMessage,
showNetworkSpeedOverlay,
networkSpeedText,
retryLoad,
handleVideoJsMounted,
tryNativePlay
......