UNPKG

unified-video-framework

Version:

Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more

290 lines (253 loc) 8 kB
import { Chapter, ChapterSegment, ChapterConfig, EventHandler } from './interfaces'; export interface ChapterManagerEvents { chapterchange: Chapter | null; segmententered: ChapterSegment; segmentexited: ChapterSegment; segmentskipped: ChapterSegment; } export class ChapterManager { private config: ChapterConfig; private chapters: Chapter[] = []; private segments: ChapterSegment[] = []; private currentChapter: Chapter | null = null; private activeSegments: Set<string> = new Set(); private lastProcessedTime: number = -1; private eventHandlers: Map<keyof ChapterManagerEvents, EventHandler[]> = new Map(); constructor(config: ChapterConfig = {}) { this.config = { ...config }; this.chapters = config.chapters || []; this.segments = config.segments || []; // Initialize event handler arrays this.eventHandlers.set('chapterchange', []); this.eventHandlers.set('segmententered', []); this.eventHandlers.set('segmentexited', []); this.eventHandlers.set('segmentskipped', []); } /** * Update the configuration */ updateConfig(config: ChapterConfig): void { this.config = { ...this.config, ...config }; this.chapters = config.chapters || this.chapters; this.segments = config.segments || this.segments; } /** * Load chapter data from a URL */ async loadChapterData(url: string): Promise<void> { try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch chapter data: ${response.status}`); } const data = await response.json(); if (data.chapters) { this.chapters = data.chapters; } if (data.segments) { this.segments = data.segments; } } catch (error) { console.error('Error loading chapter data:', error); throw error; } } /** * Initialize the chapter manager */ async initialize(): Promise<void> { if (this.config.dataUrl) { await this.loadChapterData(this.config.dataUrl); } // Sort chapters and segments by start time this.chapters.sort((a, b) => a.startTime - b.startTime); this.segments.sort((a, b) => a.startTime - b.startTime); } /** * Process current time and handle chapter/segment changes */ processTimeUpdate(currentTime: number): void { if (Math.abs(currentTime - this.lastProcessedTime) < 0.1) { return; // Skip if time hasn't changed significantly } this.lastProcessedTime = currentTime; this.processChapterChange(currentTime); this.processSegments(currentTime); } /** * Process chapter changes */ private processChapterChange(currentTime: number): void { const newChapter = this.getCurrentChapter(currentTime); if (newChapter !== this.currentChapter) { this.currentChapter = newChapter; this.emit('chapterchange', newChapter); if (this.config.onChapterChange) { this.config.onChapterChange(newChapter); } } } /** * Process segment enter/exit events */ private processSegments(currentTime: number): void { const currentSegments = this.segments.filter( segment => currentTime >= segment.startTime && currentTime <= segment.endTime ); // Check for newly entered segments for (const segment of currentSegments) { if (!this.activeSegments.has(segment.id)) { this.activeSegments.add(segment.id); this.emit('segmententered', segment); if (this.config.onSegmentEntered) { this.config.onSegmentEntered(segment); } // Auto-skip if enabled and segment has skip action if (this.config.autoSkip && segment.action === 'skip') { this.skipSegment(segment); } } } // Check for exited segments const currentSegmentIds = new Set(currentSegments.map(s => s.id)); for (const activeSegmentId of this.activeSegments) { if (!currentSegmentIds.has(activeSegmentId)) { const segment = this.segments.find(s => s.id === activeSegmentId); if (segment) { this.activeSegments.delete(activeSegmentId); this.emit('segmentexited', segment); if (this.config.onSegmentExited) { this.config.onSegmentExited(segment); } } } } } /** * Get the current chapter based on time */ getCurrentChapter(currentTime: number): Chapter | null { return this.chapters.find( chapter => currentTime >= chapter.startTime && currentTime <= chapter.endTime ) || null; } /** * Get all chapters */ getChapters(): Chapter[] { return [...this.chapters]; } /** * Get all segments */ getSegments(): ChapterSegment[] { return [...this.segments]; } /** * Get segments at a specific time */ getSegmentsAtTime(currentTime: number): ChapterSegment[] { return this.segments.filter( segment => currentTime >= segment.startTime && currentTime <= segment.endTime ); } /** * Skip a segment */ skipSegment(segment: ChapterSegment): void { this.emit('segmentskipped', segment); if (this.config.onSegmentSkipped) { this.config.onSegmentSkipped(segment); } } /** * Seek to a specific chapter */ seekToChapter(chapterId: string): Chapter | null { const chapter = this.chapters.find(c => c.id === chapterId); if (chapter) { // The actual seeking will be handled by the player // This method returns the chapter to seek to return chapter; } return null; } /** * Get next chapter */ getNextChapter(currentTime: number): Chapter | null { return this.chapters.find(chapter => chapter.startTime > currentTime) || null; } /** * Get previous chapter */ getPreviousChapter(currentTime: number): Chapter | null { const previousChapters = this.chapters .filter(chapter => chapter.startTime < currentTime) .sort((a, b) => b.startTime - a.startTime); return previousChapters[0] || null; } /** * Add event listener */ on<K extends keyof ChapterManagerEvents>(event: K, handler: EventHandler): void { const handlers = this.eventHandlers.get(event) || []; handlers.push(handler); this.eventHandlers.set(event, handlers); } /** * Remove event listener */ off<K extends keyof ChapterManagerEvents>(event: K, handler?: EventHandler): void { const handlers = this.eventHandlers.get(event) || []; if (handler) { const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); } } else { handlers.length = 0; } this.eventHandlers.set(event, handlers); } /** * Emit an event */ private emit<K extends keyof ChapterManagerEvents>(event: K, data: ChapterManagerEvents[K]): void { const handlers = this.eventHandlers.get(event) || []; handlers.forEach(handler => { try { handler(data); } catch (error) { console.error(`Error in chapter manager event handler for ${event}:`, error); } }); } /** * Reset the chapter manager state */ reset(): void { this.currentChapter = null; this.activeSegments.clear(); this.lastProcessedTime = -1; } /** * Destroy the chapter manager and clean up */ destroy(): void { this.reset(); this.eventHandlers.clear(); } /** * Get the current chapter */ getCurrentChapterInfo(): Chapter | null { return this.currentChapter; } /** * Check if chapter functionality is enabled */ isEnabled(): boolean { return this.config.enabled !== false; } }