UNPKG

jessibuca

Version:
524 lines (428 loc) 16.4 kB
import { HLSv7Demuxer } from './hlsv7'; // 配置测试URL和视频元素 const testHlsUrl = 'http://localhost:8080/hls/vod/fmp4.m3u8?start=1740972167&streamPath=live/video'; const videoElement = document.createElement('video'); videoElement.controls = true; videoElement.width = 640; videoElement.height = 360; document.body.appendChild(videoElement); // 性能监控数据 interface PerformanceMetrics { initTime: number; seekTimes: number[]; bufferSizes: number[]; errors: string[]; segmentLoadTimes: Record<number, number>; memoryUsage: number[]; playbackStats: { freezeCount: number; rebufferingTime: number; playbackRate: number[]; }; } const metrics: PerformanceMetrics = { initTime: 0, seekTimes: [], bufferSizes: [], errors: [], segmentLoadTimes: {}, memoryUsage: [], playbackStats: { freezeCount: 0, rebufferingTime: 0, playbackRate: [] } }; // 创建UI const container = document.createElement('div'); container.style.fontFamily = 'Arial, sans-serif'; container.style.padding = '20px'; container.style.maxWidth = '800px'; container.style.margin = '0 auto'; document.body.appendChild(container); // 标题 const title = document.createElement('h2'); title.textContent = 'HLSv7Demuxer 性能测试'; container.appendChild(title); // 状态指示器 const statusIndicator = document.createElement('div'); statusIndicator.style.marginBottom = '20px'; statusIndicator.style.padding = '10px'; statusIndicator.style.borderRadius = '4px'; statusIndicator.style.backgroundColor = '#f0f0f0'; container.appendChild(statusIndicator); // 控制按钮容器 const controls = document.createElement('div'); controls.style.display = 'flex'; controls.style.gap = '10px'; controls.style.marginBottom = '20px'; container.appendChild(controls); // 初始化按钮 const initButton = createButton('初始化播放器'); controls.appendChild(initButton); // 播放按钮 const playButton = createButton('播放'); controls.appendChild(playButton); // 暂停按钮 const pauseButton = createButton('暂停'); controls.appendChild(pauseButton); // 跳转按钮 - 10秒 const seek10Button = createButton('跳转+10秒'); controls.appendChild(seek10Button); // 跳转按钮 - 30秒 const seek30Button = createButton('跳转+30秒'); controls.appendChild(seek30Button); // 跳转按钮 - 自定义 const seekCustomButton = createButton('自定义跳转'); controls.appendChild(seekCustomButton); // 销毁按钮 const destroyButton = createButton('销毁播放器'); controls.appendChild(destroyButton); // 性能报告按钮 const reportButton = createButton('生成性能报告'); reportButton.style.backgroundColor = '#4CAF50'; controls.appendChild(reportButton); // 日志容器 const logContainer = document.createElement('div'); logContainer.style.border = '1px solid #ddd'; logContainer.style.padding = '10px'; logContainer.style.height = '300px'; logContainer.style.overflowY = 'scroll'; logContainer.style.backgroundColor = '#f9f9f9'; logContainer.style.fontFamily = 'monospace'; logContainer.style.fontSize = '12px'; container.appendChild(logContainer); // 性能指标容器 const metricsContainer = document.createElement('div'); metricsContainer.style.marginTop = '20px'; metricsContainer.style.border = '1px solid #ddd'; metricsContainer.style.padding = '10px'; container.appendChild(metricsContainer); // 更新性能指标显示 function updateMetricsDisplay() { metricsContainer.innerHTML = ` <h3>性能指标</h3> <p>初始化时间: ${metrics.initTime.toFixed(2)} ms</p> <p>平均跳转时间: ${calculateAverage(metrics.seekTimes).toFixed(2)} ms</p> <p>平均缓冲区大小: ${calculateAverage(metrics.bufferSizes).toFixed(2)} MB</p> <p>错误数量: ${metrics.errors.length}</p> <p>卡顿次数: ${metrics.playbackStats.freezeCount}</p> <p>重新缓冲时间: ${metrics.playbackStats.rebufferingTime.toFixed(2)} ms</p> <p>平均播放速率: ${calculateAverage(metrics.playbackStats.playbackRate).toFixed(2)}x</p> `; } // 计算平均值辅助函数 function calculateAverage(arr: number[]): number { if (arr.length === 0) return 0; return arr.reduce((a, b) => a + b, 0) / arr.length; } // 添加日志 function addLog(message: string, type: 'info' | 'error' | 'warning' | 'success' = 'info') { const logEntry = document.createElement('div'); const timestamp = new Date().toISOString().split('T')[1].replace('Z', ''); logEntry.innerHTML = `<span style="color: #888;">[${timestamp}]</span> <span style="color: ${type === 'info' ? '#333' : type === 'error' ? '#d9534f' : type === 'warning' ? '#f0ad4e' : '#5cb85c' };">${message}</span>`; logContainer.appendChild(logEntry); logContainer.scrollTop = logContainer.scrollHeight; // 如果是错误,记录到指标中 if (type === 'error') { metrics.errors.push(message); } } // 辅助函数 - 创建按钮 function createButton(text: string): HTMLButtonElement { const button = document.createElement('button'); button.textContent = text; button.style.padding = '8px 16px'; button.style.backgroundColor = '#007BFF'; button.style.color = 'white'; button.style.border = 'none'; button.style.borderRadius = '4px'; button.style.cursor = 'pointer'; return button; } // 辅助函数 - 更新状态 function updateStatus(message: string, color: string = '#f0f0f0') { statusIndicator.textContent = message; statusIndicator.style.backgroundColor = color; } // 实例化和事件处理 let demuxer: HLSv7Demuxer | null = null; // 初始化播放器 initButton.addEventListener('click', async () => { if (demuxer) { addLog('播放器已经初始化,请先销毁现有实例', 'warning'); return; } try { updateStatus('初始化中...', '#fff8e1'); addLog('初始化播放器...'); const startTime = performance.now(); // 使用重构后的构造函数,传递配置选项 demuxer = new HLSv7Demuxer(videoElement, { logLevel: 'debug', maxBufferLength: 60, codec: 'video/mp4; codecs="avc1.64001f"' }); // 设置事件监听器 demuxer.on('debug', (message) => { addLog(message); }); demuxer.on('error', (error) => { addLog(`错误: ${error.message}`, 'error'); }); demuxer.on('playlistUpdate', (playlist) => { addLog(`播放列表更新: ${playlist.length}个片段`, 'info'); }); demuxer.on('segmentLoaded', (segmentIndex) => { const loadTime = performance.now(); if (!metrics.segmentLoadTimes[segmentIndex]) { metrics.segmentLoadTimes[segmentIndex] = loadTime; } addLog(`片段${segmentIndex + 1}加载完成`, 'success'); }); demuxer.on('bufferUpdate', (ranges) => { const totalBuffered = ranges.reduce((total, range) => total + (range.end - range.start), 0); metrics.bufferSizes.push(totalBuffered); let rangesText = ranges.map(range => `${range.start.toFixed(2)}s-${range.end.toFixed(2)}s`).join(', '); addLog(`缓冲区更新: ${rangesText}`, 'info'); }); // 初始化 await demuxer.init(testHlsUrl); const endTime = performance.now(); metrics.initTime = endTime - startTime; updateStatus('初始化完成', '#e8f5e9'); addLog(`初始化完成,耗时: ${metrics.initTime.toFixed(2)}ms`, 'success'); updateMetricsDisplay(); } catch (error) { updateStatus('初始化失败', '#ffebee'); addLog(`初始化失败: ${error}`, 'error'); } }); // 播放 playButton.addEventListener('click', async () => { if (!demuxer) { addLog('请先初始化播放器', 'warning'); return; } try { updateStatus('播放中...', '#e8f5e9'); await demuxer.play(); // 记录播放速率 metrics.playbackStats.playbackRate.push(videoElement.playbackRate); addLog('开始播放', 'success'); // 监控卡顿 monitorPlaybackFreeze(); } catch (error) { updateStatus('播放失败', '#ffebee'); addLog(`播放失败: ${error}`, 'error'); } }); // 暂停 pauseButton.addEventListener('click', () => { if (!demuxer) { addLog('请先初始化播放器', 'warning'); return; } try { demuxer.pause(); updateStatus('已暂停', '#e3f2fd'); addLog('已暂停播放'); } catch (error) { addLog(`暂停失败: ${error}`, 'error'); } }); // 跳转+10秒 seek10Button.addEventListener('click', async () => { if (!demuxer) { addLog('请先初始化播放器', 'warning'); return; } try { const currentTime = demuxer.getCurrentTime(); const targetTime = currentTime + 10; updateStatus(`跳转中 (${currentTime.toFixed(2)}s → ${targetTime.toFixed(2)}s)...`, '#fff8e1'); addLog(`尝试跳转: ${currentTime.toFixed(2)}s → ${targetTime.toFixed(2)}s`); const startTime = performance.now(); await demuxer.seek(targetTime); const endTime = performance.now(); const seekTime = endTime - startTime; metrics.seekTimes.push(seekTime); updateStatus(`跳转完成,当前位置: ${demuxer.getCurrentTime().toFixed(2)}s`, '#e8f5e9'); addLog(`跳转完成,耗时: ${seekTime.toFixed(2)}ms`, 'success'); updateMetricsDisplay(); } catch (error) { updateStatus('跳转失败', '#ffebee'); addLog(`跳转失败: ${error}`, 'error'); } }); // 跳转+30秒 seek30Button.addEventListener('click', async () => { if (!demuxer) { addLog('请先初始化播放器', 'warning'); return; } try { const currentTime = demuxer.getCurrentTime(); const targetTime = currentTime + 30; updateStatus(`跳转中 (${currentTime.toFixed(2)}s → ${targetTime.toFixed(2)}s)...`, '#fff8e1'); addLog(`尝试跳转: ${currentTime.toFixed(2)}s → ${targetTime.toFixed(2)}s`); const startTime = performance.now(); await demuxer.seek(targetTime); const endTime = performance.now(); const seekTime = endTime - startTime; metrics.seekTimes.push(seekTime); updateStatus(`跳转完成,当前位置: ${demuxer.getCurrentTime().toFixed(2)}s`, '#e8f5e9'); addLog(`跳转完成,耗时: ${seekTime.toFixed(2)}ms`, 'success'); updateMetricsDisplay(); } catch (error) { updateStatus('跳转失败', '#ffebee'); addLog(`跳转失败: ${error}`, 'error'); } }); // 自定义跳转 seekCustomButton.addEventListener('click', async () => { if (!demuxer) { addLog('请先初始化播放器', 'warning'); return; } const input = prompt('请输入目标时间(秒):', '60'); if (input === null) return; const targetTime = parseFloat(input); if (isNaN(targetTime) || targetTime < 0) { addLog('无效的时间值', 'warning'); return; } try { const currentTime = demuxer.getCurrentTime(); updateStatus(`跳转中 (${currentTime.toFixed(2)}s → ${targetTime.toFixed(2)}s)...`, '#fff8e1'); addLog(`尝试跳转: ${currentTime.toFixed(2)}s → ${targetTime.toFixed(2)}s`); const startTime = performance.now(); await demuxer.seek(targetTime); const endTime = performance.now(); const seekTime = endTime - startTime; metrics.seekTimes.push(seekTime); updateStatus(`跳转完成,当前位置: ${demuxer.getCurrentTime().toFixed(2)}s`, '#e8f5e9'); addLog(`跳转完成,耗时: ${seekTime.toFixed(2)}ms`, 'success'); updateMetricsDisplay(); } catch (error) { updateStatus('跳转失败', '#ffebee'); addLog(`跳转失败: ${error}`, 'error'); } }); // 销毁播放器 destroyButton.addEventListener('click', () => { if (!demuxer) { addLog('播放器尚未初始化', 'warning'); return; } try { demuxer.destroy(); demuxer = null; updateStatus('播放器已销毁', '#f5f5f5'); addLog('播放器已销毁', 'info'); } catch (error) { addLog(`销毁失败: ${error}`, 'error'); } }); // 生成性能报告 reportButton.addEventListener('click', () => { const report = document.createElement('div'); report.style.backgroundColor = '#fff'; report.style.padding = '20px'; report.style.position = 'fixed'; report.style.top = '50%'; report.style.left = '50%'; report.style.transform = 'translate(-50%, -50%)'; report.style.width = '80%'; report.style.maxWidth = '600px'; report.style.maxHeight = '80%'; report.style.overflow = 'auto'; report.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)'; report.style.zIndex = '1000'; report.style.borderRadius = '8px'; report.innerHTML = ` <h2 style="margin-top:0">HLSv7Demuxer 性能报告</h2> <button id="closeReport" style="position:absolute;top:10px;right:10px;background:none;border:none;font-size:20px;cursor:pointer;">×</button> <h3>基本指标</h3> <p>初始化时间: <strong>${metrics.initTime.toFixed(2)} ms</strong></p> <p>测试时长: <strong>${((performance.now() - metrics.initTime) / 1000).toFixed(2)} 秒</strong></p> <h3>跳转性能</h3> <p>跳转次数: <strong>${metrics.seekTimes.length}</strong></p> <p>平均跳转时间: <strong>${calculateAverage(metrics.seekTimes).toFixed(2)} ms</strong></p> <p>最长跳转时间: <strong>${Math.max(...(metrics.seekTimes.length ? metrics.seekTimes : [0])).toFixed(2)} ms</strong></p> <p>最短跳转时间: <strong>${Math.min(...(metrics.seekTimes.length ? metrics.seekTimes : [0])).toFixed(2)} ms</strong></p> <h3>缓冲性能</h3> <p>平均缓冲区大小: <strong>${calculateAverage(metrics.bufferSizes).toFixed(2)} 秒</strong></p> <p>最大缓冲区大小: <strong>${Math.max(...(metrics.bufferSizes.length ? metrics.bufferSizes : [0])).toFixed(2)} 秒</strong></p> <h3>片段加载</h3> <p>加载片段数: <strong>${Object.keys(metrics.segmentLoadTimes).length}</strong></p> <h3>错误统计</h3> <p>总错误数: <strong>${metrics.errors.length}</strong></p> ${metrics.errors.length > 0 ? `<ul>${metrics.errors.map(err => `<li>${err}</li>`).join('')}</ul>` : ''} <h3>播放质量</h3> <p>卡顿次数: <strong>${metrics.playbackStats.freezeCount}</strong></p> <p>重新缓冲时间: <strong>${metrics.playbackStats.rebufferingTime.toFixed(2)} ms</strong></p> <h3>建议</h3> <ul> ${metrics.seekTimes.some(time => time > 1000) ? '<li>跳转时间较长,考虑优化缓冲区管理</li>' : ''} ${metrics.errors.length > 0 ? '<li>存在错误,请检查错误日志</li>' : ''} ${metrics.playbackStats.freezeCount > 3 ? '<li>播放卡顿频繁,考虑增加预加载缓冲区大小</li>' : ''} </ul> `; document.body.appendChild(report); document.getElementById('closeReport')?.addEventListener('click', () => { document.body.removeChild(report); }); }); // 监控播放卡顿 let lastTime = 0; let lastPlaybackCheck = 0; let checkInterval: any = null; function monitorPlaybackFreeze() { if (checkInterval) { clearInterval(checkInterval); } lastTime = videoElement.currentTime; lastPlaybackCheck = performance.now(); checkInterval = setInterval(() => { if (!demuxer || videoElement.paused) return; const now = performance.now(); const currentTime = videoElement.currentTime; // 检测是否卡顿 (时间过去了但视频未前进) if (now - lastPlaybackCheck > 500 && Math.abs(currentTime - lastTime) < 0.01) { metrics.playbackStats.freezeCount++; const freezeStartTime = now; addLog(`检测到卡顿!当前时间: ${currentTime.toFixed(2)}s`, 'warning'); // 检测何时恢复正常播放 const freezeCheckInterval = setInterval(() => { if (Math.abs(videoElement.currentTime - currentTime) > 0.01 || videoElement.paused) { clearInterval(freezeCheckInterval); const freezeTime = performance.now() - freezeStartTime; metrics.playbackStats.rebufferingTime += freezeTime; addLog(`播放已恢复,卡顿时长: ${freezeTime.toFixed(2)}ms`, 'success'); updateMetricsDisplay(); } }, 100); } lastTime = currentTime; lastPlaybackCheck = now; }, 1000); } // 清理函数 window.addEventListener('beforeunload', () => { if (demuxer) { demuxer.destroy(); } if (checkInterval) { clearInterval(checkInterval); } }); // 初始状态 updateStatus('准备就绪', '#f5f5f5'); addLog('调试页面已加载'); updateMetricsDisplay();