@frameset/plex-player 1.0.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/utils/helpers.ts","../src/hooks/usePlayer.ts","../src/hooks/useKeyboard.ts","../src/utils/vast.ts","../src/hooks/useVast.ts","../src/components/ProgressBar.tsx","../src/components/Icons.tsx","../src/components/VolumeControl.tsx","../src/components/SettingsMenu.tsx","../src/components/AdOverlay.tsx","../src/components/ErrorDisplay.tsx","../src/components/Loader.tsx","../src/components/PlexVideoPlayer.tsx"],"sourcesContent":["// Utility functions for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\n/**\r\n * Format time in seconds to MM:SS or HH:MM:SS format\r\n */\r\nexport const formatTime = (seconds: number): string => {\r\n if (!isFinite(seconds) || isNaN(seconds)) return '0:00';\r\n\r\n const hrs = Math.floor(seconds / 3600);\r\n const mins = Math.floor((seconds % 3600) / 60);\r\n const secs = Math.floor(seconds % 60);\r\n\r\n if (hrs > 0) {\r\n return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\r\n }\r\n\r\n return `${mins}:${secs.toString().padStart(2, '0')}`;\r\n};\r\n\r\n/**\r\n * Parse time string to seconds\r\n */\r\nexport const parseTime = (time: string): number => {\r\n const parts = time.split(':').map(Number);\r\n if (parts.length === 3) {\r\n return parts[0] * 3600 + parts[1] * 60 + parts[2];\r\n }\r\n if (parts.length === 2) {\r\n return parts[0] * 60 + parts[1];\r\n }\r\n return parts[0] || 0;\r\n};\r\n\r\n/**\r\n * Calculate percentage\r\n */\r\nexport const percentage = (value: number, total: number): number => {\r\n if (total === 0) return 0;\r\n return Math.min(100, Math.max(0, (value / total) * 100));\r\n};\r\n\r\n/**\r\n * Clamp a value between min and max\r\n */\r\nexport const clamp = (value: number, min: number, max: number): number => {\r\n return Math.min(max, Math.max(min, value));\r\n};\r\n\r\n/**\r\n * Throttle function execution\r\n */\r\nexport const throttle = <T extends (...args: unknown[]) => unknown>(\r\n func: T,\r\n limit: number\r\n): ((...args: Parameters<T>) => void) => {\r\n let inThrottle = false;\r\n return (...args: Parameters<T>) => {\r\n if (!inThrottle) {\r\n func(...args);\r\n inThrottle = true;\r\n setTimeout(() => (inThrottle = false), limit);\r\n }\r\n };\r\n};\r\n\r\n/**\r\n * Debounce function execution\r\n */\r\nexport const debounce = <T extends (...args: unknown[]) => unknown>(\r\n func: T,\r\n wait: number\r\n): ((...args: Parameters<T>) => void) => {\r\n let timeout: ReturnType<typeof setTimeout> | null = null;\r\n return (...args: Parameters<T>) => {\r\n if (timeout) clearTimeout(timeout);\r\n timeout = setTimeout(() => func(...args), wait);\r\n };\r\n};\r\n\r\n/**\r\n * Check if fullscreen is supported\r\n */\r\nexport const isFullscreenSupported = (): boolean => {\r\n return !!(\r\n document.fullscreenEnabled ||\r\n (document as Document & { webkitFullscreenEnabled?: boolean }).webkitFullscreenEnabled ||\r\n (document as Document & { mozFullScreenEnabled?: boolean }).mozFullScreenEnabled ||\r\n (document as Document & { msFullscreenEnabled?: boolean }).msFullscreenEnabled\r\n );\r\n};\r\n\r\n/**\r\n * Check if Picture-in-Picture is supported\r\n */\r\nexport const isPipSupported = (): boolean => {\r\n return 'pictureInPictureEnabled' in document && document.pictureInPictureEnabled;\r\n};\r\n\r\n/**\r\n * Get current fullscreen element\r\n */\r\nexport const getFullscreenElement = (): Element | null => {\r\n return (\r\n document.fullscreenElement ||\r\n (document as Document & { webkitFullscreenElement?: Element }).webkitFullscreenElement ||\r\n (document as Document & { mozFullScreenElement?: Element }).mozFullScreenElement ||\r\n (document as Document & { msFullscreenElement?: Element }).msFullscreenElement ||\r\n null\r\n );\r\n};\r\n\r\n/**\r\n * Request fullscreen on element\r\n */\r\nexport const requestFullscreen = async (element: HTMLElement): Promise<void> => {\r\n if (element.requestFullscreen) {\r\n await element.requestFullscreen();\r\n } else if ((element as HTMLElement & { webkitRequestFullscreen?: () => Promise<void> }).webkitRequestFullscreen) {\r\n await (element as HTMLElement & { webkitRequestFullscreen: () => Promise<void> }).webkitRequestFullscreen();\r\n } else if ((element as HTMLElement & { mozRequestFullScreen?: () => Promise<void> }).mozRequestFullScreen) {\r\n await (element as HTMLElement & { mozRequestFullScreen: () => Promise<void> }).mozRequestFullScreen();\r\n } else if ((element as HTMLElement & { msRequestFullscreen?: () => Promise<void> }).msRequestFullscreen) {\r\n await (element as HTMLElement & { msRequestFullscreen: () => Promise<void> }).msRequestFullscreen();\r\n }\r\n};\r\n\r\n/**\r\n * Exit fullscreen\r\n */\r\nexport const exitFullscreen = async (): Promise<void> => {\r\n if (document.exitFullscreen) {\r\n await document.exitFullscreen();\r\n } else if ((document as Document & { webkitExitFullscreen?: () => Promise<void> }).webkitExitFullscreen) {\r\n await (document as Document & { webkitExitFullscreen: () => Promise<void> }).webkitExitFullscreen();\r\n } else if ((document as Document & { mozCancelFullScreen?: () => Promise<void> }).mozCancelFullScreen) {\r\n await (document as Document & { mozCancelFullScreen: () => Promise<void> }).mozCancelFullScreen();\r\n } else if ((document as Document & { msExitFullscreen?: () => Promise<void> }).msExitFullscreen) {\r\n await (document as Document & { msExitFullscreen: () => Promise<void> }).msExitFullscreen();\r\n }\r\n};\r\n\r\n/**\r\n * Detect video type from URL\r\n */\r\nexport const detectVideoType = (url: string): string => {\r\n const extension = url.split('?')[0].split('.').pop()?.toLowerCase();\r\n \r\n const mimeTypes: Record<string, string> = {\r\n mp4: 'video/mp4',\r\n webm: 'video/webm',\r\n ogg: 'video/ogg',\r\n ogv: 'video/ogg',\r\n m3u8: 'application/x-mpegURL',\r\n mpd: 'application/dash+xml',\r\n mov: 'video/quicktime',\r\n avi: 'video/x-msvideo',\r\n mkv: 'video/x-matroska',\r\n };\r\n\r\n return mimeTypes[extension || ''] || 'video/mp4';\r\n};\r\n\r\n/**\r\n * Check if HLS is natively supported\r\n */\r\nexport const isHlsNativelySupported = (): boolean => {\r\n const video = document.createElement('video');\r\n return video.canPlayType('application/vnd.apple.mpegurl') !== '';\r\n};\r\n\r\n/**\r\n * Generate unique ID\r\n */\r\nexport const generateId = (): string => {\r\n return `plex-${Math.random().toString(36).substring(2, 11)}`;\r\n};\r\n\r\n/**\r\n * Check if device is mobile\r\n */\r\nexport const isMobile = (): boolean => {\r\n return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(\r\n navigator.userAgent\r\n );\r\n};\r\n\r\n/**\r\n * Check if device is touch-enabled\r\n */\r\nexport const isTouchDevice = (): boolean => {\r\n return 'ontouchstart' in window || navigator.maxTouchPoints > 0;\r\n};\r\n\r\n/**\r\n * Parse buffered time ranges\r\n */\r\nexport const getBufferedEnd = (video: HTMLVideoElement): number => {\r\n if (video.buffered.length === 0) return 0;\r\n \r\n const currentTime = video.currentTime;\r\n for (let i = 0; i < video.buffered.length; i++) {\r\n if (video.buffered.start(i) <= currentTime && video.buffered.end(i) >= currentTime) {\r\n return video.buffered.end(i);\r\n }\r\n }\r\n \r\n return video.buffered.end(video.buffered.length - 1);\r\n};\r\n\r\n/**\r\n * Check browser support for specific video format\r\n */\r\nexport const canPlayType = (type: string): boolean => {\r\n const video = document.createElement('video');\r\n const result = video.canPlayType(type);\r\n return result === 'probably' || result === 'maybe';\r\n};\r\n","// usePlayer Hook for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport { useState, useRef, useCallback, useEffect } from 'react';\r\nimport {\r\n PlayerState,\r\n UsePlayerOptions,\r\n UsePlayerReturn,\r\n} from '../types';\r\nimport {\r\n requestFullscreen,\r\n exitFullscreen,\r\n getFullscreenElement,\r\n getBufferedEnd,\r\n isPipSupported,\r\n} from '../utils/helpers';\r\n\r\nconst initialState: PlayerState = {\r\n isPlaying: false,\r\n isPaused: true,\r\n isEnded: false,\r\n isBuffering: false,\r\n isSeeking: false,\r\n isFullscreen: false,\r\n isPip: false,\r\n isMuted: false,\r\n isAdPlaying: false,\r\n volume: 1,\r\n currentTime: 0,\r\n duration: 0,\r\n buffered: 0,\r\n playbackRate: 1,\r\n currentQuality: null,\r\n error: null,\r\n};\r\n\r\nexport const usePlayer = (options: UsePlayerOptions = {}): UsePlayerReturn => {\r\n const {\r\n autoPlay = false,\r\n muted = false,\r\n loop = false,\r\n volume: initialVolume = 1,\r\n playbackRate: initialRate = 1,\r\n } = options;\r\n\r\n const videoRef = useRef<HTMLVideoElement>(null);\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const [state, setState] = useState<PlayerState>({\r\n ...initialState,\r\n volume: initialVolume,\r\n isMuted: muted,\r\n playbackRate: initialRate,\r\n });\r\n\r\n // Update state helper\r\n const updateState = useCallback((updates: Partial<PlayerState>) => {\r\n setState((prev) => ({ ...prev, ...updates }));\r\n }, []);\r\n\r\n // Play\r\n const play = useCallback(async () => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n try {\r\n await video.play();\r\n updateState({ isPlaying: true, isPaused: false, isEnded: false });\r\n } catch (error) {\r\n console.error('Play failed:', error);\r\n }\r\n }, [updateState]);\r\n\r\n // Pause\r\n const pause = useCallback(() => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n video.pause();\r\n updateState({ isPlaying: false, isPaused: true });\r\n }, [updateState]);\r\n\r\n // Toggle play/pause\r\n const togglePlay = useCallback(() => {\r\n if (state.isPlaying) {\r\n pause();\r\n } else {\r\n play();\r\n }\r\n }, [state.isPlaying, play, pause]);\r\n\r\n // Seek\r\n const seek = useCallback((time: number) => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n const clampedTime = Math.max(0, Math.min(time, video.duration || 0));\r\n video.currentTime = clampedTime;\r\n updateState({ currentTime: clampedTime });\r\n }, [updateState]);\r\n\r\n // Set volume\r\n const setVolume = useCallback((volume: number) => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n const clampedVolume = Math.max(0, Math.min(1, volume));\r\n video.volume = clampedVolume;\r\n video.muted = clampedVolume === 0;\r\n updateState({\r\n volume: clampedVolume,\r\n isMuted: clampedVolume === 0,\r\n });\r\n }, [updateState]);\r\n\r\n // Toggle mute\r\n const toggleMute = useCallback(() => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n video.muted = !video.muted;\r\n updateState({ isMuted: video.muted });\r\n }, [updateState]);\r\n\r\n // Set playback rate\r\n const setPlaybackRate = useCallback((rate: number) => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n video.playbackRate = rate;\r\n updateState({ playbackRate: rate });\r\n }, [updateState]);\r\n\r\n // Enter fullscreen\r\n const enterFullscreen = useCallback(async () => {\r\n const container = containerRef.current;\r\n if (!container) return;\r\n\r\n try {\r\n await requestFullscreen(container);\r\n updateState({ isFullscreen: true });\r\n } catch (error) {\r\n console.error('Fullscreen failed:', error);\r\n }\r\n }, [updateState]);\r\n\r\n // Exit fullscreen\r\n const exitFullscreenFn = useCallback(async () => {\r\n try {\r\n await exitFullscreen();\r\n updateState({ isFullscreen: false });\r\n } catch (error) {\r\n console.error('Exit fullscreen failed:', error);\r\n }\r\n }, [updateState]);\r\n\r\n // Toggle fullscreen\r\n const toggleFullscreen = useCallback(async () => {\r\n if (state.isFullscreen) {\r\n await exitFullscreenFn();\r\n } else {\r\n await enterFullscreen();\r\n }\r\n }, [state.isFullscreen, enterFullscreen, exitFullscreenFn]);\r\n\r\n // Enter Picture-in-Picture\r\n const enterPip = useCallback(async () => {\r\n const video = videoRef.current;\r\n if (!video || !isPipSupported()) return;\r\n\r\n try {\r\n await video.requestPictureInPicture();\r\n updateState({ isPip: true });\r\n } catch (error) {\r\n console.error('PiP failed:', error);\r\n }\r\n }, [updateState]);\r\n\r\n // Exit Picture-in-Picture\r\n const exitPipFn = useCallback(async () => {\r\n if (!document.pictureInPictureElement) return;\r\n\r\n try {\r\n await document.exitPictureInPicture();\r\n updateState({ isPip: false });\r\n } catch (error) {\r\n console.error('Exit PiP failed:', error);\r\n }\r\n }, [updateState]);\r\n\r\n // Toggle Picture-in-Picture\r\n const togglePip = useCallback(async () => {\r\n if (state.isPip) {\r\n await exitPipFn();\r\n } else {\r\n await enterPip();\r\n }\r\n }, [state.isPip, enterPip, exitPipFn]);\r\n\r\n // Video event handlers\r\n useEffect(() => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n // Set initial values\r\n video.muted = muted;\r\n video.volume = initialVolume;\r\n video.playbackRate = initialRate;\r\n video.loop = loop;\r\n\r\n const handlers = {\r\n loadedmetadata: () => {\r\n updateState({ duration: video.duration });\r\n },\r\n timeupdate: () => {\r\n updateState({\r\n currentTime: video.currentTime,\r\n buffered: getBufferedEnd(video),\r\n });\r\n },\r\n play: () => {\r\n updateState({ isPlaying: true, isPaused: false, isEnded: false });\r\n },\r\n pause: () => {\r\n updateState({ isPlaying: false, isPaused: true });\r\n },\r\n ended: () => {\r\n updateState({ isPlaying: false, isPaused: true, isEnded: true });\r\n },\r\n waiting: () => {\r\n updateState({ isBuffering: true });\r\n },\r\n canplay: () => {\r\n updateState({ isBuffering: false });\r\n },\r\n seeking: () => {\r\n updateState({ isSeeking: true });\r\n },\r\n seeked: () => {\r\n updateState({ isSeeking: false });\r\n },\r\n volumechange: () => {\r\n updateState({\r\n volume: video.volume,\r\n isMuted: video.muted,\r\n });\r\n },\r\n ratechange: () => {\r\n updateState({ playbackRate: video.playbackRate });\r\n },\r\n error: () => {\r\n updateState({ error: video.error });\r\n },\r\n enterpictureinpicture: () => {\r\n updateState({ isPip: true });\r\n },\r\n leavepictureinpicture: () => {\r\n updateState({ isPip: false });\r\n },\r\n };\r\n\r\n // Add event listeners\r\n Object.entries(handlers).forEach(([event, handler]) => {\r\n video.addEventListener(event, handler);\r\n });\r\n\r\n // Autoplay\r\n if (autoPlay) {\r\n play();\r\n }\r\n\r\n return () => {\r\n Object.entries(handlers).forEach(([event, handler]) => {\r\n video.removeEventListener(event, handler);\r\n });\r\n };\r\n }, [autoPlay, muted, loop, initialVolume, initialRate, play, updateState]);\r\n\r\n // Fullscreen change handler\r\n useEffect(() => {\r\n const handleFullscreenChange = () => {\r\n const isFs = !!getFullscreenElement();\r\n updateState({ isFullscreen: isFs });\r\n };\r\n\r\n document.addEventListener('fullscreenchange', handleFullscreenChange);\r\n document.addEventListener('webkitfullscreenchange', handleFullscreenChange);\r\n document.addEventListener('mozfullscreenchange', handleFullscreenChange);\r\n document.addEventListener('MSFullscreenChange', handleFullscreenChange);\r\n\r\n return () => {\r\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\r\n document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);\r\n document.removeEventListener('mozfullscreenchange', handleFullscreenChange);\r\n document.removeEventListener('MSFullscreenChange', handleFullscreenChange);\r\n };\r\n }, [updateState]);\r\n\r\n return {\r\n state,\r\n videoRef,\r\n containerRef,\r\n play,\r\n pause,\r\n togglePlay,\r\n seek,\r\n setVolume,\r\n toggleMute,\r\n setPlaybackRate,\r\n enterFullscreen,\r\n exitFullscreen: exitFullscreenFn,\r\n toggleFullscreen,\r\n enterPip,\r\n exitPip: exitPipFn,\r\n togglePip,\r\n };\r\n};\r\n\r\nexport default usePlayer;\r\n","// useKeyboard Hook for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport { useEffect, useCallback } from 'react';\r\nimport { HotkeyConfig } from '../types';\r\n\r\nconst DEFAULT_HOTKEYS: Required<HotkeyConfig> = {\r\n play: 'Space',\r\n mute: 'm',\r\n fullscreen: 'f',\r\n pip: 'p',\r\n seekForward: 'ArrowRight',\r\n seekBackward: 'ArrowLeft',\r\n volumeUp: 'ArrowUp',\r\n volumeDown: 'ArrowDown',\r\n};\r\n\r\ninterface UseKeyboardOptions {\r\n enabled: boolean;\r\n hotkeys?: HotkeyConfig;\r\n onPlay: () => void;\r\n onMute: () => void;\r\n onFullscreen: () => void;\r\n onPip: () => void;\r\n onSeek: (delta: number) => void;\r\n onVolume: (delta: number) => void;\r\n containerRef: React.RefObject<HTMLDivElement>;\r\n}\r\n\r\nexport const useKeyboard = ({\r\n enabled,\r\n hotkeys = {},\r\n onPlay,\r\n onMute,\r\n onFullscreen,\r\n onPip,\r\n onSeek,\r\n onVolume,\r\n containerRef,\r\n}: UseKeyboardOptions): void => {\r\n const mergedHotkeys = { ...DEFAULT_HOTKEYS, ...hotkeys };\r\n\r\n const handleKeyDown = useCallback(\r\n (event: KeyboardEvent) => {\r\n if (!enabled) return;\r\n\r\n // Ignore if typing in an input\r\n const target = event.target as HTMLElement;\r\n if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\r\n return;\r\n }\r\n\r\n // Check if focus is within the player container\r\n const container = containerRef.current;\r\n if (!container || !container.contains(document.activeElement)) {\r\n return;\r\n }\r\n\r\n const key = event.key;\r\n\r\n switch (key) {\r\n case mergedHotkeys.play:\r\n case ' ':\r\n event.preventDefault();\r\n onPlay();\r\n break;\r\n case mergedHotkeys.mute:\r\n case 'M':\r\n event.preventDefault();\r\n onMute();\r\n break;\r\n case mergedHotkeys.fullscreen:\r\n case 'F':\r\n event.preventDefault();\r\n onFullscreen();\r\n break;\r\n case mergedHotkeys.pip:\r\n case 'P':\r\n event.preventDefault();\r\n onPip();\r\n break;\r\n case mergedHotkeys.seekForward:\r\n event.preventDefault();\r\n onSeek(event.shiftKey ? 30 : 10);\r\n break;\r\n case mergedHotkeys.seekBackward:\r\n event.preventDefault();\r\n onSeek(event.shiftKey ? -30 : -10);\r\n break;\r\n case mergedHotkeys.volumeUp:\r\n event.preventDefault();\r\n onVolume(0.1);\r\n break;\r\n case mergedHotkeys.volumeDown:\r\n event.preventDefault();\r\n onVolume(-0.1);\r\n break;\r\n case '0':\r\n case '1':\r\n case '2':\r\n case '3':\r\n case '4':\r\n case '5':\r\n case '6':\r\n case '7':\r\n case '8':\r\n case '9':\r\n event.preventDefault();\r\n // Seek to percentage of video\r\n onSeek(-Infinity); // Will be handled by caller with percent\r\n break;\r\n default:\r\n break;\r\n }\r\n },\r\n [enabled, mergedHotkeys, onPlay, onMute, onFullscreen, onPip, onSeek, onVolume, containerRef]\r\n );\r\n\r\n useEffect(() => {\r\n if (!enabled) return;\r\n\r\n document.addEventListener('keydown', handleKeyDown);\r\n\r\n return () => {\r\n document.removeEventListener('keydown', handleKeyDown);\r\n };\r\n }, [enabled, handleKeyDown]);\r\n};\r\n\r\nexport default useKeyboard;\r\n","// VAST Ads Parser for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport { VastAdInfo, VastConfig } from '../types';\r\n\r\ninterface VastMediaFile {\r\n url: string;\r\n type: string;\r\n width: number;\r\n height: number;\r\n bitrate?: number;\r\n}\r\n\r\ninterface VastAd {\r\n id: string;\r\n title: string;\r\n description?: string;\r\n duration: number;\r\n skipOffset?: number;\r\n clickThrough?: string;\r\n clickTracking?: string[];\r\n impressionUrls: string[];\r\n mediaFiles: VastMediaFile[];\r\n trackingEvents: Record<string, string[]>;\r\n}\r\n\r\ninterface VastResponse {\r\n ads: VastAd[];\r\n error?: string;\r\n}\r\n\r\n/**\r\n * Parse VAST XML response\r\n */\r\nexport const parseVastXml = (xmlString: string): VastResponse => {\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(xmlString, 'text/xml');\r\n \r\n const parseError = doc.querySelector('parsererror');\r\n if (parseError) {\r\n return { ads: [], error: 'Failed to parse VAST XML' };\r\n }\r\n\r\n const ads: VastAd[] = [];\r\n const adElements = doc.querySelectorAll('Ad');\r\n\r\n adElements.forEach((adElement) => {\r\n const inLine = adElement.querySelector('InLine');\r\n if (!inLine) return;\r\n\r\n const id = adElement.getAttribute('id') || '';\r\n const title = inLine.querySelector('AdTitle')?.textContent || '';\r\n const description = inLine.querySelector('Description')?.textContent || undefined;\r\n\r\n // Parse creatives\r\n const creatives = inLine.querySelectorAll('Creative');\r\n let duration = 0;\r\n let skipOffset: number | undefined;\r\n let clickThrough: string | undefined;\r\n const clickTracking: string[] = [];\r\n const mediaFiles: VastMediaFile[] = [];\r\n const trackingEvents: Record<string, string[]> = {};\r\n\r\n creatives.forEach((creative) => {\r\n const linear = creative.querySelector('Linear');\r\n if (!linear) return;\r\n\r\n // Duration\r\n const durationStr = linear.querySelector('Duration')?.textContent;\r\n if (durationStr) {\r\n duration = parseVastDuration(durationStr);\r\n }\r\n\r\n // Skip offset\r\n const skipOffsetAttr = linear.getAttribute('skipoffset');\r\n if (skipOffsetAttr) {\r\n skipOffset = parseVastDuration(skipOffsetAttr);\r\n }\r\n\r\n // Media files\r\n const mediaFileElements = linear.querySelectorAll('MediaFile');\r\n mediaFileElements.forEach((mediaFile) => {\r\n const url = mediaFile.textContent?.trim();\r\n if (url) {\r\n mediaFiles.push({\r\n url,\r\n type: mediaFile.getAttribute('type') || 'video/mp4',\r\n width: parseInt(mediaFile.getAttribute('width') || '0', 10),\r\n height: parseInt(mediaFile.getAttribute('height') || '0', 10),\r\n bitrate: parseInt(mediaFile.getAttribute('bitrate') || '0', 10) || undefined,\r\n });\r\n }\r\n });\r\n\r\n // Video clicks\r\n const videoClicks = linear.querySelector('VideoClicks');\r\n if (videoClicks) {\r\n clickThrough = videoClicks.querySelector('ClickThrough')?.textContent?.trim();\r\n videoClicks.querySelectorAll('ClickTracking').forEach((ct) => {\r\n const url = ct.textContent?.trim();\r\n if (url) clickTracking.push(url);\r\n });\r\n }\r\n\r\n // Tracking events\r\n const trackingEventsEl = linear.querySelectorAll('TrackingEvents Tracking');\r\n trackingEventsEl.forEach((tracking) => {\r\n const event = tracking.getAttribute('event');\r\n const url = tracking.textContent?.trim();\r\n if (event && url) {\r\n if (!trackingEvents[event]) {\r\n trackingEvents[event] = [];\r\n }\r\n trackingEvents[event].push(url);\r\n }\r\n });\r\n });\r\n\r\n // Impression URLs\r\n const impressionUrls: string[] = [];\r\n inLine.querySelectorAll('Impression').forEach((impression) => {\r\n const url = impression.textContent?.trim();\r\n if (url) impressionUrls.push(url);\r\n });\r\n\r\n if (mediaFiles.length > 0) {\r\n ads.push({\r\n id,\r\n title,\r\n description,\r\n duration,\r\n skipOffset,\r\n clickThrough,\r\n clickTracking,\r\n impressionUrls,\r\n mediaFiles,\r\n trackingEvents,\r\n });\r\n }\r\n });\r\n\r\n return { ads };\r\n};\r\n\r\n/**\r\n * Parse VAST duration string (HH:MM:SS or HH:MM:SS.mmm)\r\n */\r\nconst parseVastDuration = (duration: string): number => {\r\n if (duration.includes('%')) {\r\n return -1; // Percentage-based, handle separately\r\n }\r\n\r\n const parts = duration.split(':');\r\n if (parts.length !== 3) return 0;\r\n\r\n const hours = parseInt(parts[0], 10);\r\n const minutes = parseInt(parts[1], 10);\r\n const seconds = parseFloat(parts[2]);\r\n\r\n return hours * 3600 + minutes * 60 + seconds;\r\n};\r\n\r\n/**\r\n * Fetch and parse VAST ad\r\n */\r\nexport const fetchVastAd = async (config: VastConfig): Promise<VastAd | null> => {\r\n try {\r\n const response = await fetch(config.url, {\r\n method: 'GET',\r\n headers: {\r\n 'Accept': 'application/xml',\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n console.error('VAST fetch failed:', response.status);\r\n return null;\r\n }\r\n\r\n const xmlString = await response.text();\r\n const result = parseVastXml(xmlString);\r\n\r\n if (result.error || result.ads.length === 0) {\r\n console.error('VAST parse error:', result.error || 'No ads found');\r\n return null;\r\n }\r\n\r\n const ad = result.ads[0];\r\n \r\n // Apply skip delay from config if not in VAST\r\n if (config.skipDelay !== undefined && ad.skipOffset === undefined) {\r\n ad.skipOffset = config.skipDelay;\r\n }\r\n\r\n return ad;\r\n } catch (error) {\r\n console.error('VAST fetch error:', error);\r\n return null;\r\n }\r\n};\r\n\r\n/**\r\n * Select best media file based on device capabilities\r\n */\r\nexport const selectBestMediaFile = (mediaFiles: VastMediaFile[]): VastMediaFile | null => {\r\n if (mediaFiles.length === 0) return null;\r\n\r\n const video = document.createElement('video');\r\n const supportedFiles = mediaFiles.filter((file) => {\r\n return video.canPlayType(file.type) !== '';\r\n });\r\n\r\n if (supportedFiles.length === 0) return null;\r\n\r\n // Sort by resolution (prefer higher) and bitrate\r\n supportedFiles.sort((a, b) => {\r\n const resA = a.width * a.height;\r\n const resB = b.width * b.height;\r\n if (resA !== resB) return resB - resA;\r\n return (b.bitrate || 0) - (a.bitrate || 0);\r\n });\r\n\r\n // On mobile, prefer lower resolution\r\n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\r\n if (isMobile && supportedFiles.length > 1) {\r\n return supportedFiles[supportedFiles.length - 1];\r\n }\r\n\r\n return supportedFiles[0];\r\n};\r\n\r\n/**\r\n * Fire tracking pixel\r\n */\r\nexport const fireTrackingPixel = (url: string): void => {\r\n const img = new Image();\r\n img.src = url;\r\n};\r\n\r\n/**\r\n * Fire multiple tracking pixels\r\n */\r\nexport const fireTrackingPixels = (urls: string[]): void => {\r\n urls.forEach(fireTrackingPixel);\r\n};\r\n\r\n/**\r\n * Convert VAST ad to player-friendly format\r\n */\r\nexport const convertToAdInfo = (ad: VastAd): VastAdInfo => {\r\n return {\r\n id: ad.id,\r\n title: ad.title,\r\n duration: ad.duration,\r\n skipOffset: ad.skipOffset,\r\n clickThrough: ad.clickThrough,\r\n };\r\n};\r\n\r\nexport type { VastAd, VastMediaFile, VastResponse };\r\n","// useVast Hook for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport { useState, useCallback, useRef, useEffect } from 'react';\r\nimport { VastConfig, VastAdInfo } from '../types';\r\nimport {\r\n fetchVastAd,\r\n selectBestMediaFile,\r\n fireTrackingPixels,\r\n convertToAdInfo,\r\n VastAd,\r\n} from '../utils/vast';\r\n\r\ninterface UseVastOptions {\r\n vastConfig?: VastConfig | VastConfig[];\r\n videoRef: React.RefObject<HTMLVideoElement>;\r\n onAdStart?: (ad: VastAdInfo) => void;\r\n onAdEnd?: () => void;\r\n onAdSkip?: () => void;\r\n onAdError?: (error: Error) => void;\r\n}\r\n\r\ninterface UseVastReturn {\r\n isAdPlaying: boolean;\r\n currentAd: VastAdInfo | null;\r\n adTimeRemaining: number;\r\n canSkip: boolean;\r\n skipAd: () => void;\r\n handleAdClick: () => void;\r\n checkForAd: (currentTime: number, duration: number) => void;\r\n}\r\n\r\nexport const useVast = ({\r\n vastConfig,\r\n videoRef,\r\n onAdStart,\r\n onAdEnd,\r\n onAdSkip,\r\n onAdError,\r\n}: UseVastOptions): UseVastReturn => {\r\n const [isAdPlaying, setIsAdPlaying] = useState(false);\r\n const [currentAd, setCurrentAd] = useState<VastAdInfo | null>(null);\r\n const [adTimeRemaining, setAdTimeRemaining] = useState(0);\r\n const [canSkip, setCanSkip] = useState(false);\r\n\r\n const currentVastAd = useRef<VastAd | null>(null);\r\n const originalSrc = useRef<string>('');\r\n const originalTime = useRef<number>(0);\r\n const playedPositions = useRef<Set<string>>(new Set());\r\n const adIntervalRef = useRef<number | null>(null);\r\n\r\n // Normalize config to array\r\n const vastConfigs = Array.isArray(vastConfig) ? vastConfig : vastConfig ? [vastConfig] : [];\r\n\r\n // Play ad\r\n const playAd = useCallback(\r\n async (config: VastConfig) => {\r\n const video = videoRef.current;\r\n if (!video || isAdPlaying) return;\r\n\r\n try {\r\n const ad = await fetchVastAd(config);\r\n if (!ad) {\r\n onAdError?.(new Error('Failed to fetch VAST ad'));\r\n return;\r\n }\r\n\r\n const mediaFile = selectBestMediaFile(ad.mediaFiles);\r\n if (!mediaFile) {\r\n onAdError?.(new Error('No compatible media file found'));\r\n return;\r\n }\r\n\r\n // Store original video state\r\n originalSrc.current = video.currentSrc;\r\n originalTime.current = video.currentTime;\r\n\r\n // Fire impression pixels\r\n fireTrackingPixels(ad.impressionUrls);\r\n\r\n // Set up ad\r\n currentVastAd.current = ad;\r\n setCurrentAd(convertToAdInfo(ad));\r\n setIsAdPlaying(true);\r\n setAdTimeRemaining(ad.duration);\r\n setCanSkip(ad.skipOffset === undefined || ad.skipOffset === 0);\r\n\r\n // Change video source to ad\r\n video.src = mediaFile.url;\r\n video.currentTime = 0;\r\n await video.play();\r\n\r\n // Fire start tracking\r\n if (ad.trackingEvents.start) {\r\n fireTrackingPixels(ad.trackingEvents.start);\r\n }\r\n\r\n onAdStart?.(convertToAdInfo(ad));\r\n\r\n // Start ad timer\r\n adIntervalRef.current = window.setInterval(() => {\r\n const remaining = Math.max(0, ad.duration - video.currentTime);\r\n setAdTimeRemaining(remaining);\r\n\r\n // Check if can skip\r\n if (ad.skipOffset && video.currentTime >= ad.skipOffset) {\r\n setCanSkip(true);\r\n }\r\n\r\n // Track quartiles\r\n const percent = (video.currentTime / ad.duration) * 100;\r\n if (percent >= 25 && ad.trackingEvents.firstQuartile) {\r\n fireTrackingPixels(ad.trackingEvents.firstQuartile);\r\n }\r\n if (percent >= 50 && ad.trackingEvents.midpoint) {\r\n fireTrackingPixels(ad.trackingEvents.midpoint);\r\n }\r\n if (percent >= 75 && ad.trackingEvents.thirdQuartile) {\r\n fireTrackingPixels(ad.trackingEvents.thirdQuartile);\r\n }\r\n }, 250);\r\n\r\n // Handle ad completion\r\n const handleAdEnd = () => {\r\n if (ad.trackingEvents.complete) {\r\n fireTrackingPixels(ad.trackingEvents.complete);\r\n }\r\n endAd();\r\n };\r\n\r\n video.addEventListener('ended', handleAdEnd, { once: true });\r\n } catch (error) {\r\n onAdError?.(error instanceof Error ? error : new Error('Ad playback failed'));\r\n endAd();\r\n }\r\n },\r\n [videoRef, isAdPlaying, onAdStart, onAdError]\r\n );\r\n\r\n // End ad and restore original video\r\n const endAd = useCallback(() => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n // Clear interval\r\n if (adIntervalRef.current) {\r\n clearInterval(adIntervalRef.current);\r\n adIntervalRef.current = null;\r\n }\r\n\r\n // Restore original video\r\n if (originalSrc.current) {\r\n video.src = originalSrc.current;\r\n video.currentTime = originalTime.current;\r\n video.play().catch(() => {});\r\n }\r\n\r\n setIsAdPlaying(false);\r\n setCurrentAd(null);\r\n setAdTimeRemaining(0);\r\n setCanSkip(false);\r\n currentVastAd.current = null;\r\n\r\n onAdEnd?.();\r\n }, [videoRef, onAdEnd]);\r\n\r\n // Skip ad\r\n const skipAd = useCallback(() => {\r\n if (!canSkip || !currentVastAd.current) return;\r\n\r\n const ad = currentVastAd.current;\r\n if (ad.trackingEvents.skip) {\r\n fireTrackingPixels(ad.trackingEvents.skip);\r\n }\r\n\r\n onAdSkip?.();\r\n endAd();\r\n }, [canSkip, endAd, onAdSkip]);\r\n\r\n // Handle ad click\r\n const handleAdClick = useCallback(() => {\r\n if (!currentVastAd.current) return;\r\n\r\n const ad = currentVastAd.current;\r\n if (ad.clickThrough) {\r\n window.open(ad.clickThrough, '_blank');\r\n }\r\n if (ad.clickTracking) {\r\n fireTrackingPixels(ad.clickTracking);\r\n }\r\n }, []);\r\n\r\n // Check for ad at position\r\n const checkForAd = useCallback(\r\n (currentTime: number, duration: number) => {\r\n if (isAdPlaying || vastConfigs.length === 0) return;\r\n\r\n vastConfigs.forEach((config) => {\r\n const position = config.position || 'preroll';\r\n const key = `${position}-${config.midrollTime || 0}`;\r\n\r\n if (playedPositions.current.has(key)) return;\r\n\r\n let shouldPlay = false;\r\n\r\n switch (position) {\r\n case 'preroll':\r\n shouldPlay = currentTime === 0;\r\n break;\r\n case 'midroll':\r\n if (config.midrollTime && currentTime >= config.midrollTime) {\r\n shouldPlay = true;\r\n }\r\n break;\r\n case 'postroll':\r\n shouldPlay = currentTime >= duration - 0.5;\r\n break;\r\n }\r\n\r\n if (shouldPlay) {\r\n playedPositions.current.add(key);\r\n playAd(config);\r\n }\r\n });\r\n },\r\n [vastConfigs, isAdPlaying, playAd]\r\n );\r\n\r\n // Cleanup on unmount\r\n useEffect(() => {\r\n return () => {\r\n if (adIntervalRef.current) {\r\n clearInterval(adIntervalRef.current);\r\n }\r\n };\r\n }, []);\r\n\r\n return {\r\n isAdPlaying,\r\n currentAd,\r\n adTimeRemaining,\r\n canSkip,\r\n skipAd,\r\n handleAdClick,\r\n checkForAd,\r\n };\r\n};\r\n\r\nexport default useVast;\r\n","// Progress Bar Component for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React, { useRef, useState, useCallback, useEffect } from 'react';\r\nimport { formatTime, percentage, clamp } from '../utils/helpers';\r\n\r\ninterface ProgressBarProps {\r\n currentTime: number;\r\n duration: number;\r\n buffered: number;\r\n onSeek: (time: number) => void;\r\n thumbnailPreview?: {\r\n enabled: boolean;\r\n sprites?: string;\r\n interval?: number;\r\n width?: number;\r\n height?: number;\r\n };\r\n disabled?: boolean;\r\n}\r\n\r\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\r\n currentTime,\r\n duration,\r\n buffered,\r\n onSeek,\r\n thumbnailPreview,\r\n disabled = false,\r\n}) => {\r\n const progressRef = useRef<HTMLDivElement>(null);\r\n const [isDragging, setIsDragging] = useState(false);\r\n const [hoverTime, setHoverTime] = useState<number | null>(null);\r\n const [hoverPosition, setHoverPosition] = useState(0);\r\n\r\n const playedPercent = percentage(currentTime, duration);\r\n const bufferedPercent = percentage(buffered, duration);\r\n\r\n const getTimeFromPosition = useCallback(\r\n (clientX: number): number => {\r\n if (!progressRef.current) return 0;\r\n const rect = progressRef.current.getBoundingClientRect();\r\n const percent = clamp((clientX - rect.left) / rect.width, 0, 1);\r\n return percent * duration;\r\n },\r\n [duration]\r\n );\r\n\r\n const handleMouseMove = useCallback(\r\n (e: MouseEvent | React.MouseEvent) => {\r\n if (!progressRef.current) return;\r\n const rect = progressRef.current.getBoundingClientRect();\r\n const clientX = 'clientX' in e ? e.clientX : 0;\r\n const position = clamp(clientX - rect.left, 0, rect.width);\r\n const time = getTimeFromPosition(clientX);\r\n\r\n setHoverPosition(position);\r\n setHoverTime(time);\r\n\r\n if (isDragging) {\r\n onSeek(time);\r\n }\r\n },\r\n [isDragging, getTimeFromPosition, onSeek]\r\n );\r\n\r\n const handleMouseDown = useCallback(\r\n (e: React.MouseEvent) => {\r\n if (disabled) return;\r\n e.preventDefault();\r\n setIsDragging(true);\r\n const time = getTimeFromPosition(e.clientX);\r\n onSeek(time);\r\n },\r\n [disabled, getTimeFromPosition, onSeek]\r\n );\r\n\r\n const handleMouseUp = useCallback(() => {\r\n setIsDragging(false);\r\n }, []);\r\n\r\n const handleMouseEnter = useCallback(\r\n (e: React.MouseEvent) => {\r\n const time = getTimeFromPosition(e.clientX);\r\n setHoverTime(time);\r\n },\r\n [getTimeFromPosition]\r\n );\r\n\r\n const handleMouseLeave = useCallback(() => {\r\n setHoverTime(null);\r\n }, []);\r\n\r\n // Global mouse events for dragging\r\n useEffect(() => {\r\n if (isDragging) {\r\n const mouseMoveHandler = (e: MouseEvent) => handleMouseMove(e);\r\n window.addEventListener('mousemove', mouseMoveHandler);\r\n window.addEventListener('mouseup', handleMouseUp);\r\n\r\n return () => {\r\n window.removeEventListener('mousemove', mouseMoveHandler);\r\n window.removeEventListener('mouseup', handleMouseUp);\r\n };\r\n }\r\n return undefined;\r\n }, [isDragging, handleMouseMove, handleMouseUp]);\r\n\r\n // Touch support\r\n const handleTouchStart = useCallback(\r\n (e: React.TouchEvent) => {\r\n if (disabled) return;\r\n e.preventDefault();\r\n const touch = e.touches[0];\r\n const time = getTimeFromPosition(touch.clientX);\r\n setIsDragging(true);\r\n onSeek(time);\r\n },\r\n [disabled, getTimeFromPosition, onSeek]\r\n );\r\n\r\n const handleTouchMove = useCallback(\r\n (e: React.TouchEvent) => {\r\n if (!isDragging) return;\r\n const touch = e.touches[0];\r\n const time = getTimeFromPosition(touch.clientX);\r\n onSeek(time);\r\n },\r\n [isDragging, getTimeFromPosition, onSeek]\r\n );\r\n\r\n const handleTouchEnd = useCallback(() => {\r\n setIsDragging(false);\r\n }, []);\r\n\r\n return (\r\n <div\r\n ref={progressRef}\r\n className=\"plex-video-player__progress-container\"\r\n onMouseDown={handleMouseDown}\r\n onMouseEnter={handleMouseEnter}\r\n onMouseLeave={handleMouseLeave}\r\n onMouseMove={(e) => handleMouseMove(e.nativeEvent)}\r\n onTouchStart={handleTouchStart}\r\n onTouchMove={handleTouchMove}\r\n onTouchEnd={handleTouchEnd}\r\n role=\"slider\"\r\n aria-label=\"Video progress\"\r\n aria-valuemin={0}\r\n aria-valuemax={duration}\r\n aria-valuenow={currentTime}\r\n aria-valuetext={formatTime(currentTime)}\r\n tabIndex={0}\r\n >\r\n <div className=\"plex-video-player__progress\">\r\n <div\r\n className=\"plex-video-player__progress-buffered\"\r\n style={{ width: `${bufferedPercent}%` }}\r\n />\r\n <div\r\n className=\"plex-video-player__progress-played\"\r\n style={{ width: `${playedPercent}%` }}\r\n />\r\n <div\r\n className=\"plex-video-player__progress-handle\"\r\n style={{ left: `${playedPercent}%` }}\r\n />\r\n </div>\r\n\r\n {/* Thumbnail Preview */}\r\n {thumbnailPreview?.enabled && hoverTime !== null && (\r\n <div\r\n className=\"plex-video-player__thumbnail-preview\"\r\n style={{\r\n left: `${hoverPosition}px`,\r\n width: thumbnailPreview.width || 160,\r\n height: thumbnailPreview.height || 90,\r\n }}\r\n >\r\n {thumbnailPreview.sprites && (\r\n <div\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n backgroundImage: `url(${thumbnailPreview.sprites})`,\r\n backgroundPosition: calculateSpritePosition(\r\n hoverTime,\r\n duration,\r\n thumbnailPreview.interval || 10,\r\n thumbnailPreview.width || 160,\r\n thumbnailPreview.height || 90\r\n ),\r\n backgroundSize: 'cover',\r\n }}\r\n />\r\n )}\r\n <div className=\"plex-video-player__thumbnail-time\">\r\n {formatTime(hoverTime)}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Time tooltip */}\r\n {hoverTime !== null && !thumbnailPreview?.enabled && (\r\n <div\r\n className=\"plex-video-player__thumbnail-preview\"\r\n style={{\r\n left: `${hoverPosition}px`,\r\n width: 'auto',\r\n height: 'auto',\r\n padding: '4px 8px',\r\n }}\r\n >\r\n <span style={{ color: 'white', fontSize: '12px' }}>\r\n {formatTime(hoverTime)}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n );\r\n};\r\n\r\n// Calculate sprite background position for thumbnail preview\r\nconst calculateSpritePosition = (\r\n time: number,\r\n _duration: number,\r\n interval: number,\r\n width: number,\r\n height: number\r\n): string => {\r\n const index = Math.floor(time / interval);\r\n const columns = 10; // Assuming 10 columns per row in sprite\r\n const row = Math.floor(index / columns);\r\n const col = index % columns;\r\n\r\n return `-${col * width}px -${row * height}px`;\r\n};\r\n\r\nexport default ProgressBar;\r\n","// Icon components for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React from 'react';\r\n\r\ninterface IconProps {\r\n className?: string;\r\n size?: number;\r\n}\r\n\r\nexport const PlayIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M8 5v14l11-7z\" />\r\n </svg>\r\n);\r\n\r\nexport const PauseIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M6 19h4V5H6v14zm8-14v14h4V5h-4z\" />\r\n </svg>\r\n);\r\n\r\nexport const VolumeHighIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <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\" />\r\n </svg>\r\n);\r\n\r\nexport const VolumeMediumIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z\" />\r\n </svg>\r\n);\r\n\r\nexport const VolumeLowIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M7 9v6h4l5 5V4l-5 5H7z\" />\r\n </svg>\r\n);\r\n\r\nexport const VolumeMuteIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <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\" />\r\n </svg>\r\n);\r\n\r\nexport const FullscreenIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\" />\r\n </svg>\r\n);\r\n\r\nexport const FullscreenExitIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\" />\r\n </svg>\r\n);\r\n\r\nexport const PipIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z\" />\r\n </svg>\r\n);\r\n\r\nexport const PipExitIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM5 7h8v6H5z\" />\r\n </svg>\r\n);\r\n\r\nexport const SettingsIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.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-.04.31-.06.63-.06.94s.02.63.06.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\" />\r\n </svg>\r\n);\r\n\r\nexport const CaptionsIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z\" />\r\n </svg>\r\n);\r\n\r\nexport const SpeedIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M20.38 8.57l-1.23 1.85a8 8 0 0 1-.22 7.58H5.07A8 8 0 0 1 15.58 6.85l1.85-1.23A10 10 0 0 0 3.35 19a2 2 0 0 0 1.72 1h13.85a2 2 0 0 0 1.74-1 10 10 0 0 0-.27-10.44zm-9.79 6.84a2 2 0 0 0 2.83 0l5.66-8.49-8.49 5.66a2 2 0 0 0 0 2.83z\" />\r\n </svg>\r\n);\r\n\r\nexport const QualityIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7.5 13h2v2H7.5zm4.5 0h2v2h-2zm4.5 0h2v2h-2zM7.5 9h2v2H7.5zm4.5 0h2v2h-2zm4.5 0h2v2h-2z\" />\r\n </svg>\r\n);\r\n\r\nexport const ForwardIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z\" />\r\n </svg>\r\n);\r\n\r\nexport const RewindIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z\" />\r\n </svg>\r\n);\r\n\r\nexport const SkipNextIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z\" />\r\n </svg>\r\n);\r\n\r\nexport const SkipPrevIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M6 6h2v12H6zm3.5 6l8.5 6V6z\" />\r\n </svg>\r\n);\r\n\r\nexport const ErrorIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z\" />\r\n </svg>\r\n);\r\n\r\nexport const ExternalLinkIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z\" />\r\n </svg>\r\n);\r\n\r\nexport const CheckIcon: React.FC<IconProps> = ({ className, size = 24 }) => (\r\n <svg\r\n className={className}\r\n width={size}\r\n height={size}\r\n viewBox=\"0 0 24 24\"\r\n fill=\"currentColor\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n >\r\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\" />\r\n </svg>\r\n);\r\n","// Volume Control Component for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React, { useCallback } from 'react';\r\nimport {\r\n VolumeHighIcon,\r\n VolumeMediumIcon,\r\n VolumeLowIcon,\r\n VolumeMuteIcon,\r\n} from './Icons';\r\n\r\ninterface VolumeControlProps {\r\n volume: number;\r\n muted: boolean;\r\n onVolumeChange: (volume: number) => void;\r\n onToggleMute: () => void;\r\n}\r\n\r\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\r\n volume,\r\n muted,\r\n onVolumeChange,\r\n onToggleMute,\r\n}) => {\r\n const handleSliderChange = useCallback(\r\n (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const newVolume = parseFloat(e.target.value);\r\n onVolumeChange(newVolume);\r\n },\r\n [onVolumeChange]\r\n );\r\n\r\n const getVolumeIcon = () => {\r\n if (muted || volume === 0) {\r\n return <VolumeMuteIcon />;\r\n }\r\n if (volume < 0.33) {\r\n return <VolumeLowIcon />;\r\n }\r\n if (volume < 0.66) {\r\n return <VolumeMediumIcon />;\r\n }\r\n return <VolumeHighIcon />;\r\n };\r\n\r\n return (\r\n <div className=\"plex-video-player__volume-container\">\r\n <button\r\n className=\"plex-video-player__btn\"\r\n onClick={onToggleMute}\r\n aria-label={muted ? 'Unmute' : 'Mute'}\r\n title={muted ? 'Unmute (M)' : 'Mute (M)'}\r\n type=\"button\"\r\n >\r\n {getVolumeIcon()}\r\n </button>\r\n <div className=\"plex-video-player__volume-slider-container\">\r\n <input\r\n type=\"range\"\r\n className=\"plex-video-player__volume-slider\"\r\n min={0}\r\n max={1}\r\n step={0.01}\r\n value={muted ? 0 : volume}\r\n onChange={handleSliderChange}\r\n aria-label=\"Volume\"\r\n style={{\r\n background: `linear-gradient(to right, var(--plex-primary) 0%, var(--plex-primary) ${\r\n (muted ? 0 : volume) * 100\r\n }%, var(--plex-progress-bg) ${(muted ? 0 : volume) * 100}%, var(--plex-progress-bg) 100%)`,\r\n }}\r\n />\r\n </div>\r\n </div>\r\n );\r\n};\r\n\r\nexport default VolumeControl;\r\n","// Settings Menu Component for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React, { useState, useRef, useEffect, useCallback } from 'react';\r\nimport { SettingsIcon, CheckIcon } from './Icons';\r\nimport { VideoSource, TextTrack } from '../types';\r\n\r\ninterface SettingsMenuProps {\r\n playbackRate: number;\r\n playbackSpeeds: number[];\r\n onPlaybackRateChange: (rate: number) => void;\r\n qualityEnabled: boolean;\r\n sources?: VideoSource[];\r\n currentQuality?: string;\r\n onQualityChange?: (quality: string) => void;\r\n captionsEnabled?: boolean;\r\n textTracks?: TextTrack[];\r\n currentTrack?: string;\r\n onTrackChange?: (track: string | null) => void;\r\n}\r\n\r\ntype MenuView = 'main' | 'speed' | 'quality' | 'captions';\r\n\r\nexport const SettingsMenu: React.FC<SettingsMenuProps> = ({\r\n playbackRate,\r\n playbackSpeeds,\r\n onPlaybackRateChange,\r\n qualityEnabled,\r\n sources,\r\n currentQuality,\r\n onQualityChange,\r\n captionsEnabled,\r\n textTracks,\r\n currentTrack,\r\n onTrackChange,\r\n}) => {\r\n const [isOpen, setIsOpen] = useState(false);\r\n const [currentView, setCurrentView] = useState<MenuView>('main');\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n\r\n // Close menu when clicking outside\r\n useEffect(() => {\r\n const handleClickOutside = (e: MouseEvent) => {\r\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\r\n setIsOpen(false);\r\n setCurrentView('main');\r\n }\r\n };\r\n\r\n if (isOpen) {\r\n document.addEventListener('mousedown', handleClickOutside);\r\n return () => document.removeEventListener('mousedown', handleClickOutside);\r\n }\r\n return undefined;\r\n }, [isOpen]);\r\n\r\n const handleToggle = useCallback(() => {\r\n setIsOpen((prev) => !prev);\r\n setCurrentView('main');\r\n }, []);\r\n\r\n const handleSpeedSelect = useCallback(\r\n (speed: number) => {\r\n onPlaybackRateChange(speed);\r\n setCurrentView('main');\r\n },\r\n [onPlaybackRateChange]\r\n );\r\n\r\n const handleQualitySelect = useCallback(\r\n (quality: string) => {\r\n onQualityChange?.(quality);\r\n setCurrentView('main');\r\n },\r\n [onQualityChange]\r\n );\r\n\r\n const handleTrackSelect = useCallback(\r\n (track: string | null) => {\r\n onTrackChange?.(track);\r\n setCurrentView('main');\r\n },\r\n [onTrackChange]\r\n );\r\n\r\n const renderMainMenu = () => (\r\n <>\r\n <div className=\"plex-video-player__settings-title\">Settings</div>\r\n <button\r\n className=\"plex-video-player__settings-item\"\r\n onClick={() => setCurrentView('speed')}\r\n type=\"button\"\r\n >\r\n <span>Playback Speed</span>\r\n <span>{playbackRate === 1 ? 'Normal' : `${playbackRate}x`}</span>\r\n </button>\r\n {qualityEnabled && sources && sources.length > 1 && (\r\n <button\r\n className=\"plex-video-player__settings-item\"\r\n onClick={() => setCurrentView('quality')}\r\n type=\"button\"\r\n >\r\n <span>Quality</span>\r\n <span>{currentQuality || 'Auto'}</span>\r\n </button>\r\n )}\r\n {captionsEnabled && textTracks && textTracks.length > 0 && (\r\n <button\r\n className=\"plex-video-player__settings-item\"\r\n onClick={() => setCurrentView('captions')}\r\n type=\"button\"\r\n >\r\n <span>Captions</span>\r\n <span>{currentTrack || 'Off'}</span>\r\n </button>\r\n )}\r\n </>\r\n );\r\n\r\n const renderSpeedMenu = () => (\r\n <>\r\n <button\r\n className=\"plex-video-player__settings-title\"\r\n onClick={() => setCurrentView('main')}\r\n style={{ cursor: 'pointer', border: 'none', background: 'transparent', width: '100%', textAlign: 'left' }}\r\n type=\"button\"\r\n >\r\n ← Playback Speed\r\n </button>\r\n <div className=\"plex-video-player__speed-menu\">\r\n {playbackSpeeds.map((speed) => (\r\n <button\r\n key={speed}\r\n className={`plex-video-player__speed-btn ${\r\n playbackRate === speed ? 'plex-video-player__speed-btn--active' : ''\r\n }`}\r\n onClick={() => handleSpeedSelect(speed)}\r\n type=\"button\"\r\n >\r\n {speed === 1 ? 'Normal' : `${speed}x`}\r\n </button>\r\n ))}\r\n </div>\r\n </>\r\n );\r\n\r\n const renderQualityMenu = () => (\r\n <>\r\n <button\r\n className=\"plex-video-player__settings-title\"\r\n onClick={() => setCurrentView('main')}\r\n style={{ cursor: 'pointer', border: 'none', background: 'transparent', width: '100%', textAlign: 'left' }}\r\n type=\"button\"\r\n >\r\n ← Quality\r\n </button>\r\n <div className=\"plex-video-player__quality-menu\">\r\n {sources?.map((source) => (\r\n <button\r\n key={source.quality || source.src}\r\n className={`plex-video-player__settings-item ${\r\n currentQuality === source.quality ? 'plex-video-player__settings-item--active' : ''\r\n }`}\r\n onClick={() => handleQualitySelect(source.quality || source.src)}\r\n type=\"button\"\r\n >\r\n <span>{source.label || source.quality || 'Unknown'}</span>\r\n {currentQuality === source.quality && <CheckIcon size={16} />}\r\n </button>\r\n ))}\r\n </div>\r\n </>\r\n );\r\n\r\n const renderCaptionsMenu = () => (\r\n <>\r\n <button\r\n className=\"plex-video-player__settings-title\"\r\n onClick={() => setCurrentView('main')}\r\n style={{ cursor: 'pointer', border: 'none', background: 'transparent', width: '100%', textAlign: 'left' }}\r\n type=\"button\"\r\n >\r\n ← Captions\r\n </button>\r\n <div className=\"plex-video-player__quality-menu\">\r\n <button\r\n className={`plex-video-player__settings-item ${\r\n !currentTrack ? 'plex-video-player__settings-item--active' : ''\r\n }`}\r\n onClick={() => handleTrackSelect(null)}\r\n type=\"button\"\r\n >\r\n <span>Off</span>\r\n {!currentTrack && <CheckIcon size={16} />}\r\n </button>\r\n {textTracks?.map((track) => (\r\n <button\r\n key={track.srclang}\r\n className={`plex-video-player__settings-item ${\r\n currentTrack === track.srclang ? 'plex-video-player__settings-item--active' : ''\r\n }`}\r\n onClick={() => handleTrackSelect(track.srclang)}\r\n type=\"button\"\r\n >\r\n <span>{track.label}</span>\r\n {currentTrack === track.srclang && <CheckIcon size={16} />}\r\n </button>\r\n ))}\r\n </div>\r\n </>\r\n );\r\n\r\n return (\r\n <div className=\"plex-video-player__settings-container\" ref={containerRef}>\r\n <button\r\n className=\"plex-video-player__btn\"\r\n onClick={handleToggle}\r\n aria-label=\"Settings\"\r\n aria-expanded={isOpen}\r\n title=\"Settings\"\r\n type=\"button\"\r\n >\r\n <SettingsIcon />\r\n </button>\r\n {isOpen && (\r\n <div\r\n className={`plex-video-player__settings-menu ${\r\n isOpen ? 'plex-video-player__settings-menu--open' : ''\r\n }`}\r\n >\r\n {currentView === 'main' && renderMainMenu()}\r\n {currentView === 'speed' && renderSpeedMenu()}\r\n {currentView === 'quality' && renderQualityMenu()}\r\n {currentView === 'captions' && renderCaptionsMenu()}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n};\r\n\r\nexport default SettingsMenu;\r\n","// Ad Overlay Component for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React from 'react';\r\nimport { VastAdInfo } from '../types';\r\nimport { formatTime } from '../utils/helpers';\r\nimport { ExternalLinkIcon } from './Icons';\r\n\r\ninterface AdOverlayProps {\r\n ad: VastAdInfo;\r\n timeRemaining: number;\r\n canSkip: boolean;\r\n onSkip: () => void;\r\n onClick: () => void;\r\n}\r\n\r\nexport const AdOverlay: React.FC<AdOverlayProps> = ({\r\n ad,\r\n timeRemaining,\r\n canSkip,\r\n onSkip,\r\n onClick,\r\n}) => {\r\n const skipText = canSkip\r\n ? 'Skip Ad'\r\n : `Skip in ${Math.ceil(timeRemaining)}s`;\r\n\r\n return (\r\n <div className=\"plex-video-player__ad-overlay\" onClick={onClick}>\r\n {/* Ad Info Badge */}\r\n <div className=\"plex-video-player__ad-info\">\r\n <span className=\"plex-video-player__ad-badge\">Ad</span>\r\n <span>{formatTime(timeRemaining)} remaining</span>\r\n </div>\r\n\r\n {/* Learn More Button */}\r\n {ad.clickThrough && (\r\n <button\r\n className=\"plex-video-player__ad-learn-more\"\r\n onClick={(e) => {\r\n e.stopPropagation();\r\n onClick();\r\n }}\r\n type=\"button\"\r\n >\r\n <ExternalLinkIcon size={16} />\r\n Learn More\r\n </button>\r\n )}\r\n\r\n {/* Skip Button */}\r\n <button\r\n className=\"plex-video-player__ad-skip\"\r\n onClick={(e) => {\r\n e.stopPropagation();\r\n if (canSkip) onSkip();\r\n }}\r\n disabled={!canSkip}\r\n type=\"button\"\r\n >\r\n {skipText}\r\n </button>\r\n </div>\r\n );\r\n};\r\n\r\nexport default AdOverlay;\r\n","// Error Display Component for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React from 'react';\r\nimport { ErrorIcon } from './Icons';\r\n\r\ninterface ErrorDisplayProps {\r\n error: MediaError | null;\r\n onRetry: () => void;\r\n}\r\n\r\nconst ERROR_MESSAGES: Record<number, string> = {\r\n 1: 'The video playback was aborted.',\r\n 2: 'A network error occurred while loading the video.',\r\n 3: 'The video format is not supported or cannot be decoded.',\r\n 4: 'The video source is not supported.',\r\n};\r\n\r\nexport const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onRetry }) => {\r\n const errorCode = error?.code || 0;\r\n const message = ERROR_MESSAGES[errorCode] || 'An unknown error occurred.';\r\n\r\n return (\r\n <div className=\"plex-video-player__error\">\r\n <ErrorIcon className=\"plex-video-player__error-icon\" size={60} />\r\n <div className=\"plex-video-player__error-message\">{message}</div>\r\n {errorCode > 0 && (\r\n <div className=\"plex-video-player__error-code\">Error Code: {errorCode}</div>\r\n )}\r\n <button\r\n className=\"plex-video-player__error-retry\"\r\n onClick={onRetry}\r\n type=\"button\"\r\n >\r\n Try Again\r\n </button>\r\n </div>\r\n );\r\n};\r\n\r\nexport default ErrorDisplay;\r\n","// Loading Spinner Component for PlexVideo Player\r\n// © FRAMESET STUDIO\r\n\r\nimport React from 'react';\r\n\r\ninterface LoaderProps {\r\n visible: boolean;\r\n}\r\n\r\nexport const Loader: React.FC<LoaderProps> = ({ visible }) => {\r\n if (!visible) return null;\r\n\r\n return (\r\n <div className=\"plex-video-player__loader\">\r\n <div className=\"plex-video-player__loader-spinner\" />\r\n </div>\r\n );\r\n};\r\n\r\nexport default Loader;\r\n","// PlexVideo Player - Main Component\r\n// © FRAMESET STUDIO\r\n\r\nimport React, {\r\n forwardRef,\r\n useImperativeHandle,\r\n useRef,\r\n useState,\r\n useCallback,\r\n useEffect,\r\n useMemo,\r\n} from 'react';\r\nimport {\r\n PlexVideoPlayerProps,\r\n PlexVideoPlayerRef,\r\n VideoSource,\r\n} from '../types';\r\nimport { usePlayer } from '../hooks/usePlayer';\r\nimport { useKeyboard } from '../hooks/useKeyboard';\r\nimport { useVast } from '../hooks/useVast';\r\nimport {\r\n formatTime,\r\n detectVideoType,\r\n isPipSupported,\r\n isFullscreenSupported,\r\n clamp,\r\n} from '../utils/helpers';\r\nimport { ProgressBar } from './ProgressBar';\r\nimport { VolumeControl } from './VolumeControl';\r\nimport { SettingsMenu } from './SettingsMenu';\r\nimport { AdOverlay } from './AdOverlay';\r\nimport { ErrorDisplay } from './ErrorDisplay';\r\nimport { Loader } from './Loader';\r\nimport {\r\n PlayIcon,\r\n PauseIcon,\r\n FullscreenIcon,\r\n FullscreenExitIcon,\r\n PipIcon,\r\n PipExitIcon,\r\n} from './Icons';\r\nimport '../styles/player.css';\r\n\r\nconst DEFAULT_PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];\r\nconst CONTROLS_HIDE_DELAY = 3000;\r\n\r\nexport const PlexVideoPlayer = forwardRef<PlexVideoPlayerRef, PlexVideoPlayerProps>(\r\n (props, ref) => {\r\n const {\r\n src,\r\n poster,\r\n autoPlay = false,\r\n muted = false,\r\n loop = false,\r\n preload = 'metadata',\r\n width = '100%',\r\n height = 'auto',\r\n controls = true,\r\n pip = true,\r\n fullscreen = true,\r\n playbackSpeed = true,\r\n playbackSpeeds = DEFAULT_PLAYBACK_SPEEDS,\r\n volume: volumeControl = true,\r\n initialVolume = 1,\r\n progressBar = true,\r\n timeDisplay = true,\r\n qualitySelector = true,\r\n textTracks,\r\n vast,\r\n keyboard = true,\r\n hotkeys,\r\n className = '',\r\n style,\r\n accentColor,\r\n theme = 'dark',\r\n controlsTimeout = CONTROLS_HIDE_DELAY,\r\n doubleClickFullscreen = true,\r\n clickToPlay = true,\r\n thumbnailPreview,\r\n onPlay,\r\n onPause,\r\n onEnded,\r\n onTimeUpdate,\r\n onProgress,\r\n onVolumeChange,\r\n onSeeking,\r\n onSeeked,\r\n onRateChange,\r\n onQualityChange,\r\n onFullscreenChange,\r\n onPipChange,\r\n onError,\r\n onReady,\r\n onAdStart,\r\n onAdEnd,\r\n onAdSkip,\r\n onAdError,\r\n } = props;\r\n\r\n // Refs\r\n const videoRef = useRef<HTMLVideoElement>(null);\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const controlsTimeoutRef = useRef<number | null>(null);\r\n\r\n // State\r\n const [, setIsReady] = useState(false);\r\n const [showPoster, setShowPoster] = useState(!!poster && !autoPlay);\r\n const [controlsVisible, setControlsVisible] = useState(true);\r\n const [currentQuality, setCurrentQuality] = useState<string | null>(null);\r\n const [currentTrack, setCurrentTrack] = useState<string | null>(null);\r\n\r\n // Player hook\r\n const {\r\n state,\r\n play,\r\n pause,\r\n togglePlay,\r\n seek,\r\n setVolume,\r\n toggleMute,\r\n setPlaybackRate,\r\n enterFullscreen,\r\n exitFullscreen,\r\n toggleFullscreen,\r\n enterPip,\r\n exitPip,\r\n togglePip,\r\n } = usePlayer({\r\n autoPlay,\r\n muted,\r\n loop,\r\n volume: initialVolume,\r\n playbackRate: 1,\r\n });\r\n\r\n // VAST hook\r\n const vastHook = useVast({\r\n vastConfig: vast,\r\n videoRef,\r\n onAdStart,\r\n onAdEnd,\r\n onAdSkip,\r\n onAdError,\r\n });\r\n\r\n // Normalize sources\r\n const sources: VideoSource[] = useMemo(() => {\r\n if (typeof src === 'string') {\r\n return [{ src, type: detectVideoType(src) }];\r\n }\r\n return src.map((s) => ({\r\n ...s,\r\n type: s.type || detectVideoType(s.src),\r\n }));\r\n }, [src]);\r\n\r\n // Current source\r\n const currentSource = useMemo(() => {\r\n if (!currentQuality) return sources[0];\r\n return sources.find((s) => s.quality === currentQuality) || sources[0];\r\n }, [sources, currentQuality]);\r\n\r\n // Hide controls after timeout\r\n const hideControls = useCallback(() => {\r\n if (state.isPlaying && !vastHook.isAdPlaying) {\r\n setControlsVisible(false);\r\n }\r\n }, [state.isPlaying, vastHook.isAdPlaying]);\r\n\r\n const resetControlsTimeout = useCallback(() => {\r\n setControlsVisible(true);\r\n if (controlsTimeoutRef.current) {\r\n clearTimeout(controlsTimeoutRef.current);\r\n }\r\n controlsTimeoutRef.current = window.setTimeout(hideControls, controlsTimeout);\r\n }, [hideControls, controlsTimeout]);\r\n\r\n // Event handlers\r\n const handleVideoClick = useCallback(() => {\r\n if (clickToPlay && !vastHook.isAdPlaying) {\r\n togglePlay();\r\n }\r\n resetControlsTimeout();\r\n }, [clickToPlay, vastHook.isAdPlaying, togglePlay, resetControlsTimeout]);\r\n\r\n const handleDoubleClick = useCallback(() => {\r\n if (doubleClickFullscreen && !vastHook.isAdPlaying) {\r\n toggleFullscreen();\r\n }\r\n }, [doubleClickFullscreen, vastHook.isAdPlaying, toggleFullscreen]);\r\n\r\n const handleMouseMove = useCallback(() => {\r\n resetControlsTimeout();\r\n }, [resetControlsTimeout]);\r\n\r\n const handleMouseLeave = useCallback(() => {\r\n if (state.isPlaying) {\r\n hideControls();\r\n }\r\n }, [state.isPlaying, hideControls]);\r\n\r\n const handleSeek = useCallback(\r\n (time: number) => {\r\n seek(time);\r\n onSeeking?.(time);\r\n },\r\n [seek, onSeeking]\r\n );\r\n\r\n const handleQualityChange = useCallback(\r\n (quality: string) => {\r\n const source = sources.find((s) => s.quality === quality);\r\n if (source && videoRef.current) {\r\n const currentTime = videoRef.current.currentTime;\r\n const wasPlaying = !videoRef.current.paused;\r\n \r\n setCurrentQuality(quality);\r\n videoRef.current.src = source.src;\r\n videoRef.current.currentTime = currentTime;\r\n \r\n if (wasPlaying) {\r\n videoRef.current.play();\r\n }\r\n \r\n onQualityChange?.(quality);\r\n }\r\n },\r\n [sources, onQualityChange]\r\n );\r\n\r\n const handleTrackChange = useCallback(\r\n (track: string | null) => {\r\n setCurrentTrack(track);\r\n if (videoRef.current) {\r\n const tracks = videoRef.current.textTracks;\r\n for (let i = 0; i < tracks.length; i++) {\r\n tracks[i].mode = tracks[i].language === track ? 'showing' : 'hidden';\r\n }\r\n }\r\n },\r\n []\r\n );\r\n\r\n const handleRetry = useCallback(() => {\r\n if (videoRef.current) {\r\n videoRef.current.load();\r\n play();\r\n }\r\n }, [play]);\r\n\r\n const handlePosterClick = useCallback(() => {\r\n setShowPoster(false);\r\n play();\r\n }, [play]);\r\n\r\n // Keyboard shortcuts\r\n useKeyboard({\r\n enabled: keyboard,\r\n hotkeys,\r\n onPlay: togglePlay,\r\n onMute: toggleMute,\r\n onFullscreen: toggleFullscreen,\r\n onPip: togglePip,\r\n onSeek: (delta) => {\r\n if (videoRef.current) {\r\n const newTime = clamp(\r\n videoRef.current.currentTime + delta,\r\n 0,\r\n videoRef.current.duration\r\n );\r\n seek(newTime);\r\n }\r\n },\r\n onVolume: (delta) => {\r\n setVolume(clamp(state.volume + delta, 0, 1));\r\n },\r\n containerRef,\r\n });\r\n\r\n // Video event listeners\r\n useEffect(() => {\r\n const video = videoRef.current;\r\n if (!video) return;\r\n\r\n const handlers = {\r\n loadedmetadata: () => {\r\n setIsReady(true);\r\n onReady?.();\r\n },\r\n play: () => {\r\n setShowPoster(false);\r\n onPlay?.();\r\n },\r\n pause: () => onPause?.(),\r\n ended: () => onEnded?.(),\r\n timeupdate: () => {\r\n onTimeUpdate?.(video.currentTime);\r\n vastHook.checkForAd(video.currentTime, video.duration);\r\n },\r\n progress: () => {\r\n if (video.buffered.length > 0) {\r\n onProgress?.(video.buffered.end(video.buffered.length - 1));\r\n }\r\n },\r\n volumechange: () => {\r\n onVolumeChange?.(video.volume, video.muted);\r\n },\r\n seeking: () => onSeeking?.(video.currentTime),\r\n seeked: () => onSeeked?.(video.currentTime),\r\n ratechange: () => onRateChange?.(video.playbackRate),\r\n error: () => onError?.(video.error),\r\n enterpictureinpicture: () => onPipChange?.(true),\r\n leavepictureinpicture: () => onPipChange?.(false),\r\n };\r\n\r\n Object.entries(handlers).forEach(([event, handler]) => {\r\n video.addEventListener(event, handler);\r\n });\r\n\r\n return () => {\r\n Object.entries(handlers).forEach(([event, handler]) => {\r\n video.removeEventListener(event, handler);\r\n });\r\n };\r\n }, [\r\n onPlay,\r\n onPause,\r\n onEnded,\r\n onTimeUpdate,\r\n onProgress,\r\n onVolumeChange,\r\n onSeeking,\r\n onSeeked,\r\n onRateChange,\r\n onError,\r\n onReady,\r\n onPipChange,\r\n vastHook,\r\n ]);\r\n\r\n // Fullscreen change handler\r\n useEffect(() => {\r\n const handleFullscreenChange = () => {\r\n const isFs = !!document.fullscreenElement;\r\n onFullscreenChange?.(isFs);\r\n };\r\n\r\n document.addEventListener('fullscreenchange', handleFullscreenChange);\r\n return () => {\r\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\r\n };\r\n }, [onFullscreenChange]);\r\n\r\n // Cleanup timeout on unmount\r\n useEffect(() => {\r\n return () => {\r\n if (controlsTimeoutRef.current) {\r\n clearTimeout(controlsTimeoutRef.current);\r\n }\r\n };\r\n }, []);\r\n\r\n // Expose imperative methods\r\n useImperativeHandle(ref, () => ({\r\n play: async () => {\r\n await play();\r\n },\r\n pause: () => {\r\n pause();\r\n },\r\n stop: () => {\r\n pause();\r\n if (videoRef.current) {\r\n videoRef.current.currentTime = 0;\r\n }\r\n },\r\n seek: (time: number) => {\r\n seek(time);\r\n },\r\n setVolume: (vol: number) => {\r\n setVolume(vol);\r\n },\r\n mute: () => {\r\n if (videoRef.current) {\r\n videoRef.current.muted = true;\r\n }\r\n },\r\n unmute: () => {\r\n if (videoRef.current) {\r\n videoRef.current.muted = false;\r\n }\r\n },\r\n toggleMute: () => {\r\n toggleMute();\r\n },\r\n enterFullscreen: async () => {\r\n await enterFullscreen();\r\n },\r\n exitFullscreen: async () => {\r\n await exitFullscreen();\r\n },\r\n toggleFullscreen: async () => {\r\n await toggleFullscreen();\r\n },\r\n enterPip: async () => {\r\n await enterPip();\r\n },\r\n exitPip: async () => {\r\n await exitPip();\r\n },\r\n togglePip: async () => {\r\n await togglePip();\r\n },\r\n setPlaybackRate: (rate: number) => {\r\n setPlaybackRate(rate);\r\n },\r\n setQuality: (quality: string) => {\r\n handleQualityChange(quality);\r\n },\r\n getCurrentTime: () => videoRef.current?.currentTime || 0,\r\n getDuration: () => videoRef.current?.duration || 0,\r\n getVolume: () => videoRef.current?.volume || 0,\r\n isMuted: () => videoRef.current?.muted || false,\r\n isPlaying: () => !videoRef.current?.paused,\r\n isFullscreen: () => state.isFullscreen,\r\n isPip: () => state.isPip,\r\n getVideoElement: () => videoRef.current,\r\n }));\r\n\r\n // CSS custom properties for accent color\r\n const customStyle = useMemo(() => {\r\n const styles: React.CSSProperties = {\r\n width,\r\n height,\r\n ...style,\r\n };\r\n if (accentColor) {\r\n (styles as Record<string, string>)['--plex-primary'] = accentColor;\r\n }\r\n return styles;\r\n }, [width, height, style, accentColor]);\r\n\r\n const containerClassName = [\r\n 'plex-video-player',\r\n `plex-video-player--theme-${theme}`,\r\n state.isFullscreen && 'plex-video-player--fullscreen',\r\n controlsVisible && 'plex-video-player--controls-visible',\r\n vastHook.isAdPlaying && 'plex-video-player--ad-playing',\r\n className,\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n return (\r\n <div\r\n ref={containerRef}\r\n className={containerClassName}\r\n style={customStyle}\r\n onMouseMove={handleMouseMove}\r\n onMouseLeave={handleMouseLeave}\r\n tabIndex={0}\r\n >\r\n {/* Video Element */}\r\n <video\r\n ref={videoRef}\r\n className=\"plex-video-player__video\"\r\n src={currentSource?.src}\r\n poster={showPoster ? undefined : poster}\r\n preload={preload}\r\n loop={loop}\r\n muted={muted}\r\n playsInline\r\n onClick={handleVideoClick}\r\n onDoubleClick={handleDoubleClick}\r\n >\r\n {sources.map((source, index) => (\r\n <source key={index} src={source.src} type={source.type} />\r\n ))}\r\n {textTracks?.map((track, index) => (\r\n <track\r\n key={index}\r\n src={track.src}\r\n kind={track.kind}\r\n srcLang={track.srclang}\r\n label={track.label}\r\n default={track.default}\r\n />\r\n ))}\r\n </video>\r\n\r\n {/* Poster Overlay */}\r\n {showPoster && poster && (\r\n <div\r\n className=\"plex-video-player__poster\"\r\n style={{ backgroundImage: `url(${poster})` }}\r\n onClick={handlePosterClick}\r\n />\r\n )}\r\n\r\n {/* Loading Indicator */}\r\n <Loader visible={state.isBuffering && !showPoster} />\r\n\r\n {/* Error Display */}\r\n {state.error && <ErrorDisplay error={state.error} onRetry={handleRetry} />}\r\n\r\n {/* Ad Overlay */}\r\n {vastHook.isAdPlaying && vastHook.currentAd && (\r\n <AdOverlay\r\n ad={vastHook.currentAd}\r\n timeRemaining={vastHook.adTimeRemaining}\r\n canSkip={vastHook.canSkip}\r\n onSkip={vastHook.skipAd}\r\n onClick={vastHook.handleAdClick}\r\n />\r\n )}\r\n\r\n {/* Controls */}\r\n {controls && !showPoster && !state.error && (\r\n <div className=\"plex-video-player__controls\">\r\n {/* Progress Bar */}\r\n {progressBar && (\r\n <ProgressBar\r\n currentTime={state.currentTime}\r\n duration={state.duration}\r\n buffered={state.buffered}\r\n onSeek={handleSeek}\r\n thumbnailPreview={thumbnailPreview}\r\n disabled={vastHook.isAdPlaying}\r\n />\r\n )}\r\n\r\n {/* Controls Row */}\r\n <div className=\"plex-video-player__controls-row\">\r\n {/* Left Controls */}\r\n <div className=\"plex-video-player__controls-left\">\r\n {/* Play/Pause */}\r\n <button\r\n className=\"plex-video-player__btn plex-video-player__btn--play\"\r\n onClick={togglePlay}\r\n aria-label={state.isPlaying ? 'Pause' : 'Play'}\r\n title={state.isPlaying ? 'Pause (Space)' : 'Play (Space)'}\r\n disabled={vastHook.isAdPlaying}\r\n type=\"button\"\r\n >\r\n {state.isPlaying ? <PauseIcon /> : <PlayIcon />}\r\n </button>\r\n\r\n {/* Volume */}\r\n {volumeControl && (\r\n <VolumeControl\r\n volume={state.volume}\r\n muted={state.isMuted}\r\n onVolumeChange={setVolume}\r\n onToggleMute={toggleMute}\r\n />\r\n )}\r\n\r\n {/* Time Display */}\r\n {timeDisplay && (\r\n <div className=\"plex-video-player__time\">\r\n <span>{formatTime(state.currentTime)}</span>\r\n <span className=\"plex-video-player__time-separator\">/</span>\r\n <span>{formatTime(state.duration)}</span>\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Right Controls */}\r\n <div className=\"plex-video-player__controls-right\">\r\n {/* Settings */}\r\n {(playbackSpeed || qualitySelector) && (\r\n <SettingsMenu\r\n playbackRate={state.playbackRate}\r\n playbackSpeeds={playbackSpeeds}\r\n onPlaybackRateChange={setPlaybackRate}\r\n qualityEnabled={qualitySelector}\r\n sources={sources.length > 1 ? sources : undefined}\r\n currentQuality={currentQuality || undefined}\r\n onQualityChange={handleQualityChange}\r\n captionsEnabled={!!textTracks && textTracks.length > 0}\r\n textTracks={textTracks}\r\n currentTrack={currentTrack || undefined}\r\n onTrackChange={handleTrackChange}\r\n />\r\n )}\r\n\r\n {/* Picture-in-Picture */}\r\n {pip && isPipSupported() && (\r\n <button\r\n className=\"plex-video-player__btn\"\r\n onClick={togglePip}\r\n aria-label={state.isPip ? 'Exit Picture-in-Picture' : 'Picture-in-Picture'}\r\n title={state.isPip ? 'Exit PiP (P)' : 'Picture-in-Picture (P)'}\r\n disabled={vastHook.isAdPlaying}\r\n type=\"button\"\r\n >\r\n {state.isPip ? <PipExitIcon /> : <PipIcon />}\r\n </button>\r\n )}\r\n\r\n {/* Fullscreen */}\r\n {fullscreen && isFullscreenSupported() && (\r\n <button\r\n className=\"plex-video-player__btn\"\r\n onClick={toggleFullscreen}\r\n aria-label={state.isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}\r\n title={state.isFullscreen ? 'Exit Fullscreen (F)' : 'Fullscreen (F)'}\r\n type=\"button\"\r\n >\r\n {state.isFullscreen ? <FullscreenExitIcon /> : <FullscreenIcon />}\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nPlexVideoPlayer.displayName = 'PlexVideoPlayer';\r\n\r\nexport default PlexVideoPlayer;\r\n"],"names":["formatTime","seconds","isFinite","isNaN","hrs","Math","floor","mins","secs","toString","padStart","percentage","value","total","min","max","clamp","isFullscreenSupported","document","fullscreenEnabled","webkitFullscreenEnabled","mozFullScreenEnabled","msFullscreenEnabled","isPipSupported","pictureInPictureEnabled","getFullscreenElement","fullscreenElement","webkitFullscreenElement","mozFullScreenElement","msFullscreenElement","requestFullscreen","async","element","webkitRequestFullscreen","mozRequestFullScreen","msRequestFullscreen","exitFullscreen","webkitExitFullscreen","mozCancelFullScreen","msExitFullscreen","detectVideoType","url","extension","split","pop","toLowerCase","mp4","webm","ogg","ogv","m3u8","mpd","mov","avi","mkv","getBufferedEnd","video","buffered","length","currentTime","i","start","end","initialState","isPlaying","isPaused","isEnded","isBuffering","isSeeking","isFullscreen","isPip","isMuted","isAdPlaying","volume","duration","playbackRate","currentQuality","error","usePlayer","options","autoPlay","muted","loop","initialVolume","initialRate","videoRef","useRef","containerRef","state","setState","useState","updateState","useCallback","updates","prev","play","current","pause","togglePlay","seek","time","clampedTime","setVolume","clampedVolume","toggleMute","setPlaybackRate","rate","enterFullscreen","container","exitFullscreenFn","toggleFullscreen","enterPip","requestPictureInPicture","exitPipFn","pictureInPictureElement","exitPictureInPicture","togglePip","useEffect","handlers","loadedmetadata","timeupdate","ended","waiting","canplay","seeking","seeked","volumechange","ratechange","enterpictureinpicture","leavepictureinpicture","Object","entries","forEach","event","handler","addEventListener","removeEventListener","handleFullscreenChange","isFs","exitPip","DEFAULT_HOTKEYS","mute","fullscreen","pip","seekForward","seekBackward","volumeUp","volumeDown","useKeyboard","enabled","hotkeys","onPlay","onMute","onFullscreen","onPip","onSeek","onVolume","mergedHotkeys","handleKeyDown","target","tagName","isContentEditable","contains","activeElement","key","preventDefault","shiftKey","Infinity","parseVastXml","xmlString","doc","DOMParser","parseFromString","querySelector","ads","querySelectorAll","adElement","inLine","id","getAttribute","title","textContent","description","undefined","creatives","skipOffset","clickThrough","clickTracking","mediaFiles","trackingEvents","creative","linear","durationStr","parseVastDuration","skipOffsetAttr","mediaFile","trim","push","type","width","parseInt","height","bitrate","videoClicks","ct","tracking","impressionUrls","impression","includes","parts","parseFloat","fetchVastAd","config","response","fetch","method","headers","Accept","ok","text","result","ad","skipDelay","selectBestMediaFile","createElement","supportedFiles","filter","file","canPlayType","sort","a","b","resA","resB","test","navigator","userAgent","fireTrackingPixel","Image","src","fireTrackingPixels","urls","convertToAdInfo","useVast","vastConfig","onAdStart","onAdEnd","onAdSkip","onAdError","setIsAdPlaying","currentAd","setCurrentAd","adTimeRemaining","setAdTimeRemaining","canSkip","setCanSkip","currentVastAd","originalSrc","originalTime","playedPositions","Set","adIntervalRef","vastConfigs","Array","isArray","playAd","Error","currentSrc","window","setInterval","remaining","percent","firstQuartile","midpoint","thirdQuartile","handleAdEnd","complete","endAd","once","clearInterval","catch","skipAd","skip","handleAdClick","open","checkForAd","position","midrollTime","has","shouldPlay","add","ProgressBar","thumbnailPreview","disabled","progressRef","isDragging","setIsDragging","hoverTime","setHoverTime","hoverPosition","setHoverPosition","playedPercent","bufferedPercent","getTimeFromPosition","clientX","rect","getBoundingClientRect","left","handleMouseMove","e","handleMouseDown","handleMouseUp","handleMouseEnter","handleMouseLeave","mouseMoveHandler","handleTouchStart","touch","touches","handleTouchMove","handleTouchEnd","_jsxs","ref","className","onMouseDown","onMouseEnter","onMouseLeave","onMouseMove","nativeEvent","onTouchStart","onTouchMove","onTouchEnd","role","tabIndex","children","_jsx","style","sprites","backgroundImage","backgroundPosition","calculateSpritePosition","interval","backgroundSize","padding","color","fontSize","_duration","index","PlayIcon","size","viewBox","fill","xmlns","d","PauseIcon","VolumeHighIcon","VolumeMediumIcon","VolumeLowIcon","VolumeMuteIcon","FullscreenIcon","FullscreenExitIcon","PipIcon","PipExitIcon","SettingsIcon","ErrorIcon","ExternalLinkIcon","CheckIcon","VolumeControl","onVolumeChange","onToggleMute","handleSliderChange","newVolume","onClick","step","onChange","background","SettingsMenu","playbackSpeeds","onPlaybackRateChange","qualityEnabled","sources","onQualityChange","captionsEnabled","textTracks","currentTrack","onTrackChange","isOpen","setIsOpen","currentView","setCurrentView","handleClickOutside","handleToggle","handleSpeedSelect","speed","handleQualitySelect","quality","handleTrackSelect","track","_Fragment","cursor","border","textAlign","map","source","label","srclang","AdOverlay","timeRemaining","onSkip","skipText","ceil","stopPropagation","ERROR_MESSAGES","ErrorDisplay","onRetry","errorCode","code","message","Loader","visible","DEFAULT_PLAYBACK_SPEEDS","CONTROLS_HIDE_DELAY","PlexVideoPlayer","forwardRef","props","poster","preload","controls","playbackSpeed","volumeControl","progressBar","timeDisplay","qualitySelector","vast","keyboard","accentColor","theme","controlsTimeout","doubleClickFullscreen","clickToPlay","onPause","onEnded","onTimeUpdate","onProgress","onSeeking","onSeeked","onRateChange","onFullscreenChange","onPipChange","onError","onReady","controlsTimeoutRef","setIsReady","showPoster","setShowPoster","controlsVisible","setControlsVisible","setCurrentQuality","setCurrentTrack","vastHook","useMemo","s","currentSource","find","hideControls","resetControlsTimeout","clearTimeout","setTimeout","handleVideoClick","handleDoubleClick","handleSeek","handleQualityChange","wasPlaying","paused","handleTrackChange","tracks","mode","language","handleRetry","load","handlePosterClick","delta","newTime","progress","useImperativeHandle","stop","vol","unmute","setQuality","getCurrentTime","getDuration","getVolume","getVideoElement","customStyle","styles","containerClassName","Boolean","join","playsInline","onDoubleClick","kind","srcLang","default","displayName","func","wait","timeout","args","random","substring","maxTouchPoints","Number","limit","inThrottle"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ :root{--plex-primary:#e50914;--plex-secondary:#fff;--plex-bg:rgba(0,0,0,.8);--plex-control-bg:rgba(0,0,0,.7);--plex-progress-bg:hsla(0,0%,100%,.3);--plex-buffered-bg:hsla(0,0%,100%,.5);--plex-hover:hsla(0,0%,100%,.1);--plex-shadow:0 2px 10px rgba(0,0,0,.3);--plex-transition:all 0.2s ease;--plex-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,sans-serif}.plex-video-player{-webkit-tap-highlight-color:transparent;background-color:#000;font-family:var(--plex-font);height:100%;overflow:hidden;position:relative;user-select:none;width:100%}.plex-video-player *{box-sizing:border-box}.plex-video-player:focus{outline:none}.plex-video-player__video{display:block;height:100%;object-fit:contain;width:100%}.plex-video-player__poster{background-position:50%;background-repeat:no-repeat;background-size:cover;cursor:pointer;height:100%;left:0;position:absolute;top:0;width:100%;z-index:1}.plex-video-player__poster:after{background-color:var(--plex-primary);border-radius:50%;content:"";height:80px;left:50%;opacity:.9;position:absolute;top:50%;transform:translate(-50%,-50%);transition:var(--plex-transition);width:80px}.plex-video-player__poster:before{border-color:transparent transparent transparent #fff;border-style:solid;border-width:15px 0 15px 25px;content:"";left:50%;position:absolute;top:50%;transform:translate(-45%,-50%);z-index:2}.plex-video-player__poster:hover:after{opacity:1;transform:translate(-50%,-50%) scale(1.1)}.plex-video-player__controls{background:linear-gradient(transparent,rgba(0,0,0,.8));bottom:0;left:0;opacity:0;padding:10px 15px;position:absolute;right:0;transition:opacity .3s ease,visibility .3s ease;visibility:hidden;z-index:10}.plex-video-player--controls-visible .plex-video-player__controls,.plex-video-player:hover .plex-video-player__controls{opacity:1;visibility:visible}.plex-video-player--ad-playing .plex-video-player__controls{pointer-events:none}.plex-video-player__progress-container{cursor:pointer;height:20px;padding:8px 0;position:relative;width:100%}.plex-video-player__progress{background-color:var(--plex-progress-bg);border-radius:2px;height:4px;overflow:hidden;position:relative;transition:height .1s ease;width:100%}.plex-video-player__progress-container:hover .plex-video-player__progress{height:6px}.plex-video-player__progress-buffered{background-color:var(--plex-buffered-bg);border-radius:2px;height:100%;left:0;position:absolute;top:0;transition:width .1s ease}.plex-video-player__progress-played{background-color:var(--plex-primary);border-radius:2px;height:100%;left:0;position:absolute;top:0;transition:width .05s linear}.plex-video-player__progress-handle{background-color:var(--plex-primary);border-radius:50%;box-shadow:var(--plex-shadow);height:14px;position:absolute;top:50%;transform:translate(-50%,-50%) scale(0);transition:transform .1s ease;width:14px}.plex-video-player__progress-container:hover .plex-video-player__progress-handle{transform:translate(-50%,-50%) scale(1)}.plex-video-player__thumbnail-preview{background-color:#000;border:2px solid var(--plex-secondary);border-radius:4px;bottom:30px;opacity:0;overflow:hidden;pointer-events:none;position:absolute;transform:translateX(-50%);transition:opacity .2s ease}.plex-video-player__progress-container:hover .plex-video-player__thumbnail-preview{opacity:1}.plex-video-player__thumbnail-time{background-color:rgba(0,0,0,.7);bottom:0;color:#fff;font-size:12px;left:0;padding:4px;position:absolute;right:0;text-align:center}.plex-video-player__controls-row{align-items:center;display:flex;gap:10px;justify-content:space-between;margin-top:8px}.plex-video-player__controls-left,.plex-video-player__controls-right{align-items:center;display:flex;gap:8px}.plex-video-player__btn{align-items:center;background:transparent;border:none;border-radius:4px;color:var(--plex-secondary);cursor:pointer;display:flex;height:36px;justify-content:center;padding:0;transition:var(--plex-transition);width:36px}.plex-video-player__btn:hover{background-color:var(--plex-hover)}.plex-video-player__btn:focus{outline:none}.plex-video-player__btn:focus-visible{box-shadow:0 0 0 2px var(--plex-primary)}.plex-video-player__btn svg{fill:currentColor;height:22px;width:22px}.plex-video-player__btn--play{height:40px;width:40px}.plex-video-player__btn--play svg{height:26px;width:26px}.plex-video-player__volume-container{align-items:center;display:flex;gap:6px}.plex-video-player__volume-slider-container{overflow:hidden;transition:width .2s ease;width:0}.plex-video-player__volume-container:hover .plex-video-player__volume-slider-container{width:80px}.plex-video-player__volume-slider{-webkit-appearance:none;appearance:none;background:var(--plex-progress-bg);border-radius:2px;cursor:pointer;height:4px;width:80px}.plex-video-player__volume-slider::-webkit-slider-thumb{-webkit-appearance:none;background:var(--plex-secondary);border-radius:50%;cursor:pointer;height:12px;width:12px}.plex-video-player__volume-slider::-moz-range-thumb{background:var(--plex-secondary);border:none;border-radius:50%;cursor:pointer;height:12px;width:12px}.plex-video-player__time{color:var(--plex-secondary);font-size:13px;font-variant-numeric:tabular-nums;white-space:nowrap}.plex-video-player__time-separator{margin:0 4px;opacity:.7}.plex-video-player__settings-container{position:relative}.plex-video-player__settings-menu{backdrop-filter:blur(10px);background-color:var(--plex-control-bg);border-radius:8px;bottom:100%;box-shadow:var(--plex-shadow);margin-bottom:10px;min-width:180px;opacity:0;overflow:hidden;position:absolute;right:0;transform:translateY(10px);transition:all .2s ease;visibility:hidden}.plex-video-player__settings-container:hover .plex-video-player__settings-menu,.plex-video-player__settings-menu--open{opacity:1;transform:translateY(0);visibility:visible}.plex-video-player__settings-title{border-bottom:1px solid hsla(0,0%,100%,.1);color:var(--plex-secondary);font-size:13px;font-weight:600;padding:12px 15px}.plex-video-player__settings-item{align-items:center;background:transparent;border:none;color:var(--plex-secondary);cursor:pointer;display:flex;font-size:13px;justify-content:space-between;padding:10px 15px;text-align:left;transition:var(--plex-transition);width:100%}.plex-video-player__settings-item:hover{background-color:var(--plex-hover)}.plex-video-player__settings-item--active{color:var(--plex-primary)}.plex-video-player__settings-item--active:after{content:"✓";margin-left:8px}.plex-video-player__speed-menu{display:grid;gap:4px;grid-template-columns:repeat(2,1fr);padding:8px}.plex-video-player__speed-btn{background:transparent;border:1px solid hsla(0,0%,100%,.2);border-radius:4px;color:var(--plex-secondary);cursor:pointer;font-size:12px;padding:8px 12px;transition:var(--plex-transition)}.plex-video-player__speed-btn:hover{background-color:var(--plex-hover);border-color:hsla(0,0%,100%,.4)}.plex-video-player__speed-btn--active{background-color:var(--plex-primary);border-color:var(--plex-primary)}.plex-video-player__quality-menu{padding:8px}.plex-video-player__loader{height:50px;left:50%;pointer-events:none;position:absolute;top:50%;transform:translate(-50%,-50%);width:50px;z-index:5}.plex-video-player__loader-spinner{animation:plex-spin 1s linear infinite;border:3px solid transparent;border-radius:50%;border-top:3px solid var(--plex-primary);height:100%;width:100%}@keyframes plex-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.plex-video-player__big-play{background-color:var(--plex-primary);border:none;border-radius:50%;cursor:pointer;height:80px;left:50%;opacity:.9;position:absolute;top:50%;transform:translate(-50%,-50%);transition:var(--plex-transition);width:80px;z-index:5}.plex-video-player__big-play:hover{opacity:1;transform:translate(-50%,-50%) scale(1.1)}.plex-video-player__big-play svg{fill:#fff;height:30px;left:55%;position:absolute;top:50%;transform:translate(-50%,-50%);width:30px}.plex-video-player__ad-overlay{bottom:0;cursor:pointer;left:0;position:absolute;right:0;top:0;z-index:20}.plex-video-player__ad-info{align-items:center;background-color:rgba(0,0,0,.7);border-radius:4px;color:#fff;display:flex;font-size:13px;gap:10px;left:15px;padding:8px 12px;position:absolute;top:15px}.plex-video-player__ad-badge{background-color:#f5c518;border-radius:2px;color:#000;font-size:11px;font-weight:600;padding:2px 6px;text-transform:uppercase}.plex-video-player__ad-skip{background-color:rgba(0,0,0,.8);border:1px solid #fff;border-radius:4px;bottom:80px;color:#fff;cursor:pointer;font-size:14px;padding:10px 20px;position:absolute;right:15px;transition:var(--plex-transition)}.plex-video-player__ad-skip:hover{background-color:#fff;color:#000}.plex-video-player__ad-skip:disabled{cursor:not-allowed;opacity:.5}.plex-video-player__ad-skip:disabled:hover{background-color:rgba(0,0,0,.8);color:#fff}.plex-video-player__ad-learn-more{align-items:center;background-color:rgba(0,0,0,.7);border:1px solid hsla(0,0%,100%,.5);border-radius:4px;bottom:80px;color:#fff;cursor:pointer;display:flex;font-size:13px;gap:6px;left:15px;padding:8px 14px;position:absolute;transition:var(--plex-transition)}.plex-video-player__ad-learn-more:hover{background-color:hsla(0,0%,100%,.2)}.plex-video-player__error{align-items:center;background-color:rgba(0,0,0,.9);bottom:0;color:#fff;display:flex;flex-direction:column;justify-content:center;left:0;position:absolute;right:0;top:0;z-index:25}.plex-video-player__error-icon{fill:var(--plex-primary);height:60px;margin-bottom:20px;width:60px}.plex-video-player__error-message{font-size:16px;margin-bottom:10px}.plex-video-player__error-code{font-size:12px;opacity:.7}.plex-video-player__error-retry{background-color:var(--plex-primary);border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:14px;margin-top:20px;padding:10px 25px;transition:var(--plex-transition)}.plex-video-player__error-retry:hover{opacity:.9}.plex-video-player__captions{background-color:rgba(0,0,0,.75);border-radius:4px;bottom:60px;color:#fff;font-size:18px;left:50%;line-height:1.4;max-width:80%;padding:8px 16px;position:absolute;text-align:center;transform:translateX(-50%);z-index:8}.plex-video-player--fullscreen{height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:9999}.plex-video-player--fullscreen .plex-video-player__captions{font-size:24px}.plex-video-player--theme-light .plex-video-player__controls{background:linear-gradient(transparent,hsla(0,0%,100%,.9))}.plex-video-player--theme-light .plex-video-player__btn,.plex-video-player--theme-light .plex-video-player__time{color:#333}.plex-video-player--theme-light .plex-video-player__settings-menu{background-color:hsla(0,0%,100%,.95)}.plex-video-player--theme-light .plex-video-player__settings-item,.plex-video-player--theme-light .plex-video-player__settings-title{color:#333}@media (max-width:768px){.plex-video-player__controls{padding:8px 10px}.plex-video-player__btn{height:32px;width:32px}.plex-video-player__btn svg{height:18px;width:18px}.plex-video-player__time{font-size:11px}.plex-video-player__volume-container:hover .plex-video-player__volume-slider-container{width:60px}.plex-video-player__big-play{height:60px;width:60px}.plex-video-player__big-play svg{height:24px;width:24px}.plex-video-player__captions{bottom:50px;font-size:14px}}@media (max-width:480px){.plex-video-player__controls-row{gap:5px}.plex-video-player__controls-left,.plex-video-player__controls-right{gap:4px}.plex-video-player__btn{height:28px;width:28px}.plex-video-player__volume-slider-container{display:none}}.plex-fade-in{animation:plex-fade-in .3s ease}@keyframes plex-fade-in{0%{opacity:0}to{opacity:1}}.plex-scale-in{animation:plex-scale-in .2s ease}@keyframes plex-scale-in{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}
package/package.json CHANGED
@@ -1,134 +1,96 @@
1
1
  {
2
2
  "name": "@frameset/plex-player",
3
- "version": "1.0.6",
4
- "type": "module",
5
- "description": "Professional video player with VAST ads, Chromecast, PiP, subtitles, playlists and more. Built by FRAMESET Studio.",
6
- "main": "dist/plex-player.cjs.js",
7
- "module": "dist/plex-player.esm.js",
8
- "browser": "dist/plex-player.min.js",
9
- "types": "dist/plex-player.d.ts",
10
- "exports": {
11
- ".": {
12
- "import": "./dist/plex-player.esm.js",
13
- "require": "./dist/plex-player.cjs.js",
14
- "types": "./dist/plex-player.d.ts"
15
- },
16
- "./react": {
17
- "import": "./dist/react/index.esm.js",
18
- "require": "./dist/react/index.js",
19
- "types": "./src/types/index.d.ts"
20
- },
21
- "./vue": {
22
- "import": "./dist/vue/index.esm.js",
23
- "require": "./dist/vue/index.js",
24
- "types": "./src/types/index.d.ts"
25
- },
26
- "./css": "./dist/plex-player.css",
27
- "./dist/*": "./dist/*"
28
- },
3
+ "version": "2.0.0",
4
+ "description": "Ultra-performant React video player with VAST ads support, Picture-in-Picture, and advanced controls",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
29
8
  "files": [
30
9
  "dist",
31
- "src",
32
10
  "README.md",
33
11
  "LICENSE"
34
12
  ],
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/index.esm.js",
16
+ "require": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ },
19
+ "./styles.css": "./dist/styles.css"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
35
24
  "scripts": {
36
25
  "build": "rollup -c",
37
- "build:watch": "rollup -c -w",
38
- "dev": "vite",
39
- "preview": "vite preview",
40
- "test": "vitest",
41
- "lint": "eslint src/",
42
- "prepare": "npm run build",
43
- "prepublishOnly": "npm run build"
26
+ "build:types": "tsc --emitDeclarationOnly",
27
+ "dev": "rollup -c -w",
28
+ "lint": "eslint src --ext .ts,.tsx",
29
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
30
+ "test": "jest",
31
+ "test:coverage": "jest --coverage",
32
+ "prepublishOnly": "npm run build",
33
+ "clean": "rimraf dist",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/deadseti/plex-player.git"
44
39
  },
45
40
  "keywords": [
41
+ "react",
46
42
  "video",
47
43
  "player",
48
44
  "video-player",
49
- "html5-video",
50
- "chromecast",
51
- "cast",
45
+ "react-video",
52
46
  "vast",
53
47
  "ads",
54
- "advertising",
55
- "pip",
56
48
  "picture-in-picture",
57
- "subtitles",
58
- "vtt",
59
- "srt",
60
- "ass",
61
- "playlist",
62
- "react",
63
- "vue",
64
- "typescript",
65
- "responsive",
66
- "touch",
67
- "mobile",
49
+ "pip",
50
+ "hls",
68
51
  "streaming",
69
52
  "media",
53
+ "plexvideo",
70
54
  "frameset"
71
55
  ],
72
- "author": {
73
- "name": "FRAMESET Studio",
74
- "email": "hello@frameset.dev",
75
- "url": "https://frameset.dev"
76
- },
56
+ "author": "FRAMESET STUDIO",
77
57
  "license": "MIT",
78
- "homepage": "https://frameset.dev/plex-player",
79
- "repository": {
80
- "type": "git",
81
- "url": "git+https://github.com/deadseti/plex-player.git"
82
- },
83
58
  "bugs": {
84
59
  "url": "https://github.com/deadseti/plex-player/issues"
85
60
  },
86
- "funding": {
87
- "type": "github",
88
- "url": "https://github.com/sponsors/deadseti"
89
- },
90
- "engines": {
91
- "node": ">=16.0.0"
92
- },
61
+ "homepage": "https://github.com/deadseti/plex-player#readme",
93
62
  "peerDependencies": {
94
- "react": ">=16.8.0",
95
- "react-dom": ">=16.8.0",
96
- "vue": ">=3.0.0"
97
- },
98
- "peerDependenciesMeta": {
99
- "react": {
100
- "optional": true
101
- },
102
- "react-dom": {
103
- "optional": true
104
- },
105
- "vue": {
106
- "optional": true
107
- }
63
+ "react": ">=17.0.0",
64
+ "react-dom": ">=17.0.0"
108
65
  },
109
66
  "devDependencies": {
110
- "@babel/core": "^7.24.0",
111
- "@babel/preset-env": "^7.24.0",
112
- "@babel/preset-react": "^7.24.0",
113
- "@rollup/plugin-alias": "^6.0.0",
114
- "@rollup/plugin-babel": "^6.0.0",
115
- "@rollup/plugin-commonjs": "^25.0.0",
116
- "@rollup/plugin-node-resolve": "^15.0.0",
117
- "@rollup/plugin-replace": "^6.0.3",
118
- "@rollup/plugin-terser": "^0.4.0",
119
- "@types/node": "^20.0.0",
120
- "@types/react": "^18.0.0",
121
- "@types/react-dom": "^18.0.0",
122
- "eslint": "^8.0.0",
123
- "postcss": "^8.0.0",
124
- "rollup": "^4.0.0",
125
- "rollup-plugin-copy": "^3.5.0",
126
- "rollup-plugin-postcss": "^4.0.0",
127
- "typescript": "^5.0.0",
128
- "vite": "^5.0.0",
129
- "vitest": "^1.0.0"
67
+ "@rollup/plugin-commonjs": "^25.0.7",
68
+ "@rollup/plugin-node-resolve": "^15.2.3",
69
+ "@rollup/plugin-terser": "^0.4.4",
70
+ "@rollup/plugin-typescript": "^11.1.6",
71
+ "@testing-library/jest-dom": "^6.4.2",
72
+ "@testing-library/react": "^14.2.1",
73
+ "@types/jest": "^29.5.12",
74
+ "@types/react": "^18.2.55",
75
+ "@types/react-dom": "^18.2.19",
76
+ "@typescript-eslint/eslint-plugin": "^7.0.1",
77
+ "@typescript-eslint/parser": "^7.0.1",
78
+ "eslint": "^8.56.0",
79
+ "eslint-plugin-react": "^7.33.2",
80
+ "eslint-plugin-react-hooks": "^4.6.0",
81
+ "jest": "^29.7.0",
82
+ "jest-environment-jsdom": "^29.7.0",
83
+ "react": "^18.2.0",
84
+ "react-dom": "^18.2.0",
85
+ "rimraf": "^5.0.5",
86
+ "rollup": "^4.12.0",
87
+ "rollup-plugin-dts": "^6.1.0",
88
+ "rollup-plugin-postcss": "^4.0.2",
89
+ "ts-jest": "^29.1.2",
90
+ "tslib": "^2.8.1",
91
+ "typescript": "^5.3.3"
130
92
  },
131
- "sideEffects": [
132
- "**/*.css"
133
- ]
93
+ "engines": {
94
+ "node": ">=16.0.0"
95
+ }
134
96
  }