UNPKG

serika-dev-player

Version:

A beautiful purple-themed video player component for React with multi-language and subtitle support

1,095 lines (1,084 loc) 2.14 MB
import { jsx, jsxs } from 'react/jsx-runtime'; import { useEffect, useRef, useState, useCallback } from 'react'; function _mergeNamespaces(n, m) { m.forEach(function (e) { e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { if (k !== 'default' && !(k in n)) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); }); return Object.freeze(n); } const parseASS = (assContent) => { const lines = assContent.split('\n'); const sections = {}; let currentSection = ''; // Parse sections for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) { currentSection = trimmedLine.slice(1, -1).toLowerCase(); sections[currentSection] = []; } else if (currentSection && trimmedLine) { sections[currentSection].push(trimmedLine); } } parseStyles(sections['v4+ styles'] || sections['v4 styles'] || []); const events = parseEvents(sections['events'] || []); return events.map(event => ({ startTime: event.startTime, endTime: event.endTime, text: event.text })); }; const parseStyles = (styleLines) => { const styles = new Map(); let formatLine = ''; for (const line of styleLines) { if (line.startsWith('Format:')) { formatLine = line.substring(7).trim(); } else if (line.startsWith('Style:')) { const style = parseStyleLine(line, formatLine); if (style) { styles.set(style.name, style); } } } return styles; }; const parseStyleLine = (styleLine, formatLine) => { const values = styleLine.substring(6).split(','); const fields = formatLine.split(',').map(f => f.trim()); if (values.length !== fields.length) { return null; } const style = {}; for (let i = 0; i < fields.length; i++) { const field = fields[i].toLowerCase(); const value = values[i].trim(); switch (field) { case 'name': style.name = value; break; case 'fontname': style.fontName = value; break; case 'fontsize': style.fontSize = parseInt(value) || 20; break; case 'primarycolour': case 'primarycolor': style.primaryColor = convertASSColor(value); break; case 'secondarycolour': case 'secondarycolor': style.secondaryColor = convertASSColor(value); break; case 'outlinecolour': case 'outlinecolor': style.outlineColor = convertASSColor(value); break; case 'backcolour': case 'backcolor': style.backColor = convertASSColor(value); break; case 'bold': style.bold = value === '1' || value === '-1'; break; case 'italic': style.italic = value === '1' || value === '-1'; break; case 'underline': style.underline = value === '1'; break; case 'strikeout': style.strikeOut = value === '1'; break; case 'scalex': style.scaleX = parseFloat(value) || 100; break; case 'scaley': style.scaleY = parseFloat(value) || 100; break; case 'spacing': style.spacing = parseFloat(value) || 0; break; case 'angle': style.angle = parseFloat(value) || 0; break; case 'borderstyle': style.borderStyle = parseInt(value) || 1; break; case 'outline': style.outline = parseFloat(value) || 0; break; case 'shadow': style.shadow = parseFloat(value) || 0; break; case 'alignment': style.alignment = parseInt(value) || 2; break; case 'marginl': style.marginL = parseInt(value) || 0; break; case 'marginr': style.marginR = parseInt(value) || 0; break; case 'marginv': style.marginV = parseInt(value) || 0; break; case 'encoding': style.encoding = parseInt(value) || 1; break; } } return style; }; const parseEvents = (eventLines, styles) => { const events = []; let formatLine = ''; for (const line of eventLines) { if (line.startsWith('Format:')) { formatLine = line.substring(7).trim(); } else if (line.startsWith('Dialogue:')) { const event = parseEventLine(line, formatLine); if (event) { events.push(event); } } } return events.sort((a, b) => a.startTime - b.startTime); }; const parseEventLine = (eventLine, formatLine, styles) => { const values = eventLine.substring(9).split(','); const fields = formatLine.split(',').map(f => f.trim()); if (values.length < fields.length) { return null; } const event = {}; for (let i = 0; i < fields.length; i++) { const field = fields[i].toLowerCase(); let value = i < values.length - 1 ? values[i].trim() : values.slice(i).join(',').trim(); switch (field) { case 'layer': event.layer = parseInt(value) || 0; break; case 'start': event.startTime = parseASSTime(value); break; case 'end': event.endTime = parseASSTime(value); break; case 'style': event.style = value; break; case 'name': event.name = value; break; case 'marginl': event.marginL = parseInt(value) || 0; break; case 'marginr': event.marginR = parseInt(value) || 0; break; case 'marginv': event.marginV = parseInt(value) || 0; break; case 'effect': event.effect = value; break; case 'text': event.rawText = value; event.text = processASSText(value); break; } } if (event.startTime !== undefined && event.endTime !== undefined && event.text) { return event; } return null; }; const parseASSTime = (timeStr) => { // Format: H:MM:SS.CC (centiseconds) const match = timeStr.match(/(\d+):(\d{2}):(\d{2})\.(\d{2})/); if (!match) return 0; const hours = parseInt(match[1]); const minutes = parseInt(match[2]); const seconds = parseInt(match[3]); const centiseconds = parseInt(match[4]); return hours * 3600 + minutes * 60 + seconds + centiseconds / 100; }; const convertASSColor = (colorStr) => { // ASS colors can be in format &Hbbggrr& or decimal if (colorStr.startsWith('&H') && colorStr.endsWith('&')) { const hex = colorStr.slice(2, -1); if (hex.length === 6) { // Convert BGR to RGB const b = hex.substring(0, 2); const g = hex.substring(2, 4); const r = hex.substring(4, 6); return `#${r}${g}${b}`; } } // Try parsing as decimal const decimal = parseInt(colorStr); if (!isNaN(decimal)) { const hex = decimal.toString(16).padStart(6, '0'); const b = hex.substring(0, 2); const g = hex.substring(2, 4); const r = hex.substring(4, 6); return `#${r}${g}${b}`; } return '#ffffff'; }; const processASSText = (text) => { // Remove ASS override tags and process basic formatting return text .replace(/\{[^}]*\}/g, '') // Remove override tags .replace(/\\N/g, '\n') // Soft line break .replace(/\\n/g, '\n') // Hard line break .replace(/\\h/g, ' ') // Hard space .trim(); }; const getASSStyleForCue = (event, styles) => { const style = styles.get(event.style) || styles.get('Default') || styles.values().next().value; if (!style) { return {}; } return { fontFamily: style.fontName || 'Arial', fontSize: `${style.fontSize}px`, color: style.primaryColor || '#ffffff', fontWeight: style.bold ? 'bold' : 'normal', fontStyle: style.italic ? 'italic' : 'normal', textDecoration: [ style.underline ? 'underline' : '', style.strikeOut ? 'line-through' : '' ].filter(Boolean).join(' ') || 'none', textShadow: style.outline > 0 ? `0 0 ${style.outline}px ${style.outlineColor}` : undefined, transform: `scale(${style.scaleX / 100}, ${style.scaleY / 100}) rotate(${style.angle}deg)`, letterSpacing: `${style.spacing}px`, textAlign: getAlignmentCSS(style.alignment) }; }; const getAlignmentCSS = (alignment) => { // ASS alignment: 1=left, 2=center, 3=right (bottom row) // 5=left, 6=center, 7=right (middle row) // 9=left, 10=center, 11=right (top row) switch (alignment % 4) { case 1: return 'left'; case 2: return 'center'; case 3: return 'right'; default: return 'center'; } }; const parseWebVTT$1 = (vttContent) => { const cues = []; const lines = vttContent.split('\n'); let i = 0; // Skip WEBVTT header while (i < lines.length && !lines[i].includes('-->')) { i++; } while (i < lines.length) { const line = lines[i].trim(); if (line.includes('-->')) { const timeMatch = line.match(/(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})/); if (timeMatch) { const startTime = parseTimeString(timeMatch[1]); const endTime = parseTimeString(timeMatch[2]); i++; let text = ''; // Collect text lines until empty line or next cue while (i < lines.length && lines[i].trim() !== '' && !lines[i].includes('-->')) { if (text) text += ' '; text += lines[i].trim(); i++; } if (text) { cues.push({ startTime, endTime, text: cleanText(text) }); } } } i++; } return cues; }; const parseSRT = (srtContent) => { const cues = []; const blocks = srtContent.split(/\n\s*\n/); for (const block of blocks) { const lines = block.trim().split('\n'); if (lines.length >= 3) { const timeMatch = lines[1].match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/); if (timeMatch) { const startTime = parseTimeString(timeMatch[1].replace(',', '.')); const endTime = parseTimeString(timeMatch[2].replace(',', '.')); const text = lines.slice(2).join(' '); cues.push({ startTime, endTime, text: cleanText(text) }); } } } return cues; }; const parseSubtitles = async (url) => { try { const response = await fetch(url); const content = await response.text(); // Check for ASS format if (url.toLowerCase().endsWith('.ass') || url.toLowerCase().endsWith('.ssa') || content.includes('[Script Info]') || content.includes('[V4+ Styles]')) { return parseASS(content); } // Check for WebVTT format if (url.toLowerCase().endsWith('.vtt') || content.includes('WEBVTT')) { return parseWebVTT$1(content); } // Check for SRT format if (url.toLowerCase().endsWith('.srt')) { return parseSRT(content); } // Try to auto-detect format if (content.includes('-->')) { if (content.includes('WEBVTT') || /\d{2}:\d{2}:\d{2}\.\d{3}/.test(content)) { return parseWebVTT$1(content); } else if (/\d{2}:\d{2}:\d{2},\d{3}/.test(content)) { return parseSRT(content); } } return []; } catch (error) { console.error('Error parsing subtitles:', error); return []; } }; const getCurrentSubtitle = (cues, currentTime) => { const currentCue = cues.find(cue => currentTime >= cue.startTime && currentTime <= cue.endTime); return currentCue ? currentCue.text : ''; }; const parseTimeString = (timeStr) => { const parts = timeStr.split(':'); const seconds = parseFloat(parts[2]); const minutes = parseInt(parts[1]); const hours = parseInt(parts[0]); return hours * 3600 + minutes * 60 + seconds; }; const cleanText = (text) => { // Remove WebVTT/HTML tags and clean up text return text .replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/&lt;/g, '<') .replace(/&gt;/g, '>') .replace(/&amp;/g, '&') .replace(/&quot;/g, '"') .replace(/&#39;/g, "'") .trim(); }; const formatTime = (seconds) => { if (!isFinite(seconds) || isNaN(seconds)) { return '0:00'; } const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${minutes}:${secs.toString().padStart(2, '0')}`; }; const parseTimeToSeconds = (timeStr) => { const parts = timeStr.split(':').reverse(); let seconds = 0; for (let i = 0; i < parts.length; i++) { seconds += parseFloat(parts[i]) * Math.pow(60, i); } return seconds; }; // Browser-only imports - will be loaded dynamically let Hls$1 = null; let MediaPlayer = null; // Dynamic import for browser environment const loadHls = async () => { if (typeof window !== 'undefined' && !Hls$1) { try { const hlsModule = await Promise.resolve().then(function () { return hls; }); Hls$1 = hlsModule.default; } catch (error) { console.warn('HLS.js not available:', error); } } return Hls$1; }; const loadDashjs = async () => { if (typeof window !== 'undefined' && !MediaPlayer) { try { const dashModule = await Promise.resolve().then(function () { return dash_all_min$1; }); MediaPlayer = dashModule.MediaPlayer; } catch (error) { console.warn('DASH.js not available:', error); } } return MediaPlayer; }; const loadVideo = async (videoElement, src) => { // Only run in browser environment if (typeof window === 'undefined') { return { type: 'native' }; } const extension = src.toLowerCase().split('.').pop() || ''; // Handle HLS streams if (extension === 'm3u8' || src.includes('.m3u8')) { const HlsClass = await loadHls(); if (HlsClass && HlsClass.isSupported()) { const hls = new HlsClass({ enableWorker: false, lowLatencyMode: true }); hls.loadSource(src); hls.attachMedia(videoElement); return { cleanup: () => { hls.destroy(); }, player: hls, type: 'hls' }; } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { // Native HLS support (Safari) videoElement.src = src; return { type: 'native' }; } } // Handle DASH streams if (extension === 'mpd' || src.includes('.mpd')) { try { const DashClass = await loadDashjs(); if (DashClass) { const dashPlayer = DashClass().create(); dashPlayer.initialize(videoElement, src, false); return { cleanup: () => { dashPlayer.destroy(); }, player: dashPlayer, type: 'dash' }; } } catch (error) { console.warn('DASH.js not available, falling back to native support', error); videoElement.src = src; return { type: 'native' }; } } // Handle regular video files (MP4, WebM, MKV, etc.) // Modern browsers support MKV with proper codecs const supportedFormats = [ 'mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv', 'flv', '3gp', 'wmv' ]; if (supportedFormats.indexOf(extension) !== -1 || !extension) { videoElement.src = src; } return { type: 'native' }; }; const getSupportedFormats = async () => { // Only run in browser environment if (typeof window === 'undefined') { return ['mp4', 'webm']; // Default safe formats for SSR } const video = document.createElement('video'); const formats = []; // Test common video formats const formatTests = [ { format: 'mp4', mime: 'video/mp4; codecs="avc1.42E01E"' }, { format: 'webm', mime: 'video/webm; codecs="vp8, vorbis"' }, { format: 'webm', mime: 'video/webm; codecs="vp9"' }, { format: 'ogg', mime: 'video/ogg; codecs="theora"' }, { format: 'mkv', mime: 'video/x-matroska' }, { format: 'hls', mime: 'application/vnd.apple.mpegurl' } ]; formatTests.forEach(test => { if (video.canPlayType(test.mime) !== '') { if (formats.indexOf(test.format) === -1) { formats.push(test.format); } } }); // Check HLS.js support const HlsClass = await loadHls(); if (HlsClass && HlsClass.isSupported()) { if (formats.indexOf('hls') === -1) { formats.push('hls'); } } return formats; }; const getVideoInfo = (src) => { const extension = src.toLowerCase().split('.').pop() || ''; const streamFormats = ['m3u8', 'mpd']; return { format: extension, isStream: streamFormats.indexOf(extension) !== -1 }; }; const translations = { en: { play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', fullscreen: 'Fullscreen', exitFullscreen: 'Exit Fullscreen', volume: 'Volume', currentTime: 'Current Time', duration: 'Duration', subtitles: 'Subtitles', noSubtitles: 'No Subtitles', playbackSpeed: 'Playback Speed', settings: 'Settings', loading: 'Loading...' }, es: { play: 'Reproducir', pause: 'Pausar', mute: 'Silenciar', unmute: 'Activar sonido', fullscreen: 'Pantalla completa', exitFullscreen: 'Salir de pantalla completa', volume: 'Volumen', currentTime: 'Tiempo actual', duration: 'Duración', subtitles: 'Subtítulos', noSubtitles: 'Sin subtítulos', playbackSpeed: 'Velocidad de reproducción', settings: 'Configuración', loading: 'Cargando...' }, fr: { play: 'Lire', pause: 'Pause', mute: 'Muet', unmute: 'Activer le son', fullscreen: 'Plein écran', exitFullscreen: 'Quitter le plein écran', volume: 'Volume', currentTime: 'Temps actuel', duration: 'Durée', subtitles: 'Sous-titres', noSubtitles: 'Pas de sous-titres', playbackSpeed: 'Vitesse de lecture', settings: 'Paramètres', loading: 'Chargement...' }, de: { play: 'Abspielen', pause: 'Pause', mute: 'Stumm schalten', unmute: 'Ton aktivieren', fullscreen: 'Vollbild', exitFullscreen: 'Vollbild verlassen', volume: 'Lautstärke', currentTime: 'Aktuelle Zeit', duration: 'Dauer', subtitles: 'Untertitel', noSubtitles: 'Keine Untertitel', playbackSpeed: 'Wiedergabegeschwindigkeit', settings: 'Einstellungen', loading: 'Laden...' }, nl: { play: 'Afspelen', pause: 'Pauzeren', mute: 'Dempen', unmute: 'Geluid aanzetten', fullscreen: 'Volledig scherm', exitFullscreen: 'Volledig scherm verlaten', volume: 'Volume', currentTime: 'Huidige tijd', duration: 'Duur', subtitles: 'Ondertitels', noSubtitles: 'Geen ondertitels', playbackSpeed: 'Afspeelsnelheid', settings: 'Instellingen', loading: 'Laden...' } }; const getTranslation = (language = 'en') => { return translations[language] || translations.en; }; const useCustomTheme = (theme, customTheme, elementRef) => { useEffect(() => { if (!(elementRef === null || elementRef === void 0 ? void 0 : elementRef.current)) return; const element = elementRef.current; // Default themes const defaultThemes = { dark: { primaryColor: '#8a2be2', primaryGradient: 'linear-gradient(135deg, #6a5acd, #8a2be2)', backgroundColor: '#000000', controlsBackground: 'linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent)', textColor: '#ffffff', accentColor: '#9370db', borderRadius: '12px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', progressBarColor: 'linear-gradient(90deg, #8a2be2, #9370db)', progressBarBackground: 'rgba(255, 255, 255, 0.2)', bufferColor: 'rgba(138, 43, 226, 0.4)', volumeColor: 'linear-gradient(90deg, #8a2be2, #9370db)', subtitleBackground: 'rgba(0, 0, 0, 0.7)', subtitleTextColor: '#ffffff', subtitleFontSize: '18px', loadingSpinnerColor: '#8a2be2', shadowColor: 'rgba(106, 90, 205, 0.3)' }, light: { primaryColor: '#8a2be2', primaryGradient: 'linear-gradient(135deg, #e1bee7, #ce93d8)', backgroundColor: '#ffffff', controlsBackground: 'linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent)', textColor: '#000000', accentColor: '#ba68c8', borderRadius: '12px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', progressBarColor: 'linear-gradient(90deg, #8a2be2, #ba68c8)', progressBarBackground: 'rgba(0, 0, 0, 0.2)', bufferColor: 'rgba(138, 43, 226, 0.3)', volumeColor: 'linear-gradient(90deg, #8a2be2, #ba68c8)', subtitleBackground: 'rgba(255, 255, 255, 0.9)', subtitleTextColor: '#000000', subtitleFontSize: '18px', loadingSpinnerColor: '#8a2be2', shadowColor: 'rgba(138, 43, 226, 0.2)' } }; // Apply theme const themeConfig = theme === 'custom' && customTheme ? customTheme : defaultThemes[theme] || defaultThemes.dark; // Set CSS custom properties Object.entries(themeConfig).forEach(([key, value]) => { if (value !== undefined) { const cssVar = `--serika-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; element.style.setProperty(cssVar, value); } }); }, [theme, customTheme, elementRef]); }; const VideoPlayer = ({ src, poster, width = '100%', height = 'auto', autoPlay = false, controls = true, loop = false, muted = false, preload = 'metadata', subtitles = [], onPlay, onPause, onEnded, onTimeUpdate, onLoadedMetadata, className, style, language = 'en', theme = 'dark', customTheme, rounded, ambient = false, ambientIntensity = 0.35, ambientBlur = 60 }) => { var _a; const videoRef = useRef(null); const containerRef = useRef(null); const progressRef = useRef(null); const volumeRef = useRef(null); const [state, setState] = useState({ isPlaying: false, currentTime: 0, duration: 0, volume: 1, isMuted: muted, isFullscreen: false, showControls: true, selectedSubtitle: subtitles.findIndex(sub => sub.default) || (subtitles.length > 0 ? 0 : null), buffered: null, playbackRate: 1, availableQualities: [], selectedQuality: 'auto', isLoading: true, error: null, isMiniPlayer: false, showSettings: false }); const [parsedSubtitles, setParsedSubtitles] = useState({}); const [hideControlsTimeout, setHideControlsTimeout] = useState(null); const [videoLoader, setVideoLoader] = useState(null); const t = getTranslation(language); // Apply theme variables to the container useCustomTheme(theme, customTheme, containerRef); // Initialize video source with advanced format support useEffect(() => { const video = videoRef.current; if (!video || !src) return; // Reset states when source changes setState(prev => ({ ...prev, isLoading: true, isPlaying: false, currentTime: 0, duration: 0, error: null })); // Pause video before changing source to prevent play() interruption if (!video.paused) { video.pause(); } // Cleanup previous video loader if (videoLoader === null || videoLoader === void 0 ? void 0 : videoLoader.cleanup) { videoLoader.cleanup(); } // Small delay to ensure previous operations complete const timeoutId = setTimeout(() => { loadVideo(video, src).then(newLoader => { setVideoLoader(newLoader); }).catch(error => { console.error('Error loading video:', error); setState(prev => ({ ...prev, error: error, isLoading: false })); // Fallback to native video element video.src = src; }); }, 50); return () => { clearTimeout(timeoutId); if (videoLoader === null || videoLoader === void 0 ? void 0 : videoLoader.cleanup) { videoLoader.cleanup(); } }; }, [src]); // Remove videoLoader dependency to prevent loops // Initialize subtitles useEffect(() => { const loadSubtitles = async () => { const newParsedSubtitles = {}; for (let i = 0; i < subtitles.length; i++) { const subtitle = subtitles[i]; const cues = await parseSubtitles(subtitle.src); newParsedSubtitles[i] = cues; } setParsedSubtitles(newParsedSubtitles); }; if (subtitles.length > 0) { loadSubtitles(); } }, [subtitles]); // Video event handlers useEffect(() => { const video = videoRef.current; if (!video) return; const handleLoadStart = () => setState(prev => ({ ...prev, isLoading: true })); const handleCanPlay = () => setState(prev => ({ ...prev, isLoading: false })); const handleLoadedMetadata = () => { setState(prev => ({ ...prev, duration: video.duration, isLoading: false })); onLoadedMetadata === null || onLoadedMetadata === void 0 ? void 0 : onLoadedMetadata(video.duration); }; const handleTimeUpdate = () => { setState(prev => ({ ...prev, currentTime: video.currentTime, buffered: video.buffered })); onTimeUpdate === null || onTimeUpdate === void 0 ? void 0 : onTimeUpdate(video.currentTime); }; const handlePlay = () => { setState(prev => ({ ...prev, isPlaying: true })); onPlay === null || onPlay === void 0 ? void 0 : onPlay(); }; const handlePause = () => { setState(prev => ({ ...prev, isPlaying: false })); onPause === null || onPause === void 0 ? void 0 : onPause(); }; const handleEnded = () => { setState(prev => ({ ...prev, isPlaying: false })); onEnded === null || onEnded === void 0 ? void 0 : onEnded(); }; const handleVolumeChange = () => { setState(prev => ({ ...prev, volume: video.volume, isMuted: video.muted })); }; video.addEventListener('loadstart', handleLoadStart); video.addEventListener('canplay', handleCanPlay); video.addEventListener('loadedmetadata', handleLoadedMetadata); video.addEventListener('timeupdate', handleTimeUpdate); video.addEventListener('play', handlePlay); video.addEventListener('pause', handlePause); video.addEventListener('ended', handleEnded); video.addEventListener('volumechange', handleVolumeChange); return () => { video.removeEventListener('loadstart', handleLoadStart); video.removeEventListener('canplay', handleCanPlay); video.removeEventListener('loadedmetadata', handleLoadedMetadata); video.removeEventListener('timeupdate', handleTimeUpdate); video.removeEventListener('play', handlePlay); video.removeEventListener('pause', handlePause); video.removeEventListener('ended', handleEnded); video.removeEventListener('volumechange', handleVolumeChange); }; }, [onPlay, onPause, onEnded, onTimeUpdate, onLoadedMetadata]); // Fullscreen handling useEffect(() => { const handleFullscreenChange = () => { const isCurrentlyFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); setState(prev => ({ ...prev, isFullscreen: isCurrentlyFullscreen })); }; document.addEventListener('fullscreenchange', handleFullscreenChange); document.addEventListener('webkitfullscreenchange', handleFullscreenChange); document.addEventListener('mozfullscreenchange', handleFullscreenChange); document.addEventListener('MSFullscreenChange', handleFullscreenChange); return () => { document.removeEventListener('fullscreenchange', handleFullscreenChange); document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); document.removeEventListener('mozfullscreenchange', handleFullscreenChange); document.removeEventListener('MSFullscreenChange', handleFullscreenChange); }; }, []); // Auto-hide controls const resetControlsTimeout = useCallback(() => { if (hideControlsTimeout) { clearTimeout(hideControlsTimeout); } setState(prev => ({ ...prev, showControls: true })); const timeout = setTimeout(() => { if (state.isPlaying) { setState(prev => ({ ...prev, showControls: false })); } }, 3000); setHideControlsTimeout(timeout); }, [hideControlsTimeout, state.isPlaying]); const togglePlay = async () => { const video = videoRef.current; if (!video) return; try { if (state.isPlaying) { video.pause(); } else { // Ensure video is ready before playing if (video.readyState >= 2) { // HAVE_CURRENT_DATA await video.play(); } else { // Wait for video to load enough data const playWhenReady = () => { video.removeEventListener('canplay', playWhenReady); video.play().catch(error => { console.warn('Play request failed:', error); setState(prev => ({ ...prev, error: error })); }); }; video.addEventListener('canplay', playWhenReady); } } } catch (error) { console.warn('Play request failed:', error); setState(prev => ({ ...prev, error: error })); } }; const toggleMute = () => { const video = videoRef.current; if (!video) return; video.muted = !video.muted; }; const setVolume = (volume) => { const video = videoRef.current; if (!video) return; video.volume = Math.max(0, Math.min(1, volume)); }; const seekTo = (time) => { const video = videoRef.current; if (!video) return; video.currentTime = Math.max(0, Math.min(state.duration, time)); }; const toggleFullscreen = async () => { const container = containerRef.current; if (!container) return; try { if (state.isFullscreen) { if (document.exitFullscreen) { await document.exitFullscreen(); } else if (document.webkitExitFullscreen) { await document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { await document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { await document.msExitFullscreen(); } } else { if (container.requestFullscreen) { await container.requestFullscreen(); } else if (container.webkitRequestFullscreen) { await container.webkitRequestFullscreen(); } else if (container.mozRequestFullScreen) { await container.mozRequestFullScreen(); } else if (container.msRequestFullscreen) { await container.msRequestFullscreen(); } } } catch (error) { console.error('Error toggling fullscreen:', error); } }; const setPlaybackRate = (rate) => { const video = videoRef.current; if (!video) return; video.playbackRate = rate; setState(prev => ({ ...prev, playbackRate: rate })); }; const handleProgressClick = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; seekTo(percent * state.duration); }; const handleVolumeClick = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; setVolume(percent); }; const handleMouseMove = () => { resetControlsTimeout(); }; const getCurrentSubtitleText = () => { if (state.selectedSubtitle === null || !parsedSubtitles[state.selectedSubtitle]) { return ''; } return getCurrentSubtitle(parsedSubtitles[state.selectedSubtitle], state.currentTime); }; const getBufferedPercent = () => { if (!state.buffered || state.duration === 0) return 0; const buffered = state.buffered; for (let i = 0; i < buffered.length; i++) { if (buffered.start(i) <= state.currentTime && state.currentTime <= buffered.end(i)) { return (buffered.end(i) / state.duration) * 100; } } return 0; }; const currentSubtitleText = getCurrentSubtitleText(); return (jsx("div", { ref: containerRef, className: `serika-video-player serika-video-player-${theme} ${state.isFullscreen ? 'serika-video-player-fullscreen' : ''} ${className || ''}`, style: { width: typeof width === 'number' ? `${width}px` : width, height: typeof height === 'number' ? `${height}px` : height, aspectRatio: height === 'auto' ? '16/9' : undefined, // CSS variables for runtime customization ['--serika-border-radius']: rounded !== undefined ? (typeof rounded === 'number' ? `${rounded}px` : rounded) : undefined, ['--serika-ambient-opacity']: ambient ? String(Math.max(0, Math.min(1, ambientIntensity))) : undefined, ['--serika-ambient-blur']: ambient ? `${Math.max(0, ambientBlur)}px` : undefined, ...style }, onMouseMove: handleMouseMove, onMouseLeave: () => state.isPlaying && setState(prev => ({ ...prev, showControls: false })), children: jsxs("div", { className: "serika-video-player-video-container", onClick: togglePlay, children: [ambient && jsx("div", { className: "serika-video-player-ambient" }), jsx("video", { ref: videoRef, className: "serika-video-player-video-element", poster: poster, autoPlay: autoPlay && !state.isLoading, loop: loop, muted: state.isMuted, preload: preload, playsInline: true }), state.isLoading && (jsxs("div", { className: "serika-video-player-loading", children: [jsx("div", { className: "serika-video-player-spinner" }), jsx("div", { children: t.loading || 'Loading...' })] })), state.error && (jsxs("div", { className: "serika-video-player-error", children: [jsx("div", { className: "serika-video-player-error-icon", children: "\u26A0\uFE0F" }), jsx("div", { children: "Error loading video" })] })), !state.isPlaying && !state.isLoading && (jsx("button", { className: "serika-video-player-center-button", onClick: togglePlay, children: jsx(PlayIcon, {}) })), currentSubtitleText && (jsx("div", { className: "serika-video-player-subtitle-display", children: currentSubtitleText })), controls && (jsxs("div", { className: `serika-video-player-controls ${!state.showControls ? 'serika-video-player-controls-hidden' : ''}`, children: [jsxs("div", { className: "serika-video-player-progress-container", ref: progressRef, onClick: handleProgressClick, children: [jsx("div", { className: "serika-video-player-progress-buffer", style: { width: `${getBufferedPercent()}%` } }), jsx("div", { className: "serika-video-player-progress-bar", style: { width: `${(state.currentTime / state.duration) * 100}%` }, children: jsx("div", { className: "serika-video-player-progress-handle" }) })] }), jsxs("div", { className: "serika-video-player-controls-row", children: [jsx("button", { className: "serika-video-player-control-button serika-video-player-play-button", onClick: togglePlay, children: state.isPlaying ? jsx(PauseIcon, {}) : jsx(PlayIcon, {}) }), jsxs("div", { className: "serika-video-player-volume-container", children: [jsx("button", { className: "serika-video-player-control-button", onClick: toggleMute, children: state.isMuted || state.volume === 0 ? jsx(MuteIcon, {}) : jsx(VolumeIcon, {}) }), jsx("div", { className: "serika-video-player-volume-slider", ref: volumeRef, onClick: handleVolumeClick, children: jsx("div", { className: "serika-video-player-volume-bar", style: { width: `${state.isMuted ? 0 : state.volume * 100}%` } }) })] }), jsxs("div", { className: "serika-video-player-time-display", children: [jsx("span", { children: formatTime(state.currentTime) }), jsx("span", { children: "/" }), jsx("span", { children: formatTime(state.duration) })] }), jsx("div", { style: { flex: 1 } }), jsxs("div", { className: "serika-video-player-settings-container", children: [jsx("button", { className: "serika-video-player-control-button", onClick: () => setState(prev => ({ ...prev, showSettings: !prev.showSettings })), children: jsx(SettingsIcon, {}) }), state.showSettings && (jsxs("div", { className: "serika-video-player-settings-menu", children: [jsxs("div", { className: "serika-video-player-settings-item", children: [jsx("span", { children: t.playbackSpeed }), jsxs("select", { value: state.playbackRate, onChange: (e) => setPlaybackRate(parseFloat(e.target.value)), style: { background: 'transparent', color: 'white', border: 'none' }, children: [jsx("option", { value: 0.5, children: "0.5x" }), jsx("option", { value: 0.75, children: "0.75x" }), jsx("option", { value: 1, children: "1x" }), jsx("option", { value: 1.25, children: "1.25x" }), jsx("option", { value: 1.5, children: "1.5x" }), jsx("option", { value: 2, children: "2x" })] })] }), jsxs("div", { className: "serika-video-player-settings-item", children: [jsx("span", { children: t.subtitles }), jsxs("select", { value: (_a = state.selectedSubtitle) !== null && _a !== void 0 ? _a : -1, onChange: (e) => setState(prev => ({ ...prev, selectedSubtitle: e.target.value === '-1' ? null : parseInt(e.target.value) })), style: { background: 'transparent', color: 'white', border: 'none' }, children: [jsx("option", { value: -1, children: t.noSubtitles }), subtitles.map((subtitle, index) => (jsx("option", { value: index, children: subtitle.label }, index)))] })] })] }))] }), jsx("button", { className: "serika-video-player-control-button", onClick: toggleFullscreen, children: state.isFullscreen ? jsx(ExitFullscreenIcon, {}) : jsx(FullscreenIcon, {}) })] })] }))] }) })); }; // SVG Icons const PlayIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M8 5v14l11-7z" }) })); const PauseIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) })); const VolumeIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" }) })); const MuteIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" }) })); const FullscreenIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })); const ExitFullscreenIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) })); const SettingsIcon = () => (jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" }) })); // https://caniuse.com/mdn-javascript_builtins_number_isfinite const isFiniteNumber = Number.isFinite || function (value) { return typeof value === 'number' && isFinite(value); }; // https://caniuse.com/mdn-javascript_builtins_number_issafeinteger const isSafeInteger = Number.isSafeInteger || function (value) { return typeof value === 'number' && Math.abs(value) <= MAX_SAFE_INTEGER; }; const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; let ErrorTypes = /*#__PURE__*/function (ErrorTypes) { // Identifier for a network error (loading error / timeout ...) ErrorTypes["NETWORK_ERROR"] = "networkError"; // Identifier for a media Error (video/parsing/mediasource error) ErrorTypes["MEDIA_ERROR"] = "mediaError"; // EME (encrypted media extensions) errors ErrorTypes["KEY_SYSTEM_ERROR"] = "keySystemError"; // Identifier for a mux Error (demuxing/remuxing) ErrorTypes["MUX_ERROR"] = "muxError"; // Identifier for all other errors ErrorTypes["OTHER_ERROR"] = "otherError"; return ErrorTypes; }({}); let ErrorDetails = /*#__PURE__*/function (ErrorDetails) { ErrorDetails["KEY_SYSTEM_NO_KEYS"] = "keySystemNoKeys"; ErrorDetails["KEY_SYSTEM_NO_ACCESS"] = "keySystemNoAccess"; ErrorDetails["KEY_SYSTEM_NO_SESSION"] = "keySystemNoSession"; ErrorDetails["KEY_SYSTEM_NO_CONFIGURED_LICENSE"] = "keySystemNoConfiguredLicense"; ErrorDetails["KEY_SYSTEM_LICENSE_REQUEST_FAILED"] = "keySystemLicenseRequestFailed"; ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED"] = "keySystemServerCertificateRequestFailed"; ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED"] = "keySystemServerCertificateUpdateFailed"; ErrorDetails["KEY_SYSTEM_SESSION_UPDATE_FAILED"] = "keySystemSessionUpdateFailed"; ErrorDetails["KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED"] = "keySystemStatusOutputRestricted"; ErrorDetails["KEY_SYSTEM_STATUS_INTERNAL_ERROR"] = "keySystemStatusInternalError"; ErrorDetails["KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR"] = "keySystemDestroyMediaKeysError"; ErrorDetails["KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR"] = "keySystemDestroyCloseSessionError"; ErrorDetails["KEY_SYSTEM_DESTROY_REMOVE_SESSION_ERROR"] = "keySystemDestroyRemoveSessionError"; // Identifier for a manifest load error - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["MANIFEST_LOAD_ERROR"] = "manifestLoadError"; // Identifier for a manifest load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["MANIFEST_LOAD_TIMEOUT"] = "manifestLoadTimeOut"; // Identifier for a manifest parsing error - data: { url : faulty URL, reason : error reason} ErrorDetails["MANIFEST_PARSING_ERROR"] = "manifestParsingError"; // Identifier for a manifest with only incompatible codecs error - data: { url : faulty URL, reason : error reason} ErrorDetails["MANIFEST_INCOMPATIBLE_CODECS_ERROR"] = "manifestIncompatibleCodecsError"; // Identifier for a level which contains no fragments - data: { url: faulty URL, reason: "no fragments found in level", level: index of the bad level } ErrorDetails["LEVEL_EMPTY_ERROR"] = "levelEmptyError"; // Identifier for a level load error - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["LEVEL_LOAD_ERROR"] = "levelLoadError"; // Identifier for a level load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["LEVEL_LOAD_TIMEOUT"] = "levelLoadTimeOut"; // Identifier for a level parse error - data: { url : faulty URL, error: Error, reason: error message } ErrorDetails["LEVEL_PARSING_ERROR"] = "levelParsingError"; // Identifier for a level switch error - data: { level : faulty level Id, event : error description} ErrorDetails["LEVEL_SWITCH_ERROR"] = "levelSwitchError"; // Identifier for an audio track load error - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["AUDIO_TRACK_LOAD_ERROR"] = "audioTrackLoadError"; // Identifier for an audio track load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["AUDIO_TRACK_LOAD_TIMEOUT"] = "audioTrackLoadTimeOut"; // Identifier for a subtitle track load error - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["SUBTITLE_LOAD_ERROR"] = "subtitleTrackLoadError"; // Identifier for a subtitle track load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} ErrorDetails["SUBTITLE_TRACK_LOAD_TIMEOUT"] = "subtitleTrackLoadTimeOut"; // Identifier for fragment load error - data: { frag : fragment object, response : { code: error code, text: error text }} ErrorDetails["FRAG_LOAD_ERROR"] = "fragLoadError"; // Identifier for fragment load timeout error - data: { frag : fragment object} ErrorDetails["FRAG_LOAD_TIMEOUT"] = "fragLoadTimeOut"; // Identifier for a fragment decryption error event - data: {id : demuxer Id,frag: fragment object, reason : parsing error description } ErrorDetails["FRAG_DECRYPT_ERROR"] = "fragDecryptError"; // Identifier for a fragment parsing error event - data: { id : demuxer Id, reason : parsing error description } // will be renamed DEMUX_PARSING_ERROR and switched to MUX_ERROR in the next major release ErrorDetails["FRAG_PARSING_ERROR"] = "fragParsingError"; // Identifier for a fragment