UNPKG

jessibuca

Version:
672 lines (564 loc) 20.2 kB
import { EventBus, Events } from "./EventBus"; const EXTINFRegex = /#EXTINF:(\d+\.\d+),(.*?),(.*?)\s*$/; export interface MediaSegment { index: number; // 片段索引 url: string; // 片段URL duration: number; // 片段实际持续时间 virtualStartTime: number; // 虚拟时间轴上的开始时间 virtualEndTime: number; // 虚拟时间轴上的结束时间 physicalStartTime: number; // 物理时间轴上的开始时间 physicalEndTime: number; // 物理时间轴上的结束时间 physicalTime: Date | null; // 物理时间(从EXTINF中解析) codec: string | null; // 编解码器信息(从EXTINF中解析) isLoaded: boolean; // 片段是否已加载 isLoading: boolean; // 片段是否正在加载 isBuffered: boolean; // 片段是否已缓冲 } export interface BufferRange { start: number; end: number; } export interface PlaylistInfo { segments: MediaSegment[]; totalDuration: number; } /** * VirtualTimeline - 虚拟时间轴管理器 * * 负责管理媒体片段在虚拟时间轴上的位置和状态 * 提供虚拟时间和物理时间之间的转换 * 通过事件总线与其他组件通信,不再直接依赖视频元素 */ export class VirtualTimeline { private eventBus: EventBus; private segments: MediaSegment[] = []; private currentSegmentIndex: number = -1; private virtualTotalDuration: number = 0; private isPlaying: boolean = false; // 时间状态 private currentVirtualTime: number = 0; private currentPhysicalTime: number = 0; // 缓冲区状态 private bufferedRanges: BufferRange[] = []; // 配置参数 private loadingDelay: number = 500; // 模拟加载延迟(毫秒) private playbackRate: number = 1; private playbackTimer: number | null = null; /** * 构造函数 * @param eventBus 事件总线 */ constructor(eventBus: EventBus) { this.eventBus = eventBus; this.setupEventHandlers(); } /** * 设置事件监听器 */ private setupEventHandlers(): void { // 监听视频时间更新事件 this.eventBus.on(Events.VIDEO_TIME_UPDATE, this.handleVideoTimeUpdate); // 监听视频播放控制事件 this.eventBus.on(Events.VIDEO_PLAY, this.handleVideoPlay); this.eventBus.on(Events.VIDEO_PAUSE, this.handleVideoPause); // 监听视频跳转事件 this.eventBus.on(Events.VIDEO_SEEKING, this.handleVideoSeeking); this.eventBus.on(Events.VIDEO_SEEKED, this.handleVideoSeeked); // 监听当前时间请求 this.eventBus.on(Events.CURRENT_TIME_REQUEST, this.handleCurrentTimeRequest); this.eventBus.on(Events.TOTAL_DURATION_REQUEST, this.handleTotalDurationRequest); } /** * 移除事件监听器 */ private removeEventHandlers(): void { this.eventBus.off(Events.VIDEO_TIME_UPDATE, this.handleVideoTimeUpdate); this.eventBus.off(Events.VIDEO_PLAY, this.handleVideoPlay); this.eventBus.off(Events.VIDEO_PAUSE, this.handleVideoPause); this.eventBus.off(Events.VIDEO_SEEKING, this.handleVideoSeeking); this.eventBus.off(Events.VIDEO_SEEKED, this.handleVideoSeeked); this.eventBus.off(Events.CURRENT_TIME_REQUEST, this.handleCurrentTimeRequest); this.eventBus.off(Events.TOTAL_DURATION_REQUEST, this.handleTotalDurationRequest); } /** * 加载播放列表 * @param playlist 播放列表信息 */ public loadPlaylist(playlist: PlaylistInfo): void { this.segments = playlist.segments; this.virtualTotalDuration = playlist.totalDuration; // 重置状态 this.currentSegmentIndex = -1; this.currentVirtualTime = 0; this.currentPhysicalTime = 0; this.bufferedRanges = []; // 发送播放列表加载完成事件 this.eventBus.emit(Events.LOG, `VirtualTimeline loaded playlist with ${this.segments.length} segments, total duration: ${this.virtualTotalDuration}s`, 'info'); // 更新缓冲区范围 this.updateBufferRanges(); // 通知时间轴更新 this.emitTimeUpdate(); } /** * 从M3U8内容创建播放列表 * @param m3u8Content M3U8文件内容 * @param baseUrl 基础URL * @returns 播放列表信息 */ public createPlaylistFromM3U8(m3u8Content: string, baseUrl: string): PlaylistInfo { const lines = m3u8Content.split("\n"); const segments: MediaSegment[] = []; let totalDuration = 0; let segmentIndex = 0; let segmentDuration = 0; let segmentTime: Date | null = null; let segmentCodec: string | null = null; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // 解析EXTINF行 if (line.startsWith("#EXTINF:")) { const match = line.match(EXTINFRegex); if (match) { segmentDuration = parseFloat(match[1]); const timeString = match[2] ? match[2].trim() : ""; const codecString = match[3] ? match[3].trim() : ""; // 解析物理时间 try { if (timeString) { segmentTime = new Date(timeString); } else { segmentTime = null; } } catch (e) { segmentTime = null; } // 解析编解码器信息 segmentCodec = codecString || null; this.eventBus.emit(Events.LOG, `解析EXTINF: 时长=${segmentDuration}秒, 物理时间=${segmentTime}, 编解码器=${segmentCodec}`, 'debug'); } } // 处理片段URL行 else if (!line.startsWith("#") && line !== "") { const url = this.resolveUrl(baseUrl, line); const virtualStartTime = totalDuration; const virtualEndTime = totalDuration + segmentDuration; const physicalStartTime = totalDuration; const physicalEndTime = totalDuration + segmentDuration; segments.push({ index: segmentIndex, url, duration: segmentDuration, virtualStartTime, virtualEndTime, physicalStartTime, physicalEndTime, physicalTime: segmentTime, codec: segmentCodec, isLoaded: false, isLoading: false, isBuffered: false }); totalDuration += segmentDuration; segmentIndex++; segmentTime = null; segmentCodec = null; } } return { segments, totalDuration }; } /** * 解析URL * @param baseUrl 基础URL * @param relativeUrl 相对URL * @returns 完整URL */ private resolveUrl(baseUrl: string, relativeUrl: string): string { if (relativeUrl.startsWith("http://") || relativeUrl.startsWith("https://")) { return relativeUrl; } // 删除baseUrl末尾的斜杠 const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; // 删除relativeUrl开头的斜杠 const relative = relativeUrl.startsWith("/") ? relativeUrl.slice(1) : relativeUrl; return `${base}/${relative}`; } /** * 查找指定虚拟时间对应的片段 * @param virtualTime 虚拟时间 * @returns 片段和片段内偏移 */ public findSegmentForVirtualTime(virtualTime: number): { segment: MediaSegment | null, offsetInSegment: number; } { if (this.segments.length === 0) { return { segment: null, offsetInSegment: 0 }; } // 如果时间小于0,返回第一个片段 if (virtualTime <= 0) { return { segment: this.segments[0], offsetInSegment: 0 }; } // 如果时间大于总时长,返回最后一个片段 if (virtualTime >= this.virtualTotalDuration) { const lastSegment = this.segments[this.segments.length - 1]; return { segment: lastSegment, offsetInSegment: lastSegment.duration }; } // 查找包含该虚拟时间的片段 for (const segment of this.segments) { if (virtualTime >= segment.virtualStartTime && virtualTime < segment.virtualEndTime) { return { segment, offsetInSegment: virtualTime - segment.virtualStartTime }; } } return { segment: null, offsetInSegment: 0 }; } /** * 虚拟时间转物理时间 * @param virtualTime 虚拟时间 * @returns 物理时间 */ public virtualToPhysicalTime(virtualTime: number): number { // 在不使用物理时间轴的情况下,虚拟时间等于物理时间 return virtualTime; } /** * 物理时间转虚拟时间 * @param physicalTime 物理时间 * @returns 虚拟时间 */ public physicalToVirtualTime(physicalTime: number): number { // 在不使用物理时间轴的情况下,物理时间等于虚拟时间 return physicalTime; } /** * 获取当前虚拟时间 * @returns 当前虚拟时间 */ public getCurrentVirtualTime(): number { return this.currentVirtualTime; } /** * 获取总时长 * @returns 总时长 */ public getTotalDuration(): number { return this.virtualTotalDuration; } /** * 获取当前播放进度(0-1) * @returns 当前播放进度 */ public getProgress(): number { if (this.virtualTotalDuration <= 0) { return 0; } return this.currentVirtualTime / this.virtualTotalDuration; } /** * 开始播放 */ public play(): void { if (this.isPlaying) return; this.isPlaying = true; this.startPlaybackTimer(); // 通知播放状态变化 this.eventBus.emit(Events.PLAYBACK_STATE_CHANGE, this.isPlaying); this.eventBus.emit(Events.LOG, 'VirtualTimeline playback started', 'debug'); } /** * 暂停播放 */ public pause(): void { if (!this.isPlaying) return; this.isPlaying = false; this.stopPlaybackTimer(); // 通知播放状态变化 this.eventBus.emit(Events.PLAYBACK_STATE_CHANGE, this.isPlaying); this.eventBus.emit(Events.LOG, 'VirtualTimeline playback paused', 'debug'); } /** * 跳转到指定时间 * @param virtualTime 虚拟时间 */ public seek(virtualTime: number): void { // 边界检查 if (virtualTime < 0) { virtualTime = 0; } else if (virtualTime > this.virtualTotalDuration) { virtualTime = this.virtualTotalDuration; } const prevTime = this.currentVirtualTime; this.currentVirtualTime = virtualTime; this.currentPhysicalTime = this.virtualToPhysicalTime(virtualTime); // 查找对应片段 const { segment } = this.findSegmentForVirtualTime(virtualTime); if (segment) { this.currentSegmentIndex = segment.index; } // 触发seeking事件 this.eventBus.emit(Events.TIMELINE_SEEK, virtualTime, this.currentPhysicalTime); this.eventBus.emit(Events.LOG, `VirtualTimeline seeking from ${prevTime}s to ${virtualTime}s`, 'debug'); // 如果跳转到未加载的片段,则加载该片段及其周围的片段 this.loadSegmentsForTime(virtualTime); // 触发seeked事件 this.eventBus.emit(Events.TIMELINE_SEEKED, virtualTime, this.currentPhysicalTime); // 触发时间更新事件 this.emitTimeUpdate(); } /** * 加载指定时间点所需的片段 * @param virtualTime 虚拟时间 */ private async loadSegmentsForTime(virtualTime: number): Promise<void> { const { segment } = this.findSegmentForVirtualTime(virtualTime); if (!segment) return; // 加载当前片段 if (!segment.isLoaded && !segment.isLoading) { await this.loadSegmentByIndex(segment.index); } // 预加载下一个片段 this.preloadNextSegment(); } /** * 通过索引加载片段 * @param index 片段索引 */ private async loadSegmentByIndex(index: number): Promise<void> { if (index < 0 || index >= this.segments.length) return; const segment = this.segments[index]; // 如果已经加载或正在加载,则跳过 if (segment.isLoaded || segment.isLoading) return; // 标记为正在加载 segment.isLoading = true; this.segments[index] = { ...segment }; // 触发加载开始事件 this.eventBus.emit(Events.SEGMENT_LOAD_START, segment); this.eventBus.emit(Events.LOG, `Loading segment #${index}: ${segment.url}`, 'debug'); try { // 通知SegmentLoader加载该片段 this.eventBus.emit(Events.SEGMENT_LOAD_START, index); // 模拟加载延迟 await new Promise(resolve => setTimeout(resolve, this.loadingDelay)); // 标记为已加载 segment.isLoaded = true; segment.isLoading = false; this.segments[index] = { ...segment }; // 更新缓冲区状态 this.updateBufferRanges(); // 触发加载完成事件 this.eventBus.emit(Events.SEGMENT_LOADED, segment); this.eventBus.emit(Events.LOG, `Segment #${index} loaded successfully`, 'success'); } catch (error) { // 标记为加载失败 segment.isLoading = false; this.segments[index] = { ...segment }; // 触发加载错误事件 this.eventBus.emit(Events.SEGMENT_LOAD_ERROR, segment, error); this.eventBus.emit(Events.LOG, `Failed to load segment #${index}: ${error}`, 'error'); } } /** * 更新缓冲区范围 */ private updateBufferRanges(): void { const ranges: BufferRange[] = []; let currentStart: number | null = null; let currentEnd: number | null = null; // 根据已加载片段构建缓冲区范围 for (let i = 0; i < this.segments.length; i++) { const segment = this.segments[i]; if (segment.isLoaded) { // 标记为已缓冲 segment.isBuffered = true; this.segments[i] = { ...segment }; // 如果是新范围的开始 if (currentStart === null) { currentStart = segment.virtualStartTime; currentEnd = segment.virtualEndTime; } // 如果是现有范围的延续 else if (currentEnd === segment.virtualStartTime) { currentEnd = segment.virtualEndTime; } // 如果是不连续的新范围 else { // 保存之前的范围 if (currentStart !== null && currentEnd !== null) { ranges.push({ start: currentStart, end: currentEnd }); } // 开始新的范围 currentStart = segment.virtualStartTime; currentEnd = segment.virtualEndTime; } } } // 添加最后一个范围 if (currentStart !== null && currentEnd !== null) { ranges.push({ start: currentStart, end: currentEnd }); } this.bufferedRanges = ranges; // 触发缓冲区更新事件 this.eventBus.emit(Events.BUFFER_UPDATE, this.bufferedRanges); this.eventBus.emit(Events.BUFFER_UPDATE, this.bufferedRanges); } /** * 预加载下一个片段 */ private preloadNextSegment(): void { const nextIndex = this.currentSegmentIndex + 1; if (nextIndex < this.segments.length) { this.loadSegmentByIndex(nextIndex); } } /** * 启动播放定时器 */ private startPlaybackTimer(): void { if (this.playbackTimer !== null) { this.stopPlaybackTimer(); } const updateInterval = 100; // 100ms更新一次 let lastUpdateTime = Date.now(); this.playbackTimer = window.setInterval(() => { const now = Date.now(); const deltaMs = now - lastUpdateTime; lastUpdateTime = now; // 按播放速率更新虚拟时间 const deltaSeconds = (deltaMs / 1000) * this.playbackRate; this.currentVirtualTime += deltaSeconds; this.currentPhysicalTime = this.virtualToPhysicalTime(this.currentVirtualTime); // 检查是否超出总时长 if (this.currentVirtualTime >= this.virtualTotalDuration) { this.currentVirtualTime = this.virtualTotalDuration; this.pause(); } // 更新当前片段索引 const { segment } = this.findSegmentForVirtualTime(this.currentVirtualTime); if (segment) { this.currentSegmentIndex = segment.index; } // 预加载下一个片段 this.preloadNextSegment(); // 触发时间更新事件 this.emitTimeUpdate(); }, updateInterval); } /** * 停止播放定时器 */ private stopPlaybackTimer(): void { if (this.playbackTimer !== null) { clearInterval(this.playbackTimer); this.playbackTimer = null; } } /** * 设置播放速率 * @param rate 播放速率 */ public setPlaybackRate(rate: number): void { this.playbackRate = rate; // 通过事件总线通知播放速率变化 this.eventBus.emit(Events.PLAYBACK_RATE_CHANGE, rate); this.eventBus.emit(Events.LOG, `Playback rate set to ${rate}x`, 'debug'); // 如果正在播放,重新启动定时器以应用新的播放速率 if (this.isPlaying) { this.stopPlaybackTimer(); this.startPlaybackTimer(); } } /** * 发送时间更新事件 */ private emitTimeUpdate(): void { this.eventBus.emit(Events.TIMELINE_UPDATE, this.currentVirtualTime, this.currentPhysicalTime); this.eventBus.emit(Events.TIMELINE_UPDATE, this.currentVirtualTime); } /** * 处理视频时间更新事件 */ private handleVideoTimeUpdate = (currentTime: number): void => { // 将视频当前时间转换为虚拟时间 this.currentVirtualTime = this.physicalToVirtualTime(currentTime); // 如果虚拟播放时间不更新,则使用视频时间作为备用 if (this.currentVirtualTime === 0 && currentTime > 0) { this.currentVirtualTime = currentTime; } // 更新当前片段索引 const { segment } = this.findSegmentForVirtualTime(this.currentVirtualTime); if (segment) { this.currentSegmentIndex = segment.index; } // 触发时间更新事件,通知其他组件 this.emitTimeUpdate(); } /** * 处理视频播放事件 */ private handleVideoPlay = (): void => { this.isPlaying = true; // 停止内部播放定时器,依赖视频元素的时间更新 this.stopPlaybackTimer(); // 通知播放状态变化 this.eventBus.emit(Events.PLAYBACK_STATE_CHANGE, this.isPlaying); } /** * 处理视频暂停事件 */ private handleVideoPause = (): void => { this.isPlaying = false; // 通知播放状态变化 this.eventBus.emit(Events.PLAYBACK_STATE_CHANGE, this.isPlaying); } /** * 处理视频seeking事件 */ private handleVideoSeeking = (time: number): void => { // 将物理时间转换为虚拟时间 const virtualTime = this.physicalToVirtualTime(time); // 触发seeking事件 this.eventBus.emit(Events.TIMELINE_SEEK, virtualTime, time); // 加载所需片段 this.loadSegmentsForTime(virtualTime); } /** * 处理视频seeked事件 */ private handleVideoSeeked = (time: number): void => { // 将物理时间转换为虚拟时间 const virtualTime = this.physicalToVirtualTime(time); // 更新当前时间 this.currentVirtualTime = virtualTime; this.currentPhysicalTime = time; // 触发seeked事件 this.eventBus.emit(Events.TIMELINE_SEEKED, virtualTime, time); // 更新时间 this.emitTimeUpdate(); } /** * 处理当前时间请求 */ private handleCurrentTimeRequest = (): number => { return this.currentVirtualTime; } /** * 处理总时长请求 */ private handleTotalDurationRequest = (): number => { return this.virtualTotalDuration; } /** * 销毁时间轴,清理资源 */ public destroy(): void { this.stopPlaybackTimer(); this.removeEventHandlers(); // 重置状态 this.segments = []; this.currentSegmentIndex = -1; this.virtualTotalDuration = 0; this.isPlaying = false; this.currentVirtualTime = 0; this.currentPhysicalTime = 0; this.bufferedRanges = []; } }