@vexor-video/player 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,991 @@
1
+ "use client";
2
+ import {
3
+ createVexorPlayer,
4
+ startNativeTokenRefresh
5
+ } from "./chunk-NBWDQALI.js";
6
+
7
+ // src/components/VexorPlayer.tsx
8
+ import { useState as useState4, useEffect as useEffect4, useRef as useRef3, useCallback as useCallback3 } from "react";
9
+
10
+ // src/hooks/useVexorPlayer.ts
11
+ import { useEffect, useRef, useState, useCallback } from "react";
12
+ import Hls from "hls.js";
13
+ function useVexorPlayer({
14
+ src,
15
+ assetId,
16
+ token,
17
+ baseUrl = "https://api.vexor.et",
18
+ fallbackSrc,
19
+ autoPlay = false,
20
+ muted = false,
21
+ refreshToken,
22
+ onReady,
23
+ onPlay,
24
+ onPause,
25
+ onEnded,
26
+ onTimeUpdate,
27
+ onError,
28
+ onQualityChange
29
+ }) {
30
+ const videoRef = useRef(null);
31
+ const containerRef = useRef(null);
32
+ const hlsRef = useRef(null);
33
+ const callbacksRef = useRef({ onReady, onPlay, onPause, onEnded, onTimeUpdate, onError, onQualityChange, refreshToken });
34
+ callbacksRef.current = { onReady, onPlay, onPause, onEnded, onTimeUpdate, onError, onQualityChange, refreshToken };
35
+ const activeParamsRef = useRef("");
36
+ const refreshPromiseRef = useRef(null);
37
+ const resumeAfterRefreshRef = useRef(null);
38
+ const [isPlaying, setIsPlaying] = useState(false);
39
+ const [isBuffering, setIsBuffering] = useState(true);
40
+ const [currentTime, setCurrentTime] = useState(0);
41
+ const [duration, setDuration] = useState(0);
42
+ const [buffered, setBuffered] = useState(0);
43
+ const [volume, setVolumeState] = useState(1);
44
+ const [isMuted, setIsMuted] = useState(muted);
45
+ const [isFullscreen, setIsFullscreen] = useState(false);
46
+ const [qualityLevels, setQualityLevels] = useState([]);
47
+ const [currentQuality, setCurrentQuality] = useState(-1);
48
+ const [playbackRate, setPlaybackRateState] = useState(1);
49
+ const [error, setError] = useState(null);
50
+ const [reloadTrigger, setReloadTrigger] = useState(0);
51
+ const activeUrl = src ? src : assetId ? `${baseUrl.replace(/\/+$/, "")}/v1/playback/${encodeURIComponent(assetId)}/master.m3u8${token ? `?token=${encodeURIComponent(token)}` : ""}` : "";
52
+ const destroyHls = useCallback(() => {
53
+ if (hlsRef.current) {
54
+ hlsRef.current.destroy();
55
+ hlsRef.current = null;
56
+ }
57
+ }, []);
58
+ const retry = useCallback(() => {
59
+ setError(null);
60
+ setIsBuffering(true);
61
+ setReloadTrigger((v) => v + 1);
62
+ }, []);
63
+ useEffect(() => {
64
+ const video = videoRef.current;
65
+ if (!video) return;
66
+ let cleanupFallback = () => {
67
+ };
68
+ const loadFallback = () => {
69
+ if (!fallbackSrc) return false;
70
+ destroyHls();
71
+ setIsBuffering(true);
72
+ video.src = fallbackSrc;
73
+ const onLoaded = () => {
74
+ setIsBuffering(false);
75
+ callbacksRef.current.onReady?.();
76
+ };
77
+ const onError2 = () => {
78
+ const errObj = {
79
+ code: "FALLBACK_PLAYBACK_ERROR",
80
+ message: "Unable to load fallback media source",
81
+ fatal: true
82
+ };
83
+ setError(errObj);
84
+ callbacksRef.current.onError?.(errObj);
85
+ setIsBuffering(false);
86
+ };
87
+ video.addEventListener("loadeddata", onLoaded);
88
+ video.addEventListener("error", onError2);
89
+ cleanupFallback = () => {
90
+ video.removeEventListener("loadeddata", onLoaded);
91
+ video.removeEventListener("error", onError2);
92
+ };
93
+ video.load();
94
+ return true;
95
+ };
96
+ const setFatalError = (code, message) => {
97
+ destroyHls();
98
+ const errObj = { code, message, fatal: true };
99
+ setError(errObj);
100
+ callbacksRef.current.onError?.(errObj);
101
+ setIsBuffering(false);
102
+ };
103
+ destroyHls();
104
+ setError(null);
105
+ setCurrentTime(0);
106
+ setDuration(0);
107
+ setBuffered(0);
108
+ setQualityLevels([]);
109
+ setCurrentQuality(-1);
110
+ setIsBuffering(Boolean(activeUrl));
111
+ if (!activeUrl) {
112
+ video.pause();
113
+ video.removeAttribute("src");
114
+ video.load();
115
+ return;
116
+ }
117
+ const isHlsUrl = activeUrl.includes(".m3u8") || activeUrl.includes("/playback/");
118
+ let fatalRecoveryAttempts = 0;
119
+ if (isHlsUrl && Hls.isSupported()) {
120
+ activeParamsRef.current = activeUrl.includes("?") ? activeUrl.substring(activeUrl.indexOf("?")) : "";
121
+ class VexorLoader extends Hls.DefaultConfig.loader {
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ load(context, config, callbacks) {
124
+ const activeParams = activeParamsRef.current;
125
+ if (activeParams && context?.url) {
126
+ try {
127
+ const sourceUrl = new URL(activeUrl, window.location.href);
128
+ const childUrl = new URL(context.url, sourceUrl);
129
+ if (childUrl.origin === sourceUrl.origin && !childUrl.searchParams.has("token")) {
130
+ new URLSearchParams(activeParams).forEach((value, key) => {
131
+ if (!childUrl.searchParams.has(key)) childUrl.searchParams.set(key, value);
132
+ });
133
+ context.url = childUrl.toString();
134
+ }
135
+ } catch {
136
+ }
137
+ }
138
+ super.load(context, config, callbacks);
139
+ }
140
+ }
141
+ const hls = new Hls({
142
+ startFragPrefetch: true,
143
+ maxBufferLength: 30,
144
+ maxMaxBufferLength: 60,
145
+ enableWorker: true,
146
+ lowLatencyMode: true,
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ loader: VexorLoader,
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ fLoader: VexorLoader,
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ pLoader: VexorLoader
153
+ });
154
+ hlsRef.current = hls;
155
+ hls.loadSource(activeUrl);
156
+ hls.attachMedia(video);
157
+ hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
158
+ if (hlsRef.current !== hls) return;
159
+ setIsBuffering(false);
160
+ const levels = data.levels.map((lvl, index) => {
161
+ let label = `${lvl.height}p`;
162
+ if (lvl.height === 0 || lvl.videoCodec === void 0) {
163
+ label = "Audio only";
164
+ }
165
+ return {
166
+ id: index,
167
+ height: lvl.height,
168
+ width: lvl.width,
169
+ bitrate: lvl.bitrate,
170
+ label
171
+ };
172
+ });
173
+ setQualityLevels(levels);
174
+ callbacksRef.current.onReady?.();
175
+ const resume = resumeAfterRefreshRef.current;
176
+ if (resume) {
177
+ resumeAfterRefreshRef.current = null;
178
+ if (Number.isFinite(resume.time) && resume.time > 0) video.currentTime = resume.time;
179
+ if (resume.shouldPlay) video.play().catch(() => {
180
+ });
181
+ }
182
+ if (autoPlay) {
183
+ video.play().catch(() => {
184
+ });
185
+ }
186
+ });
187
+ hls.on(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
188
+ if (hlsRef.current !== hls) return;
189
+ setCurrentQuality(hls.autoLevelEnabled ? -1 : data.level);
190
+ callbacksRef.current.onQualityChange?.(hls.autoLevelEnabled ? -1 : data.level);
191
+ });
192
+ hls.on(Hls.Events.ERROR, async (_event, data) => {
193
+ if (hlsRef.current !== hls) return;
194
+ if ((data.response?.code === 401 || data.response?.code === 403) && callbacksRef.current.refreshToken) {
195
+ try {
196
+ if (!refreshPromiseRef.current) {
197
+ refreshPromiseRef.current = callbacksRef.current.refreshToken();
198
+ }
199
+ const newToken = await refreshPromiseRef.current;
200
+ refreshPromiseRef.current = null;
201
+ if (newToken && hlsRef.current === hls) {
202
+ resumeAfterRefreshRef.current = {
203
+ time: video.currentTime,
204
+ shouldPlay: !video.paused
205
+ };
206
+ const url = new URL(activeUrl, window.location.href);
207
+ url.searchParams.set("token", newToken);
208
+ activeParamsRef.current = url.search;
209
+ hls.loadSource(url.toString());
210
+ hls.startLoad();
211
+ return;
212
+ }
213
+ } catch {
214
+ refreshPromiseRef.current = null;
215
+ }
216
+ }
217
+ if (hlsRef.current !== hls) return;
218
+ if (data.fatal) {
219
+ switch (data.type) {
220
+ case Hls.ErrorTypes.NETWORK_ERROR:
221
+ if (fatalRecoveryAttempts++ < 1) {
222
+ hls.startLoad();
223
+ } else if (!loadFallback()) {
224
+ setFatalError(data.details || "NETWORK_ERROR", `Failed to stream video: ${data.details || "network error"}`);
225
+ }
226
+ break;
227
+ case Hls.ErrorTypes.MEDIA_ERROR:
228
+ if (fatalRecoveryAttempts++ < 1) {
229
+ hls.recoverMediaError();
230
+ } else if (!loadFallback()) {
231
+ setFatalError(data.details || "MEDIA_ERROR", `Failed to stream video: ${data.details || "media error"}`);
232
+ }
233
+ break;
234
+ default:
235
+ if (!loadFallback()) {
236
+ setFatalError(data.details || "MEDIA_PLAYBACK_ERROR", `Failed to stream video: ${data.details || "network error"}`);
237
+ }
238
+ break;
239
+ }
240
+ }
241
+ });
242
+ } else if (isHlsUrl && video.canPlayType("application/vnd.apple.mpegurl")) {
243
+ video.src = activeUrl;
244
+ const nativeTokenRefresh = startNativeTokenRefresh(
245
+ video,
246
+ activeUrl,
247
+ callbacksRef.current.refreshToken
248
+ );
249
+ const onLoaded = () => {
250
+ setIsBuffering(false);
251
+ callbacksRef.current.onReady?.();
252
+ if (autoPlay) {
253
+ video.play().catch(() => {
254
+ });
255
+ }
256
+ };
257
+ const onNativeError = () => {
258
+ void nativeTokenRefresh.refreshNow().then((refreshed) => {
259
+ if (refreshed || hlsRef.current) return;
260
+ if (loadFallback()) {
261
+ video.removeEventListener("error", onNativeError);
262
+ return;
263
+ }
264
+ setFatalError("NATIVE_HLS_ERROR", "Failed to play native HLS stream");
265
+ });
266
+ };
267
+ video.addEventListener("loadedmetadata", onLoaded);
268
+ video.addEventListener("error", onNativeError);
269
+ return () => {
270
+ nativeTokenRefresh.stop();
271
+ video.removeEventListener("loadedmetadata", onLoaded);
272
+ video.removeEventListener("error", onNativeError);
273
+ };
274
+ } else if (!isHlsUrl) {
275
+ video.src = activeUrl;
276
+ const onLoaded = () => {
277
+ setIsBuffering(false);
278
+ onReady?.();
279
+ };
280
+ const onErr = () => {
281
+ const errObj = {
282
+ code: "DIRECT_PLAYBACK_ERROR",
283
+ message: "Unable to load media source",
284
+ fatal: true
285
+ };
286
+ setError(errObj);
287
+ callbacksRef.current.onError?.(errObj);
288
+ setIsBuffering(false);
289
+ };
290
+ video.addEventListener("loadeddata", onLoaded);
291
+ video.addEventListener("error", onErr);
292
+ return () => {
293
+ video.removeEventListener("loadeddata", onLoaded);
294
+ video.removeEventListener("error", onErr);
295
+ };
296
+ } else if (fallbackSrc) {
297
+ loadFallback();
298
+ } else {
299
+ const errObj = {
300
+ code: "HLS_UNSUPPORTED",
301
+ message: "This browser does not support HLS playback and no fallback source was provided",
302
+ fatal: true
303
+ };
304
+ setError(errObj);
305
+ callbacksRef.current.onError?.(errObj);
306
+ setIsBuffering(false);
307
+ }
308
+ return () => {
309
+ cleanupFallback();
310
+ destroyHls();
311
+ };
312
+ }, [activeUrl, fallbackSrc, autoPlay, reloadTrigger, destroyHls]);
313
+ useEffect(() => {
314
+ const video = videoRef.current;
315
+ if (!video) return;
316
+ const onPlayEvent = () => {
317
+ setIsPlaying(true);
318
+ callbacksRef.current.onPlay?.();
319
+ };
320
+ const onPauseEvent = () => {
321
+ setIsPlaying(false);
322
+ callbacksRef.current.onPause?.();
323
+ };
324
+ const onEndedEvent = () => {
325
+ setIsPlaying(false);
326
+ callbacksRef.current.onEnded?.();
327
+ };
328
+ const onWaiting = () => setIsBuffering(true);
329
+ const onPlaying = () => setIsBuffering(false);
330
+ const onTimeUpdateEvent = () => {
331
+ setCurrentTime(video.currentTime);
332
+ callbacksRef.current.onTimeUpdate?.(video.currentTime, video.duration);
333
+ if (video.buffered.length > 0 && video.duration > 0) {
334
+ for (let i = video.buffered.length - 1; i >= 0; i--) {
335
+ if (video.buffered.start(i) <= video.currentTime) {
336
+ setBuffered(video.buffered.end(i) / video.duration * 100);
337
+ break;
338
+ }
339
+ }
340
+ }
341
+ };
342
+ const onDurationChange = () => setDuration(video.duration || 0);
343
+ const onVolumeChangeEvent = () => {
344
+ setVolumeState(video.volume);
345
+ setIsMuted(video.muted);
346
+ };
347
+ video.addEventListener("play", onPlayEvent);
348
+ video.addEventListener("pause", onPauseEvent);
349
+ video.addEventListener("ended", onEndedEvent);
350
+ video.addEventListener("waiting", onWaiting);
351
+ video.addEventListener("playing", onPlaying);
352
+ video.addEventListener("timeupdate", onTimeUpdateEvent);
353
+ video.addEventListener("durationchange", onDurationChange);
354
+ video.addEventListener("volumechange", onVolumeChangeEvent);
355
+ return () => {
356
+ video.removeEventListener("play", onPlayEvent);
357
+ video.removeEventListener("pause", onPauseEvent);
358
+ video.removeEventListener("ended", onEndedEvent);
359
+ video.removeEventListener("waiting", onWaiting);
360
+ video.removeEventListener("playing", onPlaying);
361
+ video.removeEventListener("timeupdate", onTimeUpdateEvent);
362
+ video.removeEventListener("durationchange", onDurationChange);
363
+ video.removeEventListener("volumechange", onVolumeChangeEvent);
364
+ };
365
+ }, []);
366
+ useEffect(() => {
367
+ const handleFullscreenChange = () => {
368
+ setIsFullscreen(Boolean(document.fullscreenElement));
369
+ };
370
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
371
+ return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
372
+ }, []);
373
+ const play = useCallback(async () => {
374
+ if (videoRef.current) {
375
+ await videoRef.current.play();
376
+ }
377
+ }, []);
378
+ const pause = useCallback(() => {
379
+ if (videoRef.current) {
380
+ videoRef.current.pause();
381
+ }
382
+ }, []);
383
+ const togglePlay = useCallback(() => {
384
+ if (!videoRef.current) return;
385
+ if (videoRef.current.paused) {
386
+ videoRef.current.play().catch(() => {
387
+ });
388
+ } else {
389
+ videoRef.current.pause();
390
+ }
391
+ }, []);
392
+ const seek = useCallback((time) => {
393
+ if (videoRef.current) {
394
+ videoRef.current.currentTime = Math.max(0, Math.min(time, videoRef.current.duration || time));
395
+ }
396
+ }, []);
397
+ const setVolume = useCallback((vol) => {
398
+ if (videoRef.current) {
399
+ const clamped = Math.max(0, Math.min(1, vol));
400
+ videoRef.current.volume = clamped;
401
+ videoRef.current.muted = clamped === 0;
402
+ }
403
+ }, []);
404
+ const toggleMute = useCallback(() => {
405
+ if (videoRef.current) {
406
+ videoRef.current.muted = !videoRef.current.muted;
407
+ }
408
+ }, []);
409
+ const setQuality = useCallback((levelIndex) => {
410
+ if (hlsRef.current) {
411
+ if (levelIndex === -1) {
412
+ hlsRef.current.currentLevel = -1;
413
+ } else {
414
+ hlsRef.current.currentLevel = levelIndex;
415
+ }
416
+ setCurrentQuality(levelIndex);
417
+ }
418
+ }, []);
419
+ const setPlaybackRate = useCallback((rate) => {
420
+ if (videoRef.current) {
421
+ videoRef.current.playbackRate = rate;
422
+ setPlaybackRateState(rate);
423
+ }
424
+ }, []);
425
+ const toggleFullscreen = useCallback(() => {
426
+ const container = containerRef.current || videoRef.current;
427
+ if (!container) return;
428
+ if (!document.fullscreenElement) {
429
+ container.requestFullscreen().catch(() => {
430
+ });
431
+ } else {
432
+ document.exitFullscreen().catch(() => {
433
+ });
434
+ }
435
+ }, []);
436
+ const togglePiP = useCallback(async () => {
437
+ const video = videoRef.current;
438
+ if (!video) return;
439
+ try {
440
+ if (document.pictureInPictureElement) {
441
+ await document.exitPictureInPicture();
442
+ } else if (document.pictureInPictureEnabled) {
443
+ await video.requestPictureInPicture();
444
+ }
445
+ } catch {
446
+ }
447
+ }, []);
448
+ return {
449
+ videoRef,
450
+ containerRef,
451
+ isPlaying,
452
+ isBuffering,
453
+ currentTime,
454
+ duration,
455
+ buffered,
456
+ volume,
457
+ isMuted,
458
+ isFullscreen,
459
+ qualityLevels,
460
+ currentQuality,
461
+ playbackRate,
462
+ error,
463
+ play,
464
+ pause,
465
+ togglePlay,
466
+ seek,
467
+ setVolume,
468
+ toggleMute,
469
+ setQuality,
470
+ setPlaybackRate,
471
+ toggleFullscreen,
472
+ togglePiP,
473
+ retry
474
+ };
475
+ }
476
+
477
+ // src/components/Controls.tsx
478
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
479
+ import { jsx, jsxs } from "react/jsx-runtime";
480
+ function formatTime(seconds) {
481
+ if (isNaN(seconds) || seconds < 0) return "0:00";
482
+ const h = Math.floor(seconds / 3600);
483
+ const m = Math.floor(seconds % 3600 / 60);
484
+ const s = Math.floor(seconds % 60);
485
+ if (h > 0) {
486
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
487
+ }
488
+ return `${m}:${s.toString().padStart(2, "0")}`;
489
+ }
490
+ var Controls = ({
491
+ isPlaying,
492
+ currentTime,
493
+ duration,
494
+ buffered,
495
+ volume,
496
+ isMuted,
497
+ isFullscreen,
498
+ qualityLevels,
499
+ currentQuality,
500
+ playbackRate,
501
+ visible,
502
+ onPlayPause,
503
+ onSeek,
504
+ onVolumeChange,
505
+ onToggleMute,
506
+ onQualityChange,
507
+ onPlaybackRateChange,
508
+ onToggleFullscreen,
509
+ onTogglePiP
510
+ }) => {
511
+ const [showQualityMenu, setShowQualityMenu] = useState2(false);
512
+ const [showSpeedMenu, setShowSpeedMenu] = useState2(false);
513
+ const progressRef = useRef2(null);
514
+ const [isScrubbing, setIsScrubbing] = useState2(false);
515
+ const [pictureInPictureSupported, setPictureInPictureSupported] = useState2(false);
516
+ useEffect2(() => {
517
+ setPictureInPictureSupported(
518
+ typeof document !== "undefined" && document.pictureInPictureEnabled
519
+ );
520
+ }, []);
521
+ const playedPercent = duration > 0 ? currentTime / duration * 100 : 0;
522
+ const speedOptions = [0.5, 0.75, 1, 1.25, 1.5, 2];
523
+ const handleScrub = useCallback2(
524
+ (e) => {
525
+ if (!progressRef.current || duration <= 0) return;
526
+ const rect = progressRef.current.getBoundingClientRect();
527
+ const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
528
+ onSeek(pos * duration);
529
+ },
530
+ [duration, onSeek]
531
+ );
532
+ const handlePointerDown = (e) => {
533
+ if (e.button !== 0) return;
534
+ e.currentTarget.setPointerCapture(e.pointerId);
535
+ setIsScrubbing(true);
536
+ handleScrub(e);
537
+ };
538
+ const handleTimelineKeyDown = (e) => {
539
+ if (duration <= 0) return;
540
+ const step = e.shiftKey ? 30 : 5;
541
+ let nextTime;
542
+ if (e.key === "ArrowLeft") nextTime = currentTime - step;
543
+ if (e.key === "ArrowRight") nextTime = currentTime + step;
544
+ if (e.key === "Home") nextTime = 0;
545
+ if (e.key === "End") nextTime = duration;
546
+ if (nextTime !== void 0) {
547
+ e.preventDefault();
548
+ onSeek(Math.max(0, Math.min(duration, nextTime)));
549
+ }
550
+ };
551
+ return /* @__PURE__ */ jsxs("div", { className: `vexor-controls ${visible ? "" : "is-hidden"}`, children: [
552
+ /* @__PURE__ */ jsx(
553
+ "div",
554
+ {
555
+ className: "vexor-progress-container",
556
+ ref: progressRef,
557
+ onPointerDown: handlePointerDown,
558
+ onPointerMove: (event) => {
559
+ if (isScrubbing) handleScrub(event);
560
+ },
561
+ onPointerUp: (event) => {
562
+ if (progressRef.current?.hasPointerCapture(event.pointerId)) {
563
+ progressRef.current.releasePointerCapture(event.pointerId);
564
+ }
565
+ setIsScrubbing(false);
566
+ },
567
+ onPointerCancel: () => setIsScrubbing(false),
568
+ onKeyDown: handleTimelineKeyDown,
569
+ tabIndex: 0,
570
+ role: "slider",
571
+ "aria-label": "Video timeline",
572
+ "aria-valuemin": 0,
573
+ "aria-valuemax": duration,
574
+ "aria-valuenow": Math.min(currentTime, duration || 0),
575
+ "aria-valuetext": `${formatTime(currentTime)} of ${formatTime(duration)}`,
576
+ children: /* @__PURE__ */ jsxs("div", { className: "vexor-progress-track", children: [
577
+ /* @__PURE__ */ jsx("div", { className: "vexor-progress-buffer", style: { width: `${buffered}%` } }),
578
+ /* @__PURE__ */ jsx("div", { className: "vexor-progress-played", style: { width: `${playedPercent}%` } }),
579
+ /* @__PURE__ */ jsx("div", { className: "vexor-progress-thumb", style: { left: `${playedPercent}%` } })
580
+ ] })
581
+ }
582
+ ),
583
+ /* @__PURE__ */ jsxs("div", { className: "vexor-controls-row", children: [
584
+ /* @__PURE__ */ jsxs("div", { className: "vexor-controls-group", children: [
585
+ /* @__PURE__ */ jsx(
586
+ "button",
587
+ {
588
+ type: "button",
589
+ className: "vexor-btn",
590
+ onClick: onPlayPause,
591
+ "aria-label": isPlaying ? "Pause" : "Play",
592
+ children: isPlaying ? /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [
593
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1" }),
594
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1" })
595
+ ] }) : /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("polygon", { points: "5,3 19,12 5,21" }) })
596
+ }
597
+ ),
598
+ /* @__PURE__ */ jsxs("div", { className: "vexor-volume-wrapper", children: [
599
+ /* @__PURE__ */ jsx(
600
+ "button",
601
+ {
602
+ type: "button",
603
+ className: "vexor-btn",
604
+ onClick: onToggleMute,
605
+ "aria-label": isMuted ? "Unmute" : "Mute",
606
+ children: isMuted || volume === 0 ? /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
607
+ /* @__PURE__ */ jsx("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
608
+ /* @__PURE__ */ jsx("line", { x1: "23", y1: "9", x2: "17", y2: "15" }),
609
+ /* @__PURE__ */ jsx("line", { x1: "17", y1: "9", x2: "23", y2: "15" })
610
+ ] }) : /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
611
+ /* @__PURE__ */ jsx("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
612
+ /* @__PURE__ */ jsx("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
613
+ /* @__PURE__ */ jsx("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
614
+ ] })
615
+ }
616
+ ),
617
+ /* @__PURE__ */ jsx(
618
+ "input",
619
+ {
620
+ type: "range",
621
+ min: "0",
622
+ max: "1",
623
+ step: "0.05",
624
+ value: isMuted ? 0 : volume,
625
+ onChange: (e) => onVolumeChange(parseFloat(e.target.value)),
626
+ className: "vexor-volume-slider",
627
+ "aria-label": "Volume slider"
628
+ }
629
+ )
630
+ ] }),
631
+ /* @__PURE__ */ jsxs("span", { className: "vexor-time-display", children: [
632
+ formatTime(currentTime),
633
+ " / ",
634
+ formatTime(duration)
635
+ ] })
636
+ ] }),
637
+ /* @__PURE__ */ jsxs("div", { className: "vexor-controls-group", children: [
638
+ /* @__PURE__ */ jsxs("div", { className: "vexor-menu-anchor", children: [
639
+ /* @__PURE__ */ jsx(
640
+ "button",
641
+ {
642
+ type: "button",
643
+ className: "vexor-btn",
644
+ onClick: () => {
645
+ setShowSpeedMenu(!showSpeedMenu);
646
+ setShowQualityMenu(false);
647
+ },
648
+ "aria-label": "Playback speed",
649
+ style: { fontSize: "11px", fontWeight: 600, padding: "4px 8px" },
650
+ children: playbackRate === 1 ? "1x" : `${playbackRate}x`
651
+ }
652
+ ),
653
+ showSpeedMenu && /* @__PURE__ */ jsx("div", { className: "vexor-menu-dropdown", children: speedOptions.map((rate) => /* @__PURE__ */ jsxs(
654
+ "button",
655
+ {
656
+ type: "button",
657
+ className: `vexor-menu-item ${playbackRate === rate ? "is-active" : ""}`,
658
+ onClick: () => {
659
+ onPlaybackRateChange(rate);
660
+ setShowSpeedMenu(false);
661
+ },
662
+ children: [
663
+ /* @__PURE__ */ jsxs("span", { children: [
664
+ rate,
665
+ "x"
666
+ ] }),
667
+ playbackRate === rate && /* @__PURE__ */ jsx("span", { children: "\u2713" })
668
+ ]
669
+ },
670
+ rate
671
+ )) })
672
+ ] }),
673
+ qualityLevels.length > 0 && /* @__PURE__ */ jsxs("div", { className: "vexor-menu-anchor", children: [
674
+ /* @__PURE__ */ jsx(
675
+ "button",
676
+ {
677
+ type: "button",
678
+ className: "vexor-btn",
679
+ onClick: () => {
680
+ setShowQualityMenu(!showQualityMenu);
681
+ setShowSpeedMenu(false);
682
+ },
683
+ "aria-label": "Video quality",
684
+ children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
685
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
686
+ /* @__PURE__ */ 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" })
687
+ ] })
688
+ }
689
+ ),
690
+ showQualityMenu && /* @__PURE__ */ jsxs("div", { className: "vexor-menu-dropdown", children: [
691
+ /* @__PURE__ */ jsxs(
692
+ "button",
693
+ {
694
+ type: "button",
695
+ className: `vexor-menu-item ${currentQuality === -1 ? "is-active" : ""}`,
696
+ onClick: () => {
697
+ onQualityChange(-1);
698
+ setShowQualityMenu(false);
699
+ },
700
+ children: [
701
+ /* @__PURE__ */ jsx("span", { children: "Auto" }),
702
+ currentQuality === -1 && /* @__PURE__ */ jsx("span", { children: "\u2713" })
703
+ ]
704
+ }
705
+ ),
706
+ qualityLevels.map((lvl) => /* @__PURE__ */ jsxs(
707
+ "button",
708
+ {
709
+ type: "button",
710
+ className: `vexor-menu-item ${currentQuality === lvl.id ? "is-active" : ""}`,
711
+ onClick: () => {
712
+ onQualityChange(lvl.id);
713
+ setShowQualityMenu(false);
714
+ },
715
+ children: [
716
+ /* @__PURE__ */ jsx("span", { children: lvl.label }),
717
+ currentQuality === lvl.id && /* @__PURE__ */ jsx("span", { children: "\u2713" })
718
+ ]
719
+ },
720
+ lvl.id
721
+ ))
722
+ ] })
723
+ ] }),
724
+ pictureInPictureSupported && /* @__PURE__ */ jsx(
725
+ "button",
726
+ {
727
+ type: "button",
728
+ className: "vexor-btn",
729
+ onClick: onTogglePiP,
730
+ "aria-label": "Picture in picture",
731
+ children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
732
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
733
+ /* @__PURE__ */ jsx("rect", { x: "12", y: "9", width: "8", height: "6", rx: "1", fill: "currentColor", fillOpacity: "0.4" })
734
+ ] })
735
+ }
736
+ ),
737
+ /* @__PURE__ */ jsx(
738
+ "button",
739
+ {
740
+ type: "button",
741
+ className: "vexor-btn",
742
+ onClick: onToggleFullscreen,
743
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
744
+ children: isFullscreen ? /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ 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__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" }) })
745
+ }
746
+ )
747
+ ] })
748
+ ] })
749
+ ] });
750
+ };
751
+
752
+ // src/components/Watermark.tsx
753
+ import { useEffect as useEffect3, useState as useState3, useMemo } from "react";
754
+ import { jsx as jsx2 } from "react/jsx-runtime";
755
+ var Watermark = ({ config }) => {
756
+ const parsedConfig = useMemo(() => {
757
+ if (!config) return null;
758
+ if (typeof config === "string") {
759
+ return { text: config, position: "dynamic", intervalSeconds: 15, opacity: 0.35 };
760
+ }
761
+ return {
762
+ position: "dynamic",
763
+ intervalSeconds: 15,
764
+ opacity: 0.35,
765
+ ...config
766
+ };
767
+ }, [config]);
768
+ const [coords, setCoords] = useState3({ top: "15%", left: "15%" });
769
+ useEffect3(() => {
770
+ if (!parsedConfig || parsedConfig.position !== "dynamic") return;
771
+ const intervalMs = Math.max(2, parsedConfig.intervalSeconds || 15) * 1e3;
772
+ const updatePosition = () => {
773
+ const top = Math.floor(10 + Math.random() * 70);
774
+ const left = Math.floor(10 + Math.random() * 70);
775
+ setCoords({ top: `${top}%`, left: `${left}%` });
776
+ };
777
+ updatePosition();
778
+ const timer = setInterval(updatePosition, intervalMs);
779
+ return () => clearInterval(timer);
780
+ }, [parsedConfig?.position, parsedConfig?.intervalSeconds]);
781
+ const getPositionStyles = () => {
782
+ switch (parsedConfig?.position) {
783
+ case "top-left":
784
+ return { top: "16px", left: "16px" };
785
+ case "top-right":
786
+ return { top: "16px", right: "16px" };
787
+ case "bottom-left":
788
+ return { bottom: "48px", left: "16px" };
789
+ case "bottom-right":
790
+ return { bottom: "48px", right: "16px" };
791
+ case "center":
792
+ return { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
793
+ case "dynamic":
794
+ default:
795
+ return { top: coords.top, left: coords.left };
796
+ }
797
+ };
798
+ if (!parsedConfig) return null;
799
+ return /* @__PURE__ */ jsx2(
800
+ "div",
801
+ {
802
+ className: "vexor-watermark",
803
+ style: {
804
+ ...getPositionStyles(),
805
+ opacity: Math.max(0, Math.min(1, parsedConfig.opacity ?? 0.35)),
806
+ fontSize: parsedConfig.fontSize ?? "12px",
807
+ color: parsedConfig.color ?? "#ffffff"
808
+ },
809
+ "aria-hidden": "true",
810
+ children: parsedConfig.text
811
+ }
812
+ );
813
+ };
814
+
815
+ // src/components/VexorPlayer.tsx
816
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
817
+ var VexorPlayer = (props) => {
818
+ const {
819
+ poster,
820
+ preload = "metadata",
821
+ muted = false,
822
+ loop = false,
823
+ controls = true,
824
+ nativeControls = false,
825
+ watermark,
826
+ protect = false,
827
+ className = "",
828
+ style,
829
+ children
830
+ } = props;
831
+ const player = useVexorPlayer(props);
832
+ const [controlsVisible, setControlsVisible] = useState4(true);
833
+ const hideTimerRef = useRef3(null);
834
+ const resetHideTimer = useCallback3(() => {
835
+ setControlsVisible(true);
836
+ if (hideTimerRef.current) {
837
+ clearTimeout(hideTimerRef.current);
838
+ }
839
+ if (player.isPlaying && !player.containerRef.current?.contains(document.activeElement)) {
840
+ hideTimerRef.current = setTimeout(() => {
841
+ setControlsVisible(false);
842
+ }, 2500);
843
+ }
844
+ }, [player.isPlaying]);
845
+ useEffect4(() => {
846
+ resetHideTimer();
847
+ return () => {
848
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
849
+ };
850
+ }, [player.isPlaying, resetHideTimer]);
851
+ const handleContextMenu = (e) => {
852
+ if (protect) {
853
+ e.preventDefault();
854
+ }
855
+ };
856
+ useEffect4(() => {
857
+ const handleKeyDown = (e) => {
858
+ const target = e.target;
859
+ if (target?.closest("input, textarea, select, button, [contenteditable='true'], [role='slider']")) return;
860
+ const container = player.containerRef.current;
861
+ if (!container || !document.fullscreenElement && !container.contains(document.activeElement) && !container.matches(":hover")) {
862
+ return;
863
+ }
864
+ switch (e.key.toLowerCase()) {
865
+ case " ":
866
+ case "k":
867
+ e.preventDefault();
868
+ player.togglePlay();
869
+ resetHideTimer();
870
+ break;
871
+ case "arrowleft":
872
+ case "j":
873
+ e.preventDefault();
874
+ player.seek(player.currentTime - 5);
875
+ resetHideTimer();
876
+ break;
877
+ case "arrowright":
878
+ case "l":
879
+ e.preventDefault();
880
+ player.seek(player.currentTime + 5);
881
+ resetHideTimer();
882
+ break;
883
+ case "arrowup":
884
+ e.preventDefault();
885
+ player.setVolume(player.volume + 0.1);
886
+ resetHideTimer();
887
+ break;
888
+ case "arrowdown":
889
+ e.preventDefault();
890
+ player.setVolume(player.volume - 0.1);
891
+ resetHideTimer();
892
+ break;
893
+ case "m":
894
+ e.preventDefault();
895
+ player.toggleMute();
896
+ resetHideTimer();
897
+ break;
898
+ case "f":
899
+ e.preventDefault();
900
+ player.toggleFullscreen();
901
+ resetHideTimer();
902
+ break;
903
+ }
904
+ };
905
+ window.addEventListener("keydown", handleKeyDown);
906
+ return () => window.removeEventListener("keydown", handleKeyDown);
907
+ }, [player, resetHideTimer]);
908
+ return /* @__PURE__ */ jsxs2(
909
+ "div",
910
+ {
911
+ ref: player.containerRef,
912
+ className: `vexor-player ${player.isFullscreen ? "is-fullscreen" : ""} ${protect ? "is-protected" : ""} ${className}`,
913
+ style,
914
+ onMouseMove: resetHideTimer,
915
+ onMouseLeave: (event) => {
916
+ if (player.isPlaying && !event.currentTarget.contains(document.activeElement)) {
917
+ setControlsVisible(false);
918
+ }
919
+ },
920
+ onFocusCapture: () => {
921
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
922
+ setControlsVisible(true);
923
+ },
924
+ onBlurCapture: (event) => {
925
+ if (!event.currentTarget.contains(event.relatedTarget)) resetHideTimer();
926
+ },
927
+ onContextMenu: handleContextMenu,
928
+ tabIndex: 0,
929
+ role: "region",
930
+ "aria-label": "Video Player",
931
+ children: [
932
+ /* @__PURE__ */ jsx3(
933
+ "video",
934
+ {
935
+ ref: player.videoRef,
936
+ className: "vexor-video",
937
+ poster,
938
+ preload,
939
+ muted,
940
+ loop,
941
+ playsInline: true,
942
+ controls: nativeControls,
943
+ onClick: nativeControls ? void 0 : player.togglePlay
944
+ }
945
+ ),
946
+ watermark && /* @__PURE__ */ jsx3(Watermark, { config: watermark }),
947
+ player.isBuffering && !player.error && /* @__PURE__ */ jsx3("div", { className: "vexor-spinner-overlay", "aria-label": "Buffering video", children: /* @__PURE__ */ jsx3("div", { className: "vexor-spinner" }) }),
948
+ !nativeControls && controls && !player.isPlaying && !player.isBuffering && !player.error && /* @__PURE__ */ jsx3("button", { type: "button", className: "vexor-center-play-overlay", onClick: player.togglePlay, "aria-label": "Play video", children: /* @__PURE__ */ jsx3("span", { className: "vexor-center-play-button", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx3("polygon", { points: "6,4 20,12 6,20" }) }) }) }),
949
+ !nativeControls && controls && !player.error && /* @__PURE__ */ jsx3(
950
+ Controls,
951
+ {
952
+ isPlaying: player.isPlaying,
953
+ isBuffering: player.isBuffering,
954
+ currentTime: player.currentTime,
955
+ duration: player.duration,
956
+ buffered: player.buffered,
957
+ volume: player.volume,
958
+ isMuted: player.isMuted,
959
+ isFullscreen: player.isFullscreen,
960
+ qualityLevels: player.qualityLevels,
961
+ currentQuality: player.currentQuality,
962
+ playbackRate: player.playbackRate,
963
+ visible: controlsVisible,
964
+ onPlayPause: player.togglePlay,
965
+ onSeek: player.seek,
966
+ onVolumeChange: player.setVolume,
967
+ onToggleMute: player.toggleMute,
968
+ onQualityChange: player.setQuality,
969
+ onPlaybackRateChange: player.setPlaybackRate,
970
+ onToggleFullscreen: player.toggleFullscreen,
971
+ onTogglePiP: player.togglePiP
972
+ }
973
+ ),
974
+ player.error && /* @__PURE__ */ jsxs2("div", { className: "vexor-error-overlay", children: [
975
+ /* @__PURE__ */ jsx3("div", { className: "vexor-error-title", children: "Playback Error" }),
976
+ /* @__PURE__ */ jsx3("div", { className: "vexor-error-message", children: player.error.message }),
977
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "vexor-retry-button", onClick: player.retry, children: "Retry Playback" })
978
+ ] }),
979
+ children
980
+ ]
981
+ }
982
+ );
983
+ };
984
+ export {
985
+ Controls,
986
+ VexorPlayer,
987
+ Watermark,
988
+ createVexorPlayer,
989
+ useVexorPlayer
990
+ };
991
+ //# sourceMappingURL=index.js.map