hookehuyr

feat(video-player): 添加HLS下载速度显示和调试功能

- 在视频播放器组件中添加HLS下载速度显示和调试信息展示
- 新增debug模式用于显示详细调试信息
- 优化网络状态检测逻辑,增加微信环境下的网络类型检测
- 添加相关测试用例验证功能
...@@ -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
......