Add volume, playbackRate, and currentTime props

Introduces new props (volume, playbackRate, currentTime) to VideoPlayer and VideoElement components, allowing external control of volume, playback speed, and initial playback position. Also adds related event handlers (onRateChange, onFullscreenChange, onPictureInPictureChange, onProgress, onDurationChange, onWaiting, onCanPlay) and updates documentation and types accordingly.
This commit is contained in:
hibna
2025-11-04 07:38:06 +03:00
parent 164450dd13
commit 80bd589c03
6 changed files with 176 additions and 13 deletions
+82 -5
View File
@@ -16,6 +16,9 @@ interface VideoElementProps {
autoplay?: boolean
loop?: boolean
muted?: boolean
volume?: number
playbackRate?: number
currentTime?: number
subtitles?: SubtitleTrack[]
onPlay?: () => void
onPause?: () => void
@@ -26,6 +29,13 @@ interface VideoElementProps {
onLoadedMetadata?: () => void
onSeeking?: () => void
onSeeked?: () => void
onProgress?: (buffered: number) => void
onDurationChange?: (duration: number) => void
onRateChange?: (playbackRate: number) => void
onFullscreenChange?: (isFullscreen: boolean) => void
onPictureInPictureChange?: (isPictureInPicture: boolean) => void
onWaiting?: () => void
onCanPlay?: () => void
onAudioTracksLoaded?: (tracks: AudioTrack[]) => void
onQualityLevelsLoaded?: (qualities: VideoQuality[]) => void
onSubtitleTracksLoaded?: (tracks: SubtitleTrack[]) => void
@@ -37,6 +47,9 @@ export const VideoElement: React.FC<VideoElementProps> = ({
autoplay = false,
loop = false,
muted = false,
volume,
playbackRate,
currentTime: initialCurrentTime,
subtitles = [],
onPlay,
onPause,
@@ -47,6 +60,13 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onLoadedMetadata,
onSeeking,
onSeeked,
onProgress,
onDurationChange,
onRateChange,
onFullscreenChange,
onPictureInPictureChange,
onWaiting,
onCanPlay,
onAudioTracksLoaded,
onQualityLevelsLoaded,
onSubtitleTracksLoaded,
@@ -129,7 +149,9 @@ export const VideoElement: React.FC<VideoElementProps> = ({
duration: video.duration,
isLiveBroadcast,
}))
}, [videoRef, setVideoState])
onDurationChange?.(video.duration)
}, [videoRef, setVideoState, onDurationChange])
const handleVolumeChange = useCallback(() => {
const video = videoRef.current
@@ -156,11 +178,29 @@ export const VideoElement: React.FC<VideoElementProps> = ({
const handleWaiting = useCallback(() => {
setVideoState((prev) => ({ ...prev, loading: true }))
}, [setVideoState])
onWaiting?.()
}, [setVideoState, onWaiting])
const handleCanPlay = useCallback(() => {
setVideoState((prev) => ({ ...prev, loading: false }))
}, [setVideoState])
onCanPlay?.()
}, [setVideoState, onCanPlay])
const handleProgress = useCallback(() => {
const video = videoRef.current
if (!video) return
const buffered = video.buffered.length > 0 ? video.buffered.end(video.buffered.length - 1) : 0
onProgress?.(buffered)
}, [videoRef, onProgress])
const handleRateChange = useCallback(() => {
const video = videoRef.current
if (!video) return
setVideoState((prev) => ({ ...prev, playbackRate: video.playbackRate }))
onRateChange?.(video.playbackRate)
}, [videoRef, setVideoState, onRateChange])
const handleEnded = useCallback(() => {
setVideoState((prev) => ({ ...prev, playing: false }))
@@ -212,19 +252,21 @@ export const VideoElement: React.FC<VideoElementProps> = ({
const handleFullscreenChange = () => {
const isFullscreen = !!document.fullscreenElement
setVideoState((prev) => ({ ...prev, fullscreen: isFullscreen }))
onFullscreenChange?.(isFullscreen)
}
document.addEventListener('fullscreenchange', handleFullscreenChange)
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
}
}, [setVideoState])
}, [setVideoState, onFullscreenChange])
// Handle PIP changes
useEffect(() => {
const handlePIPChange = () => {
const isPIP = !!document.pictureInPictureElement
setVideoState((prev) => ({ ...prev, pictureInPicture: isPIP }))
onPictureInPictureChange?.(isPIP)
}
document.addEventListener('enterpictureinpicture', handlePIPChange)
@@ -234,7 +276,40 @@ export const VideoElement: React.FC<VideoElementProps> = ({
document.removeEventListener('enterpictureinpicture', handlePIPChange)
document.removeEventListener('leavepictureinpicture', handlePIPChange)
}
}, [setVideoState])
}, [setVideoState, onPictureInPictureChange])
// Apply volume prop to video element
useEffect(() => {
const video = videoRef.current
if (!video || volume === undefined) return
// Clamp volume between 0 and 1
const clampedVolume = Math.max(0, Math.min(1, volume))
if (video.volume !== clampedVolume) {
video.volume = clampedVolume
}
}, [volume, videoRef])
// Apply playbackRate prop to video element
useEffect(() => {
const video = videoRef.current
if (!video || playbackRate === undefined) return
if (video.playbackRate !== playbackRate) {
video.playbackRate = playbackRate
}
}, [playbackRate, videoRef])
// Apply currentTime prop to video element (only once on mount or when it changes)
useEffect(() => {
const video = videoRef.current
if (!video || initialCurrentTime === undefined) return
// Only seek if the difference is significant (more than 1 second)
if (Math.abs(video.currentTime - initialCurrentTime) > 1) {
video.currentTime = initialCurrentTime
}
}, [initialCurrentTime, videoRef])
// Process subtitles - convert SRT to VTT blob URLs and merge with HLS subtitles
useEffect(() => {
@@ -747,6 +822,8 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onSeeked={handleSeeked}
onWaiting={handleWaiting}
onCanPlay={handleCanPlay}
onProgress={handleProgress}
onRateChange={handleRateChange}
onEnded={handleEnded}
onError={handleError}
onClick={handleVideoClick}