@vexor-video/player 0.0.0-stage → 0.1.1

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/dist/index.cjs ADDED
@@ -0,0 +1,1312 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ Controls: () => Controls,
35
+ VexorPlayer: () => VexorPlayer,
36
+ Watermark: () => Watermark,
37
+ createVexorPlayer: () => createVexorPlayer,
38
+ useVexorPlayer: () => useVexorPlayer
39
+ });
40
+ module.exports = __toCommonJS(src_exports);
41
+
42
+ // src/components/VexorPlayer.tsx
43
+ var import_react4 = require("react");
44
+
45
+ // src/hooks/useVexorPlayer.ts
46
+ var import_react = require("react");
47
+ var import_hls = __toESM(require("hls.js"), 1);
48
+
49
+ // src/native-token-refresh.ts
50
+ var TOKEN_REFRESH_INTERVAL_MS = 4 * 60 * 1e3;
51
+ var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
52
+ function startNativeTokenRefresh(video, initialUrl, refreshToken) {
53
+ let currentUrl = initialUrl;
54
+ let disposed = false;
55
+ let timer;
56
+ let refreshInFlight = null;
57
+ let restoreListener;
58
+ const schedule = (delay = TOKEN_REFRESH_INTERVAL_MS) => {
59
+ if (!refreshToken || disposed) return;
60
+ if (timer) clearTimeout(timer);
61
+ timer = setTimeout(() => void refreshNow(), delay);
62
+ };
63
+ const refreshNow = () => {
64
+ if (!refreshToken || disposed) return Promise.resolve(false);
65
+ if (refreshInFlight) return refreshInFlight;
66
+ refreshInFlight = (async () => {
67
+ try {
68
+ const token = await refreshToken();
69
+ if (disposed || !token) return false;
70
+ const nextUrl = new URL(currentUrl, document.baseURI);
71
+ nextUrl.searchParams.set("token", token);
72
+ currentUrl = nextUrl.toString();
73
+ const resumeTime = video.currentTime;
74
+ const shouldResume = !video.paused;
75
+ restoreListener = () => {
76
+ restoreListener = void 0;
77
+ if (disposed) return;
78
+ if (Number.isFinite(resumeTime) && resumeTime > 0) {
79
+ try {
80
+ video.currentTime = resumeTime;
81
+ } catch {
82
+ }
83
+ }
84
+ if (shouldResume) video.play().catch(() => {
85
+ });
86
+ };
87
+ video.addEventListener("loadedmetadata", restoreListener, { once: true });
88
+ video.src = currentUrl;
89
+ video.load();
90
+ schedule();
91
+ return true;
92
+ } catch {
93
+ schedule(TOKEN_REFRESH_RETRY_MS);
94
+ return false;
95
+ } finally {
96
+ refreshInFlight = null;
97
+ }
98
+ })();
99
+ return refreshInFlight;
100
+ };
101
+ schedule();
102
+ return {
103
+ refreshNow,
104
+ stop: () => {
105
+ disposed = true;
106
+ if (timer) clearTimeout(timer);
107
+ if (restoreListener) video.removeEventListener("loadedmetadata", restoreListener);
108
+ }
109
+ };
110
+ }
111
+
112
+ // src/hooks/useVexorPlayer.ts
113
+ function useVexorPlayer({
114
+ src,
115
+ assetId,
116
+ token,
117
+ baseUrl = "https://api.vexor.et",
118
+ fallbackSrc,
119
+ autoPlay = false,
120
+ muted = false,
121
+ refreshToken,
122
+ onReady,
123
+ onPlay,
124
+ onPause,
125
+ onEnded,
126
+ onTimeUpdate,
127
+ onError,
128
+ onQualityChange
129
+ }) {
130
+ const videoRef = (0, import_react.useRef)(null);
131
+ const containerRef = (0, import_react.useRef)(null);
132
+ const hlsRef = (0, import_react.useRef)(null);
133
+ const callbacksRef = (0, import_react.useRef)({ onReady, onPlay, onPause, onEnded, onTimeUpdate, onError, onQualityChange, refreshToken });
134
+ callbacksRef.current = { onReady, onPlay, onPause, onEnded, onTimeUpdate, onError, onQualityChange, refreshToken };
135
+ const activeParamsRef = (0, import_react.useRef)("");
136
+ const refreshPromiseRef = (0, import_react.useRef)(null);
137
+ const resumeAfterRefreshRef = (0, import_react.useRef)(null);
138
+ const [isPlaying, setIsPlaying] = (0, import_react.useState)(false);
139
+ const [isBuffering, setIsBuffering] = (0, import_react.useState)(true);
140
+ const [currentTime, setCurrentTime] = (0, import_react.useState)(0);
141
+ const [duration, setDuration] = (0, import_react.useState)(0);
142
+ const [buffered, setBuffered] = (0, import_react.useState)(0);
143
+ const [volume, setVolumeState] = (0, import_react.useState)(1);
144
+ const [isMuted, setIsMuted] = (0, import_react.useState)(muted);
145
+ const [isFullscreen, setIsFullscreen] = (0, import_react.useState)(false);
146
+ const [qualityLevels, setQualityLevels] = (0, import_react.useState)([]);
147
+ const [currentQuality, setCurrentQuality] = (0, import_react.useState)(-1);
148
+ const [playbackRate, setPlaybackRateState] = (0, import_react.useState)(1);
149
+ const [error, setError] = (0, import_react.useState)(null);
150
+ const [reloadTrigger, setReloadTrigger] = (0, import_react.useState)(0);
151
+ const activeUrl = src ? src : assetId ? `${baseUrl.replace(/\/+$/, "")}/v1/playback/${encodeURIComponent(assetId)}/master.m3u8${token ? `?token=${encodeURIComponent(token)}` : ""}` : "";
152
+ const destroyHls = (0, import_react.useCallback)(() => {
153
+ if (hlsRef.current) {
154
+ hlsRef.current.destroy();
155
+ hlsRef.current = null;
156
+ }
157
+ }, []);
158
+ const retry = (0, import_react.useCallback)(() => {
159
+ setError(null);
160
+ setIsBuffering(true);
161
+ setReloadTrigger((v) => v + 1);
162
+ }, []);
163
+ (0, import_react.useEffect)(() => {
164
+ const video = videoRef.current;
165
+ if (!video) return;
166
+ let cleanupFallback = () => {
167
+ };
168
+ const loadFallback = () => {
169
+ if (!fallbackSrc) return false;
170
+ destroyHls();
171
+ setIsBuffering(true);
172
+ video.src = fallbackSrc;
173
+ const onLoaded = () => {
174
+ setIsBuffering(false);
175
+ callbacksRef.current.onReady?.();
176
+ };
177
+ const onError2 = () => {
178
+ const errObj = {
179
+ code: "FALLBACK_PLAYBACK_ERROR",
180
+ message: "Unable to load fallback media source",
181
+ fatal: true
182
+ };
183
+ setError(errObj);
184
+ callbacksRef.current.onError?.(errObj);
185
+ setIsBuffering(false);
186
+ };
187
+ video.addEventListener("loadeddata", onLoaded);
188
+ video.addEventListener("error", onError2);
189
+ cleanupFallback = () => {
190
+ video.removeEventListener("loadeddata", onLoaded);
191
+ video.removeEventListener("error", onError2);
192
+ };
193
+ video.load();
194
+ return true;
195
+ };
196
+ const setFatalError = (code, message) => {
197
+ destroyHls();
198
+ const errObj = { code, message, fatal: true };
199
+ setError(errObj);
200
+ callbacksRef.current.onError?.(errObj);
201
+ setIsBuffering(false);
202
+ };
203
+ destroyHls();
204
+ setError(null);
205
+ setCurrentTime(0);
206
+ setDuration(0);
207
+ setBuffered(0);
208
+ setQualityLevels([]);
209
+ setCurrentQuality(-1);
210
+ setIsBuffering(Boolean(activeUrl));
211
+ if (!activeUrl) {
212
+ video.pause();
213
+ video.removeAttribute("src");
214
+ video.load();
215
+ return;
216
+ }
217
+ const isHlsUrl = activeUrl.includes(".m3u8") || activeUrl.includes("/playback/");
218
+ let fatalRecoveryAttempts = 0;
219
+ if (isHlsUrl && import_hls.default.isSupported()) {
220
+ activeParamsRef.current = activeUrl.includes("?") ? activeUrl.substring(activeUrl.indexOf("?")) : "";
221
+ class VexorLoader extends import_hls.default.DefaultConfig.loader {
222
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
+ load(context, config, callbacks) {
224
+ const activeParams = activeParamsRef.current;
225
+ if (activeParams && context?.url) {
226
+ try {
227
+ const sourceUrl = new URL(activeUrl, window.location.href);
228
+ const childUrl = new URL(context.url, sourceUrl);
229
+ if (childUrl.origin === sourceUrl.origin && !childUrl.searchParams.has("token")) {
230
+ new URLSearchParams(activeParams).forEach((value, key) => {
231
+ if (!childUrl.searchParams.has(key)) childUrl.searchParams.set(key, value);
232
+ });
233
+ context.url = childUrl.toString();
234
+ }
235
+ } catch {
236
+ }
237
+ }
238
+ super.load(context, config, callbacks);
239
+ }
240
+ }
241
+ const hls = new import_hls.default({
242
+ startFragPrefetch: true,
243
+ maxBufferLength: 30,
244
+ maxMaxBufferLength: 60,
245
+ enableWorker: true,
246
+ lowLatencyMode: true,
247
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
+ loader: VexorLoader,
249
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
250
+ fLoader: VexorLoader,
251
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
+ pLoader: VexorLoader
253
+ });
254
+ hlsRef.current = hls;
255
+ hls.loadSource(activeUrl);
256
+ hls.attachMedia(video);
257
+ hls.on(import_hls.default.Events.MANIFEST_PARSED, (_event, data) => {
258
+ if (hlsRef.current !== hls) return;
259
+ setIsBuffering(false);
260
+ const levels = data.levels.map((lvl, index) => {
261
+ let label = `${lvl.height}p`;
262
+ if (lvl.height === 0 || lvl.videoCodec === void 0) {
263
+ label = "Audio only";
264
+ }
265
+ return {
266
+ id: index,
267
+ height: lvl.height,
268
+ width: lvl.width,
269
+ bitrate: lvl.bitrate,
270
+ label
271
+ };
272
+ });
273
+ setQualityLevels(levels);
274
+ callbacksRef.current.onReady?.();
275
+ const resume = resumeAfterRefreshRef.current;
276
+ if (resume) {
277
+ resumeAfterRefreshRef.current = null;
278
+ if (Number.isFinite(resume.time) && resume.time > 0) video.currentTime = resume.time;
279
+ if (resume.shouldPlay) video.play().catch(() => {
280
+ });
281
+ }
282
+ if (autoPlay) {
283
+ video.play().catch(() => {
284
+ });
285
+ }
286
+ });
287
+ hls.on(import_hls.default.Events.LEVEL_SWITCHED, (_event, data) => {
288
+ if (hlsRef.current !== hls) return;
289
+ setCurrentQuality(hls.autoLevelEnabled ? -1 : data.level);
290
+ callbacksRef.current.onQualityChange?.(hls.autoLevelEnabled ? -1 : data.level);
291
+ });
292
+ hls.on(import_hls.default.Events.ERROR, async (_event, data) => {
293
+ if (hlsRef.current !== hls) return;
294
+ if ((data.response?.code === 401 || data.response?.code === 403) && callbacksRef.current.refreshToken) {
295
+ try {
296
+ if (!refreshPromiseRef.current) {
297
+ refreshPromiseRef.current = callbacksRef.current.refreshToken();
298
+ }
299
+ const newToken = await refreshPromiseRef.current;
300
+ refreshPromiseRef.current = null;
301
+ if (newToken && hlsRef.current === hls) {
302
+ resumeAfterRefreshRef.current = {
303
+ time: video.currentTime,
304
+ shouldPlay: !video.paused
305
+ };
306
+ const url = new URL(activeUrl, window.location.href);
307
+ url.searchParams.set("token", newToken);
308
+ activeParamsRef.current = url.search;
309
+ hls.loadSource(url.toString());
310
+ hls.startLoad();
311
+ return;
312
+ }
313
+ } catch {
314
+ refreshPromiseRef.current = null;
315
+ }
316
+ }
317
+ if (hlsRef.current !== hls) return;
318
+ if (data.fatal) {
319
+ switch (data.type) {
320
+ case import_hls.default.ErrorTypes.NETWORK_ERROR:
321
+ if (fatalRecoveryAttempts++ < 1) {
322
+ hls.startLoad();
323
+ } else if (!loadFallback()) {
324
+ setFatalError(data.details || "NETWORK_ERROR", `Failed to stream video: ${data.details || "network error"}`);
325
+ }
326
+ break;
327
+ case import_hls.default.ErrorTypes.MEDIA_ERROR:
328
+ if (fatalRecoveryAttempts++ < 1) {
329
+ hls.recoverMediaError();
330
+ } else if (!loadFallback()) {
331
+ setFatalError(data.details || "MEDIA_ERROR", `Failed to stream video: ${data.details || "media error"}`);
332
+ }
333
+ break;
334
+ default:
335
+ if (!loadFallback()) {
336
+ setFatalError(data.details || "MEDIA_PLAYBACK_ERROR", `Failed to stream video: ${data.details || "network error"}`);
337
+ }
338
+ break;
339
+ }
340
+ }
341
+ });
342
+ } else if (isHlsUrl && video.canPlayType("application/vnd.apple.mpegurl")) {
343
+ video.src = activeUrl;
344
+ const nativeTokenRefresh = startNativeTokenRefresh(
345
+ video,
346
+ activeUrl,
347
+ callbacksRef.current.refreshToken
348
+ );
349
+ const onLoaded = () => {
350
+ setIsBuffering(false);
351
+ callbacksRef.current.onReady?.();
352
+ if (autoPlay) {
353
+ video.play().catch(() => {
354
+ });
355
+ }
356
+ };
357
+ const onNativeError = () => {
358
+ void nativeTokenRefresh.refreshNow().then((refreshed) => {
359
+ if (refreshed || hlsRef.current) return;
360
+ if (loadFallback()) {
361
+ video.removeEventListener("error", onNativeError);
362
+ return;
363
+ }
364
+ setFatalError("NATIVE_HLS_ERROR", "Failed to play native HLS stream");
365
+ });
366
+ };
367
+ video.addEventListener("loadedmetadata", onLoaded);
368
+ video.addEventListener("error", onNativeError);
369
+ return () => {
370
+ nativeTokenRefresh.stop();
371
+ video.removeEventListener("loadedmetadata", onLoaded);
372
+ video.removeEventListener("error", onNativeError);
373
+ };
374
+ } else if (!isHlsUrl) {
375
+ video.src = activeUrl;
376
+ const onLoaded = () => {
377
+ setIsBuffering(false);
378
+ callbacksRef.current.onReady?.();
379
+ };
380
+ const onErr = () => {
381
+ const errObj = {
382
+ code: "DIRECT_PLAYBACK_ERROR",
383
+ message: "Unable to load media source",
384
+ fatal: true
385
+ };
386
+ setError(errObj);
387
+ callbacksRef.current.onError?.(errObj);
388
+ setIsBuffering(false);
389
+ };
390
+ video.addEventListener("loadeddata", onLoaded);
391
+ video.addEventListener("error", onErr);
392
+ return () => {
393
+ video.removeEventListener("loadeddata", onLoaded);
394
+ video.removeEventListener("error", onErr);
395
+ };
396
+ } else if (fallbackSrc) {
397
+ loadFallback();
398
+ } else {
399
+ const errObj = {
400
+ code: "HLS_UNSUPPORTED",
401
+ message: "This browser does not support HLS playback and no fallback source was provided",
402
+ fatal: true
403
+ };
404
+ setError(errObj);
405
+ callbacksRef.current.onError?.(errObj);
406
+ setIsBuffering(false);
407
+ }
408
+ return () => {
409
+ cleanupFallback();
410
+ destroyHls();
411
+ };
412
+ }, [activeUrl, fallbackSrc, autoPlay, reloadTrigger, destroyHls]);
413
+ (0, import_react.useEffect)(() => {
414
+ const video = videoRef.current;
415
+ if (!video) return;
416
+ const onPlayEvent = () => {
417
+ setIsPlaying(true);
418
+ callbacksRef.current.onPlay?.();
419
+ };
420
+ const onPauseEvent = () => {
421
+ setIsPlaying(false);
422
+ callbacksRef.current.onPause?.();
423
+ };
424
+ const onEndedEvent = () => {
425
+ setIsPlaying(false);
426
+ callbacksRef.current.onEnded?.();
427
+ };
428
+ const onWaiting = () => setIsBuffering(true);
429
+ const onPlaying = () => setIsBuffering(false);
430
+ const onTimeUpdateEvent = () => {
431
+ setCurrentTime(video.currentTime);
432
+ callbacksRef.current.onTimeUpdate?.(video.currentTime, video.duration);
433
+ if (video.buffered.length > 0 && video.duration > 0) {
434
+ for (let i = video.buffered.length - 1; i >= 0; i--) {
435
+ if (video.buffered.start(i) <= video.currentTime) {
436
+ setBuffered(video.buffered.end(i) / video.duration * 100);
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ };
442
+ const onDurationChange = () => setDuration(video.duration || 0);
443
+ const onVolumeChangeEvent = () => {
444
+ setVolumeState(video.volume);
445
+ setIsMuted(video.muted);
446
+ };
447
+ video.addEventListener("play", onPlayEvent);
448
+ video.addEventListener("pause", onPauseEvent);
449
+ video.addEventListener("ended", onEndedEvent);
450
+ video.addEventListener("waiting", onWaiting);
451
+ video.addEventListener("playing", onPlaying);
452
+ video.addEventListener("timeupdate", onTimeUpdateEvent);
453
+ video.addEventListener("durationchange", onDurationChange);
454
+ video.addEventListener("volumechange", onVolumeChangeEvent);
455
+ return () => {
456
+ video.removeEventListener("play", onPlayEvent);
457
+ video.removeEventListener("pause", onPauseEvent);
458
+ video.removeEventListener("ended", onEndedEvent);
459
+ video.removeEventListener("waiting", onWaiting);
460
+ video.removeEventListener("playing", onPlaying);
461
+ video.removeEventListener("timeupdate", onTimeUpdateEvent);
462
+ video.removeEventListener("durationchange", onDurationChange);
463
+ video.removeEventListener("volumechange", onVolumeChangeEvent);
464
+ };
465
+ }, []);
466
+ (0, import_react.useEffect)(() => {
467
+ const handleFullscreenChange = () => {
468
+ setIsFullscreen(Boolean(document.fullscreenElement));
469
+ };
470
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
471
+ return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
472
+ }, []);
473
+ const play = (0, import_react.useCallback)(async () => {
474
+ if (videoRef.current) {
475
+ await videoRef.current.play();
476
+ }
477
+ }, []);
478
+ const pause = (0, import_react.useCallback)(() => {
479
+ if (videoRef.current) {
480
+ videoRef.current.pause();
481
+ }
482
+ }, []);
483
+ const togglePlay = (0, import_react.useCallback)(() => {
484
+ if (!videoRef.current) return;
485
+ if (videoRef.current.paused) {
486
+ videoRef.current.play().catch(() => {
487
+ });
488
+ } else {
489
+ videoRef.current.pause();
490
+ }
491
+ }, []);
492
+ const seek = (0, import_react.useCallback)((time) => {
493
+ if (videoRef.current) {
494
+ videoRef.current.currentTime = Math.max(0, Math.min(time, videoRef.current.duration || time));
495
+ }
496
+ }, []);
497
+ const setVolume = (0, import_react.useCallback)((vol) => {
498
+ if (videoRef.current) {
499
+ const clamped = Math.max(0, Math.min(1, vol));
500
+ videoRef.current.volume = clamped;
501
+ videoRef.current.muted = clamped === 0;
502
+ }
503
+ }, []);
504
+ const toggleMute = (0, import_react.useCallback)(() => {
505
+ if (videoRef.current) {
506
+ videoRef.current.muted = !videoRef.current.muted;
507
+ }
508
+ }, []);
509
+ const setQuality = (0, import_react.useCallback)((levelIndex) => {
510
+ if (hlsRef.current) {
511
+ if (levelIndex === -1) {
512
+ hlsRef.current.currentLevel = -1;
513
+ } else {
514
+ hlsRef.current.currentLevel = levelIndex;
515
+ }
516
+ setCurrentQuality(levelIndex);
517
+ }
518
+ }, []);
519
+ const setPlaybackRate = (0, import_react.useCallback)((rate) => {
520
+ if (videoRef.current) {
521
+ videoRef.current.playbackRate = rate;
522
+ setPlaybackRateState(rate);
523
+ }
524
+ }, []);
525
+ const toggleFullscreen = (0, import_react.useCallback)(() => {
526
+ const container = containerRef.current || videoRef.current;
527
+ if (!container) return;
528
+ if (!document.fullscreenElement) {
529
+ container.requestFullscreen().catch(() => {
530
+ });
531
+ } else {
532
+ document.exitFullscreen().catch(() => {
533
+ });
534
+ }
535
+ }, []);
536
+ const togglePiP = (0, import_react.useCallback)(async () => {
537
+ const video = videoRef.current;
538
+ if (!video) return;
539
+ try {
540
+ if (document.pictureInPictureElement) {
541
+ await document.exitPictureInPicture();
542
+ } else if (document.pictureInPictureEnabled) {
543
+ await video.requestPictureInPicture();
544
+ }
545
+ } catch {
546
+ }
547
+ }, []);
548
+ return {
549
+ videoRef,
550
+ containerRef,
551
+ isPlaying,
552
+ isBuffering,
553
+ currentTime,
554
+ duration,
555
+ buffered,
556
+ volume,
557
+ isMuted,
558
+ isFullscreen,
559
+ qualityLevels,
560
+ currentQuality,
561
+ playbackRate,
562
+ error,
563
+ play,
564
+ pause,
565
+ togglePlay,
566
+ seek,
567
+ setVolume,
568
+ toggleMute,
569
+ setQuality,
570
+ setPlaybackRate,
571
+ toggleFullscreen,
572
+ togglePiP,
573
+ retry
574
+ };
575
+ }
576
+
577
+ // src/components/Controls.tsx
578
+ var import_react2 = require("react");
579
+ var import_jsx_runtime = require("react/jsx-runtime");
580
+ function formatTime(seconds) {
581
+ if (isNaN(seconds) || seconds < 0) return "0:00";
582
+ const h = Math.floor(seconds / 3600);
583
+ const m = Math.floor(seconds % 3600 / 60);
584
+ const s = Math.floor(seconds % 60);
585
+ if (h > 0) {
586
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
587
+ }
588
+ return `${m}:${s.toString().padStart(2, "0")}`;
589
+ }
590
+ var Controls = ({
591
+ isPlaying,
592
+ currentTime,
593
+ duration,
594
+ buffered,
595
+ volume,
596
+ isMuted,
597
+ isFullscreen,
598
+ qualityLevels,
599
+ currentQuality,
600
+ playbackRate,
601
+ visible,
602
+ onPlayPause,
603
+ onSeek,
604
+ onVolumeChange,
605
+ onToggleMute,
606
+ onQualityChange,
607
+ onPlaybackRateChange,
608
+ onToggleFullscreen,
609
+ onTogglePiP
610
+ }) => {
611
+ const [showQualityMenu, setShowQualityMenu] = (0, import_react2.useState)(false);
612
+ const [showSpeedMenu, setShowSpeedMenu] = (0, import_react2.useState)(false);
613
+ const progressRef = (0, import_react2.useRef)(null);
614
+ const [isScrubbing, setIsScrubbing] = (0, import_react2.useState)(false);
615
+ const [pictureInPictureSupported, setPictureInPictureSupported] = (0, import_react2.useState)(false);
616
+ (0, import_react2.useEffect)(() => {
617
+ setPictureInPictureSupported(
618
+ typeof document !== "undefined" && document.pictureInPictureEnabled
619
+ );
620
+ }, []);
621
+ const playedPercent = duration > 0 ? currentTime / duration * 100 : 0;
622
+ const speedOptions = [0.5, 0.75, 1, 1.25, 1.5, 2];
623
+ const handleScrub = (0, import_react2.useCallback)(
624
+ (e) => {
625
+ if (!progressRef.current || duration <= 0) return;
626
+ const rect = progressRef.current.getBoundingClientRect();
627
+ const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
628
+ onSeek(pos * duration);
629
+ },
630
+ [duration, onSeek]
631
+ );
632
+ const handlePointerDown = (e) => {
633
+ if (e.button !== 0) return;
634
+ e.currentTarget.setPointerCapture(e.pointerId);
635
+ setIsScrubbing(true);
636
+ handleScrub(e);
637
+ };
638
+ const handleTimelineKeyDown = (e) => {
639
+ if (duration <= 0) return;
640
+ const step = e.shiftKey ? 30 : 5;
641
+ let nextTime;
642
+ if (e.key === "ArrowLeft") nextTime = currentTime - step;
643
+ if (e.key === "ArrowRight") nextTime = currentTime + step;
644
+ if (e.key === "Home") nextTime = 0;
645
+ if (e.key === "End") nextTime = duration;
646
+ if (nextTime !== void 0) {
647
+ e.preventDefault();
648
+ onSeek(Math.max(0, Math.min(duration, nextTime)));
649
+ }
650
+ };
651
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `vexor-controls ${visible ? "" : "is-hidden"}`, children: [
652
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
653
+ "div",
654
+ {
655
+ className: "vexor-progress-container",
656
+ ref: progressRef,
657
+ onPointerDown: handlePointerDown,
658
+ onPointerMove: (event) => {
659
+ if (isScrubbing) handleScrub(event);
660
+ },
661
+ onPointerUp: (event) => {
662
+ if (progressRef.current?.hasPointerCapture(event.pointerId)) {
663
+ progressRef.current.releasePointerCapture(event.pointerId);
664
+ }
665
+ setIsScrubbing(false);
666
+ },
667
+ onPointerCancel: () => setIsScrubbing(false),
668
+ onKeyDown: handleTimelineKeyDown,
669
+ tabIndex: 0,
670
+ role: "slider",
671
+ "aria-label": "Video timeline",
672
+ "aria-valuemin": 0,
673
+ "aria-valuemax": duration,
674
+ "aria-valuenow": Math.min(currentTime, duration || 0),
675
+ "aria-valuetext": `${formatTime(currentTime)} of ${formatTime(duration)}`,
676
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-progress-track", children: [
677
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vexor-progress-buffer", style: { width: `${buffered}%` } }),
678
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vexor-progress-played", style: { width: `${playedPercent}%` } }),
679
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vexor-progress-thumb", style: { left: `${playedPercent}%` } })
680
+ ] })
681
+ }
682
+ ),
683
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-controls-row", children: [
684
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-controls-group", children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
686
+ "button",
687
+ {
688
+ type: "button",
689
+ className: "vexor-btn",
690
+ onClick: onPlayPause,
691
+ "aria-label": isPlaying ? "Pause" : "Play",
692
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [
693
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1" }),
694
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1" })
695
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "5,3 19,12 5,21" }) })
696
+ }
697
+ ),
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-volume-wrapper", children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ className: "vexor-btn",
704
+ onClick: onToggleMute,
705
+ "aria-label": isMuted ? "Unmute" : "Mute",
706
+ children: isMuted || volume === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
707
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
708
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "23", y1: "9", x2: "17", y2: "15" }),
709
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "9", x2: "23", y2: "15" })
710
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
711
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
712
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
713
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
714
+ ] })
715
+ }
716
+ ),
717
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
718
+ "input",
719
+ {
720
+ type: "range",
721
+ min: "0",
722
+ max: "1",
723
+ step: "0.05",
724
+ value: isMuted ? 0 : volume,
725
+ onChange: (e) => onVolumeChange(parseFloat(e.target.value)),
726
+ className: "vexor-volume-slider",
727
+ "aria-label": "Volume slider"
728
+ }
729
+ )
730
+ ] }),
731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vexor-time-display", children: [
732
+ formatTime(currentTime),
733
+ " / ",
734
+ formatTime(duration)
735
+ ] })
736
+ ] }),
737
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-controls-group", children: [
738
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-menu-anchor", children: [
739
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
740
+ "button",
741
+ {
742
+ type: "button",
743
+ className: "vexor-btn",
744
+ onClick: () => {
745
+ setShowSpeedMenu(!showSpeedMenu);
746
+ setShowQualityMenu(false);
747
+ },
748
+ "aria-label": "Playback speed",
749
+ style: { fontSize: "11px", fontWeight: 600, padding: "4px 8px" },
750
+ children: playbackRate === 1 ? "1x" : `${playbackRate}x`
751
+ }
752
+ ),
753
+ showSpeedMenu && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vexor-menu-dropdown", children: speedOptions.map((rate) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
754
+ "button",
755
+ {
756
+ type: "button",
757
+ className: `vexor-menu-item ${playbackRate === rate ? "is-active" : ""}`,
758
+ onClick: () => {
759
+ onPlaybackRateChange(rate);
760
+ setShowSpeedMenu(false);
761
+ },
762
+ children: [
763
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
764
+ rate,
765
+ "x"
766
+ ] }),
767
+ playbackRate === rate && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u2713" })
768
+ ]
769
+ },
770
+ rate
771
+ )) })
772
+ ] }),
773
+ qualityLevels.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-menu-anchor", children: [
774
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
775
+ "button",
776
+ {
777
+ type: "button",
778
+ className: "vexor-btn",
779
+ onClick: () => {
780
+ setShowQualityMenu(!showQualityMenu);
781
+ setShowSpeedMenu(false);
782
+ },
783
+ "aria-label": "Video quality",
784
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
785
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "3" }),
786
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
787
+ ] })
788
+ }
789
+ ),
790
+ showQualityMenu && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vexor-menu-dropdown", children: [
791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ className: `vexor-menu-item ${currentQuality === -1 ? "is-active" : ""}`,
796
+ onClick: () => {
797
+ onQualityChange(-1);
798
+ setShowQualityMenu(false);
799
+ },
800
+ children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Auto" }),
802
+ currentQuality === -1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u2713" })
803
+ ]
804
+ }
805
+ ),
806
+ qualityLevels.map((lvl) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
807
+ "button",
808
+ {
809
+ type: "button",
810
+ className: `vexor-menu-item ${currentQuality === lvl.id ? "is-active" : ""}`,
811
+ onClick: () => {
812
+ onQualityChange(lvl.id);
813
+ setShowQualityMenu(false);
814
+ },
815
+ children: [
816
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: lvl.label }),
817
+ currentQuality === lvl.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u2713" })
818
+ ]
819
+ },
820
+ lvl.id
821
+ ))
822
+ ] })
823
+ ] }),
824
+ pictureInPictureSupported && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
825
+ "button",
826
+ {
827
+ type: "button",
828
+ className: "vexor-btn",
829
+ onClick: onTogglePiP,
830
+ "aria-label": "Picture in picture",
831
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "12", y: "9", width: "8", height: "6", rx: "1", fill: "currentColor", fillOpacity: "0.4" })
834
+ ] })
835
+ }
836
+ ),
837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
838
+ "button",
839
+ {
840
+ type: "button",
841
+ className: "vexor-btn",
842
+ onClick: onToggleFullscreen,
843
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
844
+ children: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" }) })
845
+ }
846
+ )
847
+ ] })
848
+ ] })
849
+ ] });
850
+ };
851
+
852
+ // src/components/Watermark.tsx
853
+ var import_react3 = require("react");
854
+ var import_jsx_runtime2 = require("react/jsx-runtime");
855
+ var Watermark = ({ config }) => {
856
+ const parsedConfig = (0, import_react3.useMemo)(() => {
857
+ if (!config) return null;
858
+ if (typeof config === "string") {
859
+ return { text: config, position: "dynamic", intervalSeconds: 15, opacity: 0.35 };
860
+ }
861
+ return {
862
+ position: "dynamic",
863
+ intervalSeconds: 15,
864
+ opacity: 0.35,
865
+ ...config
866
+ };
867
+ }, [config]);
868
+ const [coords, setCoords] = (0, import_react3.useState)({ top: "15%", left: "15%" });
869
+ (0, import_react3.useEffect)(() => {
870
+ if (!parsedConfig || parsedConfig.position !== "dynamic") return;
871
+ const intervalMs = Math.max(2, parsedConfig.intervalSeconds || 15) * 1e3;
872
+ const updatePosition = () => {
873
+ const top = Math.floor(10 + Math.random() * 70);
874
+ const left = Math.floor(10 + Math.random() * 70);
875
+ setCoords({ top: `${top}%`, left: `${left}%` });
876
+ };
877
+ updatePosition();
878
+ const timer = setInterval(updatePosition, intervalMs);
879
+ return () => clearInterval(timer);
880
+ }, [parsedConfig?.position, parsedConfig?.intervalSeconds]);
881
+ const getPositionStyles = () => {
882
+ switch (parsedConfig?.position) {
883
+ case "top-left":
884
+ return { top: "16px", left: "16px" };
885
+ case "top-right":
886
+ return { top: "16px", right: "16px" };
887
+ case "bottom-left":
888
+ return { bottom: "48px", left: "16px" };
889
+ case "bottom-right":
890
+ return { bottom: "48px", right: "16px" };
891
+ case "center":
892
+ return { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
893
+ case "dynamic":
894
+ default:
895
+ return { top: coords.top, left: coords.left };
896
+ }
897
+ };
898
+ if (!parsedConfig) return null;
899
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
900
+ "div",
901
+ {
902
+ className: "vexor-watermark",
903
+ style: {
904
+ ...getPositionStyles(),
905
+ opacity: Math.max(0, Math.min(1, parsedConfig.opacity ?? 0.35)),
906
+ fontSize: parsedConfig.fontSize ?? "12px",
907
+ color: parsedConfig.color ?? "#ffffff"
908
+ },
909
+ "aria-hidden": "true",
910
+ children: parsedConfig.text
911
+ }
912
+ );
913
+ };
914
+
915
+ // src/components/VexorPlayer.tsx
916
+ var import_jsx_runtime3 = require("react/jsx-runtime");
917
+ var VexorPlayer = (props) => {
918
+ const {
919
+ poster,
920
+ preload = "metadata",
921
+ muted = false,
922
+ loop = false,
923
+ controls = true,
924
+ nativeControls = false,
925
+ watermark,
926
+ protect = false,
927
+ className = "",
928
+ style,
929
+ children
930
+ } = props;
931
+ const player = useVexorPlayer(props);
932
+ const [controlsVisible, setControlsVisible] = (0, import_react4.useState)(true);
933
+ const hideTimerRef = (0, import_react4.useRef)(null);
934
+ const resetHideTimer = (0, import_react4.useCallback)(() => {
935
+ setControlsVisible(true);
936
+ if (hideTimerRef.current) {
937
+ clearTimeout(hideTimerRef.current);
938
+ }
939
+ if (player.isPlaying && !player.containerRef.current?.contains(document.activeElement)) {
940
+ hideTimerRef.current = setTimeout(() => {
941
+ setControlsVisible(false);
942
+ }, 2500);
943
+ }
944
+ }, [player.isPlaying]);
945
+ (0, import_react4.useEffect)(() => {
946
+ resetHideTimer();
947
+ return () => {
948
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
949
+ };
950
+ }, [player.isPlaying, resetHideTimer]);
951
+ const handleContextMenu = (e) => {
952
+ if (protect) {
953
+ e.preventDefault();
954
+ }
955
+ };
956
+ (0, import_react4.useEffect)(() => {
957
+ const handleKeyDown = (e) => {
958
+ const target = e.target;
959
+ if (target?.closest("input, textarea, select, button, [contenteditable='true'], [role='slider']")) return;
960
+ const container = player.containerRef.current;
961
+ if (!container || !document.fullscreenElement && !container.contains(document.activeElement) && !container.matches(":hover")) {
962
+ return;
963
+ }
964
+ switch (e.key.toLowerCase()) {
965
+ case " ":
966
+ case "k":
967
+ e.preventDefault();
968
+ player.togglePlay();
969
+ resetHideTimer();
970
+ break;
971
+ case "arrowleft":
972
+ case "j":
973
+ e.preventDefault();
974
+ player.seek(player.currentTime - 5);
975
+ resetHideTimer();
976
+ break;
977
+ case "arrowright":
978
+ case "l":
979
+ e.preventDefault();
980
+ player.seek(player.currentTime + 5);
981
+ resetHideTimer();
982
+ break;
983
+ case "arrowup":
984
+ e.preventDefault();
985
+ player.setVolume(player.volume + 0.1);
986
+ resetHideTimer();
987
+ break;
988
+ case "arrowdown":
989
+ e.preventDefault();
990
+ player.setVolume(player.volume - 0.1);
991
+ resetHideTimer();
992
+ break;
993
+ case "m":
994
+ e.preventDefault();
995
+ player.toggleMute();
996
+ resetHideTimer();
997
+ break;
998
+ case "f":
999
+ e.preventDefault();
1000
+ player.toggleFullscreen();
1001
+ resetHideTimer();
1002
+ break;
1003
+ }
1004
+ };
1005
+ window.addEventListener("keydown", handleKeyDown);
1006
+ return () => window.removeEventListener("keydown", handleKeyDown);
1007
+ }, [player, resetHideTimer]);
1008
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1009
+ "div",
1010
+ {
1011
+ ref: player.containerRef,
1012
+ className: `vexor-player ${player.isFullscreen ? "is-fullscreen" : ""} ${protect ? "is-protected" : ""} ${className}`,
1013
+ style,
1014
+ onMouseMove: resetHideTimer,
1015
+ onMouseLeave: (event) => {
1016
+ if (player.isPlaying && !event.currentTarget.contains(document.activeElement)) {
1017
+ setControlsVisible(false);
1018
+ }
1019
+ },
1020
+ onFocusCapture: () => {
1021
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
1022
+ setControlsVisible(true);
1023
+ },
1024
+ onBlurCapture: (event) => {
1025
+ if (!event.currentTarget.contains(event.relatedTarget)) resetHideTimer();
1026
+ },
1027
+ onContextMenu: handleContextMenu,
1028
+ tabIndex: 0,
1029
+ role: "region",
1030
+ "aria-label": "Video Player",
1031
+ children: [
1032
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1033
+ "video",
1034
+ {
1035
+ ref: player.videoRef,
1036
+ className: "vexor-video",
1037
+ poster,
1038
+ preload,
1039
+ muted,
1040
+ loop,
1041
+ playsInline: true,
1042
+ controls: nativeControls,
1043
+ onClick: nativeControls ? void 0 : player.togglePlay
1044
+ }
1045
+ ),
1046
+ watermark && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Watermark, { config: watermark }),
1047
+ player.isBuffering && !player.error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vexor-spinner-overlay", "aria-label": "Buffering video", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vexor-spinner" }) }),
1048
+ !nativeControls && controls && !player.isPlaying && !player.isBuffering && !player.error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "vexor-center-play-overlay", onClick: player.togglePlay, "aria-label": "Play video", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "vexor-center-play-button", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("polygon", { points: "6,4 20,12 6,20" }) }) }) }),
1049
+ !nativeControls && controls && !player.error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1050
+ Controls,
1051
+ {
1052
+ isPlaying: player.isPlaying,
1053
+ isBuffering: player.isBuffering,
1054
+ currentTime: player.currentTime,
1055
+ duration: player.duration,
1056
+ buffered: player.buffered,
1057
+ volume: player.volume,
1058
+ isMuted: player.isMuted,
1059
+ isFullscreen: player.isFullscreen,
1060
+ qualityLevels: player.qualityLevels,
1061
+ currentQuality: player.currentQuality,
1062
+ playbackRate: player.playbackRate,
1063
+ visible: controlsVisible,
1064
+ onPlayPause: player.togglePlay,
1065
+ onSeek: player.seek,
1066
+ onVolumeChange: player.setVolume,
1067
+ onToggleMute: player.toggleMute,
1068
+ onQualityChange: player.setQuality,
1069
+ onPlaybackRateChange: player.setPlaybackRate,
1070
+ onToggleFullscreen: player.toggleFullscreen,
1071
+ onTogglePiP: player.togglePiP
1072
+ }
1073
+ ),
1074
+ player.error && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vexor-error-overlay", children: [
1075
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vexor-error-title", children: "Playback Error" }),
1076
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vexor-error-message", children: player.error.message }),
1077
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "vexor-retry-button", onClick: player.retry, children: "Retry Playback" })
1078
+ ] }),
1079
+ children
1080
+ ]
1081
+ }
1082
+ );
1083
+ };
1084
+
1085
+ // src/vanilla/player.ts
1086
+ var import_hls2 = __toESM(require("hls.js"), 1);
1087
+ function createVexorPlayer(target, options) {
1088
+ const container = typeof target === "string" ? document.querySelector(target) : target;
1089
+ if (!container) throw new Error(`Target container not found: ${String(target)}`);
1090
+ const initialUrl = options.src ?? (options.assetId ? `${(options.baseUrl || "https://api.vexor.et").replace(/\/+$/, "")}/v1/playback/${encodeURIComponent(options.assetId)}/master.m3u8${options.token ? `?token=${encodeURIComponent(options.token)}` : ""}` : "");
1091
+ if (!initialUrl) throw new Error("Provide either src or assetId to create the Vexor player.");
1092
+ const hadPlayerClass = container.classList.contains("vexor-player");
1093
+ const hadProtectedClass = container.classList.contains("is-protected");
1094
+ container.classList.add("vexor-player");
1095
+ const video = document.createElement("video");
1096
+ video.className = "vexor-video";
1097
+ video.controls = options.controls ?? true;
1098
+ video.playsInline = true;
1099
+ video.preload = options.preload ?? "metadata";
1100
+ if (options.poster) video.poster = options.poster;
1101
+ if (options.autoPlay) video.autoplay = true;
1102
+ if (options.muted) video.muted = true;
1103
+ if (options.loop) video.loop = true;
1104
+ container.appendChild(video);
1105
+ let watermarkElement = null;
1106
+ let watermarkTimer;
1107
+ if (options.watermark) {
1108
+ const watermark = typeof options.watermark === "string" ? { text: options.watermark, position: "dynamic", intervalSeconds: 15, opacity: 0.35 } : { position: "dynamic", intervalSeconds: 15, opacity: 0.35, ...options.watermark };
1109
+ watermarkElement = document.createElement("div");
1110
+ watermarkElement.className = "vexor-watermark";
1111
+ watermarkElement.textContent = watermark.text;
1112
+ watermarkElement.style.opacity = String(Math.max(0, Math.min(1, watermark.opacity ?? 0.35)));
1113
+ watermarkElement.style.fontSize = typeof watermark.fontSize === "number" ? `${watermark.fontSize}px` : watermark.fontSize ?? "12px";
1114
+ watermarkElement.style.color = watermark.color ?? "#ffffff";
1115
+ const placeWatermark = () => {
1116
+ watermarkElement.style.removeProperty("top");
1117
+ watermarkElement.style.removeProperty("right");
1118
+ watermarkElement.style.removeProperty("bottom");
1119
+ watermarkElement.style.removeProperty("left");
1120
+ watermarkElement.style.removeProperty("transform");
1121
+ switch (watermark.position) {
1122
+ case "top-left":
1123
+ watermarkElement.style.cssText += ";top:16px;left:16px";
1124
+ break;
1125
+ case "top-right":
1126
+ watermarkElement.style.cssText += ";top:16px;right:16px";
1127
+ break;
1128
+ case "bottom-left":
1129
+ watermarkElement.style.cssText += ";bottom:48px;left:16px";
1130
+ break;
1131
+ case "bottom-right":
1132
+ watermarkElement.style.cssText += ";bottom:48px;right:16px";
1133
+ break;
1134
+ case "center":
1135
+ watermarkElement.style.cssText += ";top:50%;left:50%;transform:translate(-50%,-50%)";
1136
+ break;
1137
+ default:
1138
+ watermarkElement.style.top = `${10 + Math.floor(Math.random() * 70)}%`;
1139
+ watermarkElement.style.left = `${10 + Math.floor(Math.random() * 70)}%`;
1140
+ }
1141
+ };
1142
+ placeWatermark();
1143
+ if (watermark.position === "dynamic") {
1144
+ watermarkTimer = setInterval(placeWatermark, Math.max(2, watermark.intervalSeconds || 15) * 1e3);
1145
+ }
1146
+ container.appendChild(watermarkElement);
1147
+ }
1148
+ const onContextMenu = (event) => event.preventDefault();
1149
+ const onDragStart = (event) => event.preventDefault();
1150
+ if (options.protect) {
1151
+ container.classList.add("is-protected");
1152
+ container.addEventListener("contextmenu", onContextMenu);
1153
+ video.addEventListener("contextmenu", onContextMenu);
1154
+ video.addEventListener("dragstart", onDragStart);
1155
+ }
1156
+ let activeUrl = initialUrl;
1157
+ let hls = null;
1158
+ let nativeTokenRefresh = null;
1159
+ let nativeErrorListener;
1160
+ let destroyed = false;
1161
+ let refreshPromise = null;
1162
+ let resumeAfterRefresh = null;
1163
+ let hasSignaledReady = false;
1164
+ let activeParams = new URL(activeUrl, document.baseURI).search;
1165
+ const useFallback = (error) => {
1166
+ if (options.fallbackSrc) {
1167
+ if (hls) hls.destroy();
1168
+ hls = null;
1169
+ video.src = options.fallbackSrc;
1170
+ video.load();
1171
+ } else if (error) options.onError?.(error);
1172
+ };
1173
+ const signalReady = () => {
1174
+ if (hasSignaledReady) return;
1175
+ hasSignaledReady = true;
1176
+ options.onReady?.();
1177
+ };
1178
+ const onLoaded = () => {
1179
+ signalReady();
1180
+ const resume = resumeAfterRefresh;
1181
+ resumeAfterRefresh = null;
1182
+ if (resume && Number.isFinite(resume.time)) video.currentTime = resume.time;
1183
+ if ((resume?.shouldPlay || options.autoPlay) && !destroyed) video.play().catch(() => {
1184
+ });
1185
+ };
1186
+ video.addEventListener("loadedmetadata", onLoaded);
1187
+ video.addEventListener("loadeddata", onLoaded);
1188
+ const onPlay = () => options.onPlay?.();
1189
+ const onPause = () => options.onPause?.();
1190
+ const onEnded = () => options.onEnded?.();
1191
+ video.addEventListener("play", onPlay);
1192
+ video.addEventListener("pause", onPause);
1193
+ video.addEventListener("ended", onEnded);
1194
+ const onTimeUpdate = () => options.onTimeUpdate?.(video.currentTime, video.duration);
1195
+ video.addEventListener("timeupdate", onTimeUpdate);
1196
+ const isHlsUrl = activeUrl.includes(".m3u8") || activeUrl.includes("/playback/");
1197
+ if (isHlsUrl && import_hls2.default.isSupported()) {
1198
+ class TokenAwareLoader extends import_hls2.default.DefaultConfig.loader {
1199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1200
+ load(context, config, callbacks) {
1201
+ if (activeParams && context?.url) {
1202
+ try {
1203
+ const sourceUrl = new URL(activeUrl, document.baseURI);
1204
+ const childUrl = new URL(context.url, sourceUrl);
1205
+ if (childUrl.origin === sourceUrl.origin && !childUrl.searchParams.has("token")) {
1206
+ new URLSearchParams(activeParams).forEach((value, key) => {
1207
+ if (!childUrl.searchParams.has(key)) childUrl.searchParams.set(key, value);
1208
+ });
1209
+ context.url = childUrl.toString();
1210
+ }
1211
+ } catch {
1212
+ }
1213
+ }
1214
+ super.load(context, config, callbacks);
1215
+ }
1216
+ }
1217
+ hls = new import_hls2.default({ enableWorker: true, loader: TokenAwareLoader, fLoader: TokenAwareLoader, pLoader: TokenAwareLoader });
1218
+ const activeHls = hls;
1219
+ activeHls.on(import_hls2.default.Events.MANIFEST_PARSED, () => {
1220
+ if (hls !== activeHls || destroyed) return;
1221
+ const resume = resumeAfterRefresh;
1222
+ resumeAfterRefresh = null;
1223
+ signalReady();
1224
+ if (resume && Number.isFinite(resume.time)) video.currentTime = resume.time;
1225
+ if ((resume?.shouldPlay || options.autoPlay) && !destroyed) video.play().catch(() => {
1226
+ });
1227
+ });
1228
+ activeHls.on(import_hls2.default.Events.ERROR, async (_event, data) => {
1229
+ if (hls !== activeHls || destroyed) return;
1230
+ if ((data.response?.code === 401 || data.response?.code === 403) && options.refreshToken) {
1231
+ try {
1232
+ refreshPromise ??= options.refreshToken();
1233
+ const newToken = await refreshPromise;
1234
+ refreshPromise = null;
1235
+ if (newToken && hls === activeHls && !destroyed) {
1236
+ resumeAfterRefresh = { time: video.currentTime, shouldPlay: !video.paused };
1237
+ const url = new URL(activeUrl, document.baseURI);
1238
+ url.searchParams.set("token", newToken);
1239
+ activeUrl = url.toString();
1240
+ activeParams = url.search;
1241
+ activeHls.loadSource(activeUrl);
1242
+ activeHls.startLoad();
1243
+ return;
1244
+ }
1245
+ } catch {
1246
+ refreshPromise = null;
1247
+ }
1248
+ }
1249
+ if (hls !== activeHls || destroyed) return;
1250
+ if (data.fatal) useFallback({ code: data.details || "MEDIA_PLAYBACK_ERROR", message: `Failed to stream video: ${data.details || "network error"}`, fatal: true });
1251
+ });
1252
+ activeHls.loadSource(activeUrl);
1253
+ activeHls.attachMedia(video);
1254
+ } else if (isHlsUrl && video.canPlayType("application/vnd.apple.mpegurl")) {
1255
+ video.src = activeUrl;
1256
+ nativeTokenRefresh = startNativeTokenRefresh(video, activeUrl, options.refreshToken);
1257
+ nativeErrorListener = () => {
1258
+ void nativeTokenRefresh?.refreshNow().then((refreshed) => {
1259
+ if (refreshed || destroyed) return;
1260
+ video.removeEventListener("error", nativeErrorListener);
1261
+ useFallback({ code: "NATIVE_HLS_ERROR", message: "Failed to play native HLS stream", fatal: true });
1262
+ });
1263
+ };
1264
+ video.addEventListener("error", nativeErrorListener);
1265
+ } else if (!isHlsUrl || options.fallbackSrc) {
1266
+ video.src = isHlsUrl ? options.fallbackSrc : activeUrl;
1267
+ } else {
1268
+ useFallback({ code: "HLS_UNSUPPORTED", message: "This browser does not support HLS playback.", fatal: true });
1269
+ }
1270
+ return {
1271
+ play: () => video.play(),
1272
+ pause: () => video.pause(),
1273
+ seek: (seconds) => {
1274
+ video.currentTime = Math.max(0, Math.min(seconds, video.duration || seconds));
1275
+ },
1276
+ destroy: () => {
1277
+ if (destroyed) return;
1278
+ destroyed = true;
1279
+ nativeTokenRefresh?.stop();
1280
+ if (hls) hls.destroy();
1281
+ hls = null;
1282
+ if (watermarkTimer) clearInterval(watermarkTimer);
1283
+ container.removeEventListener("contextmenu", onContextMenu);
1284
+ video.removeEventListener("contextmenu", onContextMenu);
1285
+ video.removeEventListener("dragstart", onDragStart);
1286
+ video.removeEventListener("loadedmetadata", onLoaded);
1287
+ video.removeEventListener("loadeddata", onLoaded);
1288
+ video.removeEventListener("play", onPlay);
1289
+ video.removeEventListener("pause", onPause);
1290
+ video.removeEventListener("ended", onEnded);
1291
+ video.removeEventListener("timeupdate", onTimeUpdate);
1292
+ if (nativeErrorListener) video.removeEventListener("error", nativeErrorListener);
1293
+ video.pause();
1294
+ video.removeAttribute("src");
1295
+ video.load();
1296
+ video.remove();
1297
+ watermarkElement?.remove();
1298
+ if (!hadPlayerClass) container.classList.remove("vexor-player");
1299
+ if (options.protect && !hadProtectedClass) container.classList.remove("is-protected");
1300
+ },
1301
+ getElement: () => video
1302
+ };
1303
+ }
1304
+ // Annotate the CommonJS export names for ESM import in node:
1305
+ 0 && (module.exports = {
1306
+ Controls,
1307
+ VexorPlayer,
1308
+ Watermark,
1309
+ createVexorPlayer,
1310
+ useVexorPlayer
1311
+ });
1312
+ //# sourceMappingURL=index.cjs.map