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
JavaScript
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(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/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