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 {