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