npm package: @frameset/plex-player
Description: Ultra-performant React video player with VAST ads support, Picture-in-Picture, and advanced controls
Author: FRAMESET STUDIO
Latest version: 2.0.7

# Plex Player

<p align="center">
  <img src="https://img.shields.io/npm/v/@frameset/plex-player" alt="npm version" />
  <img src="https://img.shields.io/npm/dm/@frameset/plex-player" alt="npm downloads" />
  <img src="https://img.shields.io/bundlephobia/minzip/@frameset/plex-player" alt="bundle size" />
  <img src="https://img.shields.io/npm/l/@frameset/plex-player" alt="license" />
</p>

<p align="center">
  <strong>Ultra-performant React video player with VAST ads support, Picture-in-Picture, and advanced controls.</strong>
</p>

<p align="center">
  Built with ❤️ by <a href="https://frameset.dev">FRAMESET STUDIO</a>
</p>

---

## ✨ Features

- 🚀 **Ultra Performant** - Optimized rendering with minimal re-renders
- 🎬 **VAST Ads Support** - Pre-roll, mid-roll, and post-roll video ads
- 📺 **Picture-in-Picture** - Native PiP support for multitasking
- 🎛️ **Advanced Controls** - Customizable playback speed, quality selector, and more
- ⌨️ **Keyboard Shortcuts** - Full keyboard navigation support
- 📱 **Mobile Friendly** - Touch-optimized controls and gestures
- 🎨 **Themeable** - Dark/Light themes with custom accent colors
- ♿ **Accessible** - ARIA labels and keyboard navigation
- 📝 **Subtitles/Captions** - Multiple text track support
- 🖼️ **Thumbnail Preview** - Hover preview on progress bar
- 💪 **TypeScript** - Full TypeScript support with type definitions
- 📦 **Tree Shakeable** - Import only what you need

## 📦 Installation

```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 (
    <PlexVideoPlayer
      src="https://example.com/video.mp4"
      poster="https://example.com/poster.jpg"
      width="100%"
      height="auto"
    />
  );
}
```

## 📖 Documentation

### Basic Usage

```tsx
import { PlexVideoPlayer } from '@frameset/plex-player';
import '@frameset/plex-player/styles.css';

function VideoPlayer() {
  return (
    <PlexVideoPlayer
      src="https://example.com/video.mp4"
      poster="https://example.com/poster.jpg"
      autoPlay={false}
      muted={false}
      controls={true}
    />
  );
}
```

### Multiple Quality Sources

```tsx
<PlexVideoPlayer
  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}
/>
```

### With VAST Ads

```tsx
<PlexVideoPlayer
  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')}
/>
```

### Multiple Ads (Pre-roll, Mid-roll, Post-roll)

```tsx
<PlexVideoPlayer
  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' },
  ]}
/>
```

### With Subtitles

```tsx
<PlexVideoPlayer
  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' },
  ]}
/>
```

### Custom Styling

```tsx
<PlexVideoPlayer
  src="https://example.com/video.mp4"
  accentColor="#ff5722"
  theme="dark"
  className="my-custom-player"
  style={{ borderRadius: '12px', overflow: 'hidden' }}
/>
```

### Using Ref for Programmatic Control

```tsx
import { useRef } from 'react';
import { PlexVideoPlayer, PlexVideoPlayerRef } from '@frameset/plex-player';

function VideoPlayer() {
  const playerRef = useRef<PlexVideoPlayerRef>(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 (
    <>
      <PlexVideoPlayer
        ref={playerRef}
        src="https://example.com/video.mp4"
      />
      <div>
        <button onClick={handlePlayPause}>Play/Pause</button>
        <button onClick={handleSeek}>Skip to 0:30</button>
        <button onClick={handleFullscreen}>Fullscreen</button>
      </div>
    </>
  );
}
```

### Using the usePlayer Hook

```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 (
    <div ref={containerRef}>
      <video ref={videoRef} src="https://example.com/video.mp4" />
      <div>
        <button onClick={togglePlay}>
          {state.isPlaying ? 'Pause' : 'Play'}
        </button>
        <span>{state.currentTime} / {state.duration}</span>
      </div>
    </div>
  );
}
```

## ⚙️ 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

### CSS Custom Properties

```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;
}
```

### Custom Styling Example

```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<void>;
  pause: () => void;
  stop: () => void;
  seek: (time: number) => void;
  setVolume: (volume: number) => void;
  mute: () => void;
  unmute: () => void;
  toggleMute: () => void;
  enterFullscreen: () => Promise<void>;
  exitFullscreen: () => Promise<void>;
  toggleFullscreen: () => Promise<void>;
  enterPip: () => Promise<void>;
  exitPip: () => Promise<void>;
  togglePip: () => Promise<void>;
  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;
}
```

## 🌐 Browser Support

- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+

## 📄 License

MIT © [FRAMESET STUDIO](https://frameset.dev)

---

<p align="center">
  Made with ❤️ by <a href="https://frameset.dev">FRAMESET STUDIO</a>
</p>