import React, { useEffect, useRef, useState, useCallback, lazy, Suspense, type ReactNode } from 'react' import { usePlayerContext } from '../contexts/PlayerContext' import { PlayPauseButton } from './controls/PlayPauseButton' import { ProgressBar } from './controls/ProgressBar' import { VolumeControl } from './controls/VolumeControl' import { TimeDisplay } from './controls/TimeDisplay' import { FullscreenButton } from './controls/FullscreenButton' import { PIPButton } from './controls/PIPButton' import { SettingsButton } from './controls/SettingsButton' import { LoadingSpinner } from './overlays/LoadingSpinner' import { CenterPlayButton } from './controls/CenterPlayButton' import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts' import { useTouchGestures } from '../hooks/useTouchGestures' import { features } from '../utils/polyfills' import type { SubtitleTrack, AudioTrack, VideoQuality, KeyboardShortcutConfig, TouchConfig } from '../types' import './ControlsLayer.css' const SettingsMenu = lazy(() => import('./menus/SettingsMenu').then(module => ({ default: module.SettingsMenu }))) interface ControlsLayerProps { keyboardShortcuts?: boolean keyboardShortcutConfig?: KeyboardShortcutConfig pictureInPicture?: boolean subtitles?: SubtitleTrack[] audioTracks?: AudioTrack[] qualities?: VideoQuality[] controlsAutoHideDelay?: number playbackRates?: number[] touchConfig?: TouchConfig controlsLeftExtra?: ReactNode controlsRightExtra?: ReactNode } export const ControlsLayer: React.FC = ({ keyboardShortcuts = true, keyboardShortcutConfig, pictureInPicture = true, subtitles = [], audioTracks = [], qualities = [], controlsAutoHideDelay = 3000, playbackRates, touchConfig, controlsLeftExtra, controlsRightExtra, }) => { const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls, translations } = usePlayerContext() const [isPointerOver, setIsPointerOver] = useState(false) const [lastInteraction, setLastInteraction] = useState(0) const hideTimeoutRef = useRef(undefined) const containerRef = useRef(null) const lastClickTimeRef = useRef(0) const isMenuOpen = uiState.settingsOpen || uiState.volumeControlOpen || uiState.qualityMenuOpen || uiState.subtitleMenuOpen const autoHideEnabled = videoState.fullscreen && videoState.playing && !isMenuOpen const clearHideTimeout = useCallback(() => { if (hideTimeoutRef.current) { window.clearTimeout(hideTimeoutRef.current) hideTimeoutRef.current = undefined } }, []) const scheduleHide = useCallback(() => { if (!autoHideEnabled) { clearHideTimeout() return } clearHideTimeout() hideTimeoutRef.current = window.setTimeout(() => { hideControls() }, controlsAutoHideDelay) }, [autoHideEnabled, clearHideTimeout, hideControls, controlsAutoHideDelay]) // Keep controls visible when not playing or when any menu is open useEffect(() => { if (!videoState.playing || isMenuOpen) { clearHideTimeout() showControls() } }, [videoState.playing, isMenuOpen, showControls, clearHideTimeout]) // Manage controls visibility when leaving fullscreen useEffect(() => { if (!videoState.fullscreen) { clearHideTimeout() if (!videoState.playing || isPointerOver) { showControls() } } }, [videoState.fullscreen, videoState.playing, isPointerOver, showControls, clearHideTimeout]) // Re-schedule auto hide when interaction changes useEffect(() => { if (autoHideEnabled && lastInteraction > 0) { scheduleHide() } return () => { if (autoHideEnabled) { clearHideTimeout() } } }, [autoHideEnabled, lastInteraction, scheduleHide, clearHideTimeout]) const handleMouseEnter = useCallback(() => { setIsPointerOver(true) showControls() if (autoHideEnabled) { setLastInteraction(Date.now()) } }, [autoHideEnabled, showControls]) // Handle mouse movement const handleMouseMove = useCallback(() => { setIsPointerOver(true) showControls() if (autoHideEnabled) { setLastInteraction(Date.now()) } }, [autoHideEnabled, showControls]) const handleMouseLeave = useCallback(() => { setIsPointerOver(false) clearHideTimeout() if (videoState.fullscreen) { if (videoState.playing) { hideControls() } else { showControls() } } else if (videoState.playing) { hideControls() } }, [clearHideTimeout, videoState.fullscreen, videoState.playing, hideControls, showControls]) useEffect(() => { return () => { clearHideTimeout() } }, [clearHideTimeout]) const previousAutoHide = useRef(autoHideEnabled) useEffect(() => { if (autoHideEnabled && !previousAutoHide.current) { showControls() setLastInteraction(Date.now()) } previousAutoHide.current = autoHideEnabled }, [autoHideEnabled, showControls]) // Keyboard shortcuts useKeyboardShortcuts(keyboardShortcuts, keyboardShortcutConfig) // Touch gestures useTouchGestures(containerRef, touchConfig) // Handle click for play/pause and double-click for fullscreen const handleClick = useCallback( (e: React.MouseEvent) => { // Get the actual element that was clicked const target = e.target as HTMLElement const currentTarget = e.currentTarget as HTMLElement // Allow clicks on: // 1. The controls layer itself (when controls are hidden, pointer-events: none makes it work) // 2. The center play overlay // Don't handle clicks on control buttons or other interactive elements const isClickableArea = target === currentTarget || target.classList.contains('center-play-overlay') || target.classList.contains('controls-layer') if (!isClickableArea) { return } const now = Date.now() const timeSinceLastClick = now - lastClickTimeRef.current if (timeSinceLastClick < 300) { // Double click - toggle fullscreen e.preventDefault() // Prevent text selection on double click toggleFullscreen() lastClickTimeRef.current = 0 } else { // Single click - toggle play/pause (with delay to detect double click) setTimeout(() => { if (Date.now() - lastClickTimeRef.current >= 300) { togglePlay() } }, 300) lastClickTimeRef.current = now } }, [togglePlay, toggleFullscreen] ) const controlsClassName = `controls-layer ${uiState.controlsVisible ? 'visible' : 'hidden'} ${ videoState.playing ? 'playing' : 'paused' } ${videoState.fullscreen ? 'fullscreen' : 'windowed'}` return (
{/* Loading spinner */} {videoState.loading && } {/* Center play button (only when paused) */} {!videoState.playing && !videoState.loading && } {/* Bottom controls bar */}
{/* Progress bar (full width on top) - hidden for live broadcasts */} {!videoState.isLiveBroadcast && (
)} {/* Control buttons */}
{features.hasVolumeControl() && } {/* Time display - hidden for live broadcasts */} {!videoState.isLiveBroadcast && } {/* Show "LIVE" badge for live broadcasts */} {videoState.isLiveBroadcast && (
{translations.live}
)} {controlsLeftExtra}
{controlsRightExtra}
{pictureInPicture && }
) }