From 80bd589c039729f2cbbdd8c69550d7b5d0f82b52 Mon Sep 17 00:00:00 2001 From: hibna Date: Tue, 4 Nov 2025 07:38:06 +0300 Subject: [PATCH] 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. --- README.md | 48 +++++++++++++++--- package.json | 2 +- src/components/VideoElement.tsx | 87 +++++++++++++++++++++++++++++++-- src/components/VideoPlayer.tsx | 40 +++++++++++++++ src/index.ts | 1 + src/types/index.ts | 11 +++++ 6 files changed, 176 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8a5633a..2d2c895 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,17 @@ function App() { /> ``` +### With Volume and Playback Control + +```tsx + +``` + ### With Event Handlers ```tsx @@ -184,6 +195,9 @@ function App() { onPause={() => console.log('Video paused')} onEnded={() => console.log('Video ended')} 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)} /> ``` @@ -272,6 +286,8 @@ video-player/ ### VideoPlayer Props +#### Basic Props + | Prop | Type | Default | Description | |------|------|---------|-------------| | `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 | | `loop` | `boolean` | `false` | Loop video playback | | `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 | | `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks | -| `audioTracks` | `AudioTrack[]` | `[]` | Audio tracks | | `theme` | `PlayerTheme` | - | Custom theme colors | +| `language` | `string` | `'en'` | UI language ('en' or 'tr') | | `keyboardShortcuts` | `boolean` | `true` | Enable keyboard shortcuts | | `pictureInPicture` | `boolean` | `true` | Enable PIP button | | `className` | `string` | - | Custom CSS class | | `style` | `CSSProperties` | - | Inline styles | -| `onPlay` | `() => void` | - | Play event handler | -| `onPause` | `() => void` | - | Pause event handler | -| `onEnded` | `() => void` | - | Ended event handler | -| `onTimeUpdate` | `(time: number) => void` | - | Time update handler | -| `onVolumeChange` | `(volume: number) => void` | - | Volume change handler | -| `onError` | `(error: Error) => void` | - | Error handler | + +#### Event Handlers + +| Prop | Type | Description | +|------|------|-------------| +| `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 diff --git a/package.json b/package.json index 2139527..8b57f9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@alper/video-player", - "version": "0.1.13", + "version": "0.1.14", "description": "Modern, feature-rich video player library for React", "type": "module", "main": "./dist/video-player.umd.cjs", diff --git a/src/components/VideoElement.tsx b/src/components/VideoElement.tsx index 5f5e3fb..f8ba0b1 100644 --- a/src/components/VideoElement.tsx +++ b/src/components/VideoElement.tsx @@ -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 = ({ autoplay = false, loop = false, muted = false, + volume, + playbackRate, + currentTime: initialCurrentTime, subtitles = [], onPlay, onPause, @@ -47,6 +60,13 @@ export const VideoElement: React.FC = ({ onLoadedMetadata, onSeeking, onSeeked, + onProgress, + onDurationChange, + onRateChange, + onFullscreenChange, + onPictureInPictureChange, + onWaiting, + onCanPlay, onAudioTracksLoaded, onQualityLevelsLoaded, onSubtitleTracksLoaded, @@ -129,7 +149,9 @@ export const VideoElement: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ onSeeked={handleSeeked} onWaiting={handleWaiting} onCanPlay={handleCanPlay} + onProgress={handleProgress} + onRateChange={handleRateChange} onEnded={handleEnded} onError={handleError} onClick={handleVideoClick} diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx index 44cfa23..e8e5128 100644 --- a/src/components/VideoPlayer.tsx +++ b/src/components/VideoPlayer.tsx @@ -40,6 +40,9 @@ const VideoPlayerContent: React.FC< autoplay = false, loop = false, muted = false, + volume, + playbackRate, + currentTime, controls = true, subtitles = [], keyboardShortcuts = true, @@ -55,6 +58,13 @@ const VideoPlayerContent: React.FC< onLoadedMetadata, onSeeking, onSeeked, + onProgress, + onDurationChange, + onRateChange, + onFullscreenChange, + onPictureInPictureChange, + onWaiting, + onCanPlay, audioTracks, onAudioTracksLoadedInternal, qualities, @@ -76,6 +86,9 @@ const VideoPlayerContent: React.FC< autoplay={autoplay} loop={loop} muted={muted} + volume={volume} + playbackRate={playbackRate} + currentTime={currentTime} subtitles={subtitles} onPlay={onPlay} onPause={onPause} @@ -86,6 +99,13 @@ const VideoPlayerContent: React.FC< onLoadedMetadata={onLoadedMetadata} onSeeking={onSeeking} onSeeked={onSeeked} + onProgress={onProgress} + onDurationChange={onDurationChange} + onRateChange={onRateChange} + onFullscreenChange={onFullscreenChange} + onPictureInPictureChange={onPictureInPictureChange} + onWaiting={onWaiting} + onCanPlay={onCanPlay} onAudioTracksLoaded={onAudioTracksLoadedInternal} onQualityLevelsLoaded={onQualityLevelsLoadedInternal} onSubtitleTracksLoaded={onSubtitleTracksLoadedInternal} @@ -109,6 +129,9 @@ export const VideoPlayer: React.FC = ({ autoplay = false, loop = false, muted = false, + volume, + playbackRate, + currentTime, controls = true, subtitles = [], theme, @@ -126,6 +149,13 @@ export const VideoPlayer: React.FC = ({ onLoadedMetadata, onSeeking, onSeeked, + onProgress, + onDurationChange, + onRateChange, + onFullscreenChange, + onPictureInPictureChange, + onWaiting, + onCanPlay, }) => { const [audioTracks, setAudioTracks] = useState([]) const [qualities, setQualities] = useState([]) @@ -162,6 +192,9 @@ export const VideoPlayer: React.FC = ({ autoplay={autoplay} loop={loop} muted={muted} + volume={volume} + playbackRate={playbackRate} + currentTime={currentTime} controls={controls} subtitles={subtitles} keyboardShortcuts={keyboardShortcuts} @@ -177,6 +210,13 @@ export const VideoPlayer: React.FC = ({ onLoadedMetadata={onLoadedMetadata} onSeeking={onSeeking} onSeeked={onSeeked} + onProgress={onProgress} + onDurationChange={onDurationChange} + onRateChange={onRateChange} + onFullscreenChange={onFullscreenChange} + onPictureInPictureChange={onPictureInPictureChange} + onWaiting={onWaiting} + onCanPlay={onCanPlay} audioTracks={audioTracks} onAudioTracksLoadedInternal={handleAudioTracksLoaded} qualities={qualities} diff --git a/src/index.ts b/src/index.ts index 60c154c..9dd327f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export { PlayerProvider, usePlayerContext } from './contexts/PlayerContext' export type { VideoPlayerProps, SubtitleTrack, + AudioTrack, VideoQuality, PlayerTheme, VideoState, diff --git a/src/types/index.ts b/src/types/index.ts index d960339..faa6e14 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,6 +40,9 @@ export interface VideoPlayerProps { autoplay?: boolean loop?: 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 subtitles?: SubtitleTrack[] theme?: PlayerTheme @@ -48,6 +51,7 @@ export interface VideoPlayerProps { pictureInPicture?: boolean className?: string style?: CSSProperties + // Event callbacks onPlay?: () => void onPause?: () => void onEnded?: () => void @@ -57,6 +61,13 @@ export interface VideoPlayerProps { 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 } export interface VideoState {