@zezosoft/react-player 1.0.1 โ 1.0.3
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,370 +1,137 @@
|
|
|
1
1
|
# @zezosoft/react-player ๐ฌ
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A React video player by Zezosoft supporting HLS, MP4, DASH, preview thumbnails, tracking, subtitles, episode playback, and ads.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## โจ Features
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
โ
**Intro Skipping** - Automatic skip intro button
|
|
16
|
-
โ
**Episode Playback** - Next episode auto-play and playlist support
|
|
17
|
-
โ
**Ad Support** - Pre-roll, mid-roll, post-roll, and overlay ads
|
|
18
|
-
โ
**Watch History** - Track user progress and resume playback
|
|
19
|
-
|
|
20
|
-
---
|
|
7
|
+
- โ
**Formats** โ HLS, MP4, DASH (via hls.js and dash.js)
|
|
8
|
+
- ๐ผ๏ธ **Preview thumbnails** โ Hover over seek bar for scrubbing previews
|
|
9
|
+
- ๐ **Event tracking** โ Views, watch time, user interactions
|
|
10
|
+
- ๐ฌ **Subtitles** โ WebVTT with customizable styling
|
|
11
|
+
- โญ๏ธ **Intro skip** โ Skip intro button with configurable time range
|
|
12
|
+
- ๐บ **Episodes** โ Next episode auto-play and playlist
|
|
13
|
+
- ๐ข **Ads** โ Pre-roll, mid-roll, post-roll video ads
|
|
14
|
+
- ๐ **Watch history** โ Resume playback, progress tracking
|
|
21
15
|
|
|
22
16
|
## ๐ฆ Installation
|
|
23
17
|
|
|
24
|
-
Install the package using **npm** or **yarn**:
|
|
25
|
-
|
|
26
18
|
```sh
|
|
27
19
|
npm install @zezosoft/react-player
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
or
|
|
31
|
-
|
|
32
|
-
```sh
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @zezosoft/react-player
|
|
22
|
+
# or
|
|
33
23
|
yarn add @zezosoft/react-player
|
|
34
24
|
```
|
|
35
25
|
|
|
36
|
-
|
|
26
|
+
**Peer dependencies:** `react`, `react-dom`, `hls.js`, `lucide-react`, `zustand`
|
|
37
27
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
Here's a simple example to get you started:
|
|
28
|
+
## ๐ Quick Start
|
|
41
29
|
|
|
42
30
|
```tsx
|
|
43
31
|
import { VideoPlayer } from "@zezosoft/react-player";
|
|
44
32
|
|
|
45
33
|
function App() {
|
|
46
34
|
return (
|
|
47
|
-
<
|
|
48
|
-
video={{
|
|
49
|
-
src: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8",
|
|
50
|
-
title: "My Video",
|
|
51
|
-
poster: "https://example.com/poster.jpg",
|
|
52
|
-
}}
|
|
53
|
-
style={{
|
|
54
|
-
width: "100%",
|
|
55
|
-
height: "auto",
|
|
56
|
-
}}
|
|
57
|
-
/>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## ๐ Complete Example
|
|
65
|
-
|
|
66
|
-
Here's a comprehensive example showing all available features:
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
import { useCallback, useRef } from "react";
|
|
70
|
-
import { VideoPlayer } from "@zezosoft/react-player";
|
|
71
|
-
|
|
72
|
-
function App() {
|
|
73
|
-
const previewImage = useRef("");
|
|
74
|
-
|
|
75
|
-
const updatePreviewImage = (hoverTime: number) => {
|
|
76
|
-
const url = `https://fakeimg.pl/720x405?text=${hoverTime}`;
|
|
77
|
-
const image = document.createElement("img");
|
|
78
|
-
image.src = url;
|
|
79
|
-
image.onload = () => {
|
|
80
|
-
previewImage.current = url;
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const handleGettingPreview = useCallback((hoverTime: number) => {
|
|
85
|
-
updatePreviewImage(hoverTime);
|
|
86
|
-
return previewImage.current;
|
|
87
|
-
}, []);
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<div style={{ width: "720px", margin: "0 auto" }}>
|
|
35
|
+
<div style={{ position: "relative", width: "100%", aspectRatio: "16/9" }}>
|
|
91
36
|
<VideoPlayer
|
|
92
37
|
video={{
|
|
93
|
-
src: "https://
|
|
94
|
-
title: "
|
|
95
|
-
poster: "https://
|
|
96
|
-
type: "hls",
|
|
97
|
-
isTrailer: false,
|
|
98
|
-
showControls: true,
|
|
99
|
-
isMute: false,
|
|
100
|
-
startFrom: 0,
|
|
101
|
-
}}
|
|
102
|
-
style={{
|
|
103
|
-
width: "720px",
|
|
104
|
-
height: "405px",
|
|
105
|
-
className: "my-video-player",
|
|
106
|
-
subtitleStyle: {
|
|
107
|
-
fontSize: "1.5rem",
|
|
108
|
-
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
109
|
-
textColor: "#ffffff",
|
|
110
|
-
position: "bottom",
|
|
111
|
-
borderRadius: "8px",
|
|
112
|
-
padding: "8px 16px",
|
|
113
|
-
maxWidth: "80%",
|
|
114
|
-
},
|
|
115
|
-
}}
|
|
116
|
-
events={{
|
|
117
|
-
onEnded: (e) => console.log("Video ended", e),
|
|
118
|
-
onError: (e) => console.error("Video error", e),
|
|
119
|
-
onClose: () => console.log("Player closed"),
|
|
120
|
-
onWatchHistoryUpdate: (data) => {
|
|
121
|
-
console.log("Watch history:", data);
|
|
122
|
-
// Save to your backend
|
|
123
|
-
},
|
|
124
|
-
}}
|
|
125
|
-
features={{
|
|
126
|
-
timeCodes: [
|
|
127
|
-
{ fromMs: 0, description: "Introduction" },
|
|
128
|
-
{ fromMs: 130000, description: "Exciting Scene" },
|
|
129
|
-
{ fromMs: 270000, description: "Climax" },
|
|
130
|
-
],
|
|
131
|
-
getPreviewScreenUrl: handleGettingPreview,
|
|
132
|
-
tracking: {
|
|
133
|
-
onViewed: () => console.log("Video Viewed"),
|
|
134
|
-
onWatchTimeUpdated: (e) => {
|
|
135
|
-
console.log("Watch Time Updated", {
|
|
136
|
-
watchTime: e.watchTime,
|
|
137
|
-
currentTime: e.currentTime,
|
|
138
|
-
duration: e.duration,
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
subtitles: [
|
|
143
|
-
{
|
|
144
|
-
lang: "en",
|
|
145
|
-
label: "English",
|
|
146
|
-
url: "https://example.com/subtitles-en.vtt",
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
lang: "hi",
|
|
150
|
-
label: "Hindi",
|
|
151
|
-
url: "https://example.com/subtitles-hi.vtt",
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
episodeList: [
|
|
155
|
-
{ id: 1, title: "Episode 1", url: "https://example.com/ep1.m3u8" },
|
|
156
|
-
{ id: 2, title: "Episode 2", url: "https://example.com/ep2.m3u8" },
|
|
157
|
-
],
|
|
158
|
-
currentEpisodeIndex: 0,
|
|
159
|
-
intro: { start: 5, end: 20 },
|
|
160
|
-
nextEpisodeConfig: { showAtTime: 300, showAtEnd: true },
|
|
161
|
-
ads: {
|
|
162
|
-
preRoll: {
|
|
163
|
-
id: "preroll-1",
|
|
164
|
-
type: "pre-roll",
|
|
165
|
-
time: 0,
|
|
166
|
-
adUrl: "https://example.com/ad.mp4",
|
|
167
|
-
skipable: true,
|
|
168
|
-
skipAfter: 5,
|
|
169
|
-
},
|
|
170
|
-
midRoll: [
|
|
171
|
-
{
|
|
172
|
-
id: "midroll-1",
|
|
173
|
-
type: "mid-roll",
|
|
174
|
-
time: 120,
|
|
175
|
-
adUrl: "https://example.com/mid-ad.mp4",
|
|
176
|
-
skipable: false,
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
onAdStart: (adBreak) => console.log("Ad started", adBreak),
|
|
180
|
-
onAdEnd: (adBreak) => console.log("Ad ended", adBreak),
|
|
181
|
-
},
|
|
38
|
+
src: "https://example.com/playlist.m3u8",
|
|
39
|
+
title: "My Video",
|
|
40
|
+
poster: "https://example.com/poster.jpg",
|
|
182
41
|
}}
|
|
42
|
+
style={{ width: "100%", height: "100%" }}
|
|
183
43
|
/>
|
|
184
44
|
</div>
|
|
185
45
|
);
|
|
186
46
|
}
|
|
187
|
-
|
|
188
|
-
export default App;
|
|
189
47
|
```
|
|
190
48
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
Video
|
|
200
|
-
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
|
217
|
-
|
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
| Prop
|
|
226
|
-
|
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
currentTime: number; // Current playback time in seconds
|
|
251
|
-
duration: number; // Total video duration in seconds
|
|
252
|
-
progress: number; // Progress percentage (0-100)
|
|
253
|
-
isCompleted: boolean; // Whether video was fully watched
|
|
254
|
-
watchedAt: number; // Timestamp when watch session ended
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### `features` (Optional)
|
|
259
|
-
|
|
260
|
-
Advanced features and functionality.
|
|
261
|
-
|
|
262
|
-
| Prop | Type | Default | Description |
|
|
263
|
-
| --------------------- | ------------------------------------------------ | ----------- | ------------------------------------------------------------------------ |
|
|
264
|
-
| `timeCodes` | `Array<{ fromMs: number, description: string }>` | `[]` | Time-based chapter markers (in milliseconds) |
|
|
265
|
-
| `getPreviewScreenUrl` | `(hoverTimeValue: number) => string` | `undefined` | Function to generate preview thumbnail URLs while hovering over seek bar |
|
|
266
|
-
| `tracking` | `TrackingConfig` | `undefined` | Event tracking configuration (see below) |
|
|
267
|
-
| `subtitles` | `Array<SubtitleTrack>` | `[]` | Subtitle tracks in WebVTT format |
|
|
268
|
-
| `episodeList` | `Array<Episode>` | `[]` | List of episodes for playlist/autoplay |
|
|
269
|
-
| `currentEpisodeIndex` | `number` | `0` | Index of currently playing episode |
|
|
270
|
-
| `intro` | `{ start: number, end: number }` | `undefined` | Intro skip configuration (times in seconds) |
|
|
271
|
-
| `nextEpisodeConfig` | `{ showAtTime?: number, showAtEnd?: boolean }` | `undefined` | Configuration for next episode button |
|
|
272
|
-
| `ads` | `AdConfig` | `undefined` | Advertisement configuration (see below) |
|
|
273
|
-
|
|
274
|
-
**TrackingConfig:**
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
{
|
|
278
|
-
onViewed?: () => void;
|
|
279
|
-
onWatchTimeUpdated?: (e: {
|
|
280
|
-
watchTime?: number; // Total watch time in seconds
|
|
281
|
-
currentTime: number; // Current playback position
|
|
282
|
-
duration: number; // Total video duration
|
|
283
|
-
}) => void;
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
**SubtitleTrack:**
|
|
49
|
+
## ๐ Props
|
|
50
|
+
|
|
51
|
+
The player accepts four props: `video` (required), `style`, `events`, and `features`.
|
|
52
|
+
|
|
53
|
+
### `video` (required)
|
|
54
|
+
|
|
55
|
+
| Prop | Type | Default | Description |
|
|
56
|
+
| -------------- | --------- | --------- | ---------------------------------------- |
|
|
57
|
+
| `src` | `string` | required | Video URL (HLS, DASH, MP4) |
|
|
58
|
+
| `title` | `string` | `""` | Title shown in header |
|
|
59
|
+
| `poster` | `string` | `""` | Poster image URL |
|
|
60
|
+
| `type` | `string` | auto | `"hls"` \| `"dash"` \| `"mp4"` \| `"other"` |
|
|
61
|
+
| `isTrailer` | `boolean` | `false` | Trailer mode (hides ads) |
|
|
62
|
+
| `showControls`| `boolean` | `true` | Show/hide controls |
|
|
63
|
+
| `isMute` | `boolean` | `false` | Start muted |
|
|
64
|
+
| `startFrom` | `number` | `0` | Start time in seconds |
|
|
65
|
+
| `isLive` | `boolean` | `false` | Live stream mode |
|
|
66
|
+
|
|
67
|
+
### `style`
|
|
68
|
+
|
|
69
|
+
| Prop | Type | Description |
|
|
70
|
+
| --------------- | --------------------- | ------------------------------------------------ |
|
|
71
|
+
| `className` | `string` | Custom class for video element |
|
|
72
|
+
| `width` | `string` | Player width (e.g. `"100%"`, `"720px"`) |
|
|
73
|
+
| `height` | `string` | Player height (e.g. `"400px"`, `"auto"`) |
|
|
74
|
+
| `subtitleStyle` | `SubtitleStyleConfig` | Subtitle styling |
|
|
75
|
+
| `qualityConfig` | `VideoQualityConfig` | `defaultQuality`, `showInSettings` |
|
|
76
|
+
| `seekBarConfig` | `SeekBarConfig` | `trackColor`, `bufferColor`, `getPreviewScreenUrl`, etc. |
|
|
77
|
+
| `playPauseButtonConfig` | `PlayPauseButtonConfig` | `backgroundColor`, `borderRadius`, `padding` |
|
|
78
|
+
|
|
79
|
+
**SubtitleStyleConfig:** `fontSize`, `backgroundColor`, `textColor`, `position` (`"top"` \| `"center"` \| `"bottom"`), `borderRadius`, `padding`, `maxWidth`
|
|
80
|
+
|
|
81
|
+
### `events`
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Description |
|
|
84
|
+
| ---------------------- | -------- | ---------------------------------------- |
|
|
85
|
+
| `onEnded` | `(e) => void` | When video ends |
|
|
86
|
+
| `onError` | `(e?) => void` | On error (retry available) |
|
|
87
|
+
| `onClose` | `() => void` | When close button is clicked |
|
|
88
|
+
| `onWatchHistoryUpdate` | `(data: WatchHistoryData) => void` | On close, with progress data |
|
|
89
|
+
|
|
90
|
+
**WatchHistoryData:** `{ currentTime, duration, progress, isCompleted, watchedAt }`
|
|
91
|
+
|
|
92
|
+
### `features`
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Description |
|
|
95
|
+
| ----------------------- | -------- | ------------------------------------------------ |
|
|
96
|
+
| `timeCodes` | `Array<{ fromMs, description }>` | Chapter markers (ms) |
|
|
97
|
+
| `getPreviewScreenUrl` | `(hoverTimeValue: number) => string` | Preview thumbnail URL |
|
|
98
|
+
| `tracking` | `{ onViewed?, onWatchTimeUpdated? }` | Tracking callbacks |
|
|
99
|
+
| `subtitles` | `Array<{ lang, label, url }>` | WebVTT subtitle tracks |
|
|
100
|
+
| `episodeList` | `Array<{ id, title, url }>` | Episodes for playlist |
|
|
101
|
+
| `currentEpisodeIndex` | `number` | Active episode index (default `0`) |
|
|
102
|
+
| `intro` | `{ start, end }` | Intro skip range (seconds) |
|
|
103
|
+
| `nextEpisodeConfig` | `{ showAtTime?, showAtEnd? }` | Next episode button |
|
|
104
|
+
| `ads` | `AdConfig` | Pre-roll, mid-roll, post-roll ads |
|
|
105
|
+
|
|
106
|
+
### Ads (`AdConfig`)
|
|
288
107
|
|
|
289
108
|
```typescript
|
|
290
109
|
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**Episode:**
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
{
|
|
301
|
-
id: number; // Unique episode identifier
|
|
302
|
-
title: string; // Episode title
|
|
303
|
-
url: string; // Episode video URL
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
**AdConfig:**
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
{
|
|
311
|
-
preRoll?: AdBreak; // Pre-roll ad (plays before video)
|
|
312
|
-
midRoll?: AdBreak[]; // Mid-roll ads (plays during video)
|
|
313
|
-
postRoll?: AdBreak; // Post-roll ad (plays after video)
|
|
314
|
-
overlay?: { // Overlay ad (image overlay)
|
|
315
|
-
imageUrl: string;
|
|
316
|
-
clickUrl?: string;
|
|
317
|
-
showDuration: number;
|
|
318
|
-
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
319
|
-
};
|
|
320
|
-
smartPlacement?: { // Smart ad placement
|
|
321
|
-
enabled: boolean;
|
|
322
|
-
minVideoDuration?: number;
|
|
323
|
-
minGapBetweenAds?: number;
|
|
324
|
-
avoidNearEnd?: number;
|
|
325
|
-
preferNaturalBreaks?: boolean;
|
|
326
|
-
};
|
|
110
|
+
preRoll?: AdBreak;
|
|
111
|
+
midRoll?: AdBreak[];
|
|
112
|
+
postRoll?: AdBreak;
|
|
327
113
|
onAdStart?: (adBreak: AdBreak) => void;
|
|
328
114
|
onAdEnd?: (adBreak: AdBreak) => void;
|
|
329
115
|
onAdSkip?: (adBreak: AdBreak) => void;
|
|
330
116
|
onAdError?: (adBreak: AdBreak, error: Error) => void;
|
|
331
117
|
}
|
|
332
|
-
```
|
|
333
118
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
```typescript
|
|
119
|
+
// AdBreak
|
|
337
120
|
{
|
|
338
|
-
id: string;
|
|
339
|
-
type: "pre-roll" | "mid-roll" | "post-roll"
|
|
340
|
-
time: number;
|
|
341
|
-
adUrl: string;
|
|
342
|
-
skipable?: boolean;
|
|
343
|
-
skipAfter?: number;
|
|
344
|
-
duration?: number;
|
|
345
|
-
sponsoredUrl?: string;
|
|
346
|
-
title?: string; // Ad title
|
|
347
|
-
description?: string; // Ad description
|
|
348
|
-
relevance?: "high" | "medium" | "low";
|
|
121
|
+
id: string;
|
|
122
|
+
type: "pre-roll" | "mid-roll" | "post-roll";
|
|
123
|
+
time: number; // seconds (0 for pre-roll)
|
|
124
|
+
adUrl: string;
|
|
125
|
+
skipable?: boolean;
|
|
126
|
+
skipAfter?: number; // seconds before skip button
|
|
127
|
+
duration?: number; // optional fixed duration
|
|
128
|
+
sponsoredUrl?: string;
|
|
349
129
|
}
|
|
350
130
|
```
|
|
351
131
|
|
|
352
|
-
|
|
132
|
+
## ๐ Examples
|
|
353
133
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
### Basic Video Player
|
|
357
|
-
|
|
358
|
-
```tsx
|
|
359
|
-
<VideoPlayer
|
|
360
|
-
video={{
|
|
361
|
-
src: "https://example.com/video.mp4",
|
|
362
|
-
title: "My Video",
|
|
363
|
-
}}
|
|
364
|
-
/>
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### HLS Video with Poster
|
|
134
|
+
### ๐บ HLS with poster
|
|
368
135
|
|
|
369
136
|
```tsx
|
|
370
137
|
<VideoPlayer
|
|
@@ -374,171 +141,92 @@ Advanced features and functionality.
|
|
|
374
141
|
poster: "https://example.com/poster.jpg",
|
|
375
142
|
title: "Streaming Video",
|
|
376
143
|
}}
|
|
377
|
-
style={{
|
|
378
|
-
width: "100%",
|
|
379
|
-
height: "450px",
|
|
380
|
-
}}
|
|
144
|
+
style={{ width: "100%", height: "450px" }}
|
|
381
145
|
/>
|
|
382
146
|
```
|
|
383
147
|
|
|
384
|
-
###
|
|
148
|
+
### ๐ผ๏ธ Preview thumbnails
|
|
385
149
|
|
|
386
150
|
```tsx
|
|
387
|
-
const getPreview = (hoverTime: number) => {
|
|
388
|
-
// Generate thumbnail URL based on hover time
|
|
389
|
-
return `https://example.com/thumbnails/${Math.floor(hoverTime)}.jpg`;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
151
|
<VideoPlayer
|
|
393
152
|
video={{ src: "https://example.com/video.mp4" }}
|
|
394
153
|
features={{
|
|
395
|
-
getPreviewScreenUrl:
|
|
154
|
+
getPreviewScreenUrl: (ms) =>
|
|
155
|
+
`https://example.com/thumbs/${Math.floor(ms / 1000)}.jpg`,
|
|
396
156
|
}}
|
|
397
|
-
|
|
157
|
+
/>
|
|
398
158
|
```
|
|
399
159
|
|
|
400
|
-
###
|
|
160
|
+
### ๐ Chapters
|
|
401
161
|
|
|
402
162
|
```tsx
|
|
403
163
|
<VideoPlayer
|
|
404
164
|
video={{ src: "https://example.com/video.mp4" }}
|
|
405
165
|
features={{
|
|
406
166
|
timeCodes: [
|
|
407
|
-
{ fromMs: 0, description: "
|
|
167
|
+
{ fromMs: 0, description: "Intro" },
|
|
408
168
|
{ fromMs: 60000, description: "Chapter 1" },
|
|
409
169
|
{ fromMs: 120000, description: "Chapter 2" },
|
|
410
|
-
{ fromMs: 180000, description: "Conclusion" },
|
|
411
170
|
],
|
|
412
171
|
}}
|
|
413
172
|
/>
|
|
414
173
|
```
|
|
415
174
|
|
|
416
|
-
###
|
|
175
|
+
### ๐ฌ Subtitles
|
|
417
176
|
|
|
418
177
|
```tsx
|
|
419
178
|
<VideoPlayer
|
|
420
179
|
video={{ src: "https://example.com/video.mp4" }}
|
|
421
180
|
features={{
|
|
422
181
|
subtitles: [
|
|
423
|
-
{
|
|
424
|
-
|
|
425
|
-
label: "English",
|
|
426
|
-
url: "https://example.com/subtitles-en.vtt",
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
lang: "es",
|
|
430
|
-
label: "Spanish",
|
|
431
|
-
url: "https://example.com/subtitles-es.vtt",
|
|
432
|
-
},
|
|
182
|
+
{ lang: "en", label: "English", url: "https://example.com/en.vtt" },
|
|
183
|
+
{ lang: "es", label: "Spanish", url: "https://example.com/es.vtt" },
|
|
433
184
|
],
|
|
434
185
|
}}
|
|
435
186
|
style={{
|
|
436
187
|
subtitleStyle: {
|
|
437
188
|
fontSize: "1.25rem",
|
|
438
|
-
backgroundColor: "rgba(0,
|
|
439
|
-
textColor: "#
|
|
189
|
+
backgroundColor: "rgba(0,0,0,0.75)",
|
|
190
|
+
textColor: "#fff",
|
|
440
191
|
position: "bottom",
|
|
441
192
|
},
|
|
442
193
|
}}
|
|
443
194
|
/>
|
|
444
195
|
```
|
|
445
196
|
|
|
446
|
-
###
|
|
197
|
+
### โญ๏ธ Intro skip
|
|
447
198
|
|
|
448
199
|
```tsx
|
|
449
200
|
<VideoPlayer
|
|
450
201
|
video={{ src: "https://example.com/video.mp4" }}
|
|
451
202
|
features={{
|
|
452
|
-
intro: {
|
|
453
|
-
start: 10, // Intro starts at 10 seconds
|
|
454
|
-
end: 45, // Intro ends at 45 seconds
|
|
455
|
-
},
|
|
203
|
+
intro: { start: 10, end: 45 },
|
|
456
204
|
}}
|
|
457
205
|
/>
|
|
458
206
|
```
|
|
459
207
|
|
|
460
|
-
###
|
|
208
|
+
### ๐บ Episodes
|
|
461
209
|
|
|
462
210
|
```tsx
|
|
463
|
-
const [currentEpisode, setCurrentEpisode] = useState(0);
|
|
464
|
-
|
|
465
211
|
const episodes = [
|
|
466
212
|
{ id: 1, title: "Episode 1", url: "https://example.com/ep1.m3u8" },
|
|
467
213
|
{ id: 2, title: "Episode 2", url: "https://example.com/ep2.m3u8" },
|
|
468
|
-
{ id: 3, title: "Episode 3", url: "https://example.com/ep3.m3u8" },
|
|
469
214
|
];
|
|
470
215
|
|
|
471
216
|
<VideoPlayer
|
|
472
217
|
video={{
|
|
473
|
-
src: episodes[
|
|
474
|
-
title: episodes[
|
|
218
|
+
src: episodes[0].url,
|
|
219
|
+
title: episodes[0].title,
|
|
475
220
|
}}
|
|
476
221
|
features={{
|
|
477
222
|
episodeList: episodes,
|
|
478
|
-
currentEpisodeIndex:
|
|
479
|
-
nextEpisodeConfig: {
|
|
480
|
-
showAtTime: 300, // Show next episode button 5 minutes before end
|
|
481
|
-
showAtEnd: true, // Also show at video end
|
|
482
|
-
},
|
|
483
|
-
}}
|
|
484
|
-
events={{
|
|
485
|
-
onEnded: () => {
|
|
486
|
-
if (currentEpisode < episodes.length - 1) {
|
|
487
|
-
setCurrentEpisode(currentEpisode + 1);
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
}}
|
|
491
|
-
/>;
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### Video with Tracking
|
|
495
|
-
|
|
496
|
-
```tsx
|
|
497
|
-
<VideoPlayer
|
|
498
|
-
video={{ src: "https://example.com/video.mp4" }}
|
|
499
|
-
features={{
|
|
500
|
-
tracking: {
|
|
501
|
-
onViewed: () => {
|
|
502
|
-
// Track video view
|
|
503
|
-
analytics.track("video_viewed", { videoId: "123" });
|
|
504
|
-
},
|
|
505
|
-
onWatchTimeUpdated: ({ watchTime, currentTime, duration }) => {
|
|
506
|
-
// Track watch time
|
|
507
|
-
analytics.track("watch_time_updated", {
|
|
508
|
-
watchTime,
|
|
509
|
-
progress: (currentTime / duration) * 100,
|
|
510
|
-
});
|
|
511
|
-
},
|
|
512
|
-
},
|
|
513
|
-
}}
|
|
514
|
-
/>
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### Video with Watch History
|
|
518
|
-
|
|
519
|
-
```tsx
|
|
520
|
-
<VideoPlayer
|
|
521
|
-
video={{ src: "https://example.com/video.mp4" }}
|
|
522
|
-
events={{
|
|
523
|
-
onWatchHistoryUpdate: async (data) => {
|
|
524
|
-
// Save watch history to backend
|
|
525
|
-
await fetch("/api/watch-history", {
|
|
526
|
-
method: "POST",
|
|
527
|
-
headers: { "Content-Type": "application/json" },
|
|
528
|
-
body: JSON.stringify({
|
|
529
|
-
videoId: "123",
|
|
530
|
-
currentTime: data.currentTime,
|
|
531
|
-
duration: data.duration,
|
|
532
|
-
progress: data.progress,
|
|
533
|
-
isCompleted: data.isCompleted,
|
|
534
|
-
}),
|
|
535
|
-
});
|
|
536
|
-
},
|
|
223
|
+
currentEpisodeIndex: 0,
|
|
224
|
+
nextEpisodeConfig: { showAtTime: 300, showAtEnd: true },
|
|
537
225
|
}}
|
|
538
226
|
/>
|
|
539
227
|
```
|
|
540
228
|
|
|
541
|
-
###
|
|
229
|
+
### ๐ข Ads
|
|
542
230
|
|
|
543
231
|
```tsx
|
|
544
232
|
<VideoPlayer
|
|
@@ -549,7 +237,7 @@ const episodes = [
|
|
|
549
237
|
id: "preroll-1",
|
|
550
238
|
type: "pre-roll",
|
|
551
239
|
time: 0,
|
|
552
|
-
adUrl: "https://example.com/
|
|
240
|
+
adUrl: "https://example.com/ad.mp4",
|
|
553
241
|
skipable: true,
|
|
554
242
|
skipAfter: 5,
|
|
555
243
|
},
|
|
@@ -558,124 +246,77 @@ const episodes = [
|
|
|
558
246
|
id: "midroll-1",
|
|
559
247
|
type: "mid-roll",
|
|
560
248
|
time: 120,
|
|
561
|
-
adUrl: "https://example.com/mid-
|
|
249
|
+
adUrl: "https://example.com/mid-ad.mp4",
|
|
562
250
|
skipable: false,
|
|
563
251
|
},
|
|
564
252
|
],
|
|
565
|
-
onAdStart: (
|
|
566
|
-
|
|
567
|
-
},
|
|
568
|
-
onAdEnd: (adBreak) => {
|
|
569
|
-
console.log("Ad ended:", adBreak.id);
|
|
570
|
-
},
|
|
253
|
+
onAdStart: (ad) => console.log("Ad started", ad.id),
|
|
254
|
+
onAdEnd: (ad) => console.log("Ad ended", ad.id),
|
|
571
255
|
},
|
|
572
256
|
}}
|
|
573
257
|
/>
|
|
574
258
|
```
|
|
575
259
|
|
|
576
|
-
### Resume
|
|
260
|
+
### โถ๏ธ Resume playback
|
|
577
261
|
|
|
578
262
|
```tsx
|
|
579
|
-
const [
|
|
580
|
-
|
|
581
|
-
// Load watch history on mount
|
|
582
|
-
useEffect(() => {
|
|
583
|
-
fetch("/api/watch-history/123")
|
|
584
|
-
.then((res) => res.json())
|
|
585
|
-
.then((data) => setWatchHistory(data));
|
|
586
|
-
}, []);
|
|
263
|
+
const [savedTime, setSavedTime] = useState(0);
|
|
587
264
|
|
|
588
265
|
<VideoPlayer
|
|
589
266
|
video={{
|
|
590
267
|
src: "https://example.com/video.mp4",
|
|
591
|
-
startFrom:
|
|
268
|
+
startFrom: savedTime,
|
|
592
269
|
}}
|
|
593
270
|
events={{
|
|
594
271
|
onWatchHistoryUpdate: (data) => {
|
|
595
|
-
|
|
596
|
-
|
|
272
|
+
setSavedTime(data.currentTime);
|
|
273
|
+
// or save to backend
|
|
597
274
|
},
|
|
598
275
|
}}
|
|
599
|
-
/>;
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
---
|
|
603
|
-
|
|
604
|
-
## โ Troubleshooting
|
|
605
|
-
|
|
606
|
-
### Video Not Playing?
|
|
607
|
-
|
|
608
|
-
- **Check the video URL**: Ensure `video.src` is a valid, accessible URL
|
|
609
|
-
- **Check video format**: Verify the video format is supported (HLS, MP4, DASH, YouTube)
|
|
610
|
-
- **CORS issues**: If using HLS or external sources, ensure CORS headers are properly configured
|
|
611
|
-
- **Specify video type**: Try explicitly setting `video.type` to help with format detection
|
|
612
|
-
|
|
613
|
-
```tsx
|
|
614
|
-
<VideoPlayer
|
|
615
|
-
video={{
|
|
616
|
-
src: "https://example.com/video.m3u8",
|
|
617
|
-
type: "hls", // Explicitly specify type
|
|
618
|
-
}}
|
|
619
276
|
/>
|
|
620
277
|
```
|
|
621
278
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
|
|
643
|
-
<VideoPlayer
|
|
644
|
-
video={{ src: "https://example.com/video.mp4" }}
|
|
645
|
-
style={{
|
|
646
|
-
width: "100%",
|
|
647
|
-
height: "auto",
|
|
648
|
-
}}
|
|
649
|
-
/>
|
|
650
|
-
</div>
|
|
279
|
+
## ๐ค Exports
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import {
|
|
283
|
+
VideoPlayer,
|
|
284
|
+
useVideoStore,
|
|
285
|
+
} from "@zezosoft/react-player";
|
|
286
|
+
|
|
287
|
+
import type {
|
|
288
|
+
VideoPlayerProps,
|
|
289
|
+
Episode,
|
|
290
|
+
SubtitleTrack,
|
|
291
|
+
IntroConfig,
|
|
292
|
+
NextEpisodeConfig,
|
|
293
|
+
WatchHistoryData,
|
|
294
|
+
SubtitleStyleConfig,
|
|
295
|
+
VideoQualityConfig,
|
|
296
|
+
SeekBarConfig,
|
|
297
|
+
PlayPauseButtonConfig,
|
|
298
|
+
} from "@zezosoft/react-player";
|
|
651
299
|
```
|
|
652
300
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
- **Check ad URLs**: Ensure ad video URLs are valid and accessible
|
|
656
|
-
- **Verify ad timing**: For mid-roll ads, ensure `time` is less than video duration
|
|
657
|
-
- **Check ad format**: Ad videos should be in supported formats (MP4, HLS)
|
|
301
|
+
## ๐ง Troubleshooting
|
|
658
302
|
|
|
659
|
-
|
|
303
|
+
**Video not playing**
|
|
304
|
+
- Use a valid, accessible URL
|
|
305
|
+
- Set `type: "hls"` or `type: "dash"` explicitly for streaming
|
|
306
|
+
- Check CORS for external sources
|
|
660
307
|
|
|
661
|
-
|
|
308
|
+
**Subtitles not showing**
|
|
309
|
+
- Ensure WebVTT URLs are accessible
|
|
310
|
+
- Verify CORS for subtitle files
|
|
662
311
|
|
|
663
|
-
|
|
664
|
-
-
|
|
312
|
+
**Preview thumbnails**
|
|
313
|
+
- `getPreviewScreenUrl` must return a valid image URL
|
|
314
|
+
- URL is called with `hoverTimeValue` in milliseconds
|
|
665
315
|
|
|
666
|
-
|
|
316
|
+
## ๐ License
|
|
667
317
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
Licensed under the MIT License.
|
|
671
|
-
Developed by [Zezosoft](https://zezosoft.com). ๐
|
|
672
|
-
|
|
673
|
-
---
|
|
318
|
+
MIT ยท [Zezosoft](https://zezosoft.com)
|
|
674
319
|
|
|
675
320
|
## ๐ Credits
|
|
676
321
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
---
|
|
680
|
-
|
|
681
|
-
# ๐ Enjoy seamless video playback with @zezosoft/react-player! ๐ฅ
|
|
322
|
+
Seek bar inspired by [react-video-seek-slider](https://www.npmjs.com/package/react-video-seek-slider).
|