Initial commit: modern React video player library

Add all source files for a feature-rich, reusable video player built with React, TypeScript, and Vite. Includes core components, context, hooks, utilities, styles, demo app, and configuration files.
This commit is contained in:
hibna
2025-10-29 07:49:06 +03:00
parent d68df70124
commit b57b24d051
47 changed files with 4414 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
import React, { useEffect, useRef, useState, useCallback } 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 { SettingsMenu } from './menus/SettingsMenu'
import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts'
import { useTouchGestures } from '../hooks/useTouchGestures'
import type { SubtitleTrack, AudioTrack } from '../types'
import './ControlsLayer.css'
interface ControlsLayerProps {
keyboardShortcuts?: boolean
pictureInPicture?: boolean
subtitles?: SubtitleTrack[]
audioTracks?: AudioTrack[]
}
export const ControlsLayer: React.FC<ControlsLayerProps> = ({
keyboardShortcuts = true,
pictureInPicture = true,
subtitles = [],
audioTracks = [],
}) => {
const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls } = usePlayerContext()
const [mouseMoving, setMouseMoving] = useState(false)
const hideTimeoutRef = useRef<number>()
const containerRef = useRef<HTMLDivElement>(null)
const lastClickTimeRef = useRef<number>(0)
// Auto-hide controls after inactivity
useEffect(() => {
if (videoState.playing) {
// Show controls on mouse movement
if (mouseMoving) {
showControls()
}
// Clear existing timeout
if (hideTimeoutRef.current) {
window.clearTimeout(hideTimeoutRef.current)
}
// Hide controls after inactivity (3 seconds in all modes)
if (mouseMoving) {
const hideDelay = 3000
hideTimeoutRef.current = window.setTimeout(() => {
hideControls()
setMouseMoving(false)
}, hideDelay)
}
} else {
// Always show controls when paused
showControls()
}
return () => {
if (hideTimeoutRef.current) {
window.clearTimeout(hideTimeoutRef.current)
}
}
}, [mouseMoving, videoState.playing, videoState.fullscreen, showControls, hideControls])
// Handle mouse movement
const handleMouseMove = useCallback(() => {
if (!mouseMoving) {
setMouseMoving(true)
}
}, [mouseMoving])
const handleMouseLeave = useCallback(() => {
// Only hide controls on mouse leave when in fullscreen mode
// When player is small, controls should stay visible
setMouseMoving(false)
if (videoState.playing && videoState.fullscreen) {
hideControls()
}
}, [videoState.playing, videoState.fullscreen, hideControls])
// Keyboard shortcuts
useKeyboardShortcuts(keyboardShortcuts)
// Touch gestures
useTouchGestures(containerRef)
// 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'
}`
return (
<div
ref={containerRef}
className={controlsClassName}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
>
{/* Loading spinner */}
{videoState.loading && <LoadingSpinner />}
{/* Center play button (only when paused) */}
{!videoState.playing && !videoState.loading && <CenterPlayButton />}
{/* Bottom controls bar */}
<div className="controls-bar">
{/* Progress bar (full width on top) */}
<div className="progress-container">
<ProgressBar />
</div>
{/* Control buttons */}
<div className="controls-row">
<div className="controls-left">
<PlayPauseButton />
<VolumeControl />
<TimeDisplay />
</div>
<div className="controls-right">
<div style={{ position: 'relative' }}>
<SettingsButton />
<SettingsMenu subtitles={subtitles} audioTracks={audioTracks} />
</div>
{pictureInPicture && <PIPButton />}
<FullscreenButton />
</div>
</div>
</div>
</div>
)
}