analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 58.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["import {\n useRef,\n useState,\n useEffect,\n useCallback,\n MouseEvent,\n KeyboardEvent,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { PlayIcon } from '@phosphor-icons/react/dist/csr/Play';\nimport { PauseIcon } from '@phosphor-icons/react/dist/csr/Pause';\nimport { SpeakerHighIcon } from '@phosphor-icons/react/dist/csr/SpeakerHigh';\nimport { SpeakerSlashIcon } from '@phosphor-icons/react/dist/csr/SpeakerSlash';\nimport { ArrowsOutSimpleIcon } from '@phosphor-icons/react/dist/csr/ArrowsOutSimple';\nimport { ArrowsInSimpleIcon } from '@phosphor-icons/react/dist/csr/ArrowsInSimple';\nimport { ClosedCaptioningIcon } from '@phosphor-icons/react/dist/csr/ClosedCaptioning';\nimport { DotsThreeVerticalIcon } from '@phosphor-icons/react/dist/csr/DotsThreeVertical';\nimport { cn } from '../../utils/utils';\nimport IconButton from '../IconButton/IconButton';\nimport Text from '../Text/Text';\nimport { useMobile } from '../../hooks/useMobile';\nimport DownloadButton, {\n DownloadContent,\n} from '../DownloadButton/DownloadButton';\n\n// Constants for timeout durations\nconst CONTROLS_HIDE_TIMEOUT = 3000; // 3 seconds for normal control hiding\nconst LEAVE_HIDE_TIMEOUT = 1000; // 1 second when mouse leaves the video area\nconst INIT_DELAY = 100; // ms delay to initialize controls on mount\n\n/**\n * VideoPlayer component props interface\n */\ninterface VideoPlayerProps {\n /** Video source URL */\n src: string;\n /** Video poster/thumbnail URL */\n poster?: string;\n /** Subtitles URL */\n subtitles?: string;\n /** Video title */\n title?: string;\n /** Video subtitle/description */\n subtitle?: string;\n /** Initial playback time in seconds */\n initialTime?: number;\n /** Callback fired when video time updates (seconds) */\n onTimeUpdate?: (seconds: number) => void;\n /** Callback fired with progress percentage (0-100) */\n onProgress?: (progress: number) => void;\n /** Callback fired when video completes (>95% watched) */\n onVideoComplete?: () => void;\n /** Additional CSS classes */\n className?: string;\n /** Auto-save progress to localStorage */\n autoSave?: boolean;\n /** localStorage key for saving progress */\n storageKey?: string;\n /** User ID used to scope localStorage keys — prevents progress leaking between users */\n userId?: string;\n /** Download content URLs for lesson materials */\n downloadContent?: DownloadContent;\n /** Show download button in header */\n showDownloadButton?: boolean;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n}\n\n/**\n * Format seconds to MM:SS display format\n * @param seconds - Time in seconds\n * @returns Formatted time string\n */\nconst formatTime = (seconds: number): string => {\n if (!seconds || Number.isNaN(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n};\n\n/**\n * Progress bar component props\n */\ninterface ProgressBarProps {\n currentTime: number;\n duration: number;\n progressPercentage: number;\n onSeek: (time: number) => void;\n}\n\n/**\n * Progress bar subcomponent\n */\nconst ProgressBar = ({\n currentTime,\n duration,\n progressPercentage,\n onSeek,\n className = 'px-4 pb-2',\n}: ProgressBarProps & { className?: string }) => (\n <div className={className}>\n <input\n type=\"range\"\n min={0}\n max={duration || 100}\n value={currentTime}\n onChange={(e) => onSeek(Number.parseFloat(e.target.value))}\n className=\"w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Video progress\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Volume controls component props\n */\ninterface VolumeControlsProps {\n volume: number;\n isMuted: boolean;\n onVolumeChange: (volume: number) => void;\n onToggleMute: () => void;\n iconSize?: number;\n showSlider?: boolean;\n}\n\n/**\n * Volume controls subcomponent\n */\nconst VolumeControls = ({\n volume,\n isMuted,\n onVolumeChange,\n onToggleMute,\n iconSize = 24,\n showSlider = true,\n}: VolumeControlsProps) => (\n <div className=\"flex items-center gap-2\">\n <IconButton\n icon={\n isMuted ? (\n <SpeakerSlashIcon size={iconSize} />\n ) : (\n <SpeakerHighIcon size={iconSize} />\n )\n }\n onClick={onToggleMute}\n aria-label={isMuted ? 'Unmute' : 'Mute'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {showSlider && (\n <input\n type=\"range\"\n min={0}\n max={100}\n value={Math.round(volume * 100)}\n onChange={(e) => onVolumeChange(Number.parseInt(e.target.value))}\n className=\"w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Volume control\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`,\n }}\n />\n )}\n </div>\n);\n\n/**\n * Speed menu component props\n */\ninterface SpeedMenuProps {\n showSpeedMenu: boolean;\n playbackRate: number;\n onToggleMenu: () => void;\n onSpeedChange: (speed: number) => void;\n isFullscreen: boolean;\n iconSize?: number;\n isTinyMobile?: boolean;\n}\n\n/**\n * Speed menu subcomponent\n */\nconst SpeedMenu = ({\n showSpeedMenu,\n playbackRate,\n onToggleMenu,\n onSpeedChange,\n isFullscreen,\n iconSize = 24,\n isTinyMobile = false,\n}: SpeedMenuProps) => {\n const buttonRef = useRef<HTMLButtonElement>(null);\n const speedMenuContainerRef = useRef<HTMLDivElement>(null);\n const speedMenuRef = useRef<HTMLDivElement>(null);\n\n const getMenuPosition = () => {\n if (!buttonRef.current) return { top: 0, left: 0 };\n const rect = buttonRef.current.getBoundingClientRect();\n\n // Adjust positioning for tiny mobile screens\n const menuHeight = isTinyMobile ? 150 : 180;\n const menuWidth = isTinyMobile ? 60 : 80;\n const padding = isTinyMobile ? 4 : 8;\n\n return {\n // Fixed coords are viewport-based — no scroll offsets.\n top: Math.max(padding, rect.top - menuHeight),\n left: Math.max(padding, rect.right - menuWidth),\n };\n };\n\n const position = getMenuPosition();\n\n useEffect(() => {\n const handleClickOutside = (event: Event) => {\n const target = event.target as Node;\n\n // Check if click is outside both the container and the menu\n const isOutsideContainer =\n speedMenuContainerRef.current &&\n !speedMenuContainerRef.current.contains(target);\n const isOutsideMenu =\n speedMenuRef.current && !speedMenuRef.current.contains(target);\n\n // Only close if click is outside both refs (null-safe checks)\n if (isOutsideContainer && isOutsideMenu) {\n onToggleMenu();\n }\n };\n\n if (showSpeedMenu) {\n document.addEventListener('mousedown', handleClickOutside);\n }\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n };\n }, [showSpeedMenu, onToggleMenu]);\n\n const menuContent = (\n <div\n ref={speedMenuRef}\n role=\"menu\"\n aria-label=\"Playback speed\"\n className={\n isFullscreen\n ? 'absolute bottom-12 right-0 bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]'\n : 'fixed bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]'\n }\n style={\n isFullscreen\n ? undefined\n : {\n top: `${position.top}px`,\n left: `${position.left}px`,\n }\n }\n >\n {[0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => (\n <button\n key={speed}\n role=\"menuitemradio\"\n aria-checked={playbackRate === speed}\n onClick={() => onSpeedChange(speed)}\n className={`block w-full text-left px-3 py-1 text-sm rounded hover:bg-border-50 transition-colors ${\n playbackRate === speed\n ? 'bg-primary-950 text-secondary-100 font-medium'\n : 'text-text-950'\n }`}\n >\n {speed}x\n </button>\n ))}\n </div>\n );\n\n // SSR-safe portal content\n const portalContent =\n showSpeedMenu &&\n globalThis.window !== undefined &&\n globalThis.document !== undefined &&\n !!globalThis.document?.body\n ? createPortal(menuContent, globalThis.document.body)\n : null;\n\n return (\n <div className=\"relative\" ref={speedMenuContainerRef}>\n <IconButton\n ref={buttonRef}\n icon={<DotsThreeVerticalIcon size={iconSize} />}\n onClick={onToggleMenu}\n aria-label=\"Playback speed\"\n aria-haspopup=\"menu\"\n aria-expanded={showSpeedMenu}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n {showSpeedMenu && (isFullscreen ? menuContent : portalContent)}\n </div>\n );\n};\n\n/**\n * Video player component with controls and progress tracking\n * Integrates with backend lesson progress system\n *\n * @param props - VideoPlayer component props\n * @returns Video player element with controls\n */\nconst VideoPlayer = ({\n src,\n poster,\n subtitles,\n title,\n subtitle: subtitleText,\n initialTime = 0,\n onTimeUpdate,\n onProgress,\n onVideoComplete,\n className,\n autoSave = true,\n storageKey = 'video-progress',\n userId,\n downloadContent,\n showDownloadButton = false,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n}: VideoPlayerProps) => {\n const videoRef = useRef<HTMLVideoElement>(null);\n const { isUltraSmallMobile, isTinyMobile } = useMobile();\n const [isPlaying, setIsPlaying] = useState(false);\n const [hasStarted, setHasStarted] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [isMuted, setIsMuted] = useState(false);\n const [volume, setVolume] = useState(1);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [showControls, setShowControls] = useState(true);\n const [hasCompleted, setHasCompleted] = useState(false);\n const [showCaptions, setShowCaptions] = useState(false);\n const [subtitlesValidation, setSubtitlesValidation] = useState<\n 'idle' | 'validating' | 'valid' | 'invalid'\n >('idle');\n\n // Reset completion flag when changing videos\n useEffect(() => {\n setHasCompleted(false);\n setHasStarted(false);\n }, [src]);\n const [playbackRate, setPlaybackRate] = useState(1);\n const [showSpeedMenu, setShowSpeedMenu] = useState(false);\n const lastSaveTimeRef = useRef(0);\n const trackRef = useRef<HTMLTrackElement>(null);\n const controlsTimeoutRef = useRef<number | null>(null);\n const lastMousePositionRef = useRef({ x: 0, y: 0 });\n\n /**\n * Check if user is currently interacting with controls\n */\n const isUserInteracting = useCallback(() => {\n // Check if speed menu is open\n if (showSpeedMenu) {\n return true;\n }\n\n // Check if any control element has focus\n const activeElement = document.activeElement;\n const videoContainer = videoRef.current?.parentElement;\n\n if (activeElement && videoContainer?.contains(activeElement)) {\n // Ignore the video element itself - it should not prevent control hiding\n if (activeElement === videoRef.current) {\n return false;\n }\n\n // Check if focused element is a control (button, input, etc.)\n const isControl = activeElement.matches('button, input, [tabindex]');\n if (isControl) {\n return true;\n }\n }\n\n return false;\n }, [showSpeedMenu]);\n\n /**\n * Clear controls timeout\n */\n const clearControlsTimeout = useCallback(() => {\n if (controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current);\n controlsTimeoutRef.current = null;\n }\n }, []);\n\n /**\n * Show controls and set auto-hide timer\n */\n const showControlsWithTimer = useCallback(() => {\n setShowControls(true);\n clearControlsTimeout();\n\n // In fullscreen mode, only hide if video is playing\n if (isFullscreen) {\n if (isPlaying) {\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT) as unknown as number;\n }\n } else {\n // In normal mode, always set a timer to hide controls\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT) as unknown as number;\n }\n }, [isFullscreen, isPlaying, clearControlsTimeout]);\n\n /**\n * Handle mouse move with position detection\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n const currentX = event.clientX;\n const currentY = event.clientY;\n const lastPos = lastMousePositionRef.current;\n\n // Check if mouse actually moved (minimum 5px threshold)\n const hasMoved =\n Math.abs(currentX - lastPos.x) > 5 ||\n Math.abs(currentY - lastPos.y) > 5;\n\n if (hasMoved) {\n lastMousePositionRef.current = { x: currentX, y: currentY };\n showControlsWithTimer();\n }\n },\n [showControlsWithTimer]\n );\n\n /**\n * Handle mouse enter to show controls with appropriate timer logic\n */\n const handleMouseEnter = useCallback(() => {\n showControlsWithTimer();\n }, [showControlsWithTimer]);\n\n /**\n * Handle mouse leave to hide controls faster\n */\n const handleMouseLeave = useCallback(() => {\n const userInteracting = isUserInteracting();\n clearControlsTimeout();\n\n // Hide controls when mouse leaves, except when in fullscreen or user is interacting\n if (!isFullscreen && !userInteracting) {\n // Use shorter timeout when mouse leaves\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, LEAVE_HIDE_TIMEOUT) as unknown as number;\n }\n }, [isFullscreen, clearControlsTimeout, isUserInteracting]);\n\n /**\n * Initialize video element properties\n */\n useEffect(() => {\n // Set initial volume\n if (videoRef.current) {\n videoRef.current.volume = volume;\n videoRef.current.muted = isMuted;\n }\n }, [volume, isMuted]);\n\n /**\n * Synchronize isPlaying state with media events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onPlay = () => {\n setIsPlaying(true);\n setHasStarted(true);\n };\n const onPause = () => setIsPlaying(false);\n const onEnded = () => setIsPlaying(false);\n\n video.addEventListener('play', onPlay);\n video.addEventListener('pause', onPause);\n video.addEventListener('ended', onEnded);\n\n return () => {\n video.removeEventListener('play', onPlay);\n video.removeEventListener('pause', onPause);\n video.removeEventListener('ended', onEnded);\n };\n }, []);\n\n /**\n * Set iOS/Safari inline playback attributes imperatively\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n // Ensure inline playback on iOS/Safari\n video.setAttribute('playsinline', '');\n video.setAttribute('webkit-playsinline', '');\n }, []);\n\n /**\n * Handle controls auto-hide when play state changes\n */\n useEffect(() => {\n if (isPlaying) {\n // Start timer when video starts playing\n showControlsWithTimer();\n } else {\n // Keep controls visible when paused only in fullscreen\n clearControlsTimeout();\n if (isFullscreen) {\n setShowControls(true);\n } else {\n // In normal mode (not fullscreen), initialize timer even when paused\n // This ensures controls will hide properly from the start\n showControlsWithTimer();\n }\n }\n }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]);\n\n /**\n * Handle fullscreen state changes from browser events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleFullscreenChange = () => {\n const isCurrentlyFullscreen = !!document.fullscreenElement;\n setIsFullscreen(isCurrentlyFullscreen);\n\n // Show controls when entering fullscreen, hide after timeout if playing\n if (isCurrentlyFullscreen) {\n showControlsWithTimer();\n }\n };\n\n // Safari iOS-specific fullscreen event handlers\n const handleWebkitBeginFullscreen = () => {\n setIsFullscreen(true);\n showControlsWithTimer();\n };\n\n const handleWebkitEndFullscreen = () => {\n setIsFullscreen(false);\n };\n\n // Standard fullscreen events\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n\n // Safari iOS fullscreen events\n video.addEventListener(\n 'webkitbeginfullscreen',\n handleWebkitBeginFullscreen\n );\n video.addEventListener('webkitendfullscreen', handleWebkitEndFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\n video.removeEventListener(\n 'webkitbeginfullscreen',\n handleWebkitBeginFullscreen\n );\n video.removeEventListener(\n 'webkitendfullscreen',\n handleWebkitEndFullscreen\n );\n };\n }, [showControlsWithTimer]);\n\n /**\n * Initialize controls behavior on component mount\n * This ensures controls work correctly from the first load\n */\n useEffect(() => {\n const init = () => {\n if (!isFullscreen) {\n showControlsWithTimer();\n }\n };\n // Prefer rAF to avoid arbitrary timing if available; fall back to INIT_DELAY.\n let raf1 = 0,\n raf2 = 0,\n tid: number | undefined;\n if (globalThis.requestAnimationFrame === undefined) {\n tid = globalThis.setTimeout(init, INIT_DELAY) as unknown as number;\n return () => {\n if (tid) clearTimeout(tid);\n };\n } else {\n raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(init);\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }\n }, []); // Run only once on mount\n\n /**\n * Get initial time from props or localStorage\n */\n const getInitialTime = useCallback((): number | undefined => {\n if (!autoSave || !storageKey) {\n return Number.isFinite(initialTime) && initialTime >= 0\n ? initialTime\n : undefined;\n }\n\n const scopedKey = userId ? `${storageKey}-${userId}` : storageKey;\n const saved = Number(\n localStorage.getItem(`${scopedKey}-${src}`) || Number.NaN\n );\n const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;\n const hasValidSaved = Number.isFinite(saved) && saved >= 0;\n\n if (hasValidInitial) return initialTime;\n if (hasValidSaved) return saved;\n return undefined;\n }, [autoSave, storageKey, userId, src, initialTime]);\n\n /**\n * Load saved progress from localStorage\n */\n useEffect(() => {\n const start = getInitialTime();\n if (start !== undefined && Number.isFinite(start) && videoRef.current) {\n videoRef.current.currentTime = start;\n setCurrentTime(start);\n }\n }, [getInitialTime]);\n\n /**\n * Save progress to localStorage periodically\n */\n const saveProgress = useCallback(\n (time: number) => {\n if (!autoSave || !storageKey) return;\n\n const now = Date.now();\n if (now - lastSaveTimeRef.current > 5000) {\n const scopedKey = userId ? `${storageKey}-${userId}` : storageKey;\n localStorage.setItem(`${scopedKey}-${src}`, time.toString());\n lastSaveTimeRef.current = now;\n }\n },\n [autoSave, storageKey, userId, src]\n );\n\n /**\n * Handle play/pause toggle\n */\n const togglePlayPause = useCallback(async () => {\n const video = videoRef.current;\n if (!video) return;\n\n if (!video.paused) {\n video.pause();\n return;\n }\n\n try {\n await video.play();\n } catch {\n // Playback prevented (e.g., autoplay policy); keep state unchanged.\n }\n }, []);\n\n /**\n * Handle volume change\n */\n const handleVolumeChange = useCallback(\n (newVolume: number) => {\n const video = videoRef.current;\n if (!video) return;\n\n const volumeValue = newVolume / 100; // Convert 0-100 to 0-1\n video.volume = volumeValue;\n setVolume(volumeValue);\n\n // Auto mute/unmute based on volume\n const shouldMute = volumeValue === 0;\n const shouldUnmute = volumeValue > 0 && isMuted;\n\n if (shouldMute) {\n video.muted = true;\n setIsMuted(true);\n } else if (shouldUnmute) {\n video.muted = false;\n setIsMuted(false);\n }\n },\n [isMuted]\n );\n\n /**\n * Handle mute toggle\n */\n const toggleMute = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n if (isMuted) {\n // Unmute: restore volume or set to 50% if it was 0\n const restoreVolume = volume > 0 ? volume : 0.5;\n video.volume = restoreVolume;\n video.muted = false;\n setVolume(restoreVolume);\n setIsMuted(false);\n } else {\n // Mute: set volume to 0 and mute\n video.muted = true;\n setIsMuted(true);\n }\n }, [isMuted, volume]);\n\n /**\n * Handle video seek\n */\n const handleSeek = useCallback((newTime: number) => {\n const video = videoRef.current;\n if (video && Number.isFinite(newTime)) {\n video.currentTime = newTime;\n }\n }, []);\n\n /**\n * Detect if running on Safari iOS\n */\n const isSafariIOS = useCallback((): boolean => {\n const ua = navigator.userAgent;\n const isIOS = /iPad|iPhone|iPod/.test(ua);\n const isWebKit = /WebKit/.test(ua);\n const isNotChrome = !/CriOS|Chrome/.test(ua);\n return isIOS && isWebKit && isNotChrome;\n }, []);\n\n /**\n * Handle fullscreen toggle\n */\n const toggleFullscreen = useCallback(() => {\n const video = videoRef.current;\n const container = video?.parentElement;\n if (!video || !container) return;\n\n // Safari iOS requires using webkitEnterFullscreen on video element\n if (isSafariIOS()) {\n // Type assertion for Safari-specific API\n const videoElement = video as HTMLVideoElement & {\n webkitEnterFullscreen?: () => void;\n webkitExitFullscreen?: () => void;\n webkitDisplayingFullscreen?: boolean;\n };\n\n if (!isFullscreen && videoElement.webkitEnterFullscreen) {\n videoElement.webkitEnterFullscreen();\n } else if (isFullscreen && videoElement.webkitExitFullscreen) {\n videoElement.webkitExitFullscreen();\n }\n } else if (!isFullscreen && container.requestFullscreen) {\n // Standard fullscreen API for other browsers. The returned promise can\n // reject (e.g. document not active after a tab switch); swallow it.\n container.requestFullscreen().catch(() => {});\n } else if (isFullscreen && document.exitFullscreen) {\n document.exitFullscreen().catch(() => {});\n }\n }, [isFullscreen, isSafariIOS]);\n\n /**\n * Handle playback speed change\n */\n const handleSpeedChange = useCallback((speed: number) => {\n if (videoRef.current) {\n videoRef.current.playbackRate = speed;\n setPlaybackRate(speed);\n setShowSpeedMenu(false);\n }\n }, []);\n\n /**\n * Toggle speed menu visibility\n */\n const toggleSpeedMenu = useCallback(() => {\n setShowSpeedMenu(!showSpeedMenu);\n }, [showSpeedMenu]);\n\n /**\n * Toggle captions visibility\n */\n const toggleCaptions = useCallback(() => {\n if (\n !trackRef.current?.track ||\n !subtitles ||\n subtitlesValidation !== 'valid'\n )\n return;\n\n const newShowCaptions = !showCaptions;\n setShowCaptions(newShowCaptions);\n\n // Control track mode programmatically - we already validated subtitles above\n trackRef.current.track.mode = newShowCaptions ? 'showing' : 'hidden';\n }, [showCaptions, subtitles, subtitlesValidation]);\n\n /**\n * Check video completion and fire callback\n */\n const checkVideoCompletion = useCallback(\n (progressPercent: number) => {\n if (progressPercent >= 95 && !hasCompleted) {\n setHasCompleted(true);\n onVideoComplete?.();\n }\n },\n [hasCompleted, onVideoComplete]\n );\n\n /**\n * Handle time update\n */\n const handleTimeUpdate = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const current = video.currentTime;\n setCurrentTime(current);\n\n // Save progress periodically\n saveProgress(current);\n\n // Fire callbacks\n onTimeUpdate?.(current);\n\n if (duration > 0) {\n const progressPercent = (current / duration) * 100;\n onProgress?.(progressPercent);\n checkVideoCompletion(progressPercent);\n }\n }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);\n\n /**\n * Handle loaded metadata\n */\n const handleLoadedMetadata = useCallback(() => {\n if (videoRef.current) {\n setDuration(videoRef.current.duration);\n }\n }, []);\n\n /**\n * Validate subtitles URL before showing the button\n */\n useEffect(() => {\n const controller = new AbortController();\n\n const validateSubtitles = async () => {\n // If no subtitles, mark as idle\n if (!subtitles) {\n setSubtitlesValidation('idle');\n return;\n }\n\n // Start validation\n setSubtitlesValidation('validating');\n\n try {\n // Check if it's a data URL (inline VTT)\n if (subtitles.startsWith('data:')) {\n setSubtitlesValidation('valid');\n return;\n }\n\n // Fetch the subtitles file to validate it\n const response = await fetch(subtitles, {\n method: 'HEAD',\n signal: controller.signal,\n });\n\n if (response.ok) {\n // Optionally check content type\n const contentType = response.headers.get('content-type');\n const isValidType =\n !contentType ||\n contentType.includes('text/vtt') ||\n contentType.includes('text/plain') ||\n contentType.includes('application/octet-stream');\n\n if (isValidType) {\n setSubtitlesValidation('valid');\n } else {\n setSubtitlesValidation('invalid');\n console.warn(\n `Subtitles URL has invalid content type: ${contentType}`\n );\n }\n } else {\n setSubtitlesValidation('invalid');\n console.warn(\n `Subtitles URL returned status: ${response.status} ${response.statusText}`\n );\n }\n } catch (error) {\n // Ignore AbortError - it's expected when cleaning up\n if (error instanceof Error && error.name === 'AbortError') {\n return;\n }\n console.warn('Subtitles URL validation failed:', error);\n setSubtitlesValidation('invalid');\n }\n };\n\n validateSubtitles();\n\n // Cleanup: abort ongoing fetch to prevent stale updates\n return () => {\n controller.abort();\n };\n }, [subtitles]);\n\n /**\n * Initialize track mode when track is available\n */\n useEffect(() => {\n if (trackRef.current?.track) {\n // Set initial mode based on showCaptions state and subtitle availability\n trackRef.current.track.mode =\n showCaptions && subtitles && subtitlesValidation === 'valid'\n ? 'showing'\n : 'hidden';\n }\n }, [subtitles, showCaptions, subtitlesValidation]);\n\n /**\n * Handle visibility change and blur to pause video when losing focus\n */\n useEffect(() => {\n const handleVisibilityChange = () => {\n if (document.hidden && isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n const handleBlur = () => {\n if (isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n document.addEventListener('visibilitychange', handleVisibilityChange);\n globalThis.addEventListener('blur', handleBlur);\n\n return () => {\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n globalThis.removeEventListener('blur', handleBlur);\n // Clean up timers on unmount\n clearControlsTimeout();\n };\n }, [isPlaying, clearControlsTimeout]);\n\n const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;\n\n /**\n * Get responsive icon size based on screen size\n */\n const getIconSize = useCallback(() => {\n if (isTinyMobile) return 18;\n if (isUltraSmallMobile) return 20;\n return 24;\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive padding classes for controls\n */\n const getControlsPadding = useCallback(() => {\n if (isTinyMobile) return 'px-2 pb-2 pt-1';\n if (isUltraSmallMobile) return 'px-3 pb-3 pt-1';\n return 'px-4 pb-4';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive gap classes for controls\n */\n const getControlsGap = useCallback(() => {\n if (isTinyMobile) return 'gap-1';\n if (isUltraSmallMobile) return 'gap-2';\n return 'gap-4';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive padding for progress bar\n */\n const getProgressBarPadding = useCallback(() => {\n if (isTinyMobile) return 'px-2 pb-1';\n if (isUltraSmallMobile) return 'px-3 pb-1';\n return 'px-4 pb-2';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive positioning classes for center play button\n */\n const getCenterPlayButtonPosition = useCallback(() => {\n if (isTinyMobile) return 'items-center justify-center -translate-y-12';\n if (isUltraSmallMobile) return 'items-center justify-center -translate-y-8';\n return 'items-center justify-center';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Calculate top controls opacity based on state\n */\n const getTopControlsOpacity = useCallback(() => {\n return showControls ? 'opacity-100' : 'opacity-0';\n }, [showControls]);\n\n /**\n * Calculate bottom controls opacity based on state\n */\n const getBottomControlsOpacity = useCallback(() => {\n return showControls ? 'opacity-100' : 'opacity-0';\n }, [showControls]);\n\n /**\n * Seek video backward\n */\n const seekBackward = useCallback(() => {\n if (videoRef.current) {\n videoRef.current.currentTime -= 10;\n }\n }, []);\n\n /**\n * Seek video forward\n */\n const seekForward = useCallback(() => {\n if (videoRef.current) {\n videoRef.current.currentTime += 10;\n }\n }, []);\n\n /**\n * Increase volume\n */\n const increaseVolume = useCallback(() => {\n handleVolumeChange(Math.min(100, volume * 100 + 10));\n }, [handleVolumeChange, volume]);\n\n /**\n * Decrease volume\n */\n const decreaseVolume = useCallback(() => {\n handleVolumeChange(Math.max(0, volume * 100 - 10));\n }, [handleVolumeChange, volume]);\n\n /**\n * Handle video element keyboard events\n */\n const handleVideoKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (!e.key) return;\n\n // Prevent bubbling to parent handlers to avoid double toggles\n e.stopPropagation();\n showControlsWithTimer();\n\n // Map of key handlers for better maintainability and reduced complexity\n const keyHandlers: Record<string, () => void | Promise<void>> = {\n ' ': togglePlayPause,\n Enter: togglePlayPause,\n ArrowLeft: seekBackward,\n ArrowRight: seekForward,\n ArrowUp: increaseVolume,\n ArrowDown: decreaseVolume,\n m: toggleMute,\n M: toggleMute,\n f: toggleFullscreen,\n F: toggleFullscreen,\n };\n\n const handler = keyHandlers[e.key];\n if (handler) {\n e.preventDefault();\n handler();\n }\n },\n [\n showControlsWithTimer,\n togglePlayPause,\n seekBackward,\n seekForward,\n increaseVolume,\n decreaseVolume,\n toggleMute,\n toggleFullscreen,\n ]\n );\n\n const groupedSubTitleValid = subtitles && subtitlesValidation === 'valid';\n const showPoster = Boolean(poster) && !hasStarted;\n\n return (\n <div className={cn('flex flex-col', className)}>\n {/* Integrated Header */}\n {(title || subtitleText) && (\n <div className=\"bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20\">\n <div className=\"flex flex-col gap-1\">\n {title && (\n <Text\n as=\"h2\"\n size=\"lg\"\n weight=\"bold\"\n color=\"text-text-900\"\n className=\"leading-5 tracking-wide\"\n >\n {title}\n </Text>\n )}\n {subtitleText && (\n <Text\n as=\"p\"\n size=\"sm\"\n weight=\"normal\"\n color=\"text-text-600\"\n className=\"leading-5\"\n >\n {subtitleText}\n </Text>\n )}\n </div>\n\n {/* Download Button */}\n {showDownloadButton && downloadContent && (\n <DownloadButton\n content={downloadContent}\n lessonTitle={title}\n onDownloadStart={onDownloadStart}\n onDownloadComplete={onDownloadComplete}\n onDownloadError={onDownloadError}\n className=\"flex-shrink-0\"\n />\n )}\n </div>\n )}\n\n {/* Video Container */}\n <section\n className={cn(\n 'relative w-full bg-background overflow-hidden group',\n 'rounded-b-xl',\n // Hide cursor when controls are hidden and video is playing\n isPlaying && !showControls\n ? 'cursor-none group-hover:cursor-default'\n : 'cursor-default'\n )}\n aria-label={title ? `Video player: ${title}` : 'Video player'}\n onMouseMove={handleMouseMove}\n onMouseEnter={handleMouseEnter}\n onTouchStart={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {/* Video Element */}\n <video\n ref={videoRef}\n src={src}\n crossOrigin=\"anonymous\"\n className=\"w-full h-full object-contain analytica-video\"\n controlsList=\"nodownload\"\n playsInline\n onTimeUpdate={handleTimeUpdate}\n onLoadedMetadata={handleLoadedMetadata}\n onClick={togglePlayPause}\n onKeyDown={handleVideoKeyDown}\n tabIndex={0}\n aria-label={title ? `Video: ${title}` : 'Video player'}\n >\n <track\n ref={trackRef}\n kind=\"captions\"\n src={\n groupedSubTitleValid\n ? subtitles\n : 'data:text/vtt;charset=utf-8,WEBVTT'\n }\n srcLang=\"pt-br\"\n label={\n groupedSubTitleValid\n ? 'Legendas em Português'\n : 'Sem legendas disponíveis'\n }\n default={false}\n />\n </video>\n\n {showPoster && (\n <img\n src={poster}\n alt={title ? `Capa: ${title}` : 'Capa do vídeo'}\n className=\"absolute inset-0 w-full h-full object-cover\"\n aria-hidden=\"true\"\n />\n )}\n\n {/* Center Play Button */}\n {!isPlaying && (\n <div\n className={cn(\n 'absolute inset-0 flex bg-black/30 transition-opacity',\n getCenterPlayButtonPosition()\n )}\n >\n <IconButton\n icon={<PlayIcon size={32} weight=\"regular\" className=\"ml-1\" />}\n onClick={togglePlayPause}\n aria-label=\"Play video\"\n className=\"!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200\"\n />\n </div>\n )}\n\n {/* Top Controls */}\n <div\n className={cn(\n 'absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity',\n getTopControlsOpacity()\n )}\n >\n <div className=\"flex justify-start\">\n <IconButton\n icon={\n isFullscreen ? (\n <ArrowsInSimpleIcon size={24} />\n ) : (\n <ArrowsOutSimpleIcon size={24} />\n )\n }\n onClick={toggleFullscreen}\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div\n className={cn(\n 'absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity',\n getBottomControlsOpacity()\n )}\n >\n {/* Progress Bar */}\n <ProgressBar\n currentTime={currentTime}\n duration={duration}\n progressPercentage={progressPercentage}\n onSeek={handleSeek}\n className={getProgressBarPadding()}\n />\n\n {/* Control Buttons */}\n <div\n className={cn(\n 'flex items-center justify-between',\n getControlsPadding()\n )}\n >\n {/* Left Controls */}\n <div className={cn('flex items-center', getControlsGap())}>\n {/* Play/Pause */}\n <IconButton\n icon={\n isPlaying ? (\n <PauseIcon size={getIconSize()} />\n ) : (\n <PlayIcon size={getIconSize()} />\n )\n }\n onClick={togglePlayPause}\n aria-label={isPlaying ? 'Pause' : 'Play'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {/* Volume */}\n <VolumeControls\n volume={volume}\n isMuted={isMuted}\n onVolumeChange={handleVolumeChange}\n onToggleMute={toggleMute}\n iconSize={getIconSize()}\n showSlider={!isUltraSmallMobile}\n />\n\n {/* Captions - Only show after validation is complete and valid */}\n {groupedSubTitleValid && (\n <IconButton\n icon={<ClosedCaptioningIcon size={getIconSize()} />}\n onClick={toggleCaptions}\n aria-label={showCaptions ? 'Hide captions' : 'Show captions'}\n className={cn(\n '!bg-transparent hover:!bg-white/20',\n showCaptions ? '!text-primary-400' : '!text-white'\n )}\n />\n )}\n\n {/* Time Display */}\n <Text size=\"sm\" weight=\"medium\" color=\"text-white\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </Text>\n </div>\n\n {/* Right Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Speed Control */}\n <SpeedMenu\n showSpeedMenu={showSpeedMenu}\n playbackRate={playbackRate}\n onToggleMenu={toggleSpeedMenu}\n onSpeedChange={handleSpeedChange}\n iconSize={getIconSize()}\n isTinyMobile={isTinyMobile}\n isFullscreen={isFullscreen}\n />\n </div>\n </div>\n </div>\n </section>\n </div>\n );\n};\n\nexport default VideoPlayer;\n"],"mappings":"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