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
+184
View File
@@ -0,0 +1,184 @@
import React, { createContext, useContext, useRef, useState, useCallback } from 'react'
import type { PlayerContextValue, VideoState, UIState, PlayerSettings, AudioTrack } from '../types'
interface PlayerContextType extends PlayerContextValue {
setVideoState: React.Dispatch<React.SetStateAction<VideoState>>
setUIState: React.Dispatch<React.SetStateAction<UIState>>
}
const PlayerContext = createContext<PlayerContextType | null>(null)
export const usePlayerContext = () => {
const context = useContext(PlayerContext)
if (!context) {
throw new Error('usePlayerContext must be used within a PlayerProvider')
}
return context
}
interface PlayerProviderProps {
children: React.ReactNode
initialVolume?: number
initialMuted?: boolean
initialPlaybackRate?: number
}
export const PlayerProvider: React.FC<PlayerProviderProps> = ({
children,
initialVolume = 1,
initialMuted = false,
initialPlaybackRate = 1,
}) => {
const videoRef = useRef<HTMLVideoElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [videoState, setVideoState] = useState<VideoState>({
playing: false,
currentTime: 0,
duration: 0,
buffered: 0,
volume: initialVolume,
muted: initialMuted,
playbackRate: initialPlaybackRate,
fullscreen: false,
pictureInPicture: false,
loading: false,
error: null,
seeking: false,
})
const [uiState, setUIState] = useState<UIState>({
controlsVisible: true,
settingsOpen: false,
volumeControlOpen: false,
qualityMenuOpen: false,
subtitleMenuOpen: false,
})
const [settings, setSettings] = useState<PlayerSettings>({
quality: null,
subtitle: null,
audioTrack: null,
playbackRate: initialPlaybackRate,
})
// Video controls
const play = useCallback(() => {
videoRef.current?.play()
}, [])
const pause = useCallback(() => {
videoRef.current?.pause()
}, [])
const togglePlay = useCallback(() => {
if (videoState.playing) {
pause()
} else {
play()
}
}, [videoState.playing, play, pause])
const seek = useCallback((time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
}, [])
const setVolume = useCallback((volume: number) => {
if (videoRef.current) {
const clampedVolume = Math.max(0, Math.min(1, volume))
videoRef.current.volume = clampedVolume
setVideoState((prev) => ({ ...prev, volume: clampedVolume }))
}
}, [])
const toggleMute = useCallback(() => {
if (videoRef.current) {
videoRef.current.muted = !videoRef.current.muted
setVideoState((prev) => ({ ...prev, muted: !prev.muted }))
}
}, [])
const setPlaybackRate = useCallback((rate: number) => {
if (videoRef.current) {
videoRef.current.playbackRate = rate
setVideoState((prev) => ({ ...prev, playbackRate: rate }))
setSettings((prev) => ({ ...prev, playbackRate: rate }))
}
}, [])
// Fullscreen & PIP
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
containerRef.current?.requestFullscreen()
} else {
document.exitFullscreen()
}
}, [])
const togglePictureInPicture = useCallback(async () => {
if (!document.pictureInPictureElement) {
try {
await videoRef.current?.requestPictureInPicture()
} catch (error) {
console.error('PIP error:', error)
}
} else {
await document.exitPictureInPicture()
}
}, [])
// UI controls
const showControls = useCallback(() => {
setUIState((prev) => ({ ...prev, controlsVisible: true }))
}, [])
const hideControls = useCallback(() => {
setUIState((prev) => ({ ...prev, controlsVisible: false }))
}, [])
const toggleSettings = useCallback(() => {
setUIState((prev) => ({ ...prev, settingsOpen: !prev.settingsOpen }))
}, [])
// Settings
const setQuality = useCallback((quality: typeof settings.quality) => {
setSettings((prev) => ({ ...prev, quality }))
}, [])
const setSubtitle = useCallback((subtitle: typeof settings.subtitle) => {
setSettings((prev) => ({ ...prev, subtitle }))
}, [])
const setAudioTrack = useCallback((audioTrack: AudioTrack | null) => {
setSettings((prev) => ({ ...prev, audioTrack }))
}, [])
const value: PlayerContextType = {
videoState,
uiState,
settings,
videoRef,
containerRef,
setVideoState,
setUIState,
play,
pause,
togglePlay,
seek,
setVolume,
toggleMute,
setPlaybackRate,
toggleFullscreen,
togglePictureInPicture,
showControls,
hideControls,
toggleSettings,
setQuality,
setSubtitle,
setAudioTrack,
}
return <PlayerContext.Provider value={value}>{children}</PlayerContext.Provider>
}