feat(video-player): 添加HLS下载速度显示和调试功能
- 在视频播放器组件中添加HLS下载速度显示和调试信息展示 - 新增debug模式用于显示详细调试信息 - 优化网络状态检测逻辑,增加微信环境下的网络类型检测 - 添加相关测试用例验证功能
Showing
3 changed files
with
365 additions
and
13 deletions
| ... | @@ -33,11 +33,23 @@ | ... | @@ -33,11 +33,23 @@ |
| 33 | 33 | ||
| 34 | <div v-if="showNetworkSpeedOverlay && !showErrorOverlay" class="speed-overlay"> | 34 | <div v-if="showNetworkSpeedOverlay && !showErrorOverlay" class="speed-overlay"> |
| 35 | <div class="speed-content"> | 35 | <div class="speed-content"> |
| 36 | - <div class="speed-title">网络较慢</div> | 36 | + <div |
| 37 | - <div class="speed-value">{{ `当前网速:${networkSpeedText}` }}</div> | 37 | + class="speed-title" |
| 38 | + :class="{ 'speed-title-no-detail': !(networkSpeedText && networkSpeedText !== '未知') }" | ||
| 39 | + > | ||
| 40 | + 网络较慢 | ||
| 41 | + </div> | ||
| 42 | + <div v-if="networkSpeedText && networkSpeedText !== '未知'" class="speed-value"> | ||
| 43 | + {{ networkSpeedText.includes("Mbps") ? `当前网速:${networkSpeedText}` : `当前网络:${networkSpeedText}` }} | ||
| 44 | + </div> | ||
| 38 | </div> | 45 | </div> |
| 39 | </div> | 46 | </div> |
| 40 | 47 | ||
| 48 | + <div v-if="hlsDownloadSpeedText" class="hls-speed-badge">{{ hlsDownloadSpeedText }}</div> | ||
| 49 | + <div v-if="props.debug" class="hls-debug-badge"> | ||
| 50 | + {{ hlsSpeedDebugText || "debug:empty" }} | ||
| 51 | + </div> | ||
| 52 | + | ||
| 41 | <!-- 错误提示覆盖层 --> | 53 | <!-- 错误提示覆盖层 --> |
| 42 | <div v-if="showErrorOverlay" class="error-overlay"> | 54 | <div v-if="showErrorOverlay" class="error-overlay"> |
| 43 | <div class="error-content"> | 55 | <div class="error-content"> |
| ... | @@ -74,6 +86,10 @@ const props = defineProps({ | ... | @@ -74,6 +86,10 @@ const props = defineProps({ |
| 74 | required: false, | 86 | required: false, |
| 75 | default: true, | 87 | default: true, |
| 76 | }, | 88 | }, |
| 89 | + debug: { | ||
| 90 | + type: Boolean, | ||
| 91 | + default: false | ||
| 92 | + }, | ||
| 77 | /** | 93 | /** |
| 78 | * iOS 环境下是否强制使用原生播放器 | 94 | * iOS 环境下是否强制使用原生播放器 |
| 79 | * 默认为 true (使用原生),设为 false 则尝试使用 Video.js | 95 | * 默认为 true (使用原生),设为 false 则尝试使用 Video.js |
| ... | @@ -99,6 +115,8 @@ const { | ... | @@ -99,6 +115,8 @@ const { |
| 99 | errorMessage, | 115 | errorMessage, |
| 100 | showNetworkSpeedOverlay, | 116 | showNetworkSpeedOverlay, |
| 101 | networkSpeedText, | 117 | networkSpeedText, |
| 118 | + hlsDownloadSpeedText, | ||
| 119 | + hlsSpeedDebugText, | ||
| 102 | retryLoad, | 120 | retryLoad, |
| 103 | handleVideoJsMounted, | 121 | handleVideoJsMounted, |
| 104 | tryNativePlay | 122 | tryNativePlay |
| ... | @@ -118,7 +136,6 @@ defineExpose({ | ... | @@ -118,7 +136,6 @@ defineExpose({ |
| 118 | nativeVideoRef.value?.pause?.(); | 136 | nativeVideoRef.value?.pause?.(); |
| 119 | emit('onPause', nativeVideoRef.value); | 137 | emit('onPause', nativeVideoRef.value); |
| 120 | } catch (e) { | 138 | } catch (e) { |
| 121 | - console.warn('Video pause error:', e); | ||
| 122 | } | 139 | } |
| 123 | return; | 140 | return; |
| 124 | } | 141 | } |
| ... | @@ -128,7 +145,6 @@ defineExpose({ | ... | @@ -128,7 +145,6 @@ defineExpose({ |
| 128 | player.value.pause(); | 145 | player.value.pause(); |
| 129 | emit('onPause', player.value); | 146 | emit('onPause', player.value); |
| 130 | } catch (e) { | 147 | } catch (e) { |
| 131 | - console.warn('Video pause error:', e); | ||
| 132 | } | 148 | } |
| 133 | } | 149 | } |
| 134 | }, | 150 | }, |
| ... | @@ -137,7 +153,7 @@ defineExpose({ | ... | @@ -137,7 +153,7 @@ defineExpose({ |
| 137 | tryNativePlay(); | 153 | tryNativePlay(); |
| 138 | return; | 154 | return; |
| 139 | } | 155 | } |
| 140 | - player.value?.play()?.catch(console.warn); | 156 | + player.value?.play()?.catch(() => {}); |
| 141 | }, | 157 | }, |
| 142 | getPlayer() { | 158 | getPlayer() { |
| 143 | return useNativePlayer.value ? nativeVideoRef.value : player.value; | 159 | return useNativePlayer.value ? nativeVideoRef.value : player.value; |
| ... | @@ -227,12 +243,47 @@ defineExpose({ | ... | @@ -227,12 +243,47 @@ defineExpose({ |
| 227 | margin-bottom: 8px; | 243 | margin-bottom: 8px; |
| 228 | } | 244 | } |
| 229 | 245 | ||
| 246 | +.speed-title-no-detail { | ||
| 247 | + margin-bottom: 0; | ||
| 248 | +} | ||
| 249 | + | ||
| 230 | .speed-value { | 250 | .speed-value { |
| 231 | font-size: 14px; | 251 | font-size: 14px; |
| 232 | line-height: 1.4; | 252 | line-height: 1.4; |
| 233 | opacity: 0.95; | 253 | opacity: 0.95; |
| 234 | } | 254 | } |
| 235 | 255 | ||
| 256 | +.hls-speed-badge { | ||
| 257 | + position: absolute; | ||
| 258 | + right: 10px; | ||
| 259 | + bottom: 10px; | ||
| 260 | + z-index: 850; | ||
| 261 | + pointer-events: none; | ||
| 262 | + padding: 4px 8px; | ||
| 263 | + border-radius: 8px; | ||
| 264 | + background: rgba(0, 0, 0, 0.5); | ||
| 265 | + color: #fff; | ||
| 266 | + font-size: 12px; | ||
| 267 | + line-height: 1.2; | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +.hls-debug-badge { | ||
| 271 | + position: absolute; | ||
| 272 | + left: 10px; | ||
| 273 | + top: 10px; | ||
| 274 | + z-index: 1200; | ||
| 275 | + pointer-events: none; | ||
| 276 | + padding: 4px 8px; | ||
| 277 | + border-radius: 8px; | ||
| 278 | + background: rgba(0, 0, 0, 0.45); | ||
| 279 | + color: #fff; | ||
| 280 | + font-size: 11px; | ||
| 281 | + line-height: 1.2; | ||
| 282 | + max-width: 90%; | ||
| 283 | + white-space: pre-wrap; | ||
| 284 | + word-break: break-all; | ||
| 285 | +} | ||
| 286 | + | ||
| 236 | .retry-button { | 287 | .retry-button { |
| 237 | background: #007bff; | 288 | background: #007bff; |
| 238 | color: white; | 289 | color: white; | ... | ... |
| 1 | +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' | ||
| 2 | +import { ref } from 'vue' | ||
| 3 | + | ||
| 4 | +vi.mock('@/utils/tools', () => ({ | ||
| 5 | + wxInfo: () => ({ | ||
| 6 | + isIOSWeChat: false | ||
| 7 | + }) | ||
| 8 | +})) | ||
| 9 | + | ||
| 10 | +vi.mock('hls.js', () => ({ | ||
| 11 | + default: { | ||
| 12 | + isSupported: () => false | ||
| 13 | + } | ||
| 14 | +})) | ||
| 15 | + | ||
| 16 | +vi.mock('video.js', () => ({ | ||
| 17 | + default: { | ||
| 18 | + browser: { | ||
| 19 | + IS_SAFARI: false | ||
| 20 | + } | ||
| 21 | + } | ||
| 22 | +})) | ||
| 23 | + | ||
| 24 | +vi.mock('videojs-contrib-quality-levels', () => ({})) | ||
| 25 | +vi.mock('videojs-hls-quality-selector', () => ({})) | ||
| 26 | +vi.mock('videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css', () => ({})) | ||
| 27 | + | ||
| 28 | +import { useVideoPlayer } from '../useVideoPlayer' | ||
| 29 | + | ||
| 30 | +const createPlayerStub = ({ bandwidthBitsPerSecond, bandwidthOn = 'vhs' } = {}) => { | ||
| 31 | + const listeners = {} | ||
| 32 | + const techObject = bandwidthOn === 'hls' | ||
| 33 | + ? { hls: { bandwidth: bandwidthBitsPerSecond } } | ||
| 34 | + : { vhs: { bandwidth: bandwidthBitsPerSecond } } | ||
| 35 | + | ||
| 36 | + const player = { | ||
| 37 | + isDisposed: () => false, | ||
| 38 | + on: (eventName, callback) => { | ||
| 39 | + listeners[eventName] = callback | ||
| 40 | + }, | ||
| 41 | + tech: () => techObject, | ||
| 42 | + hlsQualitySelector: vi.fn(), | ||
| 43 | + error: () => null, | ||
| 44 | + load: vi.fn(), | ||
| 45 | + pause: vi.fn(), | ||
| 46 | + play: vi.fn(() => Promise.resolve()) | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + return { player, listeners } | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +describe('useVideoPlayer HLS 下载速度调试', () => { | ||
| 53 | + let console_warn_spy | ||
| 54 | + | ||
| 55 | + beforeEach(() => { | ||
| 56 | + console_warn_spy = vi.spyOn(console, 'warn').mockImplementation(() => {}) | ||
| 57 | + vi.useFakeTimers() | ||
| 58 | + }) | ||
| 59 | + | ||
| 60 | + afterEach(() => { | ||
| 61 | + vi.clearAllTimers() | ||
| 62 | + vi.useRealTimers() | ||
| 63 | + console_warn_spy?.mockRestore?.() | ||
| 64 | + }) | ||
| 65 | + | ||
| 66 | + it('m3u8 + 非原生模式下能从 vhs.bandwidth 计算速度', () => { | ||
| 67 | + const props = { | ||
| 68 | + options: {}, | ||
| 69 | + videoUrl: 'https://example.com/test.m3u8', | ||
| 70 | + videoId: 'v1', | ||
| 71 | + autoplay: false, | ||
| 72 | + debug: true, | ||
| 73 | + useNativeOnIos: true | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + const emit = vi.fn() | ||
| 77 | + const videoRef = ref(null) | ||
| 78 | + const nativeVideoRef = ref(null) | ||
| 79 | + | ||
| 80 | + const { handleVideoJsMounted, hlsDownloadSpeedText, hlsSpeedDebugText } = useVideoPlayer( | ||
| 81 | + props, | ||
| 82 | + emit, | ||
| 83 | + videoRef, | ||
| 84 | + nativeVideoRef | ||
| 85 | + ) | ||
| 86 | + | ||
| 87 | + const { player, listeners } = createPlayerStub({ bandwidthBitsPerSecond: 8 * 1024 * 1024, bandwidthOn: 'vhs' }) | ||
| 88 | + | ||
| 89 | + handleVideoJsMounted({ state: {}, player }) | ||
| 90 | + listeners.play() | ||
| 91 | + | ||
| 92 | + vi.advanceTimersByTime(1100) | ||
| 93 | + | ||
| 94 | + expect(hlsDownloadSpeedText.value).toBe('1.0MB/s') | ||
| 95 | + expect(hlsSpeedDebugText.value).toContain('update') | ||
| 96 | + expect(hlsSpeedDebugText.value).toContain('mode:videojs') | ||
| 97 | + expect(hlsSpeedDebugText.value).toContain('m3u8:1') | ||
| 98 | + }) | ||
| 99 | + | ||
| 100 | + it('m3u8 + 非原生模式下能从 hls.bandwidth 兜底计算速度', () => { | ||
| 101 | + const props = { | ||
| 102 | + options: {}, | ||
| 103 | + videoUrl: 'https://example.com/test.m3u8', | ||
| 104 | + videoId: 'v1', | ||
| 105 | + autoplay: false, | ||
| 106 | + debug: true, | ||
| 107 | + useNativeOnIos: true | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + const emit = vi.fn() | ||
| 111 | + const videoRef = ref(null) | ||
| 112 | + const nativeVideoRef = ref(null) | ||
| 113 | + | ||
| 114 | + const { handleVideoJsMounted, hlsDownloadSpeedText, hlsSpeedDebugText } = useVideoPlayer( | ||
| 115 | + props, | ||
| 116 | + emit, | ||
| 117 | + videoRef, | ||
| 118 | + nativeVideoRef | ||
| 119 | + ) | ||
| 120 | + | ||
| 121 | + const { player, listeners } = createPlayerStub({ bandwidthBitsPerSecond: 4 * 1024 * 1024, bandwidthOn: 'hls' }) | ||
| 122 | + | ||
| 123 | + handleVideoJsMounted({ state: {}, player }) | ||
| 124 | + listeners.play() | ||
| 125 | + | ||
| 126 | + expect(hlsDownloadSpeedText.value).toBe('512kB/s') | ||
| 127 | + expect(hlsSpeedDebugText.value).toContain('hls_bw:') | ||
| 128 | + }) | ||
| 129 | + | ||
| 130 | + it('playing 事件的 debug 文本包含 tech 与模式信息', () => { | ||
| 131 | + const props = { | ||
| 132 | + options: {}, | ||
| 133 | + videoUrl: 'https://example.com/test.m3u8', | ||
| 134 | + videoId: 'v1', | ||
| 135 | + autoplay: false, | ||
| 136 | + debug: true, | ||
| 137 | + useNativeOnIos: true | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + const emit = vi.fn() | ||
| 141 | + const videoRef = ref(null) | ||
| 142 | + const nativeVideoRef = ref(null) | ||
| 143 | + | ||
| 144 | + const { handleVideoJsMounted, hlsSpeedDebugText } = useVideoPlayer( | ||
| 145 | + props, | ||
| 146 | + emit, | ||
| 147 | + videoRef, | ||
| 148 | + nativeVideoRef | ||
| 149 | + ) | ||
| 150 | + | ||
| 151 | + const { player, listeners } = createPlayerStub({ bandwidthBitsPerSecond: 8 * 1024 * 1024, bandwidthOn: 'vhs' }) | ||
| 152 | + | ||
| 153 | + handleVideoJsMounted({ state: {}, player }) | ||
| 154 | + listeners.playing() | ||
| 155 | + | ||
| 156 | + expect(hlsSpeedDebugText.value).toContain('playing') | ||
| 157 | + expect(hlsSpeedDebugText.value).toContain('mode:videojs') | ||
| 158 | + expect(hlsSpeedDebugText.value).toContain('tech:') | ||
| 159 | + }) | ||
| 160 | +}) |
| ... | @@ -33,6 +33,36 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -33,6 +33,36 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 33 | const networkSpeedText = ref(''); | 33 | const networkSpeedText = ref(''); |
| 34 | const hasEverPlayed = ref(false); | 34 | const hasEverPlayed = ref(false); |
| 35 | let networkSpeedTimer = null; | 35 | let networkSpeedTimer = null; |
| 36 | + let lastWeixinNetworkTypeAt = 0; | ||
| 37 | + | ||
| 38 | + const hlsDownloadSpeedText = ref(''); | ||
| 39 | + let hlsSpeedTimer = null; | ||
| 40 | + const hasStartedPlayback = ref(false); | ||
| 41 | + const hlsSpeedDebugText = ref(''); | ||
| 42 | + const setHlsDebug = (eventName, extra) => { | ||
| 43 | + if (!props || props.debug !== true) return; | ||
| 44 | + | ||
| 45 | + const p = player.value; | ||
| 46 | + const hasPlayer = !!p && !p.isDisposed?.(); | ||
| 47 | + let tech = null; | ||
| 48 | + try { | ||
| 49 | + tech = hasPlayer && typeof p.tech === 'function' ? p.tech(true) : null; | ||
| 50 | + } catch (e) { | ||
| 51 | + tech = null; | ||
| 52 | + } | ||
| 53 | + const stableTech = tech || (hasPlayer ? p.tech_ : null); | ||
| 54 | + const techName = hasPlayer | ||
| 55 | + ? (p.techName_ || (stableTech && stableTech.name_) || (stableTech && stableTech.constructor && stableTech.constructor.name) || 'unknown') | ||
| 56 | + : 'none'; | ||
| 57 | + const mode = useNativePlayer.value ? 'native' : 'videojs'; | ||
| 58 | + const vhs = stableTech && stableTech.vhs ? stableTech.vhs : null; | ||
| 59 | + const hls = stableTech && stableTech.hls ? stableTech.hls : null; | ||
| 60 | + const bwKbps = vhs && typeof vhs.bandwidth === 'number' ? Math.round(vhs.bandwidth / 1000) : 0; | ||
| 61 | + const hlsBwKbps = hls && typeof hls.bandwidth === 'number' ? Math.round(hls.bandwidth / 1000) : 0; | ||
| 62 | + | ||
| 63 | + const extraText = extra ? ` ${extra}` : ''; | ||
| 64 | + hlsSpeedDebugText.value = `${eventName}${extraText}\nmode:${mode} m3u8:${isM3U8.value ? '1' : '0'} native:${useNativePlayer.value ? '1' : '0'}\ntech:${techName} vhs:${vhs ? '1' : '0'} bw:${bwKbps}kbps hls_bw:${hlsBwKbps}kbps`; | ||
| 65 | + }; | ||
| 36 | 66 | ||
| 37 | // 原生播放器状态 | 67 | // 原生播放器状态 |
| 38 | const nativeReady = ref(false); | 68 | const nativeReady = ref(false); |
| ... | @@ -213,31 +243,48 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -213,31 +243,48 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 213 | const updateNetworkSpeed = () => { | 243 | const updateNetworkSpeed = () => { |
| 214 | if (typeof navigator === 'undefined') { | 244 | if (typeof navigator === 'undefined') { |
| 215 | networkSpeedText.value = '未知'; | 245 | networkSpeedText.value = '未知'; |
| 216 | - return; | 246 | + return false; |
| 217 | } | 247 | } |
| 218 | 248 | ||
| 219 | const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; | 249 | const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; |
| 220 | const downlink = connection && typeof connection.downlink === 'number' ? connection.downlink : null; | 250 | const downlink = connection && typeof connection.downlink === 'number' ? connection.downlink : null; |
| 221 | if (downlink && downlink > 0) { | 251 | if (downlink && downlink > 0) { |
| 222 | networkSpeedText.value = `${downlink.toFixed(1)} Mbps`; | 252 | networkSpeedText.value = `${downlink.toFixed(1)} Mbps`; |
| 223 | - return; | 253 | + return true; |
| 224 | } | 254 | } |
| 225 | 255 | ||
| 226 | const effectiveType = connection && typeof connection.effectiveType === 'string' ? connection.effectiveType : ''; | 256 | const effectiveType = connection && typeof connection.effectiveType === 'string' ? connection.effectiveType : ''; |
| 227 | networkSpeedText.value = effectiveType ? `${effectiveType}` : '未知'; | 257 | networkSpeedText.value = effectiveType ? `${effectiveType}` : '未知'; |
| 258 | + return effectiveType ? true : false; | ||
| 259 | + }; | ||
| 260 | + | ||
| 261 | + const updateWeixinNetworkType = () => { | ||
| 262 | + if (typeof window === 'undefined') return; | ||
| 263 | + if (!window.WeixinJSBridge || typeof window.WeixinJSBridge.invoke !== 'function') return; | ||
| 264 | + | ||
| 265 | + const now = Date.now(); | ||
| 266 | + if (now - lastWeixinNetworkTypeAt < 3000) return; | ||
| 267 | + lastWeixinNetworkTypeAt = now; | ||
| 268 | + | ||
| 269 | + window.WeixinJSBridge.invoke('getNetworkType', {}, (res) => { | ||
| 270 | + const type = (res && (res.networkType || res.network_type)) ? String(res.networkType || res.network_type) : ''; | ||
| 271 | + if (type) networkSpeedText.value = type; | ||
| 272 | + }); | ||
| 228 | }; | 273 | }; |
| 229 | 274 | ||
| 230 | const showNetworkSpeed = () => { | 275 | const showNetworkSpeed = () => { |
| 231 | - if (!hasEverPlayed.value) return; | 276 | + if (!hasStartedPlayback.value) return; |
| 232 | if (showErrorOverlay.value) return; | 277 | if (showErrorOverlay.value) return; |
| 233 | if (showNetworkSpeedOverlay.value) return; | 278 | if (showNetworkSpeedOverlay.value) return; |
| 234 | 279 | ||
| 235 | showNetworkSpeedOverlay.value = true; | 280 | showNetworkSpeedOverlay.value = true; |
| 236 | - updateNetworkSpeed(); | 281 | + const ok = updateNetworkSpeed(); |
| 282 | + if (!ok) updateWeixinNetworkType(); | ||
| 237 | 283 | ||
| 238 | if (networkSpeedTimer) clearInterval(networkSpeedTimer); | 284 | if (networkSpeedTimer) clearInterval(networkSpeedTimer); |
| 239 | networkSpeedTimer = setInterval(() => { | 285 | networkSpeedTimer = setInterval(() => { |
| 240 | - updateNetworkSpeed(); | 286 | + const ok2 = updateNetworkSpeed(); |
| 287 | + if (!ok2) updateWeixinNetworkType(); | ||
| 241 | }, 800); | 288 | }, 800); |
| 242 | }; | 289 | }; |
| 243 | 290 | ||
| ... | @@ -249,11 +296,77 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -249,11 +296,77 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 249 | } | 296 | } |
| 250 | }; | 297 | }; |
| 251 | 298 | ||
| 299 | + const formatSpeed = (bytesPerSecond) => { | ||
| 300 | + const size = Number(bytesPerSecond) || 0; | ||
| 301 | + if (!size) return ''; | ||
| 302 | + | ||
| 303 | + const kb = 1024; | ||
| 304 | + const mb = kb * 1024; | ||
| 305 | + if (size >= mb) return `${(size / mb).toFixed(1)}MB/s`; | ||
| 306 | + if (size >= kb) return `${Math.round(size / kb)}kB/s`; | ||
| 307 | + return `${Math.round(size)}B/s`; | ||
| 308 | + }; | ||
| 309 | + | ||
| 310 | + const updateHlsDownloadSpeed = () => { | ||
| 311 | + if (!player.value || player.value.isDisposed()) { | ||
| 312 | + hlsDownloadSpeedText.value = ''; | ||
| 313 | + setHlsDebug('update', 'player:empty'); | ||
| 314 | + return; | ||
| 315 | + } | ||
| 316 | + if (!isM3U8.value || useNativePlayer.value) { | ||
| 317 | + hlsDownloadSpeedText.value = ''; | ||
| 318 | + setHlsDebug('update', 'skip'); | ||
| 319 | + return; | ||
| 320 | + } | ||
| 321 | + | ||
| 322 | + let tech = null; | ||
| 323 | + try { | ||
| 324 | + tech = typeof player.value.tech === 'function' ? player.value.tech(true) : null; | ||
| 325 | + } catch (e) { | ||
| 326 | + tech = null; | ||
| 327 | + } | ||
| 328 | + const stableTech = tech || player.value.tech_ || null; | ||
| 329 | + const vhs = stableTech && stableTech.vhs ? stableTech.vhs : null; | ||
| 330 | + const hls = stableTech && stableTech.hls ? stableTech.hls : null; | ||
| 331 | + const bandwidthBitsPerSecond = (vhs && typeof vhs.bandwidth === 'number' ? vhs.bandwidth : null) | ||
| 332 | + || (hls && typeof hls.bandwidth === 'number' ? hls.bandwidth : null); | ||
| 333 | + if (!bandwidthBitsPerSecond || bandwidthBitsPerSecond <= 0) { | ||
| 334 | + hlsDownloadSpeedText.value = ''; | ||
| 335 | + setHlsDebug('update', 'bw:0'); | ||
| 336 | + return; | ||
| 337 | + } | ||
| 338 | + | ||
| 339 | + hlsDownloadSpeedText.value = formatSpeed(bandwidthBitsPerSecond / 8); | ||
| 340 | + setHlsDebug('update', `speed:${hlsDownloadSpeedText.value}`); | ||
| 341 | + }; | ||
| 342 | + | ||
| 343 | + const startHlsDownloadSpeed = () => { | ||
| 344 | + if (hlsSpeedTimer) return; | ||
| 345 | + if (!isM3U8.value || useNativePlayer.value) return; | ||
| 346 | + | ||
| 347 | + setHlsDebug('start'); | ||
| 348 | + updateHlsDownloadSpeed(); | ||
| 349 | + hlsSpeedTimer = setInterval(() => { | ||
| 350 | + updateHlsDownloadSpeed(); | ||
| 351 | + }, 1000); | ||
| 352 | + }; | ||
| 353 | + | ||
| 354 | + const stopHlsDownloadSpeed = (reason) => { | ||
| 355 | + if (hlsSpeedTimer) { | ||
| 356 | + clearInterval(hlsSpeedTimer); | ||
| 357 | + hlsSpeedTimer = null; | ||
| 358 | + } | ||
| 359 | + hlsDownloadSpeedText.value = ''; | ||
| 360 | + setHlsDebug('stop', reason || ''); | ||
| 361 | + }; | ||
| 362 | + | ||
| 252 | // 4. 原生播放器逻辑 (iOS微信) | 363 | // 4. 原生播放器逻辑 (iOS微信) |
| 253 | const initNativePlayer = () => { | 364 | const initNativePlayer = () => { |
| 254 | const videoEl = nativeVideoRef.value; | 365 | const videoEl = nativeVideoRef.value; |
| 255 | if (!videoEl) return; | 366 | if (!videoEl) return; |
| 256 | 367 | ||
| 368 | + setHlsDebug('native:init'); | ||
| 369 | + | ||
| 257 | // HLS 处理 | 370 | // HLS 处理 |
| 258 | if (isM3U8.value && Hls.isSupported()) { | 371 | if (isM3U8.value && Hls.isSupported()) { |
| 259 | // 如果原生支持 HLS (iOS Safari),直接用 src 即可,不需要 hls.js | 372 | // 如果原生支持 HLS (iOS Safari),直接用 src 即可,不需要 hls.js |
| ... | @@ -278,8 +391,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -278,8 +391,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 278 | }; | 391 | }; |
| 279 | 392 | ||
| 280 | const onPlay = () => { | 393 | const onPlay = () => { |
| 281 | - hasEverPlayed.value = true; | ||
| 282 | hideNetworkSpeed(); | 394 | hideNetworkSpeed(); |
| 395 | + setHlsDebug('native:play'); | ||
| 283 | }; | 396 | }; |
| 284 | 397 | ||
| 285 | const onPause = () => { | 398 | const onPause = () => { |
| ... | @@ -289,15 +402,20 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -289,15 +402,20 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 289 | const onWaiting = () => { | 402 | const onWaiting = () => { |
| 290 | if (videoEl.paused) return; | 403 | if (videoEl.paused) return; |
| 291 | showNetworkSpeed(); | 404 | showNetworkSpeed(); |
| 405 | + setHlsDebug('native:waiting'); | ||
| 292 | }; | 406 | }; |
| 293 | 407 | ||
| 294 | const onStalled = () => { | 408 | const onStalled = () => { |
| 295 | if (videoEl.paused) return; | 409 | if (videoEl.paused) return; |
| 296 | showNetworkSpeed(); | 410 | showNetworkSpeed(); |
| 411 | + setHlsDebug('native:stalled'); | ||
| 297 | }; | 412 | }; |
| 298 | 413 | ||
| 299 | const onPlaying = () => { | 414 | const onPlaying = () => { |
| 415 | + hasEverPlayed.value = true; | ||
| 416 | + hasStartedPlayback.value = true; | ||
| 300 | hideNetworkSpeed(); | 417 | hideNetworkSpeed(); |
| 418 | + setHlsDebug('native:playing'); | ||
| 301 | }; | 419 | }; |
| 302 | 420 | ||
| 303 | videoEl.addEventListener("loadstart", onLoadStart); | 421 | videoEl.addEventListener("loadstart", onLoadStart); |
| ... | @@ -393,6 +511,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -393,6 +511,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 393 | player.value = payload.player; | 511 | player.value = payload.player; |
| 394 | 512 | ||
| 395 | if (player.value) { | 513 | if (player.value) { |
| 514 | + setHlsDebug('mounted'); | ||
| 515 | + | ||
| 396 | // 初始化多码率切换插件 (七牛云多码率支持) | 516 | // 初始化多码率切换插件 (七牛云多码率支持) |
| 397 | if (player.value.hlsQualitySelector) { | 517 | if (player.value.hlsQualitySelector) { |
| 398 | player.value.hlsQualitySelector({ | 518 | player.value.hlsQualitySelector({ |
| ... | @@ -415,32 +535,47 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -415,32 +535,47 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 415 | }); | 535 | }); |
| 416 | 536 | ||
| 417 | player.value.on('play', () => { | 537 | player.value.on('play', () => { |
| 418 | - hasEverPlayed.value = true; | ||
| 419 | hideNetworkSpeed(); | 538 | hideNetworkSpeed(); |
| 539 | + startHlsDownloadSpeed(); | ||
| 540 | + setHlsDebug('play'); | ||
| 420 | }); | 541 | }); |
| 421 | 542 | ||
| 422 | player.value.on('pause', () => { | 543 | player.value.on('pause', () => { |
| 423 | hideNetworkSpeed(); | 544 | hideNetworkSpeed(); |
| 545 | + stopHlsDownloadSpeed('pause'); | ||
| 546 | + setHlsDebug('pause'); | ||
| 424 | }); | 547 | }); |
| 425 | 548 | ||
| 426 | player.value.on('waiting', () => { | 549 | player.value.on('waiting', () => { |
| 427 | if (!hasEverPlayed.value) return; | 550 | if (!hasEverPlayed.value) return; |
| 428 | if (player.value?.paused?.()) return; | 551 | if (player.value?.paused?.()) return; |
| 429 | showNetworkSpeed(); | 552 | showNetworkSpeed(); |
| 553 | + startHlsDownloadSpeed(); | ||
| 554 | + setHlsDebug('waiting'); | ||
| 430 | }); | 555 | }); |
| 431 | 556 | ||
| 432 | player.value.on('stalled', () => { | 557 | player.value.on('stalled', () => { |
| 433 | if (!hasEverPlayed.value) return; | 558 | if (!hasEverPlayed.value) return; |
| 434 | if (player.value?.paused?.()) return; | 559 | if (player.value?.paused?.()) return; |
| 435 | showNetworkSpeed(); | 560 | showNetworkSpeed(); |
| 561 | + startHlsDownloadSpeed(); | ||
| 562 | + setHlsDebug('stalled'); | ||
| 436 | }); | 563 | }); |
| 437 | 564 | ||
| 438 | player.value.on('playing', () => { | 565 | player.value.on('playing', () => { |
| 566 | + hasEverPlayed.value = true; | ||
| 567 | + hasStartedPlayback.value = true; | ||
| 439 | hideNetworkSpeed(); | 568 | hideNetworkSpeed(); |
| 569 | + setHlsDebug('playing'); | ||
| 570 | + }); | ||
| 571 | + | ||
| 572 | + player.value.on('ended', () => { | ||
| 573 | + stopHlsDownloadSpeed('ended'); | ||
| 574 | + setHlsDebug('ended'); | ||
| 440 | }); | 575 | }); |
| 441 | 576 | ||
| 442 | if (props.autoplay) { | 577 | if (props.autoplay) { |
| 443 | - player.value.play().catch(console.warn); | 578 | + player.value.play().catch(() => {}); |
| 444 | } | 579 | } |
| 445 | } | 580 | } |
| 446 | }; | 581 | }; |
| ... | @@ -455,6 +590,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -455,6 +590,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 455 | retryCount.value++; | 590 | retryCount.value++; |
| 456 | showErrorOverlay.value = false; | 591 | showErrorOverlay.value = false; |
| 457 | hideNetworkSpeed(); | 592 | hideNetworkSpeed(); |
| 593 | + stopHlsDownloadSpeed('retry'); | ||
| 458 | 594 | ||
| 459 | if (useNativePlayer.value) { | 595 | if (useNativePlayer.value) { |
| 460 | const videoEl = nativeVideoRef.value; | 596 | const videoEl = nativeVideoRef.value; |
| ... | @@ -480,7 +616,9 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -480,7 +616,9 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 480 | retryCount.value = 0; | 616 | retryCount.value = 0; |
| 481 | showErrorOverlay.value = false; | 617 | showErrorOverlay.value = false; |
| 482 | hideNetworkSpeed(); | 618 | hideNetworkSpeed(); |
| 619 | + stopHlsDownloadSpeed('url_change'); | ||
| 483 | hasEverPlayed.value = false; | 620 | hasEverPlayed.value = false; |
| 621 | + hasStartedPlayback.value = false; | ||
| 484 | void probeVideo(); | 622 | void probeVideo(); |
| 485 | 623 | ||
| 486 | // 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境) | 624 | // 如果是原生播放器且 URL 变化,需要手动处理 HLS (如果是非 iOS Safari 环境) |
| ... | @@ -514,6 +652,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -514,6 +652,7 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 514 | } | 652 | } |
| 515 | 653 | ||
| 516 | hideNetworkSpeed(); | 654 | hideNetworkSpeed(); |
| 655 | + stopHlsDownloadSpeed('unmount'); | ||
| 517 | if (videoRef.value?.$player) { | 656 | if (videoRef.value?.$player) { |
| 518 | videoRef.value.$player.dispose(); | 657 | videoRef.value.$player.dispose(); |
| 519 | } | 658 | } |
| ... | @@ -529,6 +668,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { | ... | @@ -529,6 +668,8 @@ export function useVideoPlayer(props, emit, videoRef, nativeVideoRef) { |
| 529 | errorMessage, | 668 | errorMessage, |
| 530 | showNetworkSpeedOverlay, | 669 | showNetworkSpeedOverlay, |
| 531 | networkSpeedText, | 670 | networkSpeedText, |
| 671 | + hlsDownloadSpeedText, | ||
| 672 | + hlsSpeedDebugText, | ||
| 532 | retryLoad, | 673 | retryLoad, |
| 533 | handleVideoJsMounted, | 674 | handleVideoJsMounted, |
| 534 | tryNativePlay | 675 | tryNativePlay | ... | ... |
-
Please register or login to post a comment