UNPKG

unified-video-framework

Version:

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

372 lines 14.9 kB
import { DEFAULT_CHAPTER_CONFIG, SEGMENT_COLORS } from './types/ChapterTypes.js'; import { SkipButtonController } from './SkipButtonController.js'; import { CreditsButtonController } from './CreditsButtonController.js'; export class ChapterManager { constructor(playerContainer, videoElement, config = DEFAULT_CHAPTER_CONFIG) { this.playerContainer = playerContainer; this.videoElement = videoElement; this.chapters = null; this.currentSegment = null; this.previousSegment = null; this.eventListeners = new Map(); this.isDestroyed = false; this.config = { ...DEFAULT_CHAPTER_CONFIG, ...config }; this.skipButtonController = new SkipButtonController(playerContainer, this.config, (segment) => this.skipToNextSegment(segment), (segment) => this.emit('skipButtonShown', { segment, currentTime: this.videoElement.currentTime }), (segment, reason) => this.emit('skipButtonHidden', { segment, currentTime: this.videoElement.currentTime, reason: reason })); this.creditsButtonController = new CreditsButtonController(playerContainer, this.config, { onWatchCredits: (segment) => this.handleWatchCredits(segment), onNextEpisode: (segment, url) => this.handleNextEpisode(segment, url), onAutoRedirect: (segment, url) => this.handleAutoRedirect(segment, url), onButtonsShown: (segment) => this.emit('skipButtonShown', { segment, currentTime: this.videoElement.currentTime }), onButtonsHidden: (segment, reason) => this.emit('skipButtonHidden', { segment, currentTime: this.videoElement.currentTime, reason: reason }) }); this.setupTimeUpdateListener(); if (this.config.data) { this.loadChapters(this.config.data); } else if (this.config.dataUrl) { this.loadChaptersFromUrl(this.config.dataUrl); } } async loadChapters(chapters) { try { this.validateChapters(chapters); this.chapters = chapters; this.sortSegments(); this.emit('chaptersLoaded', { chapters: this.chapters, segmentCount: this.chapters.segments.length }); if (this.config.showChapterMarkers) { this.updateChapterMarkers(); } this.checkCurrentSegment(this.videoElement.currentTime); } catch (error) { this.emit('chaptersLoadError', { error: error }); throw error; } } async loadChaptersFromUrl(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to load chapters: ${response.statusText}`); } const chapters = await response.json(); await this.loadChapters(chapters); } catch (error) { this.emit('chaptersLoadError', { error: error, url }); throw error; } } getCurrentSegment(currentTime) { if (!this.chapters) return null; return this.chapters.segments.find(segment => currentTime >= segment.startTime && currentTime < segment.endTime) || null; } skipToNextSegment(currentSegment) { if (!this.chapters) return; const nextSegment = this.getNextContentSegment(currentSegment); const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime; const wasPlaying = !this.videoElement.paused; this.emit('segmentSkipped', { fromSegment: currentSegment, toSegment: nextSegment || undefined, skipMethod: 'button', currentTime: this.videoElement.currentTime }); this.videoElement.currentTime = targetTime; const shouldResumePlayback = this.config.userPreferences?.resumePlaybackAfterSkip !== false; if (shouldResumePlayback && wasPlaying && this.videoElement.paused) { setTimeout(() => { if (!this.videoElement.paused) return; this.videoElement.play().catch(() => { console.warn('[ChapterManager] Could not resume playback after skip - user interaction may be required'); }); }, 50); } } skipToSegment(segmentId) { if (!this.chapters) return; const segment = this.chapters.segments.find(s => s.id === segmentId); if (!segment) return; const fromSegment = this.currentSegment; const wasPlaying = !this.videoElement.paused; if (fromSegment) { this.emit('segmentSkipped', { fromSegment, toSegment: segment, skipMethod: 'manual', currentTime: this.videoElement.currentTime }); } this.videoElement.currentTime = segment.startTime; const shouldResumePlayback = this.config.userPreferences?.resumePlaybackAfterSkip !== false; if (shouldResumePlayback && wasPlaying && this.videoElement.paused) { setTimeout(() => { if (!this.videoElement.paused) return; this.videoElement.play().catch(() => { console.warn('[ChapterManager] Could not resume playback after skip - user interaction may be required'); }); }, 50); } } getSegments() { return this.chapters?.segments || []; } getSegment(segmentId) { if (!this.chapters) return null; return this.chapters.segments.find(s => s.id === segmentId) || null; } getSegmentsByType(type) { if (!this.chapters) return []; return this.chapters.segments.filter(s => s.type === type); } getChapterMarkers() { if (!this.chapters || !this.config.showChapterMarkers) return []; return this.chapters.segments .filter(segment => segment.type !== 'content') .map(segment => { const customColor = this.config.customStyles?.progressMarkers?.[segment.type]; const color = customColor || SEGMENT_COLORS[segment.type]; return { segment, position: (segment.startTime / this.chapters.duration) * 100, color, label: segment.title || segment.type }; }); } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; if (newConfig.skipButtonPosition) { this.skipButtonController.updatePosition(newConfig.skipButtonPosition); } if ('showChapterMarkers' in newConfig) { if (newConfig.showChapterMarkers) { this.updateChapterMarkers(); } else { this.removeChapterMarkers(); } } } on(event, listener) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(listener); } off(event, listener) { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } } destroy() { this.isDestroyed = true; this.skipButtonController.destroy(); this.creditsButtonController.destroy(); this.removeChapterMarkers(); this.eventListeners.clear(); this.chapters = null; this.currentSegment = null; this.previousSegment = null; } hasChapters() { return this.chapters !== null && this.chapters.segments.length > 0; } getChapters() { return this.chapters; } setupTimeUpdateListener() { const handleTimeUpdate = () => { if (this.isDestroyed) return; this.checkCurrentSegment(this.videoElement.currentTime); }; this.videoElement.addEventListener('timeupdate', handleTimeUpdate); } checkCurrentSegment(currentTime) { if (!this.chapters) return; const newSegment = this.getCurrentSegment(currentTime); if (this.currentSegment?.type === 'credits' && this.currentSegment.nextEpisodeUrl && this.creditsButtonController.isUserWatchingCredits()) { if (currentTime >= this.currentSegment.endTime) { const redirectUrl = this.currentSegment.nextEpisodeUrl; const creditsSegment = this.currentSegment; this.emit('creditsFullyWatched', { segment: creditsSegment, nextEpisodeUrl: redirectUrl, currentTime }); this.creditsButtonController.hideCreditsButtons('segment-end'); window.location.href = redirectUrl; return; } } if (newSegment !== this.currentSegment) { if (this.currentSegment) { this.emit('segmentExited', { segment: this.currentSegment, currentTime, nextSegment: newSegment || undefined }); if (this.shouldShowSkipButton(this.currentSegment)) { this.skipButtonController.hideSkipButton('segment-end'); } if (this.currentSegment.type === 'credits' && this.currentSegment.nextEpisodeUrl) { this.creditsButtonController.hideCreditsButtons('segment-end'); } } this.previousSegment = this.currentSegment; this.currentSegment = newSegment; if (this.currentSegment) { this.emit('segmentEntered', { segment: this.currentSegment, currentTime, previousSegment: this.previousSegment || undefined }); if (this.currentSegment.type === 'credits' && this.currentSegment.nextEpisodeUrl) { this.creditsButtonController.showCreditsButtons(this.currentSegment, currentTime); } else if (this.shouldShowSkipButton(this.currentSegment)) { this.skipButtonController.showSkipButton(this.currentSegment, currentTime); } } } } shouldShowSkipButton(segment) { if (segment.type === 'content') { return segment.showSkipButton === true; } return segment.showSkipButton !== false; } handleWatchCredits(segment) { this.emit('creditsWatched', { segment, currentTime: this.videoElement.currentTime }); } handleNextEpisode(segment, url) { this.emit('nextEpisodeClicked', { segment, nextEpisodeUrl: url, currentTime: this.videoElement.currentTime }); } handleAutoRedirect(segment, url) { this.emit('creditsAutoRedirect', { segment, nextEpisodeUrl: url, currentTime: this.videoElement.currentTime }); } getNextContentSegment(currentSegment) { if (!this.chapters) return null; const sortedSegments = [...this.chapters.segments].sort((a, b) => a.startTime - b.startTime); const currentIndex = sortedSegments.findIndex(s => s.id === currentSegment.id); if (currentIndex === -1) return null; for (let i = currentIndex + 1; i < sortedSegments.length; i++) { if (sortedSegments[i].type === 'content') { return sortedSegments[i]; } } return null; } sortSegments() { if (this.chapters) { this.chapters.segments.sort((a, b) => a.startTime - b.startTime); } } validateChapters(chapters) { if (!chapters.videoId) { throw new Error('Chapters must have a videoId'); } if (!chapters.duration || chapters.duration <= 0) { throw new Error('Chapters must have a valid duration'); } if (!Array.isArray(chapters.segments)) { throw new Error('Chapters must have a segments array'); } chapters.segments.forEach((segment, index) => { if (!segment.id) { throw new Error(`Segment at index ${index} must have an id`); } if (!segment.type) { throw new Error(`Segment at index ${index} must have a type`); } if (segment.startTime < 0 || segment.endTime <= segment.startTime) { throw new Error(`Segment at index ${index} has invalid time range`); } if (segment.endTime > chapters.duration) { throw new Error(`Segment at index ${index} extends beyond video duration`); } }); } updateChapterMarkers() { if (!this.chapters || !this.config.showChapterMarkers) return; const progressBar = this.playerContainer.querySelector('.uvf-progress-bar'); if (!progressBar) return; this.removeChapterMarkers(); const markers = this.getChapterMarkers(); markers.forEach(marker => { const markerElement = document.createElement('div'); markerElement.className = `uvf-chapter-marker uvf-chapter-marker-${marker.segment.type}`; markerElement.style.left = `${marker.position}%`; markerElement.style.backgroundColor = marker.color || SEGMENT_COLORS[marker.segment.type]; markerElement.setAttribute('title', marker.label || ''); markerElement.setAttribute('data-segment-id', marker.segment.id); progressBar.appendChild(markerElement); }); } removeChapterMarkers() { const markers = this.playerContainer.querySelectorAll('.uvf-chapter-marker'); markers.forEach(marker => marker.remove()); } emit(event, data) { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach(listener => { try { listener(data); } catch (error) { console.error(`Error in chapter event listener for ${event}:`, error); } }); } } } //# sourceMappingURL=ChapterManager.js.map