UNPKG

jessibuca

Version:
630 lines (535 loc) 18.9 kB
import { MediaSegment } from './VirtualTimeline'; import { EventBus, Events } from './EventBus'; /** * 滑动窗口配置 */ export interface SlidingWindowConfig { forward: number; // 预加载前面的片段数 backward: number; // 保留后面的片段数 enabled: boolean; // 是否启用滑动窗口 } /** * 缓冲区范围 */ export interface BufferRange { start: number; end: number; } /** * SegmentLoader - 片段加载器 * * 负责媒体片段的加载、缓冲和管理 * 通过事件总线与其他组件通信,不再直接依赖MediaSource和视频元素 */ export class SegmentLoader { private eventBus: EventBus; // 滑动窗口配置 private slidingWindowConfig: SlidingWindowConfig = { forward: 3, // 预加载3个片段 backward: 2, // 保留2个已播放片段 enabled: true // 默认启用滑动窗口 }; // 片段状态 private segments: MediaSegment[] = []; private currentSegmentIndex: number = -1; private segmentQueue: number[] = []; private pendingSegmentLoads: number[] = []; private isProcessingQueue: boolean = false; // 媒体资源状态 private isMediaSourceReady: boolean = false; private fmp4InitSegmentUrl: string | null = null; private fmp4InitSegmentLoaded: boolean = false; // 缓冲区状态 private bufferRanges: BufferRange[] = []; /** * 构造函数 * @param eventBus 事件总线 */ constructor(eventBus: EventBus) { this.eventBus = eventBus; this.setupEventHandlers(); } /** * 设置事件监听器 */ private setupEventHandlers(): void { // 监听MediaSource事件 this.eventBus.on(Events.MEDIA_SOURCE_OPEN, this.handleMediaSourceOpen); this.eventBus.on(Events.MEDIA_SOURCE_CLOSE, this.handleMediaSourceClose); this.eventBus.on(Events.SOURCE_BUFFER_CREATED, this.handleSourceBufferCreated); this.eventBus.on(Events.SOURCE_BUFFER_UPDATE_END, this.handleSourceBufferUpdateEnd); // 监听片段和播放列表事件 this.eventBus.on(Events.PLAYLIST_LOADED, this.handlePlaylistLoaded); this.eventBus.on(Events.SEGMENT_LOAD_START, this.handleSegmentLoadStart); this.eventBus.on(Events.TIMELINE_UPDATE, this.handleTimelineUpdate); this.eventBus.on(Events.TIMELINE_SEEK, this.handleTimelineSeek); } /** * 移除事件监听器 */ private removeEventHandlers(): void { this.eventBus.off(Events.MEDIA_SOURCE_OPEN, this.handleMediaSourceOpen); this.eventBus.off(Events.MEDIA_SOURCE_CLOSE, this.handleMediaSourceClose); this.eventBus.off(Events.SOURCE_BUFFER_CREATED, this.handleSourceBufferCreated); this.eventBus.off(Events.SOURCE_BUFFER_UPDATE_END, this.handleSourceBufferUpdateEnd); this.eventBus.off(Events.PLAYLIST_LOADED, this.handlePlaylistLoaded); this.eventBus.off(Events.SEGMENT_LOAD_START, this.handleSegmentLoadStart); this.eventBus.off(Events.TIMELINE_UPDATE, this.handleTimelineUpdate); this.eventBus.off(Events.TIMELINE_SEEK, this.handleTimelineSeek); } /** * 初始化片段加载器 * @param segments 媒体片段列表 * @param fmp4InitSegmentUrl FMP4初始化片段URL */ public initialize(segments: MediaSegment[], fmp4InitSegmentUrl: string | null = null): void { this.segments = segments; this.fmp4InitSegmentUrl = fmp4InitSegmentUrl; this.currentSegmentIndex = -1; this.segmentQueue = []; this.pendingSegmentLoads = []; this.isProcessingQueue = false; this.bufferRanges = []; this.fmp4InitSegmentLoaded = false; // 记录初始化状态 this.log(`Initialized SegmentLoader with ${segments.length} segments`, 'info'); if (fmp4InitSegmentUrl) { this.log(`FMP4 init segment URL: ${fmp4InitSegmentUrl}`, 'info'); } // 如果MediaSource已就绪,加载初始化片段 if (this.isMediaSourceReady && fmp4InitSegmentUrl) { this.loadInitSegment(); } } /** * 处理MediaSource打开事件 */ private handleMediaSourceOpen = (mediaSource: MediaSource): void => { this.isMediaSourceReady = true; this.log('MediaSource is now ready', 'info'); // 如果有初始化片段URL,加载初始化片段 if (this.fmp4InitSegmentUrl && !this.fmp4InitSegmentLoaded) { this.loadInitSegment(); } // 处理待加载的片段 this.processPendingSegmentLoads(); } /** * 处理MediaSource关闭事件 */ private handleMediaSourceClose = (): void => { this.isMediaSourceReady = false; this.log('MediaSource is now closed', 'info'); } /** * 处理SourceBuffer创建事件 */ private handleSourceBufferCreated = (sourceBuffer: SourceBuffer): void => { this.log('SourceBuffer has been created', 'info'); // 处理待加载的片段 this.processPendingSegmentLoads(); } /** * 处理SourceBuffer更新结束事件 */ private handleSourceBufferUpdateEnd = (sourceBuffer: SourceBuffer): void => { // 更新缓冲区范围 this.updateBufferRanges(sourceBuffer); // 继续处理队列中的下一个片段 this.processNextSegment(); } /** * 处理播放列表加载事件 */ private handlePlaylistLoaded = (playlistInfo: any): void => { this.log(`Playlist loaded with ${playlistInfo.segments.length} segments`, 'info'); this.initialize(playlistInfo.segments, this.fmp4InitSegmentUrl); } /** * 处理片段加载开始事件 */ private handleSegmentLoadStart = (segmentIndex: number): void => { this.loadSegmentWithMSE(segmentIndex); } /** * 处理时间轴更新事件 */ private handleTimelineUpdate = (currentVirtualTime: number): void => { // 查找当前片段索引 for (let i = 0; i < this.segments.length; i++) { const segment = this.segments[i]; if (currentVirtualTime >= segment.virtualStartTime && currentVirtualTime < segment.virtualEndTime) { if (this.currentSegmentIndex !== i) { this.setCurrentSegmentIndex(i); } break; } } } /** * 处理时间轴跳转事件 */ private handleTimelineSeek = (seekTime: number): void => { // 查找跳转位置对应的片段索引 for (let i = 0; i < this.segments.length; i++) { const segment = this.segments[i]; if (seekTime >= segment.virtualStartTime && seekTime < segment.virtualEndTime) { this.setCurrentSegmentIndex(i); break; } } } /** * 设置滑动窗口配置 * @param config 滑动窗口配置 */ public setSlidingWindowConfig(config: Partial<SlidingWindowConfig>): void { this.slidingWindowConfig = { ...this.slidingWindowConfig, ...config }; this.log(`Updated sliding window config: forward=${this.slidingWindowConfig.forward}, backward=${this.slidingWindowConfig.backward}, enabled=${this.slidingWindowConfig.enabled}`, 'info'); // 如果当前有活跃的片段索引,应用新的滑动窗口 if (this.currentSegmentIndex >= 0) { this.applySegmentSlidingWindow(this.currentSegmentIndex); } } /** * 获取滑动窗口配置 * @returns 滑动窗口配置 */ public getSlidingWindowConfig(): SlidingWindowConfig { return { ...this.slidingWindowConfig }; } /** * 设置当前片段索引 * @param index 片段索引 */ public setCurrentSegmentIndex(index: number): void { if (index < 0 || index >= this.segments.length || index === this.currentSegmentIndex) { return; } this.log(`Current segment index changed from ${this.currentSegmentIndex} to ${index}`, 'debug'); this.currentSegmentIndex = index; // 应用滑动窗口 if (this.slidingWindowConfig.enabled) { this.applySegmentSlidingWindow(index); } // 通知当前片段索引更改 this.eventBus.emit(Events.CURRENT_SEGMENT_INDEX_CHANGE, index); } /** * 使用MSE加载片段 * @param segmentIndex 片段索引 */ public loadSegmentWithMSE(segmentIndex: number): void { if (segmentIndex < 0 || segmentIndex >= this.segments.length) { this.log(`Invalid segment index: ${segmentIndex}`, 'error'); return; } const segment = this.segments[segmentIndex]; // 如果片段已经加载,跳过 if (segment.isLoaded) { this.log(`Segment #${segmentIndex} already loaded, skipping`, 'debug'); return; } // 如果片段正在加载,跳过 if (segment.isLoading) { this.log(`Segment #${segmentIndex} already loading, skipping`, 'debug'); return; } // 如果MediaSource未就绪,将片段添加到待处理队列 if (!this.isMediaSourceReady) { this.log(`MediaSource not ready, adding segment #${segmentIndex} to pending queue`, 'debug'); this.pendingSegmentLoads.push(segmentIndex); return; } // 将片段添加到加载队列 this.enqueueSegment(segmentIndex); } /** * 加载所有片段 */ public loadAllSegments(): void { for (let i = 0; i < this.segments.length; i++) { this.loadSegmentWithMSE(i); } } /** * 清除所有片段 */ public clearAllSegments(): void { // 请求清除SourceBuffer this.eventBus.emit(Events.REQUEST_CLEAR_BUFFER); // 重置片段状态 for (let i = 0; i < this.segments.length; i++) { const segment = this.segments[i]; segment.isLoaded = false; segment.isLoading = false; segment.isBuffered = false; this.segments[i] = { ...segment }; } // 清空队列 this.segmentQueue = []; this.pendingSegmentLoads = []; this.isProcessingQueue = false; // 更新缓冲区范围 this.bufferRanges = []; this.eventBus.emit(Events.BUFFER_UPDATE, this.bufferRanges); this.log('All segments cleared', 'info'); } /** * 判断片段是否在滑动窗口内 * @param segmentIndex 片段索引 * @param currentIndex 当前片段索引 * @returns 是否在滑动窗口内 */ public isSegmentInSlidingWindow(segmentIndex: number, currentIndex: number): boolean { if (!this.slidingWindowConfig.enabled) { return true; } // 计算片段与当前索引的距离 const distance = segmentIndex - currentIndex; // 如果是未来片段,检查forward配置 if (distance > 0) { return distance <= this.slidingWindowConfig.forward; } // 如果是过去片段,检查backward配置 if (distance <= 0) { return Math.abs(distance) <= this.slidingWindowConfig.backward; } return true; } /** * 应用滑动窗口 * @param currentIndex 当前片段索引 */ public applySegmentSlidingWindow(currentIndex: number): void { if (!this.slidingWindowConfig.enabled) { return; } // 检查每个片段,决定是否加载或移除 for (let i = 0; i < this.segments.length; i++) { const isInWindow = this.isSegmentInSlidingWindow(i, currentIndex); if (isInWindow) { // 如果在窗口内且未加载,则加载 if (!this.segments[i].isLoaded && !this.segments[i].isLoading) { this.loadSegmentWithMSE(i); } } else { // 如果不在窗口内但已加载,则考虑移除 if (this.segments[i].isLoaded && this.segments[i].isBuffered) { // 计算与当前片段的距离 const distance = Math.abs(i - currentIndex); // 只有当距离超过阈值时才移除 if (i < currentIndex && distance > this.slidingWindowConfig.backward * 2) { // 请求移除片段 this.eventBus.emit(Events.REQUEST_REMOVE_SEGMENT, i); // 更新片段状态 const segment = this.segments[i]; segment.isBuffered = false; this.segments[i] = { ...segment }; this.log(`Removed segment #${i} from buffer (outside sliding window)`, 'debug'); this.eventBus.emit(Events.SEGMENT_REMOVED, i); } } } } } /** * 将片段添加到加载队列 * @param segmentIndex 片段索引 */ private enqueueSegment(segmentIndex: number): void { // 更新片段状态 const segment = this.segments[segmentIndex]; segment.isLoading = true; this.segments[segmentIndex] = { ...segment }; // 添加到队列 if (!this.segmentQueue.includes(segmentIndex)) { this.segmentQueue.push(segmentIndex); this.log(`Segment #${segmentIndex} queued for loading: ${segment.url}`, 'debug'); this.eventBus.emit(Events.SEGMENT_QUEUED, segmentIndex); } // 如果队列未在处理中,开始处理 if (!this.isProcessingQueue) { this.processNextSegment(); } } /** * 处理队列中的下一个片段 */ private processNextSegment(): void { if (this.isProcessingQueue || this.segmentQueue.length === 0) { return; } this.isProcessingQueue = true; // 获取队列中的下一个片段 const segmentIndex = this.segmentQueue.shift()!; const segment = this.segments[segmentIndex]; this.log(`Starting to load segment #${segmentIndex}: ${segment.url}`, 'debug'); this.eventBus.emit(Events.SEGMENT_LOAD_START, segmentIndex); // 请求加载片段数据 this.fetchSegment(segment.url) .then(data => { // 请求将片段数据添加到SourceBuffer this.eventBus.emit(Events.APPEND_BUFFER, data, segmentIndex); // 更新片段状态 segment.isLoaded = true; segment.isLoading = false; this.segments[segmentIndex] = { ...segment }; this.log(`Segment #${segmentIndex} loaded successfully`, 'success'); this.eventBus.emit(Events.SEGMENT_LOADED, segmentIndex); // 继续处理队列 this.isProcessingQueue = false; this.processNextSegment(); }) .catch(error => { // 更新片段状态 segment.isLoading = false; this.segments[segmentIndex] = { ...segment }; this.log(`Failed to load segment #${segmentIndex}: ${error}`, 'error'); this.eventBus.emit(Events.SEGMENT_LOAD_ERROR, segmentIndex, error); // 继续处理队列 this.isProcessingQueue = false; this.processNextSegment(); }); } /** * 获取片段数据 * @param url 片段URL * @returns 片段数据 */ private async fetchSegment(url: string): Promise<ArrayBuffer> { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.arrayBuffer(); } catch (error) { this.log(`Fetch error: ${error}`, 'error'); throw error; } } /** * 处理待加载的片段 */ public processPendingSegmentLoads(): void { if (!this.isMediaSourceReady) { this.log('MediaSource not ready, cannot process pending segment loads', 'warning'); return; } // 处理所有待加载的片段 while (this.pendingSegmentLoads.length > 0) { const segmentIndex = this.pendingSegmentLoads.shift()!; this.loadSegmentWithMSE(segmentIndex); } } /** * 设置MediaSource就绪状态 * @param isReady MediaSource是否就绪 */ public setMediaSourceReady(isReady: boolean): void { if (this.isMediaSourceReady === isReady) { return; } this.isMediaSourceReady = isReady; this.log(`MediaSource ready state changed to: ${isReady}`, 'debug'); // 如果变为就绪状态,处理待加载的片段 if (isReady) { this.processPendingSegmentLoads(); } } /** * 加载初始化片段 */ private loadInitSegment(): void { if (!this.fmp4InitSegmentUrl || this.fmp4InitSegmentLoaded || !this.isMediaSourceReady) { return; } this.log(`Loading FMP4 init segment: ${this.fmp4InitSegmentUrl}`, 'info'); this.fetchSegment(this.fmp4InitSegmentUrl) .then(data => { // 请求将初始化片段数据添加到SourceBuffer this.eventBus.emit(Events.APPEND_INIT_SEGMENT, data); this.fmp4InitSegmentLoaded = true; this.log('FMP4 init segment loaded successfully', 'success'); // 处理待加载的片段 this.processPendingSegmentLoads(); }) .catch(error => { this.log(`Failed to load FMP4 init segment: ${error}`, 'error'); this.eventBus.emit(Events.ERROR, 'Init segment load error', error); }); } /** * 更新缓冲区范围 * @param sourceBuffer SourceBuffer对象 */ private updateBufferRanges(sourceBuffer: SourceBuffer): void { if (!sourceBuffer) { return; } const ranges: BufferRange[] = []; const buffered = sourceBuffer.buffered; for (let i = 0; i < buffered.length; i++) { ranges.push({ start: buffered.start(i), end: buffered.end(i) }); } this.bufferRanges = ranges; // 根据缓冲区范围更新片段的缓冲状态 for (let i = 0; i < this.segments.length; i++) { const segment = this.segments[i]; let isBuffered = false; // 检查片段是否在缓冲区范围内 for (const range of ranges) { if (segment.virtualStartTime >= range.start && segment.virtualEndTime <= range.end) { isBuffered = true; break; } } // 更新片段缓冲状态 if (segment.isBuffered !== isBuffered) { segment.isBuffered = isBuffered; this.segments[i] = { ...segment }; } } // 通知缓冲区更新 this.eventBus.emit(Events.BUFFER_UPDATE, this.bufferRanges); // 记录缓冲区范围 if (ranges.length > 0) { const rangesStr = ranges.map(r => `${r.start.toFixed(2)}-${r.end.toFixed(2)}`).join(', '); this.log(`Buffer ranges updated: [${rangesStr}]`, 'debug'); } else { this.log('Buffer is empty', 'debug'); } } /** * 日志输出 * @param message 日志消息 * @param type 日志类型 */ private log(message: string, type: 'info' | 'success' | 'warning' | 'error' | 'debug'): void { this.eventBus.emit(Events.LOG, `[SegmentLoader] ${message}`, type); } /** * 销毁加载器,清理资源 */ public destroy(): void { this.removeEventHandlers(); // 清空队列 this.segmentQueue = []; this.pendingSegmentLoads = []; this.isProcessingQueue = false; // 重置状态 this.segments = []; this.currentSegmentIndex = -1; this.isMediaSourceReady = false; this.fmp4InitSegmentUrl = null; this.fmp4InitSegmentLoaded = false; this.bufferRanges = []; } }