UNPKG

jessibuca

Version:
1,087 lines (896 loc) 36.4 kB
import { HLSv7Demuxer } from 'jv4-demuxer'; import { h, render } from 'vue'; import Timeline from './Timeline'; import { TimeRange } from './TimelineBase'; import './HLSPlayer.css'; import { SegmentLoader, VirtualTimeline, MediaSegment, BufferRange, SlidingWindowConfig } from 'jv4-demuxer'; export type { TimeRange }; export interface HLSPlayerOptions { showPlaybackRate?: boolean; showProgress?: boolean; playbackRates?: number[]; autoGenerateUI?: boolean; timeRangeMode?: boolean; // 是否使用时间片段模式 timeRanges?: TimeRange[]; // 可播放的时间片段数组 debug?: { enabled?: boolean; showTimeRanges?: boolean; showMediaTimeline?: boolean; }; } interface PlaylistSegment { duration: number; time: Date; url: string; } export class HLSPlayer { private demuxer: HLSv7Demuxer; private options: HLSPlayerOptions; private videoElement: HTMLVideoElement; private controlsElement?: HTMLDivElement; private progressInput?: HTMLInputElement; private progressCanvas?: HTMLCanvasElement; private timeDisplay?: HTMLSpanElement; private playButton?: HTMLButtonElement; private rateButtons: HTMLButtonElement[] = []; private updateProgressInterval?: number; private currentTimeRange?: TimeRange; private currentMediaTime: number = 0; private debugContainer?: HTMLDivElement; private debugTimelineCanvas?: HTMLCanvasElement; private debugMediaCanvas?: HTMLCanvasElement; private totalPlaylistDuration: number = 0; private loadingIndicator?: HTMLDivElement; private container?: HTMLDivElement; private progressInfoLabel?: HTMLDivElement; // 静态属性,用于记录上次日志输出时间 private static lastProgressLogTime = 0; // 静态属性,用于记录上次时间轴渲染日志输出时间 private static lastTimelineLogTime = 0; constructor(videoElement: HTMLVideoElement, options: HLSPlayerOptions = {}) { this.videoElement = videoElement; this.options = { showPlaybackRate: true, showProgress: true, playbackRates: [0.5, 1, 1.5, 2, 3], autoGenerateUI: true, timeRangeMode: false, timeRanges: [], ...options }; // Create the HLSv7Demuxer instance with optimized configuration this.demuxer = new HLSv7Demuxer(this.videoElement, { logLevel: (options.debug?.enabled ? 'debug' : 'info'), // Configure sliding window for efficient segment management slidingWindow: { enabled: true, forward: 2, // Preload 2 segments ahead backward: 1 // Keep 1 segment in history } }); // Setup event listeners for demuxer this.setupDemuxerListeners(); // Create UI if enabled if (this.options.autoGenerateUI) { this.createUI(); } // Create debug UI if enabled if (this.options.debug?.enabled) { this.createDebugUI(); } // Initialize time ranges if provided if (this.options.timeRanges?.length) { this.initializeTimeRanges(this.options.timeRanges); } // Setup video element event listeners this.videoElement.addEventListener('timeupdate', this.handleTimeUpdate.bind(this)); // 启动进度更新 if (this.options.showProgress) { this.startProgressUpdate(); } } /** * Setup event listeners for demuxer to handle various events */ private setupDemuxerListeners(): void { // Listen for playlist updates this.demuxer.on('playlistUpdate', (playlist) => { this.log(`播放列表更新: 片段数=${playlist.length}`, 'info'); // Calculate total duration this.totalPlaylistDuration = this.demuxer.getTotalDuration(); this.log(`总时长更新: ${this.totalPlaylistDuration}秒`, 'info'); // Update progress display this.updateProgress(); }); // Listen for buffer updates this.demuxer.on('bufferUpdate', (ranges: BufferRange[]) => { // Update buffer visualization if in debug mode if (this.options.debug?.enabled) { this.updateDebugDisplay(); } }); // Listen for segment loaded events this.demuxer.on('segmentLoaded', (segmentIndex: number) => { this.log(`片段 #${segmentIndex} 已加载`, 'debug'); // Update debug display if enabled if (this.options.debug?.enabled) { this.updateDebugDisplay(); } }); // Listen for errors this.demuxer.on('error', (error: Error) => { this.log(`播放器错误: ${error.message}`, 'error'); }); // Listen for debug messages this.demuxer.on('debug', (message: string) => { if (this.options.debug?.enabled) { this.log(message, 'debug'); } }); } private initializeTimeRanges(ranges: TimeRange[]): void { let currentMediaStart = 0; ranges.forEach(range => { range.mediaStart = currentMediaStart; range.mediaDuration = range.end - range.start; currentMediaStart += range.mediaDuration; }); } private handleTimeUpdate(): void { if (!this.options.timeRangeMode) return; this.currentMediaTime = this.videoElement.currentTime; const originalTime = this.mediaToOriginalTime(this.currentMediaTime); // 检查是否需要切换片段 const targetRange = this.findTimeRangeForTime(originalTime); if (targetRange && targetRange !== this.currentTimeRange) { this.switchToTimeRange(targetRange); } } /** * 查找指定时间所属的时间范围 */ private findTimeRangeForTime(time: number): TimeRange | undefined { if (!this.options.timeRanges?.length) return undefined; // 遍历所有时间范围,查找包含指定时间的范围 for (const range of this.options.timeRanges) { const rangeEnd = range.end; if (time >= range.start && time <= rangeEnd) { return range; } } // 如果未找到,使用最接近的范围 let nearestRange: TimeRange | undefined; let minDistance = Number.MAX_VALUE; for (const range of this.options.timeRanges) { const startDistance = Math.abs(time - range.start); const endDistance = Math.abs(time - range.end); const minRangeDistance = Math.min(startDistance, endDistance); if (minRangeDistance < minDistance) { minDistance = minRangeDistance; nearestRange = range; } } // 如果存在最近的范围,记录日志 if (nearestRange) { this.log(`时间 ${time}秒 不在任何时间范围内,使用最近的范围 [${nearestRange.start}-${nearestRange.end}]`, 'debug'); } return nearestRange; } private async switchToTimeRange(range: TimeRange): Promise<void> { this.currentTimeRange = range; const relativeTime = this.currentMediaTime - range.mediaStart; await this.load(range.url); this.seek(range.start + relativeTime); } private originalToMediaTime(time: number): number { if (!this.options.timeRanges?.length) return time; // 查找时间所在的范围 const range = this.findTimeRangeForTime(time); if (!range) return time; // 如果找不到范围,返回原始时间 // 计算在原始视频中的偏移量 const offsetInRange = time - range.start; // 转换为媒体时间 return range.mediaStart + offsetInRange; } private mediaToOriginalTime(time: number): number { if (!this.options.timeRanges?.length) return time; for (const range of this.options.timeRanges) { if (time >= range.mediaStart && time < range.mediaStart + range.mediaDuration) { return range.start + (time - range.mediaStart); } } return this.options.timeRanges[0].start; } private createUI(): void { const container = document.createElement('div'); container.className = 'hls-player'; this.videoElement.parentNode?.insertBefore(container, this.videoElement); container.appendChild(this.videoElement); this.videoElement.className = 'hls-player-video'; this.container = container; this.controlsElement = document.createElement('div'); this.controlsElement.className = 'hls-player-controls'; container.appendChild(this.controlsElement); this.playButton = document.createElement('button'); this.playButton.textContent = '播放'; this.playButton.onclick = this.togglePlay.bind(this); this.controlsElement.appendChild(this.playButton); if (this.options.showProgress) { const progressContainer = document.createElement('div'); progressContainer.className = 'hls-player-progress'; // 添加详细进度信息标签 const progressInfoLabel = document.createElement('div'); progressInfoLabel.className = 'progress-info-label'; progressInfoLabel.style.cssText = 'font-size: 12px; color: #666; margin-bottom: 4px; white-space: nowrap;'; progressInfoLabel.textContent = '加载中...'; progressContainer.appendChild(progressInfoLabel); this.progressInfoLabel = progressInfoLabel; // 创建进度条容器,包含 canvas 和 input const progressWrapper = document.createElement('div'); progressWrapper.className = 'progress-wrapper'; // 创建 canvas 用于绘制时间片段 this.progressCanvas = document.createElement('canvas'); this.progressCanvas.className = 'progress-canvas'; progressWrapper.appendChild(this.progressCanvas); this.progressInput = document.createElement('input'); this.progressInput.type = 'range'; this.progressInput.min = '0'; this.progressInput.max = '100'; // 设置初始最大值为100 this.progressInput.value = '0'; this.progressInput.setAttribute('aria-label', '视频进度条'); this.progressInput.oninput = this.handleSeek.bind(this); progressWrapper.appendChild(this.progressInput); progressContainer.appendChild(progressWrapper); this.timeDisplay = document.createElement('span'); // 确保初始时间显示正确 const initialTimeText = `${this.formatTime(0)} / ${this.formatTime(0)}`; console.log(`[HLSPlayer] 初始时间显示: ${initialTimeText}`); this.timeDisplay.textContent = initialTimeText; progressContainer.appendChild(this.timeDisplay); this.controlsElement.appendChild(progressContainer); // 初始化时间片段显示 if (this.options.timeRangeMode) { this.updateTimeRangesDisplay(); } } if (this.options.showPlaybackRate) { const rateContainer = document.createElement('div'); rateContainer.className = 'hls-player-rate'; this.options.playbackRates?.forEach(rate => { const button = document.createElement('button'); button.textContent = `${rate}x`; button.onclick = () => this.setPlaybackRate(rate); button.className = rate === 1 ? 'rate-active' : ''; this.rateButtons.push(button); rateContainer.appendChild(button); }); this.controlsElement.appendChild(rateContainer); } if (this.options.debug?.enabled) { this.createDebugUI(); } this.startProgressUpdate(); } private createDebugUI(): void { this.debugContainer = document.createElement('div'); this.debugContainer.className = 'hls-player-debug'; // Create timeline component using Vue render function const timelineVNode = h(Timeline, { timeRanges: this.options.timeRanges, showOriginalTimeline: this.options.debug?.showTimeRanges, showMediaTimeline: this.options.debug?.showMediaTimeline, height: 20, onTimeUpdate: (time: number) => { this.seek(time); }, onSeek: (time: number) => { this.seek(time); } }); // Create a container for the timeline const timelineContainer = document.createElement('div'); render(timelineVNode, timelineContainer); this.debugContainer.appendChild(timelineContainer); const container = this.videoElement.closest('.hls-player'); container?.appendChild(this.debugContainer); } private updateTimeRangesDisplay(): void { if (!this.progressCanvas || !this.options.timeRanges) return; const ctx = this.progressCanvas.getContext('2d'); if (!ctx) return; // 设置 canvas 尺寸 const canvas = this.progressCanvas; canvas.width = canvas.offsetWidth * window.devicePixelRatio; canvas.height = canvas.offsetHeight * window.devicePixelRatio; // 清除画布 ctx.clearRect(0, 0, canvas.width, canvas.height); if (!this.options.timeRanges.length) return; // 计算总时间范围 const totalStart = Math.min(...this.options.timeRanges.map(r => r.start)); const totalEnd = Math.max(...this.options.timeRanges.map(r => r.end)); const totalDuration = totalEnd - totalStart; // 绘制时间片段 ctx.fillStyle = 'rgba(0, 160, 255, 0.3)'; this.options.timeRanges.forEach(range => { const startX = ((range.start - totalStart) / totalDuration) * canvas.width; const width = ((range.end - range.start) / totalDuration) * canvas.width; ctx.fillRect(startX, 0, width, canvas.height); }); } private calculateSeekTime(inputValue: number): { time: number; url: string; } { if (!this.options.timeRangeMode || !this.options.timeRanges?.length) { return { time: inputValue, url: '' }; } const totalStart = Math.min(...this.options.timeRanges.map(r => r.start)); const totalEnd = Math.max(...this.options.timeRanges.map(r => r.end)); const targetTime = totalStart + (inputValue / 100) * (totalEnd - totalStart); const range = this.findTimeRangeForTime(targetTime); if (!range) { // 如果没找到对应的时间片段,找最近的一个 const nextRange = this.options.timeRanges.find(r => r.start > targetTime); const prevRange = [...this.options.timeRanges].reverse().find(r => r.end < targetTime); const targetRange = nextRange || prevRange; if (targetRange) { return { time: nextRange ? nextRange.start : prevRange!.end, url: targetRange.url }; } return { time: targetTime, url: '' }; } return { time: targetTime, url: range.url }; } private handleSeek(e: Event): void { const target = e.target as HTMLInputElement; const value = Number(target.value); const duration = this.demuxer.getTotalDuration(); if (this.options.timeRangeMode) { const { time, url } = this.calculateSeekTime(value); if (url && url !== this.currentTimeRange?.url) { this.load(url).then(() => this.seek(time)); } else { this.seek(time); } } else { // 将百分比转换为实际时间 const seekTime = (value / 100) * duration; console.log(`[HLSPlayer] 跳转: value=${value}%, duration=${duration}秒, seekTime=${seekTime}秒`); this.seek(seekTime); } } private updateProgress(): void { if (!this.options.showProgress) return; // 直接从视频元素获取当前时间,确保时间显示最新状态 const currentTime = this.videoElement.currentTime; const rawDuration = this.demuxer.getTotalDuration(); const demuxerTime = this.demuxer.getCurrentTime(); // 记录当前时间和总时长,用于调试 const now = Date.now(); // 确保至少间隔 5 秒才输出一次日志 if (now - HLSPlayer.lastProgressLogTime >= 5000) { console.log(`[HLSPlayer] 更新进度: currentTime=${currentTime.toFixed(2)}秒, totalDuration=${rawDuration.toFixed(2)}秒, demuxerTime=${demuxerTime.toFixed(2)}秒`); HLSPlayer.lastProgressLogTime = now; } // 更新进度信息标签 if (this.progressInfoLabel) { // 获取缓冲区信息 let bufferInfo = ''; if (this.videoElement.buffered.length > 0) { for (let i = 0; i < this.videoElement.buffered.length; i++) { const start = this.videoElement.buffered.start(i); const end = this.videoElement.buffered.end(i); bufferInfo += `缓冲区 #${i}: ${start.toFixed(1)}-${end.toFixed(1)}秒 `; } } else { bufferInfo = '无缓冲区'; } // 计算进度百分比 const validDuration = isNaN(rawDuration) || rawDuration <= 0 || !isFinite(rawDuration) ? 100 : rawDuration; const progress = (currentTime / validDuration) * 100; // 更新标签内容 this.progressInfoLabel.textContent = `当前时间: ${currentTime.toFixed(2)}秒 | ` + `Demuxer时间: ${demuxerTime.toFixed(2)}秒 | ` + `总时长: ${rawDuration.toFixed(2)}秒 | ` + `进度: ${progress.toFixed(1)}% | ` + `${bufferInfo} | ` + `播放状态: ${this.isPlaying() ? '播放中' : '已暂停'} | ` + `ReadyState: ${this.videoElement.readyState}`; } // Make sure we have valid duration to prevent NaN calculations const validDuration = isNaN(rawDuration) || rawDuration <= 0 || !isFinite(rawDuration) ? 100 : rawDuration; if (this.options.timeRangeMode && this.options.timeRanges?.length) { const totalStart = Math.min(...this.options.timeRanges.map(r => r.start)); const totalEnd = Math.max(...this.options.timeRanges.map(r => r.end)); const range = this.findTimeRangeForTime(currentTime); if (range) { this.currentTimeRange = range; if (this.progressInput) { this.progressInput.value = String(((currentTime - totalStart) / (totalEnd - totalStart)) * 100); this.progressInput.max = '100'; } if (this.timeDisplay) { // 确保传递给 formatTime 的是有效数字 const adjustedCurrentTime = Number(currentTime - totalStart); const adjustedTotalTime = Number(totalEnd - totalStart); console.log(`[HLSPlayer] 时间范围模式时间: adjustedCurrentTime=${adjustedCurrentTime}秒, adjustedTotalTime=${adjustedTotalTime}秒`); this.timeDisplay.textContent = `${this.formatTime(adjustedCurrentTime)} / ${this.formatTime(adjustedTotalTime)}`; } } // Update progress display for time ranges mode this.updateProgressDisplay(); } else { if (this.progressInput) { // 计算进度百分比 const progress = (currentTime / validDuration) * 100; this.progressInput.max = '100'; this.progressInput.value = String(Math.min(100, Math.max(0, progress))); } if (this.timeDisplay) { // 使用视频元素的当前时间和 demuxer 提供的总时长,确保显示正确的时间 // 确保传递给 formatTime 的是有效数字 const numCurrentTime = Number(currentTime); const numValidDuration = Number(validDuration); console.log(`[HLSPlayer] 标准模式时间: numCurrentTime=${numCurrentTime}秒, numValidDuration=${numValidDuration}秒`); const formattedCurrentTime = this.formatTime(numCurrentTime); const formattedTotalTime = this.formatTime(numValidDuration); const timeDisplayText = `${formattedCurrentTime} / ${formattedTotalTime}`; console.log(`[HLSPlayer] 时间显示: ${timeDisplayText}`); this.timeDisplay.textContent = timeDisplayText; } // Update progress display for normal mode this.updateProgressDisplay(); } if (this.playButton) { this.playButton.textContent = this.isPlaying() ? '暂停' : '播放'; } // 更新调试显示 if (this.options.debug?.enabled) { this.updateDebugDisplay(); } } private formatTime(seconds: number): string { // 添加调试日志,查看传入的秒数 console.log(`[HLSPlayer] formatTime 输入: ${seconds}秒, 类型: ${typeof seconds}, isNaN: ${isNaN(seconds)}, isFinite: ${isFinite(seconds)}`); // 确保 seconds 是有效的数字 if (isNaN(seconds) || !isFinite(seconds) || seconds < 0) { console.log(`[HLSPlayer] formatTime 检测到无效时间,重置为0`); seconds = 0; } // 确保 seconds 是数字类型 seconds = Number(seconds); const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); let formattedTime; if (hours > 0) { formattedTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } else { formattedTime = `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } // 添加调试日志,查看格式化后的时间 console.log(`[HLSPlayer] formatTime 输出: ${formattedTime}, 原始秒数: ${seconds}秒`); return formattedTime; } private async togglePlay(): Promise<void> { if (this.isPlaying()) { this.pause(); } else { await this.play(); } } private startProgressUpdate(): void { this.updateProgressInterval = window.setInterval(() => { this.updateProgress(); if (this.options.debug?.enabled) { this.updateDebugDisplay(); } }, 1000 / 30); // 更新频率提高到 30fps } private stopProgressUpdate(): void { if (this.updateProgressInterval) { clearInterval(this.updateProgressInterval); } } public async load(url: string): Promise<void> { this.log(`加载HLS地址: ${url}`, 'info'); try { // 销毁之前的实例(如果有) if (this.demuxer) { this.demuxer.destroy(); // 重新创建demuxer实例 this.demuxer = new HLSv7Demuxer(this.videoElement, { logLevel: (this.options.debug?.enabled ? 'debug' : 'info'), slidingWindow: { enabled: true, forward: 2, backward: 1 } }); // 重新设置事件监听器 this.setupDemuxerListeners(); } // 显示加载指示器 if (this.loadingIndicator) { this.loadingIndicator.style.display = 'block'; } // 初始化解复用器 await this.demuxer.init(url); // 初始化进度条 if (this.options.showProgress) { this.updateProgressDisplay(); } // 更新调试UI(如果启用) if (this.options.debug?.enabled) { this.updateDebugDisplay(); } // 隐藏加载指示器 if (this.loadingIndicator) { this.loadingIndicator.style.display = 'none'; } } catch (error) { console.error('加载媒体失败:', error); // 隐藏加载指示器 if (this.loadingIndicator) { this.loadingIndicator.style.display = 'none'; } throw error; } } public async play(): Promise<void> { this.log(`开始播放`, 'info'); try { // 让demuxer处理播放逻辑 await this.demuxer.play(); // 开始进度更新 this.startProgressUpdate(); // 更新播放按钮状态 if (this.playButton) { this.playButton.textContent = '暂停'; } } catch (error) { console.error(`播放失败:`, error); // 尝试直接播放 try { await this.videoElement.play(); this.startProgressUpdate(); if (this.playButton) { this.playButton.textContent = '暂停'; } } catch (innerError) { console.error(`直接播放也失败:`, innerError); } } } public pause(): void { // 使用demuxer的pause方法 this.demuxer.pause(); this.stopProgressUpdate(); // 更新播放按钮状态 if (this.playButton) { this.playButton.textContent = '播放'; } this.updateProgress(); } /** * 跳转到指定时间 */ public seek(time: number): void { if (!this.demuxer) return; const startTime = performance.now(); this.log(`尝试跳转: ${this.videoElement.currentTime.toFixed(2)}s → ${time.toFixed(2)}s`, 'info'); // 如果使用时间范围模式,则需要转换时间 if (this.options.timeRangeMode && this.options.timeRanges?.length) { // 查找时间所在的范围 const range = this.findTimeRangeForTime(time); if (range && this.currentTimeRange !== range) { // 如果跳转到了不同的时间范围,需要切换范围 this.switchToTimeRange(range); return; } // 转换为媒体时间 const mediaTime = this.originalToMediaTime(time); // 使用demuxer进行跳转 this.demuxer.seek(mediaTime); } else { // 普通模式,直接跳转 this.demuxer.seek(time); } // 强制更新进度显示 this.forceUpdateProgress(); const seekDuration = performance.now() - startTime; this.log(`跳转完成,耗时 ${seekDuration.toFixed(2)}ms`, 'debug'); } /** * 获取当前播放时间 */ public getCurrentTime(): number { if (!this.demuxer) return 0; const time = this.demuxer.getCurrentTime(); // 如果使用时间范围模式,则需要转换时间 if (this.options.timeRangeMode && this.options.timeRanges?.length) { return this.mediaToOriginalTime(time); } return time; } /** * 获取总时长 */ public getDuration(): number { if (!this.demuxer) return 0; // 如果使用时间范围模式,返回所有范围的总时长 if (this.options.timeRangeMode && this.options.timeRanges?.length) { return this.options.timeRanges.reduce((total, range) => total + (range.end - range.start), 0); } return this.demuxer.getTotalDuration(); } /** * 设置播放速率 */ public setPlaybackRate(rate: number): void { if (!this.demuxer) return; this.demuxer.setPlaybackRate(rate); this.videoElement.playbackRate = rate; // 更新播放速率按钮状态 this.rateButtons.forEach(button => { button.classList.toggle('active', parseFloat(button.dataset.rate || '1') === rate); }); } /** * 销毁播放器实例 */ public destroy(): void { // 停止进度更新 this.stopProgressUpdate(); // 销毁demuxer if (this.demuxer) { this.demuxer.destroy(); } // 移除事件监听器 this.videoElement.removeEventListener('timeupdate', this.handleTimeUpdate.bind(this)); // 清理UI元素 if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } } public getPlaybackRate(): number { return this.videoElement.playbackRate; } public getOptions(): HLSPlayerOptions { return this.options; } public setTimeRanges(ranges: TimeRange[]): void { this.options.timeRanges = ranges; this.options.timeRangeMode = true; this.initializeTimeRanges(ranges); if (this.options.autoGenerateUI) { this.updateTimeRangesDisplay(); if (this.options.debug?.enabled) { // 重新创建 debug UI 以更新时间轴 if (this.debugContainer) { const container = this.videoElement.closest('.hls-player'); container?.removeChild(this.debugContainer); } this.createDebugUI(); } } } private updateProgressDisplay(): void { if (!this.progressCanvas) return; const ctx = this.progressCanvas.getContext('2d'); if (!ctx) return; const canvas = this.progressCanvas; // 设置 canvas 尺寸 canvas.width = canvas.offsetWidth * window.devicePixelRatio; canvas.height = canvas.offsetHeight * window.devicePixelRatio; // 清除画布 ctx.clearRect(0, 0, canvas.width, canvas.height); if (this.options.timeRangeMode && this.options.timeRanges?.length) { // 计算总媒体时长 const totalDuration = this.options.timeRanges.reduce( (acc, range) => acc + range.mediaDuration, 0 ); // 绘制时间片段 ctx.fillStyle = 'rgba(0, 160, 255, 0.3)'; this.options.timeRanges.forEach(range => { const startX = (range.mediaStart / totalDuration) * canvas.width; const width = (range.mediaDuration / totalDuration) * canvas.width; ctx.fillRect(startX, 0, width, canvas.height); }); // 绘制当前位置 if (this.currentMediaTime > 0) { ctx.fillStyle = '#18a058'; const x = (this.currentMediaTime / totalDuration) * canvas.width; ctx.fillRect(x - 1, 0, 2, canvas.height); } } else { // 直接从视频元素获取当前时间,确保时间轴显示最新状态 const currentTime = this.videoElement.currentTime; // 从 demuxer 获取总时长 const duration = this.demuxer.getTotalDuration(); const validDuration = isNaN(duration) || duration <= 0 || !isFinite(duration) ? 100 : duration; // 静态属性,用于记录上次时间轴渲染日志输出时间 if (!HLSPlayer.lastTimelineLogTime) { HLSPlayer.lastTimelineLogTime = 0; } const now = Date.now(); // 确保至少间隔 10 秒才输出一次日志 if (now - HLSPlayer.lastTimelineLogTime >= 10000) { console.log(`[HLSPlayer] 渲染时间轴: currentTime=${currentTime.toFixed(2)}秒, duration=${validDuration.toFixed(2)}秒, videoCurrentTime=${this.videoElement.currentTime.toFixed(2)}秒`); HLSPlayer.lastTimelineLogTime = now; } // 绘制背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制已缓冲区域 if (this.videoElement.buffered.length > 0) { ctx.fillStyle = 'rgba(0, 160, 255, 0.3)'; for (let i = 0; i < this.videoElement.buffered.length; i++) { const start = this.videoElement.buffered.start(i); const end = this.videoElement.buffered.end(i); const startX = (start / validDuration) * canvas.width; const width = ((end - start) / validDuration) * canvas.width; ctx.fillRect(startX, 0, width, canvas.height); } } // 绘制已播放区域 if (currentTime > 0) { ctx.fillStyle = 'rgba(24, 160, 88, 0.5)'; const playedWidth = (currentTime / validDuration) * canvas.width; ctx.fillRect(0, 0, playedWidth, canvas.height); // 绘制当前播放位置指示器 ctx.fillStyle = '#18a058'; ctx.fillRect(playedWidth - 1, 0, 2, canvas.height); } } } private updateDebugDisplay(): void { if (!this.options.debug?.enabled) return; if (this.options.debug?.showTimeRanges) { this.updateTimelineDebugCanvas(); } if (this.options.debug?.showMediaTimeline) { this.updateMediaDebugCanvas(); } } private updateTimelineDebugCanvas(): void { if (!this.debugTimelineCanvas || !this.options.timeRanges?.length) return; const canvas = this.debugTimelineCanvas; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size canvas.width = canvas.offsetWidth * window.devicePixelRatio; canvas.height = canvas.offsetHeight * window.devicePixelRatio; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Calculate total range const totalStart = Math.min(...this.options.timeRanges.map(r => r.start)); const totalEnd = Math.max(...this.options.timeRanges.map(r => r.end)); const totalDuration = totalEnd - totalStart; // Draw gaps ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; for (let i = 0; i < this.options.timeRanges.length - 1; i++) { const gapStart = this.options.timeRanges[i].end; const gapEnd = this.options.timeRanges[i + 1].start; const startX = ((gapStart - totalStart) / totalDuration) * canvas.width; const width = ((gapEnd - gapStart) / totalDuration) * canvas.width; ctx.fillRect(startX, 0, width, canvas.height); } // Draw segments ctx.fillStyle = 'rgba(0, 160, 255, 0.3)'; this.options.timeRanges.forEach(range => { const startX = ((range.start - totalStart) / totalDuration) * canvas.width; const width = ((range.end - range.start) / totalDuration) * canvas.width; ctx.fillRect(startX, 0, width, canvas.height); }); // Draw current position const currentTime = this.getCurrentTime(); ctx.fillStyle = '#18a058'; const x = ((currentTime - totalStart) / totalDuration) * canvas.width; ctx.fillRect(x - 1, 0, 2, canvas.height); } private updateMediaDebugCanvas(): void { if (!this.debugMediaCanvas || !this.options.timeRanges?.length) return; const canvas = this.debugMediaCanvas; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size canvas.width = canvas.offsetWidth * window.devicePixelRatio; canvas.height = canvas.offsetHeight * window.devicePixelRatio; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); const totalDuration = this.getDuration(); // Draw segments ctx.fillStyle = 'rgba(0, 160, 255, 0.3)'; this.options.timeRanges.forEach(range => { const startX = (range.mediaStart / totalDuration) * canvas.width; const width = (range.mediaDuration / totalDuration) * canvas.width; ctx.fillRect(startX, 0, width, canvas.height); }); // Draw current position const mediaTime = this.videoElement.currentTime; ctx.fillStyle = '#18a058'; const x = (mediaTime / totalDuration) * canvas.width; ctx.fillRect(x - 1, 0, 2, canvas.height); } // 添加一个方法,强制更新时间轴 public forceUpdateProgress(): void { // 立即更新进度显示 this.updateProgress(); // 确保时间轴渲染使用最新的时间值 if (this.videoElement) { // 触发 timeupdate 事件,确保 UI 更新 this.videoElement.dispatchEvent(new Event('timeupdate')); // 额外更新进度信息标签,添加强制更新标记 if (this.progressInfoLabel) { const currentText = this.progressInfoLabel.textContent || ''; this.progressInfoLabel.textContent = `${currentText} | [强制更新]`; // 短暂改变标签颜色以突出显示更新 const originalColor = this.progressInfoLabel.style.color; this.progressInfoLabel.style.color = '#ff5500'; setTimeout(() => { if (this.progressInfoLabel) { this.progressInfoLabel.style.color = originalColor; } }, 1000); } // 直接更新时间显示,确保显示正确 if (this.timeDisplay) { const currentTime = Number(this.videoElement.currentTime); const duration = Number(this.demuxer.getTotalDuration()); const validDuration = isNaN(duration) || duration <= 0 || !isFinite(duration) ? 100 : duration; const formattedCurrentTime = this.formatTime(currentTime); const formattedTotalTime = this.formatTime(validDuration); const timeDisplayText = `${formattedCurrentTime} / ${formattedTotalTime}`; console.log(`[HLSPlayer] 强制更新时间显示: ${timeDisplayText}`); this.timeDisplay.textContent = timeDisplayText; } } // 记录强制更新日志 console.log(`[HLSPlayer] 强制更新进度: currentTime=${this.videoElement.currentTime.toFixed(2)}秒, demuxerTime=${this.demuxer.getCurrentTime().toFixed(2)}秒`); } /** * 记录日志信息 */ private log(message: string, level: 'info' | 'debug' | 'error' = 'info'): void { const prefix = '[HLSPlayer]'; if (level === 'error') { console.error(`${prefix} ${message}`); } else if (level === 'info') { console.log(`${prefix} ${message}`); } else if (level === 'debug' && this.options.debug?.enabled) { console.debug(`${prefix} ${message}`); } } /** * 配置滑动窗口参数 */ public setSlidingWindowConfig(config: Partial<SlidingWindowConfig>): void { if (!this.demuxer) return; this.demuxer.setSlidingWindowConfig(config); this.log(`已更新滑动窗口配置: ${JSON.stringify(config)}`, 'debug'); } /** * 获取当前滑动窗口配置 */ public getSlidingWindowConfig(): SlidingWindowConfig | undefined { if (!this.demuxer) return undefined; return this.demuxer.getSlidingWindowConfig(); } /** * 获取视频片段总数 */ public getTotalSegments(): number { if (!this.demuxer) return 0; return this.demuxer.getTotalSegments(); } /** * 检查播放器是否正在播放 */ public isPlaying(): boolean { return !this.videoElement.paused; } }