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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user