feat: add optional subtitle style editor with live preview

This commit is contained in:
hibna
2026-02-13 05:35:27 +03:00
parent 69d7706967
commit fa66472c74
15 changed files with 1102 additions and 190 deletions
+110 -85
View File
@@ -11,17 +11,17 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
## 🏆 Why Choose This Player?
| Feature | @source/player | video.js | react-player | plyr |
|---------|---------------------|----------|--------------|------|
| **Bundle Size (gzipped)** | **~18KB JS + ~3.5KB CSS** ✅ | ~500KB ❌ | ~50KB ⚠️ | ~30KB ⚠️ |
| **Runtime Dependencies** | **0** ✅ | Many ❌ | Few ⚠️ | Few ⚠️ |
| **React (Web)** | **Yes** ✅ | Wrapper ⚠️ | **Yes** ✅ | Wrapper ⚠️ |
| **TypeScript Native** | **Yes** ✅ | Types ⚠️ | Partial ⚠️ | Types ⚠️ |
| **HLS Support** | **Yes** ✅ | Yes ✅ | Yes ✅ | No ❌ |
| **Quality Switching** | **Yes** ✅ | Yes ✅ | Limited ⚠️ | No ❌ |
| **Touch Gestures** | **15+** ✅ | Limited ⚠️ | No ❌ | Limited ⚠️ |
| **Keyboard Shortcuts** | **15+** ✅ | ~8 ⚠️ | Basic ⚠️ | ~10 ⚠️ |
| **i18n Support** | **Yes** ✅ | Yes ✅ | No ❌ | Yes ✅ |
| Feature | @source/player | video.js | react-player | plyr |
| ------------------------- | ---------------------------- | ---------- | ------------ | ---------- |
| **Bundle Size (gzipped)** | **~18KB JS + ~3.5KB CSS** ✅ | ~500KB ❌ | ~50KB ⚠️ | ~30KB ⚠️ |
| **Runtime Dependencies** | **0** | Many ❌ | Few ⚠️ | Few ⚠️ |
| **React (Web)** | **Yes** | Wrapper ⚠️ | **Yes** | Wrapper ⚠️ |
| **TypeScript Native** | **Yes** | Types ⚠️ | Partial ⚠️ | Types ⚠️ |
| **HLS Support** | **Yes** | Yes ✅ | Yes ✅ | No ❌ |
| **Quality Switching** | **Yes** | Yes ✅ | Limited ⚠️ | No ❌ |
| **Touch Gestures** | **15+** | Limited ⚠️ | No ❌ | Limited ⚠️ |
| **Keyboard Shortcuts** | **15+** | ~8 ⚠️ | Basic ⚠️ | ~10 ⚠️ |
| **i18n Support** | **Yes** | Yes ✅ | No ❌ | Yes ✅ |
### Key Advantages
@@ -37,6 +37,7 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
## ✨ Features
### 🎮 Core Playback
- ▶️ Play/Pause controls
- ⏭️ Seek/scrub with progress bar
- 🔊 Volume control with slider
@@ -45,6 +46,7 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
- 🖼️ Custom poster/thumbnail
### 🎨 Modern UI
- Clean, minimalist design with red theme
- Smooth animations and transitions
- Auto-hiding controls
@@ -54,6 +56,7 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
- Center play button
### ⌨️ Keyboard Shortcuts
- `Space` or `K` - Play/Pause
- `←` / `→` - Seek 5 seconds
- `J` / `L` - Seek 10 seconds
@@ -66,6 +69,7 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
- Shortcuts only work for the currently active/focused player instance
### 📱 Touch Gestures
- **Tap** - Play/Pause
- **Double tap left** - Rewind 10 seconds
- **Double tap right** - Forward 10 seconds
@@ -73,10 +77,12 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
- **Swipe up/down** - Volume control
### 🚀 Advanced Features
- **HLS Streaming** - Automatic HLS.js integration for .m3u8 files
- **IPTV Support** - MPEG-TS (.ts) streams for IPTV services
- **HTTP Range Request** - Progressive download for large MP4 files
- **Subtitles** - WebVTT and SRT support
- **Subtitle Style Editor** - Optional in-player style controls with live preview and local persistence
- **Multiple Audio Tracks** - Switch between different audio streams
- **Picture-in-Picture** - Native browser PIP support
- **Fullscreen** - Native fullscreen API
@@ -113,6 +119,7 @@ yarn add @source/player
> **Note:** This package requires `react` (>=18) and `react-dom` (>=18) at runtime but does **not** list them as `peerDependencies` to avoid install conflicts with private registries. Make sure your project already has React installed.
> **Streaming libraries (optional):** HLS, FLV and MPEG-TS streaming libraries are loaded automatically from CDN when needed. If you prefer to bundle them locally, install them separately:
>
> ```bash
> npm install hls.js # HLS (.m3u8) streams
> npm install flv.js # FLV/RTMP streams
@@ -139,12 +146,7 @@ import { VideoPlayer } from '@source/player'
import '@source/player/styles.css'
function App() {
return (
<VideoPlayer
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
/>
)
return <VideoPlayer src="https://example.com/video.mp4" poster="https://example.com/poster.jpg" />
}
```
@@ -193,22 +195,32 @@ function App() {
/>
```
### HLS Streaming
### Subtitle Style Editor (Optional)
```tsx
<VideoPlayer
src="https://example.com/stream/playlist.m3u8"
autoplay={false}
src="https://example.com/video.mp4"
subtitles={[{ src: '/subtitles/en.vtt', lang: 'en', label: 'English', default: true }]}
subtitleStyleEditor={{
enabled: true,
storageKey: 'my-app-player-subtitle-style',
}}
/>
```
`enabled: true` adds a subtitle style editor in `Settings > Subtitles`.
Changes are previewed in real-time and saved to `localStorage` only when the user presses `Save`.
### HLS Streaming
```tsx
<VideoPlayer src="https://example.com/stream/playlist.m3u8" autoplay={false} />
```
### Force Protocol (Override Auto Detection)
```tsx
<VideoPlayer
src="https://cdn.example.com/video"
protocol="hls"
/>
<VideoPlayer src="https://cdn.example.com/video" protocol="hls" />
```
### IPTV Streaming
@@ -244,9 +256,9 @@ function App() {
```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
volume={0.5} // 50% volume
playbackRate={1.5} // 1.5x speed
currentTime={30} // Start at 30 seconds
/>
```
@@ -358,83 +370,91 @@ video-player/
#### Basic Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `src` | `string` | **required** | Video source URL (MP4, WebM, HLS, IPTV .ts) |
| `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 |
| `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 |
| `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 |
| `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks |
| `subtitleStyle` | `SubtitleStyle` | - | Custom subtitle text/background style |
| `subtitlePosition` | `'top' \| 'center' \| 'bottom'` | `'bottom'` | Subtitle vertical placement |
| `subtitleOffset` | `number \| string` | - | Subtitle offset (`px` if number) |
| `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 |
| Prop | Type | Default | Description |
| --------------------- | ------------------------------------------------------------- | ------------ | -------------------------------------------------------------- |
| `src` | `string` | **required** | Video source URL (MP4, WebM, HLS, IPTV .ts) |
| `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 |
| `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 |
| `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 |
| `subtitles` | `SubtitleTrack[]` | `[]` | Subtitle tracks |
| `subtitleStyle` | `SubtitleStyle` | - | Custom subtitle text/background style |
| `subtitleStyleEditor` | `boolean \| SubtitleStyleEditorConfig` | `false` | Optional subtitle style editor UI and localStorage persistence |
| `subtitlePosition` | `'top' \| 'center' \| 'bottom'` | `'bottom'` | Subtitle vertical placement |
| `subtitleOffset` | `number \| string` | - | Subtitle offset (`px` if number) |
| `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 |
#### 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 |
| 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 |
### PlayerErrorBoundary Props
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Wrapped player/content tree |
| `fallback` | `ReactNode \| (error: Error, retry: () => void) => ReactNode` | Optional custom fallback UI |
| `onError` | `(error: Error, errorInfo: React.ErrorInfo) => void` | Called when render errors are captured |
| `onReset` | `() => void` | Called when retry/reset is triggered |
| `resetKeys` | `readonly unknown[]` | Resets boundary when any key changes |
| Prop | Type | Description |
| ----------- | ------------------------------------------------------------- | -------------------------------------- |
| `children` | `ReactNode` | Wrapped player/content tree |
| `fallback` | `ReactNode \| (error: Error, retry: () => void) => ReactNode` | Optional custom fallback UI |
| `onError` | `(error: Error, errorInfo: React.ErrorInfo) => void` | Called when render errors are captured |
| `onReset` | `() => void` | Called when retry/reset is triggered |
| `resetKeys` | `readonly unknown[]` | Resets boundary when any key changes |
### SubtitleTrack
```typescript
interface SubtitleTrack {
src: string // Subtitle file URL (.vtt or .srt)
lang: string // Language code (e.g., 'en', 'tr')
label: string // Display label
src: string // Subtitle file URL (.vtt or .srt)
lang: string // Language code (e.g., 'en', 'tr')
label: string // Display label
default?: boolean // Set as default subtitle
}
```
```typescript
interface SubtitleStyleEditorConfig {
enabled?: boolean // Enables subtitle style editor in settings
storageKey?: string // localStorage key (default: 'source-player-subtitle-style')
}
```
### PlayerTheme
```typescript
interface PlayerTheme {
primaryColor?: string // Primary color (default: #ef4444)
accentColor?: string // Accent/hover color (default: #dc2626)
backgroundColor?: string // Background color (default: #000000)
textColor?: string // Text color (default: #ffffff)
primaryColor?: string // Primary color (default: #ef4444)
accentColor?: string // Accent/hover color (default: #dc2626)
backgroundColor?: string // Background color (default: #000000)
textColor?: string // Text color (default: #ffffff)
}
```
@@ -457,6 +477,7 @@ interface PlayerTheme {
## 🔧 Technical Details
### Native Browser APIs Used
- HTML5 Video API
- Fullscreen API
- Picture-in-Picture API
@@ -468,18 +489,21 @@ interface PlayerTheme {
### Streaming Support
**MP4/WebM (Progressive Download)**
- Uses HTTP Range Requests
- Browser automatically chunks the download
- No additional library needed
- Works with any standard web server that supports Range headers
**HLS (.m3u8)**
- Automatically detects HLS sources
- Lazy loads hls.js library when needed
- Safari has native HLS support (no library needed)
- Adaptive bitrate streaming
### Performance Optimizations
- Lazy loading of HLS.js with CDN fallback
- CSS-only animations
- Debounced control hiding
@@ -489,6 +513,7 @@ interface PlayerTheme {
- Polyfills for older browser support
### Error Handling & Reliability
- **CORS Detection**: Automatically detects and reports CORS issues with helpful error messages
- **HLS.js Fallback**: If npm package fails to load, automatically falls back to CDN
- **Memory Management**: Proper cleanup of HLS instances to prevent memory leaks