# GuardVideo Player SDK

🎬 **Secure HLS video player with embed token authentication — React, Next.js, and vanilla JavaScript**

[![npm version](https://img.shields.io/npm/v/@guardvideo/player-sdk.svg)](https://www.npmjs.com/package/@guardvideo/player-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

## Features

✅ **Custom Professional Player UI** — No native browser controls; full custom design  
✅ **Adaptive Bitrate Streaming** — Automatic quality switching based on network conditions  
✅ **Secure Token Authentication** — API key never exposed to the browser  
✅ **Multi-Quality Support** — 1080p, 720p, 480p, 360p  
✅ **AES-128 Encryption** — Secure video segment delivery  
✅ **Forensic Watermark** — Viewer-identifying overlay baked into playback (default: on)  
✅ **React & Next.js Support** — First-class TypeScript support  
✅ **Vanilla JS / CDN Support** — Full custom UI without any framework  
✅ **TypeScript Types** — Full type safety included  
✅ **Browser Support** — All modern browsers + Safari/iOS native HLS  

---

## Installation

### NPM / Yarn (React / Next.js)

```bash
npm install @guardvideo/player-sdk
# or
yarn add @guardvideo/player-sdk
```

### CDN (Vanilla JavaScript)

```html
<!-- full bundle (recommended for development) -->
<script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.js"></script>

<!-- minified bundle (recommended for production) -->
<script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.min.js"></script>
```

---

## Quick Start

### React / Next.js Component

```tsx
import { GuardVideoPlayer } from '@guardvideo/player-sdk';

function VideoPage() {
  return (
    <GuardVideoPlayer
      videoId="your-video-id"
      embedTokenEndpoint="/api/embed-token"
      width="100%"
      height="500px"
      viewerEmail="user@example.com"
      viewerName="Jane Doe"
      forensicWatermark    // default: true
      onReady={() => console.log('Video ready!')}
      onError={(error) => console.error(error)}
      onQualityChange={(quality) => console.log('Quality:', quality)}
      onStateChange={(state) => console.log('State:', state)}
    />
  );
}
```

> **Note:** The component renders a fully custom player UI with play/pause, seek bar, volume control, quality selector, playback speed menu, and fullscreen. You do **not** need to add the native `controls` attribute or wrap it in any overlay.

### React Hook (build your own controls)

```tsx
import { useGuardVideoPlayer } from '@guardvideo/player-sdk';

function CustomPlayer({ videoId }: { videoId: string }) {
  const {
    videoRef,
    isReady,
    isPlaying,
    currentTime,
    duration,
    qualityLevels,
    play,
    pause,
    seek,
    setVolume,
    setQuality,
  } = useGuardVideoPlayer({
    videoId,
    embedTokenEndpoint: '/api/embed-token',
    viewerEmail: 'user@example.com',
  });

  const formatTime = (s: number) =>
    `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;

  return (
    <div>
      <video ref={videoRef} style={{ width: '100%' }} />
      {isReady && (
        <div>
          <button onClick={isPlaying ? pause : play}>
            {isPlaying ? 'Pause' : 'Play'}
          </button>
          <input
            type="range" min={0} max={duration} value={currentTime}
            onChange={(e) => seek(Number(e.target.value))}
          />
          <span>{formatTime(currentTime)} / {formatTime(duration)}</span>
          <select onChange={(e) => setQuality(Number(e.target.value))}>
            <option value="-1">Auto</option>
            {qualityLevels.map((l) => (
              <option key={l.index} value={l.index}>{l.name}</option>
            ))}
          </select>
        </div>
      )}
    </div>
  );
}
```

### Next.js App Router

```tsx
// app/watch/[videoId]/page.tsx
'use client';

import { GuardVideoPlayer } from '@guardvideo/player-sdk';

export default function WatchPage({ params }: { params: { videoId: string } }) {
  return (
    <GuardVideoPlayer
      videoId={params.videoId}
      embedTokenEndpoint="/api/embed-token"
      width="100%"
      height="600px"
      viewerEmail="user@example.com"
      onReady={() => console.log('Ready!')}
      onError={(e) => console.error(e)}
    />
  );
}
```

### Vanilla JavaScript (CDN)

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>GuardVideo Player</title>
  <style>
    #player-container {
      width: 100%;
      max-width: 900px;
      aspect-ratio: 16/9;
      margin: 40px auto;
    }
  </style>
</head>
<body>
  <div id="player-container"></div>

  <script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.min.js"></script>
  <script>
    const player = GuardVideoPlayer.create('player-container', 'your-video-id', {
      embedTokenEndpoint: '/api/embed-token',
      viewerEmail: 'user@example.com',
      viewerName: 'Jane Doe',

      onReady: function () {
        console.log('Ready! Qualities:', player.getQualityLevels());
      },
      onError: function (err) {
        console.error('Player error:', err);
      },
      onQualityChange: function (quality) {
        console.log('Quality changed to:', quality);
      },
      onStateChange: function (state) {
        console.log('State:', state);
      },
    });
  </script>
</body>
</html>
```

> The vanilla build bundles **everything** (hls.js included). No npm install, no bundler needed. The player renders the same full custom UI as the React component.

---

## Backend Setup

Your backend needs to create embed tokens using your API key:

```typescript
// Next.js API Route: app/api/embed-token/[videoId]/route.ts
export async function POST(
  request: Request,
  { params }: { params: { videoId: string } }
) {
  const VIDEO_API_KEY  = process.env.VIDEO_API_KEY!;
  const VIDEO_API_BASE = process.env.NEXT_PUBLIC_VIDEO_API_BASE!;

  const response = await fetch(
    `${VIDEO_API_BASE}/videos/${params.videoId}/embed-token`,
    {
      method: 'POST',
      headers: {
        'X-API-Key': VIDEO_API_KEY,   // API key stays on the server!
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        allowedDomain: request.headers.get('origin'),
        expiresInMinutes: 120,
        maxViews: null,
        viewerEmail: /* optionally forward from session */ undefined,
        forensicWatermark: true,
      }),
    }
  );

  return Response.json(await response.json());
}
```

### Environment Variables

```env
VIDEO_API_KEY=your-secret-api-key
NEXT_PUBLIC_VIDEO_API_BASE=http://localhost:3001
```

---

## API Reference

### React Component Props (`GuardVideoPlayerProps`)

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `videoId` | `string` | — | **Required.** Video ID to load |
| `embedTokenEndpoint` | `string` | — | **Required.** Your backend endpoint that mints embed tokens |
| `width` | `string \| number` | `"100%"` | Player container width |
| `height` | `string \| number` | `"auto"` | Player container height |
| `apiBaseUrl` | `string` | — | Override default API base URL |
| `autoplay` | `boolean` | `false` | Auto-play on load |
| `debug` | `boolean` | `false` | Enable verbose debug logging |
| `viewerName` | `string` | — | Viewer name (used for forensic watermark) |
| `viewerEmail` | `string` | — | Viewer email (used for forensic watermark) |
| `forensicWatermark` | `boolean` | `true` | Show viewer-identifying watermark overlay |
| `branding` | `BrandingConfig` | — | Custom accent colour and brand name |
| `security` | `SecurityConfig` | — | Security hardening options |
| `contextMenuItems` | `ContextMenuItem[]` | — | Extra context menu items |
| `hlsConfig` | `HlsConfig` | — | HLS.js configuration overrides |
| `onReady` | `() => void` | — | Called when video is ready to play |
| `onError` | `(err: PlayerError) => void` | — | Called on error |
| `onQualityChange` | `(quality: string) => void` | — | Called when quality changes |
| `onStateChange` | `(state: PlayerState) => void` | — | Called on state transitions |
| `onTimeUpdate` | `(time: number) => void` | — | Called on time updates |
| `onEnded` | `() => void` | — | Called when video ends |

### Vanilla JS Config (`PlayerConfig`)

All React props above except JSX-specific ones (`className`, `style`) are available.
Extra vanilla-only props:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `width` | `string` | `"100%"` | Player container width CSS value |
| `height` | `string` | `"auto"` | Player container height CSS value |

### Player Methods

```typescript
player.play(): Promise<void>           // Start playback
player.pause(): void                   // Pause
player.seek(time: number): void        // Seek to time in seconds
player.getCurrentTime(): number        // Current playback position (seconds)
player.getDuration(): number           // Total duration (seconds)
player.getVolume(): number             // Current volume (0–1)
player.setVolume(v: number): void      // Set volume (0–1)
player.getQualityLevels(): QualityLevel[]       // Available quality levels
player.getCurrentQuality(): QualityLevel | null // Active quality
player.setQuality(index: number): void          // Set quality (-1 = auto)
player.getState(): PlayerState         // Current player state
player.getVideoElement(): HTMLVideoElement | null  // Underlying <video> element
player.destroy(): void                 // Clean up and remove player
```

### Player States

```typescript
enum PlayerState {
  IDLE      = 'idle',       // Not yet initialized
  LOADING   = 'loading',    // Loading video
  READY     = 'ready',      // Ready to play
  PLAYING   = 'playing',    // Currently playing
  PAUSED    = 'paused',     // Paused
  BUFFERING = 'buffering',  // Re-buffering mid-play
  ERROR     = 'error',      // Unrecoverable error
}
```

### BrandingConfig

```typescript
interface BrandingConfig {
  accentColor?: string;   // Hex color, default: '#44c09b'
  name?: string;          // Brand name shown in secure badge, default: 'GuardVideo'
  logoUrl?: string;       // Logo URL (used in context menu)
  websiteUrl?: string;    // Website URL (used in context menu)
}
```

### SecurityConfig

```typescript
interface SecurityConfig {
  disableContextMenu?: boolean;    // Block right-click (default: true)
  disableDevTools?: boolean;       // Detect/block DevTools (default: false)
  disablePiP?: boolean;            // Block Picture-in-Picture (default: false)
  disableDragDrop?: boolean;       // Block drag-and-drop (default: true)
  disableScreenshot?: boolean;     // Attempt screenshot blocking (default: false)
}
```

---

## Keyboard Shortcuts

When the player has focus:

| Key | Action |
|-----|--------|
| `Space` / `K` | Play / Pause |
| `←` | Seek back 5 s |
| `→` | Seek forward 5 s |
| `↑` | Volume +10% |
| `↓` | Volume −10% |
| `M` | Toggle mute |
| `F` | Toggle fullscreen |

---

## Using Ref for Imperative Control (React)

```tsx
import { useRef } from 'react';
import { GuardVideoPlayer, GuardVideoPlayerRef } from '@guardvideo/player-sdk';

function VideoWithRef() {
  const playerRef = useRef<GuardVideoPlayerRef>(null);

  return (
    <div>
      <GuardVideoPlayer
        ref={playerRef}
        videoId="your-video-id"
        embedTokenEndpoint="/api/embed-token"
      />
      <button onClick={() => playerRef.current?.seek(playerRef.current.getCurrentTime() + 10)}>
        +10 s
      </button>
    </div>
  );
}
```

---

## TypeScript Support

```typescript
import type {
  PlayerConfig,
  PlayerInstance,
  PlayerState,
  PlayerError,
  QualityLevel,
  BrandingConfig,
  SecurityConfig,
} from '@guardvideo/player-sdk';
```

---

## Browser Support

| Browser | Minimum version |
|---------|----------------|
| Chrome  | 90+ |
| Firefox | 88+ |
| Safari  | 14+ (native HLS) |
| Edge    | 90+ |
| iOS Safari | 14+ |
| Android Chrome | 90+ |

---

## License

MIT © GuardVideo

---

## Support

- GitHub Issues: [github.com/guardvideo/player-sdk](https://github.com/guardvideo/player-sdk)
- Documentation: [docs.guardvideo.com](https://docs.guardvideo.com)
| `Space` / `K` | Play / Pause |
| `←` | Seek back 5 s |
| `→` | Seek forward 5 s |
| `↑` | Volume +10% |
| `↓` | Volume −10% |
| `M` | Toggle mute |
| `F` | Toggle fullscreen |

---

## Using Ref for Imperative Control (React)

```tsx
import { useRef } from 'react';
import { GuardVideoPlayer, GuardVideoPlayerRef } from '@guardvideo/player-sdk';

function VideoWithRef() {
  const playerRef = useRef<GuardVideoPlayerRef>(null);

  return (
    <div>
      <GuardVideoPlayer
        ref={playerRef}
        videoId="your-video-id"
        embedTokenEndpoint="/api/embed-token"
      />
      <button onClick={() => playerRef.current?.seek(playerRef.current.getCurrentTime() + 10)}>
        +10 s
      </button>
    </div>
  );
}
```

---

## TypeScript Support

```typescript
import type {
  PlayerConfig,
  PlayerInstance,
  PlayerState,
  PlayerError,
  QualityLevel,
  BrandingConfig,
  SecurityConfig,
} from '@guardvideo/player-sdk';
```

---

## Browser Support

| Browser | Minimum version |
|---------|----------------|
| Chrome  | 90+ |
| Firefox | 88+ |
| Safari  | 14+ (native HLS) |
| Edge    | 90+ |
| iOS Safari | 14+ |
| Android Chrome | 90+ |

---

## License

MIT © GuardVideo

---

## Support

- GitHub Issues: [github.com/guardvideo/player-sdk](https://github.com/guardvideo/player-sdk)
- Documentation: [docs.guardvideo.com](https://docs.guardvideo.com)

| Safari  | 14+ (native HLS) |
| Edge    | 90+ |
| iOS Safari | 14+ |
| Android Chrome | 90+ |

---

## License

MIT (C) GuardVideo

---

## Support

- GitHub Issues: https://github.com/guardvideo/player-sdk
- Documentation: https://docs.guardvideo.com
