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
+41 -7
View File
@@ -175,6 +175,17 @@ function App() {
/> />
``` ```
### With Volume and Playback Control
```tsx
<VideoPlayer
src="https://example.com/video.mp4"
volume={0.5} // 50% volume
playbackRate={1.5} // 1.5x speed
currentTime={30} // Start at 30 seconds
/>
```
### With Event Handlers ### With Event Handlers
```tsx ```tsx
@@ -184,6 +195,9 @@ function App() {
onPause={() => console.log('Video paused')} onPause={() => console.log('Video paused')}
onEnded={() => console.log('Video ended')} onEnded={() => console.log('Video ended')}
onTimeUpdate={(time) => console.log('Current time:', time)} onTimeUpdate={(time) => console.log('Current time:', time)}
onVolumeChange={(volume) => console.log('Volume:', volume)}
onRateChange={(rate) => console.log('Playback rate:', rate)}
onFullscreenChange={(isFs) => console.log('Fullscreen:', isFs)}
onError={(error) => console.error('Video error:', error)} onError={(error) => console.error('Video error:', error)}
/> />
``` ```
@@ -272,6 +286,8 @@ video-player/
### VideoPlayer Props ### VideoPlayer Props
#### Basic Props
| Prop | Type | Default | Description | | Prop | Type | Default | Description |
|------|------|---------|-------------| |------|------|---------|-------------|
| `src` | `string` | **required** | Video source URL (MP4, WebM, HLS, IPTV .ts) | | `src` | `string` | **required** | Video source URL (MP4, WebM, HLS, IPTV .ts) |
@@ -279,20 +295,38 @@ video-player/
| `autoplay` | `boolean` | `false` | Auto-play video on load | | `autoplay` | `boolean` | `false` | Auto-play video on load |
| `loop` | `boolean` | `false` | Loop video playback | | `loop` | `boolean` | `false` | Loop video playback |
| `muted` | `boolean` | `false` | Start muted | | `muted` | `boolean` | `false` | Start muted |
| `volume` | `number` | - | Initial volume (0-1) |
| `playbackRate` | `number` | - | Playback speed (0.25, 0.5, 1, 1.5, 2, etc.) |
| `currentTime` | `number` | - | Initial playback position in seconds |
| `controls` | `boolean` | `true` | Show player controls | | `controls` | `boolean` | `true` | Show player controls |
| `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks | | `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks |
| `audioTracks` | `AudioTrack[]` | `[]` | Audio tracks |
| `theme` | `PlayerTheme` | - | Custom theme colors | | `theme` | `PlayerTheme` | - | Custom theme colors |
| `language` | `string` | `'en'` | UI language ('en' or 'tr') |
| `keyboardShortcuts` | `boolean` | `true` | Enable keyboard shortcuts | | `keyboardShortcuts` | `boolean` | `true` | Enable keyboard shortcuts |
| `pictureInPicture` | `boolean` | `true` | Enable PIP button | | `pictureInPicture` | `boolean` | `true` | Enable PIP button |
| `className` | `string` | - | Custom CSS class | | `className` | `string` | - | Custom CSS class |
| `style` | `CSSProperties` | - | Inline styles | | `style` | `CSSProperties` | - | Inline styles |
| `onPlay` | `() => void` | - | Play event handler |
| `onPause` | `() => void` | - | Pause event handler | #### Event Handlers
| `onEnded` | `() => void` | - | Ended event handler |
| `onTimeUpdate` | `(time: number) => void` | - | Time update handler | | Prop | Type | Description |
| `onVolumeChange` | `(volume: number) => void` | - | Volume change handler | |------|------|-------------|
| `onError` | `(error: Error) => void` | - | Error handler | | `onPlay` | `() => void` | Fired when playback starts |
| `onPause` | `() => void` | Fired when playback pauses |
| `onEnded` | `() => void` | Fired when playback ends |
| `onTimeUpdate` | `(currentTime: number) => void` | Fired during playback with current time |
| `onVolumeChange` | `(volume: number) => void` | Fired when volume changes |
| `onError` | `(error: Error) => void` | Fired on playback error |
| `onLoadedMetadata` | `() => void` | Fired when video metadata is loaded |
| `onSeeking` | `() => void` | Fired when seeking starts |
| `onSeeked` | `() => void` | Fired when seeking completes |
| `onProgress` | `(buffered: number) => void` | Fired during download progress |
| `onDurationChange` | `(duration: number) => void` | Fired when duration changes |
| `onRateChange` | `(playbackRate: number) => void` | Fired when playback rate changes |
| `onFullscreenChange` | `(isFullscreen: boolean) => void` | Fired when fullscreen state changes |
| `onPictureInPictureChange` | `(isPip: boolean) => void` | Fired when PIP state changes |
| `onWaiting` | `() => void` | Fired when buffering starts |
| `onCanPlay` | `() => void` | Fired when enough data is available to play |
### SubtitleTrack ### SubtitleTrack
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@alper/video-player", "name": "@alper/video-player",
"version": "0.1.13", "version": "0.1.14",
"description": "Modern, feature-rich video player library for React", "description": "Modern, feature-rich video player library for React",
"type": "module", "type": "module",
"main": "./dist/video-player.umd.cjs", "main": "./dist/video-player.umd.cjs",
+82 -5
View File
@@ -16,6 +16,9 @@ interface VideoElementProps {
autoplay?: boolean autoplay?: boolean
loop?: boolean loop?: boolean
muted?: boolean muted?: boolean
volume?: number
playbackRate?: number
currentTime?: number
subtitles?: SubtitleTrack[] subtitles?: SubtitleTrack[]
onPlay?: () => void onPlay?: () => void
onPause?: () => void onPause?: () => void
@@ -26,6 +29,13 @@ interface VideoElementProps {
onLoadedMetadata?: () => void onLoadedMetadata?: () => void
onSeeking?: () => void onSeeking?: () => void
onSeeked?: () => 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 onAudioTracksLoaded?: (tracks: AudioTrack[]) => void
onQualityLevelsLoaded?: (qualities: VideoQuality[]) => void onQualityLevelsLoaded?: (qualities: VideoQuality[]) => void
onSubtitleTracksLoaded?: (tracks: SubtitleTrack[]) => void onSubtitleTracksLoaded?: (tracks: SubtitleTrack[]) => void
@@ -37,6 +47,9 @@ export const VideoElement: React.FC<VideoElementProps> = ({
autoplay = false, autoplay = false,
loop = false, loop = false,
muted = false, muted = false,
volume,
playbackRate,
currentTime: initialCurrentTime,
subtitles = [], subtitles = [],
onPlay, onPlay,
onPause, onPause,
@@ -47,6 +60,13 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onLoadedMetadata, onLoadedMetadata,
onSeeking, onSeeking,
onSeeked, onSeeked,
onProgress,
onDurationChange,
onRateChange,
onFullscreenChange,
onPictureInPictureChange,
onWaiting,
onCanPlay,
onAudioTracksLoaded, onAudioTracksLoaded,
onQualityLevelsLoaded, onQualityLevelsLoaded,
onSubtitleTracksLoaded, onSubtitleTracksLoaded,
@@ -129,7 +149,9 @@ export const VideoElement: React.FC<VideoElementProps> = ({
duration: video.duration, duration: video.duration,
isLiveBroadcast, isLiveBroadcast,
})) }))
}, [videoRef, setVideoState])
onDurationChange?.(video.duration)
}, [videoRef, setVideoState, onDurationChange])
const handleVolumeChange = useCallback(() => { const handleVolumeChange = useCallback(() => {
const video = videoRef.current const video = videoRef.current
@@ -156,11 +178,29 @@ export const VideoElement: React.FC<VideoElementProps> = ({
const handleWaiting = useCallback(() => { const handleWaiting = useCallback(() => {
setVideoState((prev) => ({ ...prev, loading: true })) setVideoState((prev) => ({ ...prev, loading: true }))
}, [setVideoState]) onWaiting?.()
}, [setVideoState, onWaiting])
const handleCanPlay = useCallback(() => { const handleCanPlay = useCallback(() => {
setVideoState((prev) => ({ ...prev, loading: false })) 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(() => { const handleEnded = useCallback(() => {
setVideoState((prev) => ({ ...prev, playing: false })) setVideoState((prev) => ({ ...prev, playing: false }))
@@ -212,19 +252,21 @@ export const VideoElement: React.FC<VideoElementProps> = ({
const handleFullscreenChange = () => { const handleFullscreenChange = () => {
const isFullscreen = !!document.fullscreenElement const isFullscreen = !!document.fullscreenElement
setVideoState((prev) => ({ ...prev, fullscreen: isFullscreen })) setVideoState((prev) => ({ ...prev, fullscreen: isFullscreen }))
onFullscreenChange?.(isFullscreen)
} }
document.addEventListener('fullscreenchange', handleFullscreenChange) document.addEventListener('fullscreenchange', handleFullscreenChange)
return () => { return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange) document.removeEventListener('fullscreenchange', handleFullscreenChange)
} }
}, [setVideoState]) }, [setVideoState, onFullscreenChange])
// Handle PIP changes // Handle PIP changes
useEffect(() => { useEffect(() => {
const handlePIPChange = () => { const handlePIPChange = () => {
const isPIP = !!document.pictureInPictureElement const isPIP = !!document.pictureInPictureElement
setVideoState((prev) => ({ ...prev, pictureInPicture: isPIP })) setVideoState((prev) => ({ ...prev, pictureInPicture: isPIP }))
onPictureInPictureChange?.(isPIP)
} }
document.addEventListener('enterpictureinpicture', handlePIPChange) document.addEventListener('enterpictureinpicture', handlePIPChange)
@@ -234,7 +276,40 @@ export const VideoElement: React.FC<VideoElementProps> = ({
document.removeEventListener('enterpictureinpicture', handlePIPChange) document.removeEventListener('enterpictureinpicture', handlePIPChange)
document.removeEventListener('leavepictureinpicture', 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 // Process subtitles - convert SRT to VTT blob URLs and merge with HLS subtitles
useEffect(() => { useEffect(() => {
@@ -747,6 +822,8 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onSeeked={handleSeeked} onSeeked={handleSeeked}
onWaiting={handleWaiting} onWaiting={handleWaiting}
onCanPlay={handleCanPlay} onCanPlay={handleCanPlay}
onProgress={handleProgress}
onRateChange={handleRateChange}
onEnded={handleEnded} onEnded={handleEnded}
onError={handleError} onError={handleError}
onClick={handleVideoClick} onClick={handleVideoClick}
+40
View File
@@ -40,6 +40,9 @@ const VideoPlayerContent: React.FC<
autoplay = false, autoplay = false,
loop = false, loop = false,
muted = false, muted = false,
volume,
playbackRate,
currentTime,
controls = true, controls = true,
subtitles = [], subtitles = [],
keyboardShortcuts = true, keyboardShortcuts = true,
@@ -55,6 +58,13 @@ const VideoPlayerContent: React.FC<
onLoadedMetadata, onLoadedMetadata,
onSeeking, onSeeking,
onSeeked, onSeeked,
onProgress,
onDurationChange,
onRateChange,
onFullscreenChange,
onPictureInPictureChange,
onWaiting,
onCanPlay,
audioTracks, audioTracks,
onAudioTracksLoadedInternal, onAudioTracksLoadedInternal,
qualities, qualities,
@@ -76,6 +86,9 @@ const VideoPlayerContent: React.FC<
autoplay={autoplay} autoplay={autoplay}
loop={loop} loop={loop}
muted={muted} muted={muted}
volume={volume}
playbackRate={playbackRate}
currentTime={currentTime}
subtitles={subtitles} subtitles={subtitles}
onPlay={onPlay} onPlay={onPlay}
onPause={onPause} onPause={onPause}
@@ -86,6 +99,13 @@ const VideoPlayerContent: React.FC<
onLoadedMetadata={onLoadedMetadata} onLoadedMetadata={onLoadedMetadata}
onSeeking={onSeeking} onSeeking={onSeeking}
onSeeked={onSeeked} onSeeked={onSeeked}
onProgress={onProgress}
onDurationChange={onDurationChange}
onRateChange={onRateChange}
onFullscreenChange={onFullscreenChange}
onPictureInPictureChange={onPictureInPictureChange}
onWaiting={onWaiting}
onCanPlay={onCanPlay}
onAudioTracksLoaded={onAudioTracksLoadedInternal} onAudioTracksLoaded={onAudioTracksLoadedInternal}
onQualityLevelsLoaded={onQualityLevelsLoadedInternal} onQualityLevelsLoaded={onQualityLevelsLoadedInternal}
onSubtitleTracksLoaded={onSubtitleTracksLoadedInternal} onSubtitleTracksLoaded={onSubtitleTracksLoadedInternal}
@@ -109,6 +129,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
autoplay = false, autoplay = false,
loop = false, loop = false,
muted = false, muted = false,
volume,
playbackRate,
currentTime,
controls = true, controls = true,
subtitles = [], subtitles = [],
theme, theme,
@@ -126,6 +149,13 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onLoadedMetadata, onLoadedMetadata,
onSeeking, onSeeking,
onSeeked, onSeeked,
onProgress,
onDurationChange,
onRateChange,
onFullscreenChange,
onPictureInPictureChange,
onWaiting,
onCanPlay,
}) => { }) => {
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([]) const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([])
const [qualities, setQualities] = useState<VideoQuality[]>([]) const [qualities, setQualities] = useState<VideoQuality[]>([])
@@ -162,6 +192,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
autoplay={autoplay} autoplay={autoplay}
loop={loop} loop={loop}
muted={muted} muted={muted}
volume={volume}
playbackRate={playbackRate}
currentTime={currentTime}
controls={controls} controls={controls}
subtitles={subtitles} subtitles={subtitles}
keyboardShortcuts={keyboardShortcuts} keyboardShortcuts={keyboardShortcuts}
@@ -177,6 +210,13 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onLoadedMetadata={onLoadedMetadata} onLoadedMetadata={onLoadedMetadata}
onSeeking={onSeeking} onSeeking={onSeeking}
onSeeked={onSeeked} onSeeked={onSeeked}
onProgress={onProgress}
onDurationChange={onDurationChange}
onRateChange={onRateChange}
onFullscreenChange={onFullscreenChange}
onPictureInPictureChange={onPictureInPictureChange}
onWaiting={onWaiting}
onCanPlay={onCanPlay}
audioTracks={audioTracks} audioTracks={audioTracks}
onAudioTracksLoadedInternal={handleAudioTracksLoaded} onAudioTracksLoadedInternal={handleAudioTracksLoaded}
qualities={qualities} qualities={qualities}
+1
View File
@@ -8,6 +8,7 @@ export { PlayerProvider, usePlayerContext } from './contexts/PlayerContext'
export type { export type {
VideoPlayerProps, VideoPlayerProps,
SubtitleTrack, SubtitleTrack,
AudioTrack,
VideoQuality, VideoQuality,
PlayerTheme, PlayerTheme,
VideoState, VideoState,
+11
View File
@@ -40,6 +40,9 @@ export interface VideoPlayerProps {
autoplay?: boolean autoplay?: boolean
loop?: boolean loop?: boolean
muted?: boolean muted?: boolean
volume?: number // 0-1 arası ses seviyesi
playbackRate?: number // Oynatma hızı (0.25, 0.5, 1, 1.5, 2, vb.)
currentTime?: number // Başlangıç zamanı (saniye)
controls?: boolean controls?: boolean
subtitles?: SubtitleTrack[] subtitles?: SubtitleTrack[]
theme?: PlayerTheme theme?: PlayerTheme
@@ -48,6 +51,7 @@ export interface VideoPlayerProps {
pictureInPicture?: boolean pictureInPicture?: boolean
className?: string className?: string
style?: CSSProperties style?: CSSProperties
// Event callbacks
onPlay?: () => void onPlay?: () => void
onPause?: () => void onPause?: () => void
onEnded?: () => void onEnded?: () => void
@@ -57,6 +61,13 @@ export interface VideoPlayerProps {
onLoadedMetadata?: () => void onLoadedMetadata?: () => void
onSeeking?: () => void onSeeking?: () => void
onSeeked?: () => 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
} }
export interface VideoState { export interface VideoState {