Ultra-performant React video player with VAST ads support, Picture-in-Picture, and advanced controls
npm install @frameset/plex-player
Ultra-performant React video player with VAST ads support, Picture-in-Picture, and advanced controls.
Built with ❤️ by FRAMESET STUDIO
bash
npm install @frameset/plex-player
`
`bash
yarn add @frameset/plex-player
`
`bash
pnpm add @frameset/plex-player
`
🚀 Quick Start
`tsx
import { PlexVideoPlayer } from '@frameset/plex-player';
import '@frameset/plex-player/styles.css';
function App() {
return (
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
width="100%"
height="auto"
/>
);
}
`
📖 Documentation
$3
`tsx
import { PlexVideoPlayer } from '@frameset/plex-player';
import '@frameset/plex-player/styles.css';
function VideoPlayer() {
return (
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
autoPlay={false}
muted={false}
controls={true}
/>
);
}
`
$3
`tsx
src={[
{ src: 'video-1080p.mp4', quality: '1080p', label: '1080p HD' },
{ src: 'video-720p.mp4', quality: '720p', label: '720p' },
{ src: 'video-480p.mp4', quality: '480p', label: '480p' },
{ src: 'video-360p.mp4', quality: '360p', label: '360p' },
]}
qualitySelector={true}
/>
`
$3
`tsx
src="https://example.com/video.mp4"
vast={{
url: 'https://example.com/vast.xml',
skipDelay: 5,
position: 'preroll',
}}
onAdStart={(ad) => console.log('Ad started:', ad)}
onAdEnd={() => console.log('Ad ended')}
onAdSkip={() => console.log('Ad skipped')}
/>
`
$3
`tsx
src="https://example.com/video.mp4"
vast={[
{ url: 'https://example.com/preroll.xml', position: 'preroll' },
{ url: 'https://example.com/midroll.xml', position: 'midroll', midrollTime: 60 },
{ url: 'https://example.com/postroll.xml', position: 'postroll' },
]}
/>
`
$3
`tsx
src="https://example.com/video.mp4"
textTracks={[
{ src: 'subtitles-en.vtt', kind: 'subtitles', srclang: 'en', label: 'English' },
{ src: 'subtitles-es.vtt', kind: 'subtitles', srclang: 'es', label: 'Spanish' },
{ src: 'subtitles-fr.vtt', kind: 'subtitles', srclang: 'fr', label: 'French' },
]}
/>
`
$3
`tsx
src="https://example.com/video.mp4"
accentColor="#ff5722"
theme="dark"
className="my-custom-player"
style={{ borderRadius: '12px', overflow: 'hidden' }}
/>
`
$3
`tsx
import { useRef } from 'react';
import { PlexVideoPlayer, PlexVideoPlayerRef } from '@frameset/plex-player';
function VideoPlayer() {
const playerRef = useRef(null);
const handlePlayPause = () => {
if (playerRef.current?.isPlaying()) {
playerRef.current.pause();
} else {
playerRef.current?.play();
}
};
const handleSeek = () => {
playerRef.current?.seek(30); // Seek to 30 seconds
};
const handleFullscreen = () => {
playerRef.current?.toggleFullscreen();
};
return (
<>
ref={playerRef}
src="https://example.com/video.mp4"
/>
>
);
}
`
$3
`tsx
import { usePlayer } from '@frameset/plex-player';
function CustomPlayer() {
const {
state,
videoRef,
containerRef,
play,
pause,
togglePlay,
seek,
setVolume,
toggleMute,
setPlaybackRate,
toggleFullscreen,
togglePip,
} = usePlayer({ autoPlay: false, muted: false });
return (
{state.currentTime} / {state.duration}
);
}
`
⚙️ Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| src | string \| VideoSource[] | required | Video source URL or array of sources |
| poster | string | - | Poster image URL |
| autoPlay | boolean | false | Auto-play video on load |
| muted | boolean | false | Mute video on load |
| loop | boolean | false | Loop video playback |
| preload | 'none' \| 'metadata' \| 'auto' | 'metadata' | Preload behavior |
| width | number \| string | '100%' | Player width |
| height | number \| string | 'auto' | Player height |
| controls | boolean | true | Show controls |
| pip | boolean | true | Enable Picture-in-Picture |
| fullscreen | boolean | true | Enable fullscreen |
| playbackSpeed | boolean | true | Enable playback speed control |
| playbackSpeeds | number[] | [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] | Available speeds |
| volume | boolean | true | Enable volume control |
| initialVolume | number | 1 | Initial volume (0-1) |
| progressBar | boolean | true | Show progress bar |
| timeDisplay | boolean | true | Show time display |
| qualitySelector | boolean | true | Enable quality selector |
| textTracks | TextTrack[] | - | Subtitle tracks |
| vast | VastConfig \| VastConfig[] | - | VAST ads config |
| keyboard | boolean | true | Enable keyboard shortcuts |
| hotkeys | HotkeyConfig | - | Custom hotkey mappings |
| className | string | - | Custom CSS class |
| style | CSSProperties | - | Inline styles |
| accentColor | string | - | Custom accent color |
| theme | 'dark' \| 'light' \| 'auto' | 'dark' | Color theme |
| controlsTimeout | number | 3000 | Controls hide timeout (ms) |
| doubleClickFullscreen | boolean | true | Double-click for fullscreen |
| clickToPlay | boolean | true | Click to play/pause |
| thumbnailPreview | ThumbnailConfig | - | Thumbnail preview config |
🎯 Event Handlers
| Event | Parameters | Description |
|-------|------------|-------------|
| onPlay | - | Fired when playback starts |
| onPause | - | Fired when playback pauses |
| onEnded | - | Fired when video ends |
| onTimeUpdate | time: number | Fired on time update |
| onProgress | buffered: number | Fired on buffer progress |
| onVolumeChange | volume: number, muted: boolean | Fired on volume change |
| onSeeking | time: number | Fired when seeking |
| onSeeked | time: number | Fired after seek completes |
| onRateChange | rate: number | Fired on playback rate change |
| onQualityChange | quality: string | Fired on quality change |
| onFullscreenChange | isFullscreen: boolean | Fired on fullscreen toggle |
| onPipChange | isPip: boolean | Fired on PiP toggle |
| onError | error: MediaError | Fired on error |
| onReady | - | Fired when player is ready |
| onAdStart | ad: VastAdInfo | Fired when ad starts |
| onAdEnd | - | Fired when ad ends |
| onAdSkip | - | Fired when ad is skipped |
| onAdError | error: Error | Fired on ad error |
⌨️ Keyboard Shortcuts
| Key | Action |
|-----|--------|
| Space | Play/Pause |
| M | Mute/Unmute |
| F | Toggle Fullscreen |
| P | Toggle Picture-in-Picture |
| ← | Seek backward 10s |
| → | Seek forward 10s |
| Shift + ← | Seek backward 30s |
| Shift + → | Seek forward 30s |
| ↑ | Volume up |
| ↓ | Volume down |
🎨 Theming
$3
`css
:root {
--plex-primary: #e50914;
--plex-secondary: #ffffff;
--plex-bg: rgba(0, 0, 0, 0.8);
--plex-control-bg: rgba(0, 0, 0, 0.7);
--plex-progress-bg: rgba(255, 255, 255, 0.3);
--plex-buffered-bg: rgba(255, 255, 255, 0.5);
--plex-hover: rgba(255, 255, 255, 0.1);
--plex-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
--plex-transition: all 0.2s ease;
}
`
$3
`css
.my-custom-player {
--plex-primary: #00bcd4;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.my-custom-player .plex-video-player__btn:hover {
background-color: rgba(0, 188, 212, 0.2);
}
`
📋 Types
`typescript
interface VideoSource {
src: string;
type?: string;
quality?: string;
label?: string;
}
interface TextTrack {
src: string;
kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
srclang: string;
label: string;
default?: boolean;
}
interface VastConfig {
url: string;
skipDelay?: number;
position?: 'preroll' | 'midroll' | 'postroll';
midrollTime?: number;
}
interface PlexVideoPlayerRef {
play: () => Promise;
pause: () => void;
stop: () => void;
seek: (time: number) => void;
setVolume: (volume: number) => void;
mute: () => void;
unmute: () => void;
toggleMute: () => void;
enterFullscreen: () => Promise;
exitFullscreen: () => Promise;
toggleFullscreen: () => Promise;
enterPip: () => Promise;
exitPip: () => Promise;
togglePip: () => Promise;
setPlaybackRate: (rate: number) => void;
setQuality: (quality: string) => void;
getCurrentTime: () => number;
getDuration: () => number;
getVolume: () => number;
isMuted: () => boolean;
isPlaying: () => boolean;
isFullscreen: () => boolean;
isPip: () => boolean;
getVideoElement: () => HTMLVideoElement | null;
}
``
Made with ❤️ by FRAMESET STUDIO