feat: implement phase2 player isolation and media config
This commit is contained in:
@@ -952,6 +952,7 @@ function QualitySelector() {
|
|||||||
#### Özellikler
|
#### Özellikler
|
||||||
|
|
||||||
- ✅ Input/textarea alanlarında devre dışı
|
- ✅ Input/textarea alanlarında devre dışı
|
||||||
|
- ✅ Sadece aktif/focus olan player için çalışır (çoklu player uyumu)
|
||||||
- ✅ Default tarayıcı davranışını önler
|
- ✅ Default tarayıcı davranışını önler
|
||||||
- ✅ Enable/disable ile açılıp kapatılabilir
|
- ✅ Enable/disable ile açılıp kapatılabilir
|
||||||
- ✅ Fullscreen'de de çalışır
|
- ✅ Fullscreen'de de çalışır
|
||||||
@@ -1070,6 +1071,9 @@ interface VideoPlayerProps {
|
|||||||
// Poster/thumbnail resmi
|
// Poster/thumbnail resmi
|
||||||
poster?: string
|
poster?: string
|
||||||
|
|
||||||
|
// Protokol override (default: auto)
|
||||||
|
protocol?: 'auto' | 'native' | 'hls' | 'rtmp' | 'dash' | 'mpegts'
|
||||||
|
|
||||||
// Otomatik oynat
|
// Otomatik oynat
|
||||||
autoplay?: boolean
|
autoplay?: boolean
|
||||||
|
|
||||||
@@ -1079,6 +1083,21 @@ interface VideoPlayerProps {
|
|||||||
// Başlangıçta sessiz
|
// Başlangıçta sessiz
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
|
|
||||||
|
// Başlangıç ses seviyesi (0-1)
|
||||||
|
volume?: number
|
||||||
|
|
||||||
|
// Oynatma hızı
|
||||||
|
playbackRate?: number
|
||||||
|
|
||||||
|
// Başlangıç zamanı (saniye)
|
||||||
|
currentTime?: number
|
||||||
|
|
||||||
|
// Video element attribute'ları
|
||||||
|
crossOrigin?: '' | 'anonymous' | 'use-credentials'
|
||||||
|
preload?: 'none' | 'metadata' | 'auto'
|
||||||
|
playsInline?: boolean
|
||||||
|
controlsList?: string
|
||||||
|
|
||||||
// Kontrolleri göster (default: true)
|
// Kontrolleri göster (default: true)
|
||||||
controls?: boolean
|
controls?: boolean
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
|
|||||||
- `P` - Picture-in-Picture
|
- `P` - Picture-in-Picture
|
||||||
- `0-9` - Jump to percentage (10%-90%)
|
- `0-9` - Jump to percentage (10%-90%)
|
||||||
- `Home` / `End` - Jump to start/end
|
- `Home` / `End` - Jump to start/end
|
||||||
|
- Shortcuts only work for the currently active/focused player instance
|
||||||
|
|
||||||
### 📱 Touch Gestures
|
### 📱 Touch Gestures
|
||||||
- **Tap** - Play/Pause
|
- **Tap** - Play/Pause
|
||||||
@@ -165,6 +166,15 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Force Protocol (Override Auto Detection)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<VideoPlayer
|
||||||
|
src="https://cdn.example.com/video"
|
||||||
|
protocol="hls"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
### IPTV Streaming
|
### IPTV Streaming
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -310,12 +320,17 @@ video-player/
|
|||||||
|------|------|---------|-------------|
|
|------|------|---------|-------------|
|
||||||
| `src` | `string` | **required** | Video source URL (MP4, WebM, HLS, IPTV .ts) |
|
| `src` | `string` | **required** | Video source URL (MP4, WebM, HLS, IPTV .ts) |
|
||||||
| `poster` | `string` | - | Poster image URL |
|
| `poster` | `string` | - | Poster image URL |
|
||||||
|
| `protocol` | `'auto' \| 'native' \| 'hls' \| 'rtmp' \| 'dash' \| 'mpegts'` | `'auto'` | Force playback engine instead of URL auto-detection |
|
||||||
| `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) |
|
| `volume` | `number` | - | Initial volume (0-1) |
|
||||||
| `playbackRate` | `number` | - | Playback speed (0.25, 0.5, 1, 1.5, 2, etc.) |
|
| `playbackRate` | `number` | - | Playback speed (0.25, 0.5, 1, 1.5, 2, etc.) |
|
||||||
| `currentTime` | `number` | - | Initial playback position in seconds |
|
| `currentTime` | `number` | - | Initial playback position in seconds |
|
||||||
|
| `crossOrigin` | `'' \| 'anonymous' \| 'use-credentials'` | - | Sets the video `crossOrigin` attribute |
|
||||||
|
| `preload` | `'none' \| 'metadata' \| 'auto'` | `'metadata'` | Sets the video preload strategy |
|
||||||
|
| `playsInline` | `boolean` | `true` | Enables inline playback on mobile browsers |
|
||||||
|
| `controlsList` | `string` | - | Passes `controlsList` attribute to the video element |
|
||||||
| `controls` | `boolean` | `true` | Show player controls |
|
| `controls` | `boolean` | `true` | Show player controls |
|
||||||
| `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks |
|
| `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks |
|
||||||
| `theme` | `PlayerTheme` | - | Custom theme colors |
|
| `theme` | `PlayerTheme` | - | Custom theme colors |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useCallback, useState } from 'react'
|
import React, { useEffect, useCallback, useState } from 'react'
|
||||||
import { usePlayerContext } from '../contexts/PlayerContext'
|
import { usePlayerContext } from '../contexts/PlayerContext'
|
||||||
import type { SubtitleTrack, AudioTrack, VideoQuality } from '../types'
|
import type { SubtitleTrack, AudioTrack, VideoQuality, VideoProtocol } from '../types'
|
||||||
import { validateVideoURL, getCORSErrorMessage, isCORSError } from '../utils/corsHelper'
|
import { validateVideoURL, getCORSErrorMessage, isCORSError } from '../utils/corsHelper'
|
||||||
import { setHlsAudioTrack, setHlsQualityLevel } from '../utils/hlsControl'
|
import { setHlsAudioTrack, setHlsQualityLevel } from '../utils/hlsControl'
|
||||||
import { setupHlsInstance } from '../utils/hlsSetup'
|
import { setupHlsInstance } from '../utils/hlsSetup'
|
||||||
@@ -14,12 +14,17 @@ import './VideoElement.css'
|
|||||||
interface VideoElementProps {
|
interface VideoElementProps {
|
||||||
src: string
|
src: string
|
||||||
poster?: string
|
poster?: string
|
||||||
|
protocol?: 'auto' | VideoProtocol
|
||||||
autoplay?: boolean
|
autoplay?: boolean
|
||||||
loop?: boolean
|
loop?: boolean
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
volume?: number
|
volume?: number
|
||||||
playbackRate?: number
|
playbackRate?: number
|
||||||
currentTime?: number
|
currentTime?: number
|
||||||
|
crossOrigin?: '' | 'anonymous' | 'use-credentials'
|
||||||
|
preload?: 'none' | 'metadata' | 'auto'
|
||||||
|
playsInline?: boolean
|
||||||
|
controlsList?: string
|
||||||
subtitles?: SubtitleTrack[]
|
subtitles?: SubtitleTrack[]
|
||||||
onPlay?: () => void
|
onPlay?: () => void
|
||||||
onPause?: () => void
|
onPause?: () => void
|
||||||
@@ -45,12 +50,17 @@ interface VideoElementProps {
|
|||||||
export const VideoElement: React.FC<VideoElementProps> = ({
|
export const VideoElement: React.FC<VideoElementProps> = ({
|
||||||
src,
|
src,
|
||||||
poster,
|
poster,
|
||||||
|
protocol = 'auto',
|
||||||
autoplay = false,
|
autoplay = false,
|
||||||
loop = false,
|
loop = false,
|
||||||
muted = false,
|
muted = false,
|
||||||
volume,
|
volume,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
currentTime: initialCurrentTime,
|
currentTime: initialCurrentTime,
|
||||||
|
crossOrigin,
|
||||||
|
preload = 'metadata',
|
||||||
|
playsInline = true,
|
||||||
|
controlsList,
|
||||||
subtitles = [],
|
subtitles = [],
|
||||||
onPlay,
|
onPlay,
|
||||||
onPause,
|
onPause,
|
||||||
@@ -416,7 +426,15 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect video protocol
|
// Detect video protocol
|
||||||
const detection = detectVideoProtocol(src)
|
const detectedProtocol = detectVideoProtocol(src)
|
||||||
|
const detection =
|
||||||
|
protocol === 'auto'
|
||||||
|
? detectedProtocol
|
||||||
|
: {
|
||||||
|
...detectedProtocol,
|
||||||
|
protocol,
|
||||||
|
needsSpecialPlayer: protocol !== 'native',
|
||||||
|
}
|
||||||
let cleanupFn: (() => void) | null = null
|
let cleanupFn: (() => void) | null = null
|
||||||
|
|
||||||
const teardownPlayer = () => {
|
const teardownPlayer = () => {
|
||||||
@@ -596,6 +614,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
src,
|
src,
|
||||||
|
protocol,
|
||||||
autoplay,
|
autoplay,
|
||||||
videoRef,
|
videoRef,
|
||||||
handleError,
|
handleError,
|
||||||
@@ -762,8 +781,10 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
poster={poster}
|
poster={poster}
|
||||||
loop={loop}
|
loop={loop}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
playsInline
|
crossOrigin={crossOrigin}
|
||||||
preload="metadata"
|
playsInline={playsInline}
|
||||||
|
preload={preload}
|
||||||
|
controlsList={controlsList}
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
onPause={handlePause}
|
onPause={handlePause}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react'
|
import React, { useMemo, useState, useCallback } from 'react'
|
||||||
import { PlayerProvider, usePlayerContext } from '../contexts/PlayerContext'
|
import { PlayerProvider, usePlayerContext } from '../contexts/PlayerContext'
|
||||||
import { VideoElement } from './VideoElement'
|
import { VideoElement } from './VideoElement'
|
||||||
import { ControlsLayer } from './ControlsLayer'
|
import { ControlsLayer } from './ControlsLayer'
|
||||||
@@ -38,14 +38,20 @@ const VideoPlayerContent: React.FC<
|
|||||||
> = ({
|
> = ({
|
||||||
src,
|
src,
|
||||||
poster,
|
poster,
|
||||||
|
protocol = 'auto',
|
||||||
autoplay = false,
|
autoplay = false,
|
||||||
loop = false,
|
loop = false,
|
||||||
muted = false,
|
muted = false,
|
||||||
volume,
|
volume,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
crossOrigin,
|
||||||
|
preload = 'metadata',
|
||||||
|
playsInline = true,
|
||||||
|
controlsList,
|
||||||
controls = true,
|
controls = true,
|
||||||
subtitles = [],
|
subtitles = [],
|
||||||
|
theme,
|
||||||
keyboardShortcuts = true,
|
keyboardShortcuts = true,
|
||||||
pictureInPicture = true,
|
pictureInPicture = true,
|
||||||
className = '',
|
className = '',
|
||||||
@@ -75,21 +81,55 @@ const VideoPlayerContent: React.FC<
|
|||||||
}) => {
|
}) => {
|
||||||
const { containerRef, uiState } = usePlayerContext()
|
const { containerRef, uiState } = usePlayerContext()
|
||||||
const controlsHiddenClass = !uiState.controlsVisible ? 'controls-hidden' : ''
|
const controlsHiddenClass = !uiState.controlsVisible ? 'controls-hidden' : ''
|
||||||
|
const themedStyle = useMemo<React.CSSProperties>(() => {
|
||||||
|
if (!theme) {
|
||||||
|
return style || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssVariables: Record<string, string> = {}
|
||||||
|
if (theme.primaryColor) {
|
||||||
|
cssVariables['--player-primary'] = theme.primaryColor
|
||||||
|
}
|
||||||
|
if (theme.accentColor) {
|
||||||
|
cssVariables['--player-primary-hover'] = theme.accentColor
|
||||||
|
}
|
||||||
|
if (theme.backgroundColor) {
|
||||||
|
cssVariables['--player-bg'] = theme.backgroundColor
|
||||||
|
}
|
||||||
|
if (theme.textColor) {
|
||||||
|
cssVariables['--player-text'] = theme.textColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cssVariables,
|
||||||
|
...(style || {}),
|
||||||
|
} as React.CSSProperties
|
||||||
|
}, [theme, style])
|
||||||
|
|
||||||
// Merge manual subtitles and HLS-detected subtitles
|
// Merge manual subtitles and HLS-detected subtitles
|
||||||
const allSubtitles = [...subtitles, ...hlsSubtitles]
|
const allSubtitles = [...subtitles, ...hlsSubtitles]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={`video-player ${controlsHiddenClass} ${className}`} style={style}>
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`video-player ${controlsHiddenClass} ${className}`}
|
||||||
|
style={themedStyle}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<VideoElement
|
<VideoElement
|
||||||
src={src}
|
src={src}
|
||||||
poster={poster}
|
poster={poster}
|
||||||
|
protocol={protocol}
|
||||||
autoplay={autoplay}
|
autoplay={autoplay}
|
||||||
loop={loop}
|
loop={loop}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
playbackRate={playbackRate}
|
playbackRate={playbackRate}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
|
crossOrigin={crossOrigin}
|
||||||
|
preload={preload}
|
||||||
|
playsInline={playsInline}
|
||||||
|
controlsList={controlsList}
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
onPlay={onPlay}
|
onPlay={onPlay}
|
||||||
onPause={onPause}
|
onPause={onPause}
|
||||||
@@ -127,12 +167,17 @@ const VideoPlayerContent: React.FC<
|
|||||||
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||||
src,
|
src,
|
||||||
poster,
|
poster,
|
||||||
|
protocol = 'auto',
|
||||||
autoplay = false,
|
autoplay = false,
|
||||||
loop = false,
|
loop = false,
|
||||||
muted = false,
|
muted = false,
|
||||||
volume,
|
volume,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
crossOrigin,
|
||||||
|
preload = 'metadata',
|
||||||
|
playsInline = true,
|
||||||
|
controlsList,
|
||||||
controls = true,
|
controls = true,
|
||||||
subtitles = [],
|
subtitles = [],
|
||||||
theme,
|
theme,
|
||||||
@@ -162,17 +207,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const [qualities, setQualities] = useState<VideoQuality[]>([])
|
const [qualities, setQualities] = useState<VideoQuality[]>([])
|
||||||
const [hlsSubtitles, setHlsSubtitles] = useState<SubtitleTrack[]>([])
|
const [hlsSubtitles, setHlsSubtitles] = useState<SubtitleTrack[]>([])
|
||||||
|
|
||||||
// Apply theme CSS variables
|
|
||||||
useEffect(() => {
|
|
||||||
if (theme) {
|
|
||||||
const root = document.documentElement
|
|
||||||
if (theme.primaryColor) root.style.setProperty('--player-primary', theme.primaryColor)
|
|
||||||
if (theme.accentColor) root.style.setProperty('--player-primary-hover', theme.accentColor)
|
|
||||||
if (theme.backgroundColor) root.style.setProperty('--player-bg', theme.backgroundColor)
|
|
||||||
if (theme.textColor) root.style.setProperty('--player-text', theme.textColor)
|
|
||||||
}
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
const handleAudioTracksLoaded = useCallback((tracks: AudioTrack[]) => {
|
const handleAudioTracksLoaded = useCallback((tracks: AudioTrack[]) => {
|
||||||
setAudioTracks(tracks)
|
setAudioTracks(tracks)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -190,14 +224,20 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
<VideoPlayerContent
|
<VideoPlayerContent
|
||||||
src={src}
|
src={src}
|
||||||
poster={poster}
|
poster={poster}
|
||||||
|
protocol={protocol}
|
||||||
autoplay={autoplay}
|
autoplay={autoplay}
|
||||||
loop={loop}
|
loop={loop}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
playbackRate={playbackRate}
|
playbackRate={playbackRate}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
|
crossOrigin={crossOrigin}
|
||||||
|
preload={preload}
|
||||||
|
playsInline={playsInline}
|
||||||
|
controlsList={controlsList}
|
||||||
controls={controls}
|
controls={controls}
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
|
theme={theme}
|
||||||
keyboardShortcuts={keyboardShortcuts}
|
keyboardShortcuts={keyboardShortcuts}
|
||||||
pictureInPicture={pictureInPicture}
|
pictureInPicture={pictureInPicture}
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@@ -77,6 +77,17 @@ describe('SettingsMenu', () => {
|
|||||||
audioTrack: 'Audio Track',
|
audioTrack: 'Audio Track',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
level: 'Level',
|
level: 'Level',
|
||||||
|
play: 'Play',
|
||||||
|
pause: 'Pause',
|
||||||
|
mute: 'Mute',
|
||||||
|
unmute: 'Unmute',
|
||||||
|
enterFullscreen: 'Enter fullscreen',
|
||||||
|
exitFullscreen: 'Exit fullscreen',
|
||||||
|
enterPictureInPicture: 'Enter picture-in-picture',
|
||||||
|
exitPictureInPicture: 'Exit picture-in-picture',
|
||||||
|
videoProgress: 'Video progress',
|
||||||
|
volume: 'Volume',
|
||||||
|
live: 'LIVE',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { fireEvent, render } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { useKeyboardShortcuts } from './useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from './useKeyboardShortcuts'
|
||||||
|
|
||||||
const { contextState } = vi.hoisted(() => ({
|
const { contextState } = vi.hoisted(() => ({
|
||||||
@@ -17,6 +17,23 @@ const TestComponent = ({ enabled = true }: { enabled?: boolean }) => {
|
|||||||
return <div>keyboard-shortcuts-test</div>
|
return <div>keyboard-shortcuts-test</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ActivePlayerTestComponent = ({ enabled = true }: { enabled?: boolean }) => {
|
||||||
|
useKeyboardShortcuts(enabled)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="player"
|
||||||
|
ref={(node) => {
|
||||||
|
if (contextState.value?.containerRef) {
|
||||||
|
contextState.value.containerRef.current = node
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
keyboard-shortcuts-active-test
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
describe('useKeyboardShortcuts', () => {
|
describe('useKeyboardShortcuts', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
contextState.value = {
|
contextState.value = {
|
||||||
@@ -71,4 +88,16 @@ describe('useKeyboardShortcuts', () => {
|
|||||||
fireEvent.keyDown(document.querySelector('input')!, { key: 'k' })
|
fireEvent.keyDown(document.querySelector('input')!, { key: 'k' })
|
||||||
expect(contextState.value.togglePlay).not.toHaveBeenCalled()
|
expect(contextState.value.togglePlay).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('triggers shortcuts only for active/focused player', () => {
|
||||||
|
contextState.value.containerRef = { current: null }
|
||||||
|
render(<ActivePlayerTestComponent />)
|
||||||
|
|
||||||
|
fireEvent.keyDown(window, { key: 'k' })
|
||||||
|
expect(contextState.value.togglePlay).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
fireEvent.mouseDown(screen.getByTestId('player'))
|
||||||
|
fireEvent.keyDown(window, { key: 'k' })
|
||||||
|
expect(contextState.value.togglePlay).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { usePlayerContext } from '../contexts/PlayerContext'
|
import { usePlayerContext } from '../contexts/PlayerContext'
|
||||||
|
|
||||||
export const useKeyboardShortcuts = (enabled: boolean = true) => {
|
export const useKeyboardShortcuts = (enabled: boolean = true) => {
|
||||||
const {
|
const {
|
||||||
videoState,
|
videoState,
|
||||||
|
containerRef,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
seek,
|
seek,
|
||||||
setVolume,
|
setVolume,
|
||||||
@@ -11,13 +12,76 @@ export const useKeyboardShortcuts = (enabled: boolean = true) => {
|
|||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
togglePictureInPicture,
|
togglePictureInPicture,
|
||||||
} = usePlayerContext()
|
} = usePlayerContext()
|
||||||
|
const [isActivePlayer, setIsActivePlayer] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
setIsActivePlayer(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerDown = (e: MouseEvent | TouchEvent) => {
|
||||||
|
const container = containerRef?.current
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = e.target as Node | null
|
||||||
|
const isInsidePlayer = !!target && container.contains(target)
|
||||||
|
setIsActivePlayer(isInsidePlayer)
|
||||||
|
|
||||||
|
if (isInsidePlayer && document.activeElement !== container) {
|
||||||
|
container.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocusIn = (e: FocusEvent) => {
|
||||||
|
const container = containerRef?.current
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = e.target as Node | null
|
||||||
|
setIsActivePlayer(!!target && container.contains(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWindowBlur = () => {
|
||||||
|
setIsActivePlayer(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = containerRef?.current
|
||||||
|
if (container && container.contains(document.activeElement)) {
|
||||||
|
setIsActivePlayer(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handlePointerDown)
|
||||||
|
document.addEventListener('touchstart', handlePointerDown, { passive: true })
|
||||||
|
document.addEventListener('focusin', handleFocusIn)
|
||||||
|
window.addEventListener('blur', handleWindowBlur)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handlePointerDown)
|
||||||
|
document.removeEventListener('touchstart', handlePointerDown)
|
||||||
|
document.removeEventListener('focusin', handleFocusIn)
|
||||||
|
window.removeEventListener('blur', handleWindowBlur)
|
||||||
|
}
|
||||||
|
}, [enabled, containerRef])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const container = containerRef?.current
|
||||||
|
if (container && !isActivePlayer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Don't trigger if user is typing in an input
|
// Don't trigger if user is typing in an input
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
if (
|
||||||
|
e.target instanceof HTMLInputElement ||
|
||||||
|
e.target instanceof HTMLTextAreaElement ||
|
||||||
|
(e.target instanceof HTMLElement && e.target.isContentEditable)
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +172,8 @@ export const useKeyboardShortcuts = (enabled: boolean = true) => {
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [
|
}, [
|
||||||
enabled,
|
enabled,
|
||||||
|
isActivePlayer,
|
||||||
|
containerRef,
|
||||||
videoState.currentTime,
|
videoState.currentTime,
|
||||||
videoState.duration,
|
videoState.duration,
|
||||||
videoState.volume,
|
videoState.volume,
|
||||||
|
|||||||
@@ -37,12 +37,17 @@ export interface PlayerTheme {
|
|||||||
export interface VideoPlayerProps {
|
export interface VideoPlayerProps {
|
||||||
src: string
|
src: string
|
||||||
poster?: string
|
poster?: string
|
||||||
|
protocol?: 'auto' | VideoProtocol
|
||||||
autoplay?: boolean
|
autoplay?: boolean
|
||||||
loop?: boolean
|
loop?: boolean
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
volume?: number // 0-1 arası ses seviyesi
|
volume?: number // 0-1 arası ses seviyesi
|
||||||
playbackRate?: number // Oynatma hızı (0.25, 0.5, 1, 1.5, 2, vb.)
|
playbackRate?: number // Oynatma hızı (0.25, 0.5, 1, 1.5, 2, vb.)
|
||||||
currentTime?: number // Başlangıç zamanı (saniye)
|
currentTime?: number // Başlangıç zamanı (saniye)
|
||||||
|
crossOrigin?: '' | 'anonymous' | 'use-credentials'
|
||||||
|
preload?: 'none' | 'metadata' | 'auto'
|
||||||
|
playsInline?: boolean
|
||||||
|
controlsList?: string
|
||||||
controls?: boolean
|
controls?: boolean
|
||||||
subtitles?: SubtitleTrack[]
|
subtitles?: SubtitleTrack[]
|
||||||
theme?: PlayerTheme
|
theme?: PlayerTheme
|
||||||
|
|||||||
Reference in New Issue
Block a user