@guardvideo/player-sdk 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,28 @@
1
1
  # GuardVideo Player SDK
2
2
 
3
- 🎬 **Secure HLS video player with embed token authentication for React, Next.js, and vanilla JavaScript**
3
+ 🎬 **Secure HLS video player with embed token authentication React, Next.js, and vanilla JavaScript**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@guardvideo/player-sdk.svg)](https://www.npmjs.com/package/@guardvideo/player-sdk)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  ## Features
9
9
 
10
- ✅ **Adaptive Bitrate Streaming** - Automatic quality switching based on network conditions
11
- ✅ **Secure Token Authentication** - API key never exposed to browser
12
- ✅ **Multi-Quality Support** - 1080p, 720p, 480p, 360p
13
- ✅ **AES-128 Encryption** - Secure video segment delivery
14
- ✅ **React & Next.js Support** - First-class TypeScript support
15
- ✅ **Vanilla JS Support** - Use without any framework
16
- ✅ **TypeScript Types** - Full type safety included
17
- ✅ **Browser Support** - All modern browsers + Safari/iOS native HLS
10
+ ✅ **Custom Professional Player UI** No native browser controls; full custom design
11
+ ✅ **Adaptive Bitrate Streaming** Automatic quality switching based on network conditions
12
+ ✅ **Secure Token Authentication** API key never exposed to the browser
13
+ ✅ **Multi-Quality Support** 1080p, 720p, 480p, 360p
14
+ ✅ **AES-128 Encryption** Secure video segment delivery
15
+ ✅ **Forensic Watermark** — Viewer-identifying overlay baked into playback (default: on)
16
+ ✅ **React & Next.js Support** — First-class TypeScript support
17
+ ✅ **Vanilla JS / CDN Support** Full custom UI without any framework
18
+ ✅ **TypeScript Types** — Full type safety included
19
+ ✅ **Browser Support** — All modern browsers + Safari/iOS native HLS
18
20
 
19
21
  ---
20
22
 
21
23
  ## Installation
22
24
 
23
- ### NPM/Yarn (for React/Next.js)
25
+ ### NPM / Yarn (React / Next.js)
24
26
 
25
27
  ```bash
26
28
  npm install @guardvideo/player-sdk
@@ -28,43 +30,52 @@ npm install @guardvideo/player-sdk
28
30
  yarn add @guardvideo/player-sdk
29
31
  ```
30
32
 
31
- ### CDN (for vanilla JavaScript)
33
+ ### CDN (Vanilla JavaScript)
32
34
 
33
35
  ```html
36
+ <!-- full bundle (recommended for development) -->
34
37
  <script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.js"></script>
38
+
39
+ <!-- minified bundle (recommended for production) -->
40
+ <script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.min.js"></script>
35
41
  ```
36
42
 
37
43
  ---
38
44
 
39
45
  ## Quick Start
40
46
 
41
- ### React Component
47
+ ### React / Next.js Component
42
48
 
43
49
  ```tsx
44
50
  import { GuardVideoPlayer } from '@guardvideo/player-sdk';
45
51
 
46
- function App() {
52
+ function VideoPage() {
47
53
  return (
48
54
  <GuardVideoPlayer
49
55
  videoId="your-video-id"
50
56
  embedTokenEndpoint="/api/embed-token"
51
57
  width="100%"
52
58
  height="500px"
53
- autoplay={false}
54
- controls={true}
59
+ viewerEmail="user@example.com"
60
+ viewerName="Jane Doe"
61
+ forensicWatermark // default: true
55
62
  onReady={() => console.log('Video ready!')}
56
63
  onError={(error) => console.error(error)}
64
+ onQualityChange={(quality) => console.log('Quality:', quality)}
65
+ onStateChange={(state) => console.log('State:', state)}
57
66
  />
58
67
  );
59
68
  }
60
69
  ```
61
70
 
62
- ### React Hook
71
+ > **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.
72
+
73
+ ### React Hook (build your own controls)
63
74
 
64
75
  ```tsx
65
76
  import { useGuardVideoPlayer } from '@guardvideo/player-sdk';
66
77
 
67
- function VideoPlayer({ videoId }: { videoId: string }) {
78
+ function CustomPlayer({ videoId }: { videoId: string }) {
68
79
  const {
69
80
  videoRef,
70
81
  isReady,
@@ -75,31 +86,38 @@ function VideoPlayer({ videoId }: { videoId: string }) {
75
86
  play,
76
87
  pause,
77
88
  seek,
89
+ setVolume,
78
90
  setQuality,
79
91
  } = useGuardVideoPlayer({
80
92
  videoId,
81
93
  embedTokenEndpoint: '/api/embed-token',
82
- debug: true,
94
+ viewerEmail: 'user@example.com',
83
95
  });
84
96
 
97
+ const formatTime = (s: number) =>
98
+ `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
99
+
85
100
  return (
86
101
  <div>
87
- <video ref={videoRef} style={{ width: '100%', height: 'auto' }} />
88
-
89
- <div>
90
- <button onClick={play}>Play</button>
91
- <button onClick={pause}>Pause</button>
92
- <span>{currentTime} / {duration}</span>
93
- </div>
94
-
95
- <select onChange={(e) => setQuality(Number(e.target.value))}>
96
- <option value="-1">Auto</option>
97
- {qualityLevels.map((level) => (
98
- <option key={level.index} value={level.index}>
99
- {level.name}
100
- </option>
101
- ))}
102
- </select>
102
+ <video ref={videoRef} style={{ width: '100%' }} />
103
+ {isReady && (
104
+ <div>
105
+ <button onClick={isPlaying ? pause : play}>
106
+ {isPlaying ? 'Pause' : 'Play'}
107
+ </button>
108
+ <input
109
+ type="range" min={0} max={duration} value={currentTime}
110
+ onChange={(e) => seek(Number(e.target.value))}
111
+ />
112
+ <span>{formatTime(currentTime)} / {formatTime(duration)}</span>
113
+ <select onChange={(e) => setQuality(Number(e.target.value))}>
114
+ <option value="-1">Auto</option>
115
+ {qualityLevels.map((l) => (
116
+ <option key={l.index} value={l.index}>{l.name}</option>
117
+ ))}
118
+ </select>
119
+ </div>
120
+ )}
103
121
  </div>
104
122
  );
105
123
  }
@@ -108,149 +126,72 @@ function VideoPlayer({ videoId }: { videoId: string }) {
108
126
  ### Next.js App Router
109
127
 
110
128
  ```tsx
129
+ // app/watch/[videoId]/page.tsx
111
130
  'use client';
112
131
 
113
132
  import { GuardVideoPlayer } from '@guardvideo/player-sdk';
114
133
 
115
- export default function VideoPage({ params }: { params: { videoId: string } }) {
134
+ export default function WatchPage({ params }: { params: { videoId: string } }) {
116
135
  return (
117
- <div className="container">
118
- <h1>Watch Video</h1>
119
- <GuardVideoPlayer
120
- videoId={params.videoId}
121
- embedTokenEndpoint="/api/embed-token"
122
- width="100%"
123
- height="600px"
124
- autoplay={false}
125
- controls={true}
126
- onReady={() => console.log('Ready!')}
127
- onError={(error) => console.error('Error:', error)}
128
- onQualityChange={(quality) => console.log('Quality:', quality)}
129
- />
130
- </div>
136
+ <GuardVideoPlayer
137
+ videoId={params.videoId}
138
+ embedTokenEndpoint="/api/embed-token"
139
+ width="100%"
140
+ height="600px"
141
+ viewerEmail="user@example.com"
142
+ onReady={() => console.log('Ready!')}
143
+ onError={(e) => console.error(e)}
144
+ />
131
145
  );
132
146
  }
133
147
  ```
134
148
 
135
- ### Vanilla JavaScript
149
+ ### Vanilla JavaScript (CDN)
136
150
 
137
151
  ```html
138
152
  <!DOCTYPE html>
139
- <html>
153
+ <html lang="en">
140
154
  <head>
155
+ <meta charset="UTF-8" />
141
156
  <title>GuardVideo Player</title>
142
157
  <style>
143
- #video-container {
158
+ #player-container {
144
159
  width: 100%;
145
- max-width: 800px;
160
+ max-width: 900px;
146
161
  aspect-ratio: 16/9;
147
- margin: 0 auto;
162
+ margin: 40px auto;
148
163
  }
149
164
  </style>
150
165
  </head>
151
166
  <body>
152
- <div id="video-container"></div>
167
+ <div id="player-container"></div>
153
168
 
154
- <script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.js"></script>
169
+ <script src="https://cdn.jsdelivr.net/npm/@guardvideo/player-sdk@latest/dist/vanilla/guardvideo-player.min.js"></script>
155
170
  <script>
156
- // Create player
157
- const player = GuardVideoPlayer.create('video-container', 'your-video-id', {
171
+ const player = GuardVideoPlayer.create('player-container', 'your-video-id', {
158
172
  embedTokenEndpoint: '/api/embed-token',
159
- debug: true,
160
- autoplay: false,
161
- controls: true,
162
- onReady: function() {
163
- console.log('Video is ready!');
164
- console.log('Available qualities:', player.getQualityLevels());
173
+ viewerEmail: 'user@example.com',
174
+ viewerName: 'Jane Doe',
175
+
176
+ onReady: function () {
177
+ console.log('Ready! Qualities:', player.getQualityLevels());
165
178
  },
166
- onError: function(error) {
167
- console.error('Player error:', error);
179
+ onError: function (err) {
180
+ console.error('Player error:', err);
168
181
  },
169
- onQualityChange: function(quality) {
182
+ onQualityChange: function (quality) {
170
183
  console.log('Quality changed to:', quality);
171
184
  },
172
- onStateChange: function(state) {
185
+ onStateChange: function (state) {
173
186
  console.log('State:', state);
174
- }
175
- });
176
-
177
- // Control the player
178
- document.getElementById('play-btn').addEventListener('click', function() {
179
- player.play();
180
- });
181
-
182
- document.getElementById('pause-btn').addEventListener('click', function() {
183
- player.pause();
184
- });
185
-
186
- document.getElementById('quality-select').addEventListener('change', function(e) {
187
- player.setQuality(parseInt(e.target.value));
187
+ },
188
188
  });
189
189
  </script>
190
190
  </body>
191
191
  </html>
192
192
  ```
193
193
 
194
- ---
195
-
196
- ## API Reference
197
-
198
- ### PlayerConfig
199
-
200
- ```typescript
201
- interface PlayerConfig {
202
- // Required
203
- embedTokenEndpoint: string; // Your backend API endpoint
204
-
205
- // Optional
206
- apiBaseUrl?: string; // Video API base URL
207
- debug?: boolean; // Enable debug logging (default: false)
208
- autoplay?: boolean; // Auto-play on load (default: false)
209
- controls?: boolean; // Show controls (default: true)
210
- className?: string; // CSS class name
211
- style?: CSSProperties; // Inline styles
212
- hlsConfig?: HlsConfig; // HLS.js config overrides
213
-
214
- // Callbacks
215
- onReady?: () => void;
216
- onError?: (error: PlayerError) => void;
217
- onQualityChange?: (quality: string) => void;
218
- onStateChange?: (state: PlayerState) => void;
219
- }
220
- ```
221
-
222
- ### Player Methods
223
-
224
- ```typescript
225
- interface PlayerInstance {
226
- play(): Promise<void>; // Play video
227
- pause(): void; // Pause video
228
- getCurrentTime(): number; // Get current time (seconds)
229
- seek(time: number): void; // Seek to time (seconds)
230
- getDuration(): number; // Get duration (seconds)
231
- getVolume(): number; // Get volume (0-1)
232
- setVolume(volume: number): void; // Set volume (0-1)
233
- getQualityLevels(): QualityLevel[]; // Get available qualities
234
- getCurrentQuality(): QualityLevel | null; // Get current quality
235
- setQuality(levelIndex: number): void; // Set quality (-1 for auto)
236
- getState(): PlayerState; // Get current state
237
- destroy(): void; // Cleanup and destroy
238
- }
239
- ```
240
-
241
- ### Player States
242
-
243
- ```typescript
244
- enum PlayerState {
245
- IDLE = 'idle', // Not initialized
246
- LOADING = 'loading', // Loading video
247
- READY = 'ready', // Ready to play
248
- PLAYING = 'playing', // Currently playing
249
- PAUSED = 'paused', // Paused
250
- BUFFERING = 'buffering', // Buffering
251
- ERROR = 'error', // Error occurred
252
- }
253
- ```
194
+ > 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.
254
195
 
255
196
  ---
256
197
 
@@ -264,27 +205,28 @@ export async function POST(
264
205
  request: Request,
265
206
  { params }: { params: { videoId: string } }
266
207
  ) {
267
- const VIDEO_API_KEY = process.env.VIDEO_API_KEY;
268
- const VIDEO_API_BASE = process.env.NEXT_PUBLIC_VIDEO_API_BASE;
208
+ const VIDEO_API_KEY = process.env.VIDEO_API_KEY!;
209
+ const VIDEO_API_BASE = process.env.NEXT_PUBLIC_VIDEO_API_BASE!;
269
210
 
270
211
  const response = await fetch(
271
212
  `${VIDEO_API_BASE}/videos/${params.videoId}/embed-token`,
272
213
  {
273
214
  method: 'POST',
274
215
  headers: {
275
- 'X-API-Key': VIDEO_API_KEY, // API key stays on server!
276
- 'Content-Type': 'application/json'
216
+ 'X-API-Key': VIDEO_API_KEY, // API key stays on the server!
217
+ 'Content-Type': 'application/json',
277
218
  },
278
219
  body: JSON.stringify({
279
220
  allowedDomain: request.headers.get('origin'),
280
221
  expiresInMinutes: 120,
281
- maxViews: null
282
- })
222
+ maxViews: null,
223
+ viewerEmail: /* optionally forward from session */ undefined,
224
+ forensicWatermark: true,
225
+ }),
283
226
  }
284
227
  );
285
228
 
286
- const embedData = await response.json();
287
- return Response.json(embedData);
229
+ return Response.json(await response.json());
288
230
  }
289
231
  ```
290
232
 
@@ -297,86 +239,117 @@ NEXT_PUBLIC_VIDEO_API_BASE=http://localhost:3001
297
239
 
298
240
  ---
299
241
 
300
- ## Advanced Usage
242
+ ## API Reference
301
243
 
302
- ### Custom Controls with React Hook
244
+ ### React Component Props (`GuardVideoPlayerProps`)
245
+
246
+ | Prop | Type | Default | Description |
247
+ |------|------|---------|-------------|
248
+ | `videoId` | `string` | — | **Required.** Video ID to load |
249
+ | `embedTokenEndpoint` | `string` | — | **Required.** Your backend endpoint that mints embed tokens |
250
+ | `width` | `string \| number` | `"100%"` | Player container width |
251
+ | `height` | `string \| number` | `"auto"` | Player container height |
252
+ | `apiBaseUrl` | `string` | — | Override default API base URL |
253
+ | `autoplay` | `boolean` | `false` | Auto-play on load |
254
+ | `debug` | `boolean` | `false` | Enable verbose debug logging |
255
+ | `viewerName` | `string` | — | Viewer name (used for forensic watermark) |
256
+ | `viewerEmail` | `string` | — | Viewer email (used for forensic watermark) |
257
+ | `forensicWatermark` | `boolean` | `true` | Show viewer-identifying watermark overlay |
258
+ | `branding` | `BrandingConfig` | — | Custom accent colour and brand name |
259
+ | `security` | `SecurityConfig` | — | Security hardening options |
260
+ | `contextMenuItems` | `ContextMenuItem[]` | — | Extra context menu items |
261
+ | `hlsConfig` | `HlsConfig` | — | HLS.js configuration overrides |
262
+ | `onReady` | `() => void` | — | Called when video is ready to play |
263
+ | `onError` | `(err: PlayerError) => void` | — | Called on error |
264
+ | `onQualityChange` | `(quality: string) => void` | — | Called when quality changes |
265
+ | `onStateChange` | `(state: PlayerState) => void` | — | Called on state transitions |
266
+ | `onTimeUpdate` | `(time: number) => void` | — | Called on time updates |
267
+ | `onEnded` | `() => void` | — | Called when video ends |
268
+
269
+ ### Vanilla JS Config (`PlayerConfig`)
270
+
271
+ All React props above except JSX-specific ones (`className`, `style`) are available.
272
+ Extra vanilla-only props:
273
+
274
+ | Prop | Type | Default | Description |
275
+ |------|------|---------|-------------|
276
+ | `width` | `string` | `"100%"` | Player container width CSS value |
277
+ | `height` | `string` | `"auto"` | Player container height CSS value |
303
278
 
304
- ```tsx
305
- function CustomVideoPlayer({ videoId }: { videoId: string }) {
306
- const {
307
- videoRef,
308
- isReady,
309
- isPlaying,
310
- currentTime,
311
- duration,
312
- volume,
313
- qualityLevels,
314
- currentQuality,
315
- play,
316
- pause,
317
- seek,
318
- setVolume,
319
- setQuality,
320
- } = useGuardVideoPlayer({
321
- videoId,
322
- embedTokenEndpoint: '/api/embed-token',
323
- debug: true,
324
- });
279
+ ### Player Methods
325
280
 
326
- const formatTime = (seconds: number) => {
327
- const mins = Math.floor(seconds / 60);
328
- const secs = Math.floor(seconds % 60);
329
- return `${mins}:${secs.toString().padStart(2, '0')}`;
330
- };
281
+ ```typescript
282
+ player.play(): Promise<void> // Start playback
283
+ player.pause(): void // Pause
284
+ player.seek(time: number): void // Seek to time in seconds
285
+ player.getCurrentTime(): number // Current playback position (seconds)
286
+ player.getDuration(): number // Total duration (seconds)
287
+ player.getVolume(): number // Current volume (0–1)
288
+ player.setVolume(v: number): void // Set volume (0–1)
289
+ player.getQualityLevels(): QualityLevel[] // Available quality levels
290
+ player.getCurrentQuality(): QualityLevel | null // Active quality
291
+ player.setQuality(index: number): void // Set quality (-1 = auto)
292
+ player.getState(): PlayerState // Current player state
293
+ player.getVideoElement(): HTMLVideoElement | null // Underlying <video> element
294
+ player.destroy(): void // Clean up and remove player
295
+ ```
331
296
 
332
- return (
333
- <div className="video-player">
334
- <video ref={videoRef} className="video-element" />
335
-
336
- {isReady && (
337
- <div className="controls">
338
- <button onClick={isPlaying ? pause : play}>
339
- {isPlaying ? 'Pause' : 'Play'}
340
- </button>
297
+ ### Player States
341
298
 
342
- <input
343
- type="range"
344
- min="0"
345
- max={duration}
346
- value={currentTime}
347
- onChange={(e) => seek(Number(e.target.value))}
348
- />
299
+ ```typescript
300
+ enum PlayerState {
301
+ IDLE = 'idle', // Not yet initialized
302
+ LOADING = 'loading', // Loading video
303
+ READY = 'ready', // Ready to play
304
+ PLAYING = 'playing', // Currently playing
305
+ PAUSED = 'paused', // Paused
306
+ BUFFERING = 'buffering', // Re-buffering mid-play
307
+ ERROR = 'error', // Unrecoverable error
308
+ }
309
+ ```
349
310
 
350
- <span>{formatTime(currentTime)} / {formatTime(duration)}</span>
311
+ ### BrandingConfig
351
312
 
352
- <input
353
- type="range"
354
- min="0"
355
- max="1"
356
- step="0.1"
357
- value={volume}
358
- onChange={(e) => setVolume(Number(e.target.value))}
359
- />
313
+ ```typescript
314
+ interface BrandingConfig {
315
+ accentColor?: string; // Hex color, default: '#44c09b'
316
+ name?: string; // Brand name shown in secure badge, default: 'GuardVideo'
317
+ logoUrl?: string; // Logo URL (used in context menu)
318
+ websiteUrl?: string; // Website URL (used in context menu)
319
+ }
320
+ ```
360
321
 
361
- <select
362
- value={currentQuality?.index ?? -1}
363
- onChange={(e) => setQuality(Number(e.target.value))}
364
- >
365
- <option value="-1">Auto Quality</option>
366
- {qualityLevels.map((level) => (
367
- <option key={level.index} value={level.index}>
368
- {level.name} ({Math.round(level.bitrate / 1000)}kbps)
369
- </option>
370
- ))}
371
- </select>
372
- </div>
373
- )}
374
- </div>
375
- );
322
+ ### SecurityConfig
323
+
324
+ ```typescript
325
+ interface SecurityConfig {
326
+ disableContextMenu?: boolean; // Block right-click (default: true)
327
+ disableDevTools?: boolean; // Detect/block DevTools (default: false)
328
+ disablePiP?: boolean; // Block Picture-in-Picture (default: false)
329
+ disableDragDrop?: boolean; // Block drag-and-drop (default: true)
330
+ disableScreenshot?: boolean; // Attempt screenshot blocking (default: false)
376
331
  }
377
332
  ```
378
333
 
379
- ### Using Ref for Imperative Control
334
+ ---
335
+
336
+ ## Keyboard Shortcuts
337
+
338
+ When the player has focus:
339
+
340
+ | Key | Action |
341
+ |-----|--------|
342
+ | `Space` / `K` | Play / Pause |
343
+ | `←` | Seek back 5 s |
344
+ | `→` | Seek forward 5 s |
345
+ | `↑` | Volume +10% |
346
+ | `↓` | Volume −10% |
347
+ | `M` | Toggle mute |
348
+ | `F` | Toggle fullscreen |
349
+
350
+ ---
351
+
352
+ ## Using Ref for Imperative Control (React)
380
353
 
381
354
  ```tsx
382
355
  import { useRef } from 'react';
@@ -385,17 +358,6 @@ import { GuardVideoPlayer, GuardVideoPlayerRef } from '@guardvideo/player-sdk';
385
358
  function VideoWithRef() {
386
359
  const playerRef = useRef<GuardVideoPlayerRef>(null);
387
360
 
388
- const handleSkipForward = () => {
389
- if (playerRef.current) {
390
- const currentTime = playerRef.current.getCurrentTime();
391
- playerRef.current.seek(currentTime + 10);
392
- }
393
- };
394
-
395
- const handleChangeQuality = (qualityIndex: number) => {
396
- playerRef.current?.setQuality(qualityIndex);
397
- };
398
-
399
361
  return (
400
362
  <div>
401
363
  <GuardVideoPlayer
@@ -403,7 +365,9 @@ function VideoWithRef() {
403
365
  videoId="your-video-id"
404
366
  embedTokenEndpoint="/api/embed-token"
405
367
  />
406
- <button onClick={handleSkipForward}>Skip +10s</button>
368
+ <button onClick={() => playerRef.current?.seek(playerRef.current.getCurrentTime() + 10)}>
369
+ +10 s
370
+ </button>
407
371
  </div>
408
372
  );
409
373
  }
@@ -413,8 +377,6 @@ function VideoWithRef() {
413
377
 
414
378
  ## TypeScript Support
415
379
 
416
- The SDK is written in TypeScript and includes full type definitions:
417
-
418
380
  ```typescript
419
381
  import type {
420
382
  PlayerConfig,
@@ -422,7 +384,8 @@ import type {
422
384
  PlayerState,
423
385
  PlayerError,
424
386
  QualityLevel,
425
- EmbedTokenResponse,
387
+ BrandingConfig,
388
+ SecurityConfig,
426
389
  } from '@guardvideo/player-sdk';
427
390
  ```
428
391
 
@@ -430,12 +393,14 @@ import type {
430
393
 
431
394
  ## Browser Support
432
395
 
433
- - Chrome 90+
434
- - ✅ Firefox 88+
435
- - Safari 14+ (native HLS)
436
- - Edge 90+
437
- - ✅ iOS Safari 14+
438
- - Android Chrome 90+
396
+ | Browser | Minimum version |
397
+ |---------|----------------|
398
+ | Chrome | 90+ |
399
+ | Firefox | 88+ |
400
+ | Safari | 14+ (native HLS) |
401
+ | Edge | 90+ |
402
+ | iOS Safari | 14+ |
403
+ | Android Chrome | 90+ |
439
404
 
440
405
  ---
441
406
 
@@ -447,12 +412,98 @@ MIT © GuardVideo
447
412
 
448
413
  ## Support
449
414
 
450
- For issues and questions:
451
415
  - GitHub Issues: [github.com/guardvideo/player-sdk](https://github.com/guardvideo/player-sdk)
452
416
  - Documentation: [docs.guardvideo.com](https://docs.guardvideo.com)
417
+ | `Space` / `K` | Play / Pause |
418
+ | `←` | Seek back 5 s |
419
+ | `→` | Seek forward 5 s |
420
+ | `↑` | Volume +10% |
421
+ | `↓` | Volume −10% |
422
+ | `M` | Toggle mute |
423
+ | `F` | Toggle fullscreen |
453
424
 
454
425
  ---
455
426
 
456
- ## Contributing
427
+ ## Using Ref for Imperative Control (React)
457
428
 
458
- Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) first.
429
+ ```tsx
430
+ import { useRef } from 'react';
431
+ import { GuardVideoPlayer, GuardVideoPlayerRef } from '@guardvideo/player-sdk';
432
+
433
+ function VideoWithRef() {
434
+ const playerRef = useRef<GuardVideoPlayerRef>(null);
435
+
436
+ return (
437
+ <div>
438
+ <GuardVideoPlayer
439
+ ref={playerRef}
440
+ videoId="your-video-id"
441
+ embedTokenEndpoint="/api/embed-token"
442
+ />
443
+ <button onClick={() => playerRef.current?.seek(playerRef.current.getCurrentTime() + 10)}>
444
+ +10 s
445
+ </button>
446
+ </div>
447
+ );
448
+ }
449
+ ```
450
+
451
+ ---
452
+
453
+ ## TypeScript Support
454
+
455
+ ```typescript
456
+ import type {
457
+ PlayerConfig,
458
+ PlayerInstance,
459
+ PlayerState,
460
+ PlayerError,
461
+ QualityLevel,
462
+ BrandingConfig,
463
+ SecurityConfig,
464
+ } from '@guardvideo/player-sdk';
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Browser Support
470
+
471
+ | Browser | Minimum version |
472
+ |---------|----------------|
473
+ | Chrome | 90+ |
474
+ | Firefox | 88+ |
475
+ | Safari | 14+ (native HLS) |
476
+ | Edge | 90+ |
477
+ | iOS Safari | 14+ |
478
+ | Android Chrome | 90+ |
479
+
480
+ ---
481
+
482
+ ## License
483
+
484
+ MIT © GuardVideo
485
+
486
+ ---
487
+
488
+ ## Support
489
+
490
+ - GitHub Issues: [github.com/guardvideo/player-sdk](https://github.com/guardvideo/player-sdk)
491
+ - Documentation: [docs.guardvideo.com](https://docs.guardvideo.com)
492
+
493
+ | Safari | 14+ (native HLS) |
494
+ | Edge | 90+ |
495
+ | iOS Safari | 14+ |
496
+ | Android Chrome | 90+ |
497
+
498
+ ---
499
+
500
+ ## License
501
+
502
+ MIT (C) GuardVideo
503
+
504
+ ---
505
+
506
+ ## Support
507
+
508
+ - GitHub Issues: https://github.com/guardvideo/player-sdk
509
+ - Documentation: https://docs.guardvideo.com