Files
player/DOCUMENTATION.md
T

819 lines
22 KiB
Markdown

# @source/player Documentation
This document reflects the current codebase in this repository (`version 3.2.0`) and replaces older, drifted documentation.
## Table of contents
1. [Overview](#1-overview)
2. [Architecture](#2-architecture)
3. [Installation and setup](#3-installation-and-setup)
4. [Quick usage](#4-quick-usage)
5. [Streaming and protocol handling](#5-streaming-and-protocol-handling)
6. [Subtitles and subtitle style editor](#6-subtitles-and-subtitle-style-editor)
7. [Customization and modular composition](#7-customization-and-modular-composition)
8. [Keyboard and touch interaction](#8-keyboard-and-touch-interaction)
9. [Internationalization](#9-internationalization)
10. [API reference](#10-api-reference)
11. [Public exports](#11-public-exports)
12. [Error handling and reliability](#12-error-handling-and-reliability)
13. [Browser behavior and known limitations](#13-browser-behavior-and-known-limitations)
14. [Testing and development](#14-testing-and-development)
## 1. Overview
`@source/player` is a React media player library with:
- Protocol-aware playback (`native`, `hls`, `rtmp/flv`, `mpegts`)
- Animated image playback (`.gif`, `.webp`, `.apng`, `.avif`) through a minimal render path
- Dedicated audio playback component for music/podcast style use-cases
- Built-in controls with settings menus (speed, quality, subtitles, subtitle styling, audio tracks)
- Modular extension points for custom controls and overlays
- Strong TypeScript API (props, handle types, context types, utility exports)
- Runtime loading strategy for optional stream engines (`hls.js`, `flv.js`, `mpegts.js`)
## 2. Architecture
The player is split into focused modules.
### 2.1 High-level component graph
- `VideoPlayer`
- `AudioPlayer`
- `PlayerErrorBoundary`
- `PlayerProvider` (context + state + actions + i18n)
- `VideoElement` (media element, protocol setup, media events, subtitle rendering)
- `mediaSource` utils (media kind detection for video/audio/animated image)
- `ControlsLayer` (controls visibility, settings menu, keyboard/touch integration)
- `SettingsMenu` (lazy-loaded, menu subviews)
### 2.2 Core responsibilities
- `VideoPlayer`
- Normalizes props and default values
- Resolves subtitle style editor config (`enabled` + `storageKey`)
- Wraps content with `PlayerErrorBoundary` and `PlayerProvider`
- Exposes imperative API via ref (`VideoPlayerHandle`)
- `AudioPlayer`
- Uses native `HTMLAudioElement` with a custom control surface
- Supports keyboard shortcuts, playback rate cycling, and metadata/artwork UI
- Exposes imperative API via ref (`AudioPlayerHandle`)
- `PlayerProvider`
- Owns central `videoState`, `uiState`, and `settings`
- Manages subtitle style draft/commit/persist lifecycle
- Merges built-in translations with custom translations
- Exposes playback and UI actions through context
- `VideoElement`
- Validates URL and detects protocol
- Initializes and tears down stream engine instances
- Subscribes to media events and forwards callbacks
- Converts `.srt` subtitles to VTT Blob URLs
- Renders subtitles using a custom overlay (native rendering disabled)
- `ControlsLayer`
- Handles auto-hide controls behavior
- Loads `SettingsMenu` lazily
- Enables keyboard shortcuts and touch gestures hooks
- Renders center play, spinner, progress, volume, settings, PIP, fullscreen
## 3. Installation and setup
This package is published to a private registry.
1. Add `.npmrc`:
```ini
@source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
//gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITS_NPM_TOKEN}
```
2. Install package:
```bash
npm install @source/player
# or
pnpm add @source/player
# or
yarn add @source/player
```
3. Ensure app already includes React and ReactDOM (`>=18`).
4. Import player styles:
```tsx
import '@source/player/styles.css'
```
Optional local dependencies (instead of CDN fallback):
```bash
npm install hls.js flv.js mpegts.js
```
## 4. Quick usage
### 4.1 Minimal usage
```tsx
import { VideoPlayer } from '@source/player'
import '@source/player/styles.css'
export function App() {
return <VideoPlayer src="https://example.com/video.mp4" />
}
```
### 4.2 With subtitles + editor
```tsx
<VideoPlayer
src="https://example.com/video.mp4"
subtitles={[
{ src: '/subs/en.vtt', lang: 'en', label: 'English', default: true },
{ src: '/subs/tr.srt', lang: 'tr', label: 'Turkish' },
]}
subtitleStyleEditor={{ enabled: true, storageKey: 'player-subtitle-style' }}
/>
```
### 4.3 With theme and modular controls
```tsx
<VideoPlayer
src="https://example.com/video.mp4"
theme={{
primaryColor: '#22c55e',
accentColor: '#16a34a',
backgroundColor: '#000000',
textColor: '#f8fafc',
borderRadius: 16,
fontFamily: 'Manrope, sans-serif',
}}
controlsLeftExtra={<button type="button">Bookmark</button>}
controlsRightExtra={<button type="button">Share</button>}
>
<div style={{ position: 'absolute', top: 12, right: 12 }}>Custom overlay</div>
</VideoPlayer>
```
### 4.4 Animated image support
```tsx
<VideoPlayer src="https://example.com/animations/loader.gif" />
```
`VideoPlayer` auto-detects animated image sources and renders them with a lightweight `<img>` path.
In this mode, control layer is hidden automatically for minimal runtime cost.
### 4.5 Dedicated audio player
```tsx
import { AudioPlayer } from '@source/player'
<AudioPlayer
src="https://example.com/audio/episode.mp3"
title="Episode 12"
subtitle="Engineering Notes"
artwork="https://example.com/audio/cover.jpg"
playbackRates={[0.75, 1, 1.25, 1.5, 2]}
/>
```
## 5. Streaming and protocol handling
### 5.1 Auto protocol detection
When `protocol="auto"` (default), `detectVideoProtocol(src)` chooses by URL pattern.
| Pattern | Detected protocol | Live hint | Needs special engine |
| --- | --- | --- | --- |
| `rtmp://`, `rtmps://`, `rtmpt://`, `rtmpe://` | `rtmp` | true | true |
| contains `.m3u8` | `hls` | true if contains `/live/` or `live.m3u8` | true |
| contains `.mpd` | `dash` | true if contains `/live/` or `live.mpd` | true |
| contains `.flv` or `flv?` | `rtmp` | true if contains `/live/` or `live.flv` | true |
| `.ts` stream URL | `mpegts` | true | true |
| other URLs | `native` | false | false |
You can override with `protocol="native" | "hls" | "rtmp" | "dash" | "mpegts"`.
### 5.2 HLS flow
- For HLS URLs:
- Safari/native HLS path is preferred when appropriate.
- Other browsers use `hls.js`.
Loading strategy:
1. Dynamic import from npm (`hls.js`)
2. If unavailable, CDN fallback: `https://cdn.jsdelivr.net/npm/hls.js@1.6.13/dist/hls.min.js`
HLS setup behavior:
- Tracks audio tracks, quality levels, subtitle tracks
- Handles fatal errors:
- `NETWORK_ERROR` -> `startLoad()`
- `MEDIA_ERROR` -> `recoverMediaError()`
- others -> emit `onError`
### 5.3 RTMP / FLV flow
- Uses `flv.js` runtime loader.
- npm dynamic import first, then CDN fallback:
- `https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js`
Important note:
- Browser playback is effectively HTTP-FLV based.
- Direct RTMP endpoints usually require server-side proxy/conversion to HTTP-FLV.
### 5.4 MPEG-TS flow
- Uses `mpegts.js` runtime loader.
- npm dynamic import first, then CDN fallback:
- `https://cdn.jsdelivr.net/npm/mpegts.js@1.7.3/dist/mpegts.js`
### 5.5 DASH status
- DASH (`.mpd`) is detected but not implemented.
- Player emits `Error('DASH streaming is not yet supported')`.
### 5.6 Live broadcast UI behavior
`videoState.isLiveBroadcast` is set from media duration (`Infinity` or `0`):
- Progress bar and time display are hidden
- Live badge is shown in controls
### 5.7 Animated image detection behavior
`VideoPlayer` can detect animated image sources by extension or data MIME:
- `.gif`, `.webp`, `.apng`, `.avif`
- `data:image/gif`, `data:image/webp`, `data:image/apng`, `data:image/avif`
When detected, player uses image render path instead of protocol setup and stream engines.
## 6. Subtitles and subtitle style editor
### 6.1 Subtitle sources
`subtitles` prop accepts manual tracks:
```ts
type SubtitleTrack = {
src: string
lang: string
label: string
default?: boolean
}
```
HLS subtitle tracks are merged with manual tracks internally.
### 6.2 SRT support
- `.srt` tracks are fetched and converted to VTT Blob URLs.
- Blob URLs are revoked on cleanup.
### 6.3 Custom subtitle rendering
- Native text track rendering is disabled.
- Active cues are read from selected `TextTrack` and rendered inside `.sp-subtitle-overlay`.
- Cue markup is stripped before rendering.
### 6.4 Subtitle styling
`subtitleStyle` supports:
- `fontFamily`
- `fontSize`
- `fontWeight`
- `color`
- `backgroundColor`
- `backgroundOpacity` (0..1)
Applied style is merged as:
- base: `subtitleStyle` prop
- override: context `settings.subtitleStyle` (including editor changes)
### 6.5 Subtitle style editor
Enable via:
```tsx
subtitleStyleEditor={true}
// or
subtitleStyleEditor={{ enabled: true, storageKey: 'my-key' }}
```
Behavior:
- Editor appears under `Settings -> Subtitles`
- Draft updates preview in real time
- `Save` persists to localStorage
- `Cancel` reverts to last committed style
- `Reset` sets editor defaults (not persisted until save)
Default storage key:
- `source-player-subtitle-style`
## 7. Customization and modular composition
### 7.1 Theme API
`theme` maps to CSS variables:
- `primaryColor` -> `--player-primary`
- `accentColor` -> `--player-primary-hover`
- `backgroundColor` -> `--player-bg`
- `textColor` -> `--player-text`
- `fontFamily` -> `--player-font-family`
- `borderRadius` -> `--player-radius`
- `overlayOpacity` -> `--player-overlay-soft`
- `controlsBackground` -> `--player-surface`
- `textSecondaryColor` -> `--player-text-secondary`
- `textMutedColor` -> `--player-text-muted`
### 7.2 Layout and slots
- `aspectRatio`: `'16:9' | '4:3' | '21:9' | '1:1' | '9:16' | number`
- `children`: overlay content slot
- `controlsLeftExtra`: inject extra controls on left side
- `controlsRightExtra`: inject extra controls on right side
### 7.3 Controls auto-hide
`controlsAutoHideDelay` (default `3000`) affects full-screen auto-hide while:
- video is playing
- full-screen is active
- no settings sub-menu is open
## 8. Keyboard and touch interaction
### 8.1 Keyboard shortcuts
Enabled by default (`keyboardShortcuts=true`).
The player only reacts when active/focused (clicked/touched/focused instance).
Default shortcuts:
- `Space` / `K`: play-pause
- `ArrowLeft` / `ArrowRight`: seek `5s`
- `J` / `L`: seek `10s`
- `ArrowUp` / `ArrowDown`: volume +/- `0.1`
- `M`: mute toggle
- `F`: fullscreen toggle
- `P`: picture-in-picture toggle
- `0` / `Home`: seek to start
- `End`: seek to end
- `1..9`: seek to 10%..90%
Config:
```ts
type KeyboardShortcutConfig = {
seekSmall?: number
seekLarge?: number
volumeStep?: number
disabled?: string[]
}
```
### 8.2 Touch gestures
Enabled via `useTouchGestures` in control layer.
Default gestures:
- single tap: play-pause
- double tap left/right: seek -/+ `10s`
- horizontal swipe: seek proportional to swipe distance
- vertical swipe: volume change proportional to swipe distance
Config:
```ts
type TouchConfig = {
maxSeekSeconds?: number
maxVolumeChange?: number
doubleTapSeekSeconds?: number
}
```
Default values:
- `maxSeekSeconds = 30`
- `maxVolumeChange = 0.5`
- `doubleTapSeekSeconds = 10`
## 9. Internationalization
Built-in locales:
- `en`
- `tr`
Selection flow:
- If `language` prop is provided, it is used.
- Else browser language is detected.
- Region codes fallback to base language (`en-US` -> `en`).
- Unknown language fallback: `en`.
You can override any key with `translations?: Partial<Translations>`.
## 10. API reference
### 10.1 `VideoPlayerProps`
#### Source and playback
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `src` | `string` | required | media URL |
| `mediaType` | `'auto' \| 'video' \| 'animated-image'` | `'auto'` | force image/video mode |
| `protocol` | `'auto' \| 'native' \| 'hls' \| 'rtmp' \| 'dash' \| 'mpegts'` | `'auto'` | force engine |
| `poster` | `string` | - | poster image |
| `autoplay` | `boolean` | `false` | autoplay attempt on load |
| `loop` | `boolean` | `false` | loop playback |
| `muted` | `boolean` | `false` | initial muted state |
| `volume` | `number` | - | clamped to `0..1` |
| `playbackRate` | `number` | - | initial/current rate |
| `currentTime` | `number` | - | seeks when difference is significant |
#### Media element attributes
| Prop | Type | Default |
| --- | --- | --- |
| `crossOrigin` | `'' \| 'anonymous' \| 'use-credentials'` | - |
| `preload` | `'none' \| 'metadata' \| 'auto'` | `'metadata'` |
| `playsInline` | `boolean` | `true` |
| `controlsList` | `string` | - |
#### UI and feature toggles
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `controls` | `boolean` | `true` | hide entire control layer when false |
| `keyboardShortcuts` | `boolean` | `true` | enables keyboard hook |
| `pictureInPicture` | `boolean` | `true` | PIP button toggle |
| `controlsAutoHideDelay` | `number` | `3000` | ms |
| `playbackRates` | `number[]` | `[0.25,0.5,0.75,1,1.25,1.5,1.75,2]` | settings menu speeds |
| `aspectRatio` | `'16:9' \| '4:3' \| '21:9' \| '1:1' \| '9:16' \| number` | `'16:9'` equivalent | CSS ratio |
#### Subtitles and settings
| Prop | Type | Default |
| --- | --- | --- |
| `subtitles` | `SubtitleTrack[]` | `[]` |
| `subtitleStyle` | `SubtitleStyle` | - |
| `subtitleStyleEditor` | `boolean \| SubtitleStyleEditorConfig` | `false` |
| `subtitlePosition` | `'top' \| 'center' \| 'bottom'` | `'bottom'` |
| `subtitleOffset` | `number \| string` | - |
#### Styling and composition
| Prop | Type | Default |
| --- | --- | --- |
| `theme` | `PlayerTheme` | - |
| `className` | `string` | `''` |
| `style` | `CSSProperties` | - |
| `children` | `ReactNode` | - |
| `controlsLeftExtra` | `ReactNode` | - |
| `controlsRightExtra` | `ReactNode` | - |
#### Localization and input config
| Prop | Type | Default |
| --- | --- | --- |
| `language` | `string` | browser language |
| `translations` | `Partial<Translations>` | - |
| `keyboardShortcutConfig` | `KeyboardShortcutConfig` | - |
| `touchConfig` | `TouchConfig` | - |
#### Events
| Prop | Type |
| --- | --- |
| `onPlay` | `() => void` |
| `onPause` | `() => void` |
| `onEnded` | `() => void` |
| `onTimeUpdate` | `(currentTime: number) => void` |
| `onVolumeChange` | `(volume: number) => void` |
| `onError` | `(error: Error) => void` |
| `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` |
| `onQualityChange` | `(quality: VideoQuality) => void` |
| `onBufferStart` | `() => void` |
| `onBufferEnd` | `() => void` |
| `onFirstPlay` | `() => void` |
### 10.2 `VideoPlayerHandle`
```ts
interface VideoPlayerHandle {
video: HTMLVideoElement | null
container: HTMLDivElement | null
play(): void
pause(): void
seek(time: number): void
setVolume(volume: number): void
toggleMute(): void
toggleFullscreen(): void
togglePictureInPicture(): void
setPlaybackRate(rate: number): void
}
```
### 10.3 `AudioPlayerProps`
#### Source and playback
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `src` | `string` | required | audio URL |
| `autoplay` | `boolean` | `false` | autoplay attempt on load |
| `loop` | `boolean` | `false` | loop playback |
| `muted` | `boolean` | `false` | initial muted state |
| `volume` | `number` | - | clamped to `0..1` |
| `playbackRate` | `number` | - | initial/current rate |
| `currentTime` | `number` | - | seeks when difference is significant |
| `preload` | `'none' \| 'metadata' \| 'auto'` | `'metadata'` | media preload hint |
| `crossOrigin` | `'' \| 'anonymous' \| 'use-credentials'` | - | CORS mode |
#### UI, metadata and composition
| Prop | Type | Default |
| --- | --- | --- |
| `controls` | `boolean` | `true` |
| `keyboardShortcuts` | `boolean` | `true` |
| `playbackRates` | `number[]` | `[0.5,0.75,1,1.25,1.5,2]` |
| `title` | `string` | - |
| `subtitle` | `string` | - |
| `artwork` | `string` | - |
| `theme` | `PlayerTheme` | - |
| `className` | `string` | `''` |
| `style` | `CSSProperties` | - |
| `children` | `ReactNode` | - |
| `controlsLeftExtra` | `ReactNode` | - |
| `controlsRightExtra` | `ReactNode` | - |
| `language` | `string` | browser language |
| `translations` | `Partial<Translations>` | - |
#### Events
| Prop | Type |
| --- | --- |
| `onPlay` | `() => void` |
| `onPause` | `() => void` |
| `onEnded` | `() => void` |
| `onTimeUpdate` | `(currentTime: number) => void` |
| `onVolumeChange` | `(volume: number) => void` |
| `onError` | `(error: Error) => void` |
| `onLoadedMetadata` | `() => void` |
| `onSeeking` | `() => void` |
| `onSeeked` | `() => void` |
| `onProgress` | `(buffered: number) => void` |
| `onDurationChange` | `(duration: number) => void` |
| `onRateChange` | `(playbackRate: number) => void` |
| `onWaiting` | `() => void` |
| `onCanPlay` | `() => void` |
### 10.4 `AudioPlayerHandle`
```ts
interface AudioPlayerHandle {
audio: HTMLAudioElement | null
container: HTMLDivElement | null
play(): void
pause(): void
seek(time: number): void
setVolume(volume: number): void
toggleMute(): void
setPlaybackRate(rate: number): void
}
```
### 10.5 `PlayerErrorBoundaryProps`
```ts
interface PlayerErrorBoundaryProps {
children: React.ReactNode
fallback?: React.ReactNode | ((error: Error, retry: () => void) => React.ReactNode)
onError?: (error: Error, errorInfo: React.ErrorInfo) => void
onReset?: () => void
resetKeys?: ReadonlyArray<unknown>
}
```
### 10.6 Core types
```ts
type SubtitleTrack = {
src: string
lang: string
label: string
default?: boolean
}
type SubtitleStyle = {
fontFamily?: string
fontSize?: number | string
fontWeight?: number | string
color?: string
backgroundColor?: string
backgroundOpacity?: number
}
type SubtitleStyleEditorConfig = {
enabled?: boolean
storageKey?: string
}
type VideoQuality = {
height?: number
label: string
url?: string
width?: number
bitrate?: number
levelIndex?: number
}
type AudioTrack = {
name: string
language: string
url: string
groupId: string
default?: boolean
autoselect?: boolean
}
type PlayerTheme = {
primaryColor?: string
accentColor?: string
backgroundColor?: string
textColor?: string
fontFamily?: string
borderRadius?: number | string
overlayOpacity?: number
controlsBackground?: string
textSecondaryColor?: string
textMutedColor?: string
}
```
## 11. Public exports
From `src/index.ts`:
- Components
- `VideoPlayer`
- `AudioPlayer`
- `PlayerErrorBoundary`
- Context
- `PlayerProvider`
- `usePlayerContext`
- Hooks
- `useKeyboardShortcuts`
- `useTouchGestures`
- i18n
- `getTranslations`
- `detectBrowserLanguage`
- `translations`
- Utilities
- `formatTime`, `parseTime`
- `parseSRT`, `createSubtitleBlobURL`, `fetchSubtitle`
- `validateVideoURL`, `getCORSErrorMessage`, `isCORSError`, `checkVideoCORS`
- `initializePolyfills`, `features`
- `detectPlayerMediaType`, `isAnimatedImageSource`, `isAudioSource`
- Types
- `VideoPlayerProps`, `VideoPlayerHandle`, `AudioPlayerProps`, `AudioPlayerHandle`
- `SubtitleTrack`, `SubtitleStyle`, `SubtitleStyleEditorConfig`
- `SubtitlePosition`, `AudioTrack`, `VideoQuality`, `PlayerTheme`
- `VideoMediaType`, `VideoMediaTypeInput`
- `KeyboardShortcutConfig`, `TouchConfig`
- `VideoState`, `UIState`, `PlayerSettings`, `PlayerContextValue`
- `Translations`
## 12. Error handling and reliability
### 12.1 URL and CORS helpers
Use exported helpers before rendering:
```ts
import { validateVideoURL, checkVideoCORS } from '@source/player'
const validation = validateVideoURL(src)
if (!validation.valid) {
throw new Error(validation.error)
}
const cors = await checkVideoCORS(src)
if (!cors.supported) {
console.warn(cors.error)
}
```
### 12.2 Error boundary
`VideoPlayer` already wraps content in `PlayerErrorBoundary` with `resetKeys={[src]}`.
You can also wrap manually for custom fallback UI:
```tsx
import { PlayerErrorBoundary, VideoPlayer } from '@source/player'
<PlayerErrorBoundary fallback={(error, retry) => (
<div>
<p>{error.message}</p>
<button onClick={retry}>Retry</button>
</div>
)}>
<VideoPlayer src="https://example.com/video.mp4" />
</PlayerErrorBoundary>
```
### 12.3 Stream instance lifecycle
For HLS/FLV/MPEG-TS setups:
- instance is attached to video element for runtime control
- cleanup runs on source/protocol change and unmount
- orphan instances are explicitly destroyed if still present
## 13. Browser behavior and known limitations
- PIP button renders only when browser supports PIP APIs.
- iOS Safari lacks full programmatic volume control.
- Direct RTMP URLs usually need HTTP-FLV proxying.
- DASH detection exists but playback is not implemented yet.
- MPEG-TS behavior depends on MSE support and stream/server conditions.
- Animated image mode is display-only (no timeline controls by design).
- Some browsers may restrict autoplay for audio unless muted or user-initiated.
Polyfill and feature utilities:
```ts
import { initializePolyfills, features } from '@source/player'
initializePolyfills()
features.hasNativeHLS()
features.hasMSE()
features.hasPIP()
features.hasFullscreen()
features.hasTouch()
features.isIOSSafari()
features.hasVolumeControl()
```
## 14. Testing and development
### 14.1 Useful commands
```bash
npm install
npm run dev
npm run lint
npm run typecheck
npm run test:run
npm run build:lib
npm run validate:publish
```
### 14.2 Test coverage areas in repository
- Core rendering and props (`VideoPlayer.test.tsx`)
- Audio rendering and controls (`AudioPlayer.test.tsx`)
- Settings interactions (`SettingsMenu.test.tsx`)
- Error boundary reset and fallback behavior
- Keyboard and touch hook behavior
- Protocol detection and streaming setup utilities
- Media type detection (`mediaSource.test.ts`)
- CORS helper validation
---
If you maintain this document, update it together with changes to `src/types/index.ts`, `src/index.ts`, `src/components/VideoPlayer.tsx`, and `src/components/AudioPlayer.tsx` to avoid API drift.