@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vexor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,222 @@
1
- # Temporary Holding Version
1
+ # @vexor-video/player
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official React and Vanilla JS video player for [Vexor](https://vexor.et) token-gated HLS playback.
4
+
5
+ - **Encrypted HLS Streaming**: Seamless playback of AES-128 CBC encrypted adaptive video (240p up to 1080p).
6
+ - **Zero Boilerplate**: Automatically manages Hls.js initialization, quality levels, segment loading, and token parameters.
7
+ - **Dynamic Watermarking**: Optional visible viewer label that periodically moves, adding friction to casual sharing.
8
+ - **Interaction Deterrents**: Optional context-menu and drag suppression. These do not prevent screen capture or replace server-side access controls.
9
+ - **Token Refresh**: Hls.js playback refreshes after an authorization failure; native Safari/iOS HLS refreshes proactively before token expiry when a callback is supplied. Playback resumes from the current playhead; a brief rebuffer may occur.
10
+ - **Modern Controls**: Sleek, accessible UI controls including timeline scrubbing, volume slider, playback rate, quality selector, PiP, and fullscreen.
11
+ - **SSR & Next.js Friendly**: Safe to render on the server and works with React 18 & 19, Next.js, Vite, Remix, or plain HTML.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @vexor-video/player
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Quickstart (React & Next.js)
24
+
25
+ ```tsx
26
+ import { VexorPlayer } from "@vexor-video/player";
27
+ import "@vexor-video/player/styles.css";
28
+
29
+ export default function VideoSection() {
30
+ return (
31
+ <div className="max-w-4xl mx-auto rounded-xl overflow-hidden shadow-2xl">
32
+ <VexorPlayer
33
+ src="https://api.vexor.et/v1/playback/ea56fdde-ad35-44f6-ad9a-8cd01c4f10d1/master.m3u8?token=eyJhbGci..."
34
+ poster="https://api.vexor.et/v1/playback/ea56fdde-ad35-44f6-ad9a-8cd01c4f10d1/thumbnail.jpg"
35
+ watermark="student@example.com"
36
+ protect
37
+ />
38
+ </div>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Alternatively, pass `assetId` and `token` directly:
44
+
45
+ ```tsx
46
+ <VexorPlayer
47
+ assetId="ea56fdde-ad35-44f6-ad9a-8cd01c4f10d1"
48
+ token={playbackToken}
49
+ baseUrl="https://api.vexor.et"
50
+ watermark={{
51
+ text: "Yonas Abebe · ID: 9021",
52
+ position: "dynamic",
53
+ intervalSeconds: 20,
54
+ opacity: 0.35,
55
+ }}
56
+ protect
57
+ />
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Props Reference
63
+
64
+ | Prop | Type | Default | Description |
65
+ |---|---|---|---|
66
+ | `src` | `string` | `undefined` | Full authenticated manifest URL (`master.m3u8?token=...`). |
67
+ | `assetId` | `string` | `undefined` | Vexor video asset UUID (used when `src` is omitted). |
68
+ | `token` | `string` | `undefined` | Short-lived Ed25519 token issued by `/v1/tokens`. |
69
+ | `baseUrl` | `string` | `"https://api.vexor.et"` | Vexor server base URL. |
70
+ | `poster` | `string` | `undefined` | Poster thumbnail image URL. |
71
+ | `autoPlay` | `boolean` | `false` | Automatically begin playback (browsers may require muted=true). |
72
+ | `muted` | `boolean` | `false` | Start audio in muted state. |
73
+ | `loop` | `boolean` | `false` | Loop video playback. |
74
+ | `controls` | `boolean` | `true` | Display playback controls. |
75
+ | `nativeControls` | `boolean` | `false` | Use browser native HTML5 controls instead of custom UI. |
76
+ | `watermark` | `string \| WatermarkConfig` | `undefined` | Moving watermark text or config object. |
77
+ | `protect` | `boolean` | `false` | Suppress right-click and drag interactions as a casual-copy deterrent. It does not prevent capture. |
78
+ | `refreshToken` | `() => Promise<string>` | `undefined` | Callback used to obtain a replacement token after an authorization failure. |
79
+ | `fallbackSrc` | `string` | `undefined` | Fallback MP4 URL if HLS is completely unsupported. |
80
+ | `onReady` | `() => void` | `undefined` | Fires when manifest is parsed and player is ready. |
81
+ | `onPlay` | `() => void` | `undefined` | Fires on playback resume. |
82
+ | `onPause` | `() => void` | `undefined` | Fires on playback pause. |
83
+ | `onEnded` | `() => void` | `undefined` | Fires when video reaches end. |
84
+ | `onTimeUpdate` | `(time, duration) => void` | `undefined` | Fires on progress update. |
85
+ | `onError` | `(error) => void` | `undefined` | Fires on fatal playback error. |
86
+ | `onQualityChange` | `(levelIndex) => void` | `undefined` | Fires when active quality rendition switches. |
87
+
88
+ ---
89
+
90
+ ## Visible Watermark
91
+
92
+ The player can move a visible watermark across the video. It can discourage casual sharing, but it is not DRM and cannot prevent recording, cropping, or redistribution. Keep authorization and playback-token enforcement on your server. Avoid putting sensitive personal data such as IP addresses in the watermark.
93
+
94
+ ```tsx
95
+ <VexorPlayer
96
+ src={manifestUrl}
97
+ watermark={{
98
+ text: "Confidential · viewer-9021",
99
+ position: "dynamic", // 'dynamic' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'
100
+ intervalSeconds: 15, // Moves every 15 seconds
101
+ opacity: 0.35, // Subtle readable overlay
102
+ fontSize: "12px",
103
+ color: "#ffffff",
104
+ }}
105
+ protect
106
+ />
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Token Refresh and Playback Resume
112
+
113
+ Vexor playback tokens expire after a limited period. Provide `refreshToken` to obtain replacement access. Hls.js requests a new token after an authorization failure. Safari/iOS native HLS cannot expose individual playlist or segment authorization failures to JavaScript, so it refreshes proactively about once every four minutes. The player reloads the manifest and attempts to resume at the current playhead; depending on the browser and stream, viewers may see a brief buffer:
114
+
115
+ ```tsx
116
+ async function handleRefreshToken(): Promise<string> {
117
+ const res = await fetch(`/api/courses/${courseId}/lessons/${lessonId}/refresh-token`);
118
+ const data = await res.json();
119
+ return data.token;
120
+ }
121
+
122
+ <VexorPlayer
123
+ src={manifestUrl}
124
+ refreshToken={handleRefreshToken}
125
+ />
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Headless Hook (`useVexorPlayer`)
131
+
132
+ If you want to build completely custom UI controls while letting Vexor manage Hls.js, quality switching, and decryption:
133
+
134
+ ```tsx
135
+ import { useVexorPlayer } from "@vexor-video/player";
136
+
137
+ function CustomPlayer({ src }) {
138
+ const {
139
+ videoRef,
140
+ containerRef,
141
+ isPlaying,
142
+ currentTime,
143
+ duration,
144
+ togglePlay,
145
+ seek,
146
+ qualityLevels,
147
+ setQuality,
148
+ } = useVexorPlayer({ src });
149
+
150
+ return (
151
+ <div ref={containerRef}>
152
+ <video ref={videoRef} />
153
+ <button onClick={togglePlay}>{isPlaying ? "Pause" : "Play"}</button>
154
+ <span>{currentTime} / {duration}</span>
155
+ </div>
156
+ );
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Vanilla JavaScript (Non-React)
163
+
164
+ Use the dedicated `@vexor-video/player/vanilla` entry in a JavaScript bundler or framework. It does not import React. Install the package as usual:
165
+
166
+ ```html
167
+ <link rel="stylesheet" href="node_modules/@vexor-video/player/dist/index.css">
168
+ <div id="player-container"></div>
169
+
170
+ <script type="module">
171
+ import { createVexorPlayer } from "@vexor-video/player/vanilla";
172
+
173
+ const player = createVexorPlayer("#player-container", {
174
+ src: "https://api.vexor.et/v1/playback/:id/master.m3u8?token=...",
175
+ watermark: "user@example.com",
176
+ protect: true,
177
+ });
178
+
179
+ // Programmatic controls:
180
+ // player.play();
181
+ // player.pause();
182
+ // player.destroy(); // removes only elements created by this player instance
183
+ </script>
184
+ ```
185
+
186
+ For a plain HTML page without a bundler, load the standalone browser bundle and use its global API:
187
+
188
+ ```html
189
+ <link rel="stylesheet" href="/node_modules/@vexor-video/player/dist/index.css">
190
+ <div id="player-container"></div>
191
+ <script src="/node_modules/@vexor-video/player/dist/vexor-player.global.js"></script>
192
+ <script>
193
+ const player = VexorPlayer.createVexorPlayer("#player-container", {
194
+ src: "https://api.vexor.et/v1/playback/ASSET_ID/master.m3u8?token=...",
195
+ });
196
+ </script>
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Keyboard Shortcuts
202
+
203
+ When the player is focused or hovered:
204
+ - `Space` or `K`: Play / Pause
205
+ - `Left Arrow` / `J`: Seek backward 5 seconds
206
+ - `Right Arrow` / `L`: Seek forward 5 seconds
207
+ - `Up Arrow`: Volume up 10%
208
+ - `Down Arrow`: Volume down 10%
209
+ - `M`: Mute / Unmute
210
+ - `F`: Fullscreen toggle
211
+
212
+ ---
213
+
214
+ ## Support Vexor
215
+
216
+ If Vexor helps your product, you can support its continued development on [Buy Me a Coffee](https://buymeacoffee.com/eyob_dev).
217
+
218
+ ---
219
+
220
+ ## License
221
+
222
+ MIT © [Vexor](https://vexor.et)
@@ -0,0 +1,290 @@
1
+ // src/vanilla/player.ts
2
+ import Hls from "hls.js";
3
+
4
+ // src/native-token-refresh.ts
5
+ var TOKEN_REFRESH_INTERVAL_MS = 4 * 60 * 1e3;
6
+ var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
7
+ function startNativeTokenRefresh(video, initialUrl, refreshToken) {
8
+ let currentUrl = initialUrl;
9
+ let disposed = false;
10
+ let timer;
11
+ let refreshInFlight = null;
12
+ let restoreListener;
13
+ const schedule = (delay = TOKEN_REFRESH_INTERVAL_MS) => {
14
+ if (!refreshToken || disposed) return;
15
+ if (timer) clearTimeout(timer);
16
+ timer = setTimeout(() => void refreshNow(), delay);
17
+ };
18
+ const refreshNow = () => {
19
+ if (!refreshToken || disposed) return Promise.resolve(false);
20
+ if (refreshInFlight) return refreshInFlight;
21
+ refreshInFlight = (async () => {
22
+ try {
23
+ const token = await refreshToken();
24
+ if (disposed || !token) return false;
25
+ const nextUrl = new URL(currentUrl, document.baseURI);
26
+ nextUrl.searchParams.set("token", token);
27
+ currentUrl = nextUrl.toString();
28
+ const resumeTime = video.currentTime;
29
+ const shouldResume = !video.paused;
30
+ restoreListener = () => {
31
+ restoreListener = void 0;
32
+ if (disposed) return;
33
+ if (Number.isFinite(resumeTime) && resumeTime > 0) {
34
+ try {
35
+ video.currentTime = resumeTime;
36
+ } catch {
37
+ }
38
+ }
39
+ if (shouldResume) video.play().catch(() => {
40
+ });
41
+ };
42
+ video.addEventListener("loadedmetadata", restoreListener, { once: true });
43
+ video.src = currentUrl;
44
+ video.load();
45
+ schedule();
46
+ return true;
47
+ } catch {
48
+ schedule(TOKEN_REFRESH_RETRY_MS);
49
+ return false;
50
+ } finally {
51
+ refreshInFlight = null;
52
+ }
53
+ })();
54
+ return refreshInFlight;
55
+ };
56
+ schedule();
57
+ return {
58
+ refreshNow,
59
+ stop: () => {
60
+ disposed = true;
61
+ if (timer) clearTimeout(timer);
62
+ if (restoreListener) video.removeEventListener("loadedmetadata", restoreListener);
63
+ }
64
+ };
65
+ }
66
+
67
+ // src/vanilla/player.ts
68
+ function createVexorPlayer(target, options) {
69
+ const container = typeof target === "string" ? document.querySelector(target) : target;
70
+ if (!container) throw new Error(`Target container not found: ${String(target)}`);
71
+ 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)}` : ""}` : "");
72
+ if (!initialUrl) throw new Error("Provide either src or assetId to create the Vexor player.");
73
+ const hadPlayerClass = container.classList.contains("vexor-player");
74
+ const hadProtectedClass = container.classList.contains("is-protected");
75
+ container.classList.add("vexor-player");
76
+ const video = document.createElement("video");
77
+ video.className = "vexor-video";
78
+ video.controls = options.controls ?? true;
79
+ video.playsInline = true;
80
+ video.preload = options.preload ?? "metadata";
81
+ if (options.poster) video.poster = options.poster;
82
+ if (options.autoPlay) video.autoplay = true;
83
+ if (options.muted) video.muted = true;
84
+ if (options.loop) video.loop = true;
85
+ container.appendChild(video);
86
+ let watermarkElement = null;
87
+ let watermarkTimer;
88
+ if (options.watermark) {
89
+ 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 };
90
+ watermarkElement = document.createElement("div");
91
+ watermarkElement.className = "vexor-watermark";
92
+ watermarkElement.textContent = watermark.text;
93
+ watermarkElement.style.opacity = String(Math.max(0, Math.min(1, watermark.opacity ?? 0.35)));
94
+ watermarkElement.style.fontSize = typeof watermark.fontSize === "number" ? `${watermark.fontSize}px` : watermark.fontSize ?? "12px";
95
+ watermarkElement.style.color = watermark.color ?? "#ffffff";
96
+ const placeWatermark = () => {
97
+ watermarkElement.style.removeProperty("top");
98
+ watermarkElement.style.removeProperty("right");
99
+ watermarkElement.style.removeProperty("bottom");
100
+ watermarkElement.style.removeProperty("left");
101
+ watermarkElement.style.removeProperty("transform");
102
+ switch (watermark.position) {
103
+ case "top-left":
104
+ watermarkElement.style.cssText += ";top:16px;left:16px";
105
+ break;
106
+ case "top-right":
107
+ watermarkElement.style.cssText += ";top:16px;right:16px";
108
+ break;
109
+ case "bottom-left":
110
+ watermarkElement.style.cssText += ";bottom:48px;left:16px";
111
+ break;
112
+ case "bottom-right":
113
+ watermarkElement.style.cssText += ";bottom:48px;right:16px";
114
+ break;
115
+ case "center":
116
+ watermarkElement.style.cssText += ";top:50%;left:50%;transform:translate(-50%,-50%)";
117
+ break;
118
+ default:
119
+ watermarkElement.style.top = `${10 + Math.floor(Math.random() * 70)}%`;
120
+ watermarkElement.style.left = `${10 + Math.floor(Math.random() * 70)}%`;
121
+ }
122
+ };
123
+ placeWatermark();
124
+ if (watermark.position === "dynamic") {
125
+ watermarkTimer = setInterval(placeWatermark, Math.max(2, watermark.intervalSeconds || 15) * 1e3);
126
+ }
127
+ container.appendChild(watermarkElement);
128
+ }
129
+ const onContextMenu = (event) => event.preventDefault();
130
+ const onDragStart = (event) => event.preventDefault();
131
+ if (options.protect) {
132
+ container.classList.add("is-protected");
133
+ container.addEventListener("contextmenu", onContextMenu);
134
+ video.addEventListener("contextmenu", onContextMenu);
135
+ video.addEventListener("dragstart", onDragStart);
136
+ }
137
+ let activeUrl = initialUrl;
138
+ let hls = null;
139
+ let nativeTokenRefresh = null;
140
+ let nativeErrorListener;
141
+ let destroyed = false;
142
+ let refreshPromise = null;
143
+ let resumeAfterRefresh = null;
144
+ let hasSignaledReady = false;
145
+ let activeParams = new URL(activeUrl, document.baseURI).search;
146
+ const useFallback = (error) => {
147
+ if (options.fallbackSrc) {
148
+ if (hls) hls.destroy();
149
+ hls = null;
150
+ video.src = options.fallbackSrc;
151
+ video.load();
152
+ } else if (error) options.onError?.(error);
153
+ };
154
+ const signalReady = () => {
155
+ if (hasSignaledReady) return;
156
+ hasSignaledReady = true;
157
+ options.onReady?.();
158
+ };
159
+ const onLoaded = () => {
160
+ signalReady();
161
+ const resume = resumeAfterRefresh;
162
+ resumeAfterRefresh = null;
163
+ if (resume && Number.isFinite(resume.time)) video.currentTime = resume.time;
164
+ if ((resume?.shouldPlay || options.autoPlay) && !destroyed) video.play().catch(() => {
165
+ });
166
+ };
167
+ video.addEventListener("loadedmetadata", onLoaded);
168
+ video.addEventListener("loadeddata", onLoaded);
169
+ const onPlay = () => options.onPlay?.();
170
+ const onPause = () => options.onPause?.();
171
+ const onEnded = () => options.onEnded?.();
172
+ video.addEventListener("play", onPlay);
173
+ video.addEventListener("pause", onPause);
174
+ video.addEventListener("ended", onEnded);
175
+ const onTimeUpdate = () => options.onTimeUpdate?.(video.currentTime, video.duration);
176
+ video.addEventListener("timeupdate", onTimeUpdate);
177
+ const isHlsUrl = activeUrl.includes(".m3u8") || activeUrl.includes("/playback/");
178
+ if (isHlsUrl && Hls.isSupported()) {
179
+ class TokenAwareLoader extends Hls.DefaultConfig.loader {
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ load(context, config, callbacks) {
182
+ if (activeParams && context?.url) {
183
+ try {
184
+ const sourceUrl = new URL(activeUrl, document.baseURI);
185
+ const childUrl = new URL(context.url, sourceUrl);
186
+ if (childUrl.origin === sourceUrl.origin && !childUrl.searchParams.has("token")) {
187
+ new URLSearchParams(activeParams).forEach((value, key) => {
188
+ if (!childUrl.searchParams.has(key)) childUrl.searchParams.set(key, value);
189
+ });
190
+ context.url = childUrl.toString();
191
+ }
192
+ } catch {
193
+ }
194
+ }
195
+ super.load(context, config, callbacks);
196
+ }
197
+ }
198
+ hls = new Hls({ enableWorker: true, loader: TokenAwareLoader, fLoader: TokenAwareLoader, pLoader: TokenAwareLoader });
199
+ const activeHls = hls;
200
+ activeHls.on(Hls.Events.MANIFEST_PARSED, () => {
201
+ if (hls !== activeHls || destroyed) return;
202
+ const resume = resumeAfterRefresh;
203
+ resumeAfterRefresh = null;
204
+ signalReady();
205
+ if (resume && Number.isFinite(resume.time)) video.currentTime = resume.time;
206
+ if ((resume?.shouldPlay || options.autoPlay) && !destroyed) video.play().catch(() => {
207
+ });
208
+ });
209
+ activeHls.on(Hls.Events.ERROR, async (_event, data) => {
210
+ if (hls !== activeHls || destroyed) return;
211
+ if ((data.response?.code === 401 || data.response?.code === 403) && options.refreshToken) {
212
+ try {
213
+ refreshPromise ??= options.refreshToken();
214
+ const newToken = await refreshPromise;
215
+ refreshPromise = null;
216
+ if (newToken && hls === activeHls && !destroyed) {
217
+ resumeAfterRefresh = { time: video.currentTime, shouldPlay: !video.paused };
218
+ const url = new URL(activeUrl, document.baseURI);
219
+ url.searchParams.set("token", newToken);
220
+ activeUrl = url.toString();
221
+ activeParams = url.search;
222
+ activeHls.loadSource(activeUrl);
223
+ activeHls.startLoad();
224
+ return;
225
+ }
226
+ } catch {
227
+ refreshPromise = null;
228
+ }
229
+ }
230
+ if (hls !== activeHls || destroyed) return;
231
+ if (data.fatal) useFallback({ code: data.details || "MEDIA_PLAYBACK_ERROR", message: `Failed to stream video: ${data.details || "network error"}`, fatal: true });
232
+ });
233
+ activeHls.loadSource(activeUrl);
234
+ activeHls.attachMedia(video);
235
+ } else if (isHlsUrl && video.canPlayType("application/vnd.apple.mpegurl")) {
236
+ video.src = activeUrl;
237
+ nativeTokenRefresh = startNativeTokenRefresh(video, activeUrl, options.refreshToken);
238
+ nativeErrorListener = () => {
239
+ void nativeTokenRefresh?.refreshNow().then((refreshed) => {
240
+ if (refreshed || destroyed) return;
241
+ video.removeEventListener("error", nativeErrorListener);
242
+ useFallback({ code: "NATIVE_HLS_ERROR", message: "Failed to play native HLS stream", fatal: true });
243
+ });
244
+ };
245
+ video.addEventListener("error", nativeErrorListener);
246
+ } else if (!isHlsUrl || options.fallbackSrc) {
247
+ video.src = isHlsUrl ? options.fallbackSrc : activeUrl;
248
+ } else {
249
+ useFallback({ code: "HLS_UNSUPPORTED", message: "This browser does not support HLS playback.", fatal: true });
250
+ }
251
+ return {
252
+ play: () => video.play(),
253
+ pause: () => video.pause(),
254
+ seek: (seconds) => {
255
+ video.currentTime = Math.max(0, Math.min(seconds, video.duration || seconds));
256
+ },
257
+ destroy: () => {
258
+ if (destroyed) return;
259
+ destroyed = true;
260
+ nativeTokenRefresh?.stop();
261
+ if (hls) hls.destroy();
262
+ hls = null;
263
+ if (watermarkTimer) clearInterval(watermarkTimer);
264
+ container.removeEventListener("contextmenu", onContextMenu);
265
+ video.removeEventListener("contextmenu", onContextMenu);
266
+ video.removeEventListener("dragstart", onDragStart);
267
+ video.removeEventListener("loadedmetadata", onLoaded);
268
+ video.removeEventListener("loadeddata", onLoaded);
269
+ video.removeEventListener("play", onPlay);
270
+ video.removeEventListener("pause", onPause);
271
+ video.removeEventListener("ended", onEnded);
272
+ video.removeEventListener("timeupdate", onTimeUpdate);
273
+ if (nativeErrorListener) video.removeEventListener("error", nativeErrorListener);
274
+ video.pause();
275
+ video.removeAttribute("src");
276
+ video.load();
277
+ video.remove();
278
+ watermarkElement?.remove();
279
+ if (!hadPlayerClass) container.classList.remove("vexor-player");
280
+ if (options.protect && !hadProtectedClass) container.classList.remove("is-protected");
281
+ },
282
+ getElement: () => video
283
+ };
284
+ }
285
+
286
+ export {
287
+ startNativeTokenRefresh,
288
+ createVexorPlayer
289
+ };
290
+ //# sourceMappingURL=chunk-NBWDQALI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vanilla/player.ts","../src/native-token-refresh.ts"],"sourcesContent":["import Hls from \"hls.js\";\nimport type { VexorVanillaPlayerOptions } from \"./types\";\nimport { startNativeTokenRefresh } from \"../native-token-refresh\";\n\nexport interface VanillaPlayerInstance {\n play: () => Promise<void>;\n pause: () => void;\n seek: (seconds: number) => void;\n destroy: () => void;\n getElement: () => HTMLVideoElement;\n}\n\n/** Mount the Vexor player into an element without requiring React. */\nexport function createVexorPlayer(\n target: HTMLElement | string,\n options: VexorVanillaPlayerOptions\n): VanillaPlayerInstance {\n const container = typeof target === \"string\" ? document.querySelector<HTMLElement>(target) : target;\n if (!container) throw new Error(`Target container not found: ${String(target)}`);\n const initialUrl = options.src ?? (options.assetId\n ? `${(options.baseUrl || \"https://api.vexor.et\").replace(/\\/+$/, \"\")}/v1/playback/${encodeURIComponent(options.assetId)}/master.m3u8${options.token ? `?token=${encodeURIComponent(options.token)}` : \"\"}`\n : \"\");\n if (!initialUrl) throw new Error(\"Provide either src or assetId to create the Vexor player.\");\n\n const hadPlayerClass = container.classList.contains(\"vexor-player\");\n const hadProtectedClass = container.classList.contains(\"is-protected\");\n container.classList.add(\"vexor-player\");\n\n const video = document.createElement(\"video\");\n video.className = \"vexor-video\";\n video.controls = options.controls ?? true;\n video.playsInline = true;\n video.preload = options.preload ?? \"metadata\";\n if (options.poster) video.poster = options.poster;\n if (options.autoPlay) video.autoplay = true;\n if (options.muted) video.muted = true;\n if (options.loop) video.loop = true;\n container.appendChild(video);\n\n let watermarkElement: HTMLDivElement | null = null;\n let watermarkTimer: ReturnType<typeof setInterval> | undefined;\n if (options.watermark) {\n const watermark = typeof options.watermark === \"string\"\n ? { text: options.watermark, position: \"dynamic\" as const, intervalSeconds: 15, opacity: 0.35 }\n : { position: \"dynamic\" as const, intervalSeconds: 15, opacity: 0.35, ...options.watermark };\n watermarkElement = document.createElement(\"div\");\n watermarkElement.className = \"vexor-watermark\";\n watermarkElement.textContent = watermark.text;\n watermarkElement.style.opacity = String(Math.max(0, Math.min(1, watermark.opacity ?? 0.35)));\n watermarkElement.style.fontSize = typeof watermark.fontSize === \"number\" ? `${watermark.fontSize}px` : watermark.fontSize ?? \"12px\";\n watermarkElement.style.color = watermark.color ?? \"#ffffff\";\n\n const placeWatermark = () => {\n watermarkElement!.style.removeProperty(\"top\");\n watermarkElement!.style.removeProperty(\"right\");\n watermarkElement!.style.removeProperty(\"bottom\");\n watermarkElement!.style.removeProperty(\"left\");\n watermarkElement!.style.removeProperty(\"transform\");\n switch (watermark.position) {\n case \"top-left\": watermarkElement!.style.cssText += \";top:16px;left:16px\"; break;\n case \"top-right\": watermarkElement!.style.cssText += \";top:16px;right:16px\"; break;\n case \"bottom-left\": watermarkElement!.style.cssText += \";bottom:48px;left:16px\"; break;\n case \"bottom-right\": watermarkElement!.style.cssText += \";bottom:48px;right:16px\"; break;\n case \"center\": watermarkElement!.style.cssText += \";top:50%;left:50%;transform:translate(-50%,-50%)\"; break;\n default:\n watermarkElement!.style.top = `${10 + Math.floor(Math.random() * 70)}%`;\n watermarkElement!.style.left = `${10 + Math.floor(Math.random() * 70)}%`;\n }\n };\n placeWatermark();\n if (watermark.position === \"dynamic\") {\n watermarkTimer = setInterval(placeWatermark, Math.max(2, watermark.intervalSeconds || 15) * 1000);\n }\n container.appendChild(watermarkElement);\n }\n\n const onContextMenu = (event: Event) => event.preventDefault();\n const onDragStart = (event: Event) => event.preventDefault();\n if (options.protect) {\n container.classList.add(\"is-protected\");\n container.addEventListener(\"contextmenu\", onContextMenu);\n video.addEventListener(\"contextmenu\", onContextMenu);\n video.addEventListener(\"dragstart\", onDragStart);\n }\n\n let activeUrl = initialUrl;\n\n let hls: Hls | null = null;\n let nativeTokenRefresh: ReturnType<typeof startNativeTokenRefresh> | null = null;\n let nativeErrorListener: (() => void) | undefined;\n let destroyed = false;\n let refreshPromise: Promise<string> | null = null;\n let resumeAfterRefresh: { time: number; shouldPlay: boolean } | null = null;\n let hasSignaledReady = false;\n let activeParams = new URL(activeUrl, document.baseURI).search;\n const useFallback = (error?: { code: string; message: string; fatal: boolean }) => {\n if (options.fallbackSrc) {\n if (hls) hls.destroy();\n hls = null;\n video.src = options.fallbackSrc;\n video.load();\n } else if (error) options.onError?.(error);\n };\n const signalReady = () => {\n if (hasSignaledReady) return;\n hasSignaledReady = true;\n options.onReady?.();\n };\n const onLoaded = () => {\n signalReady();\n const resume = resumeAfterRefresh;\n resumeAfterRefresh = null;\n if (resume && Number.isFinite(resume.time)) video.currentTime = resume.time;\n if ((resume?.shouldPlay || options.autoPlay) && !destroyed) video.play().catch(() => {});\n };\n\n video.addEventListener(\"loadedmetadata\", onLoaded);\n video.addEventListener(\"loadeddata\", onLoaded);\n const onPlay = () => options.onPlay?.();\n const onPause = () => options.onPause?.();\n const onEnded = () => options.onEnded?.();\n video.addEventListener(\"play\", onPlay);\n video.addEventListener(\"pause\", onPause);\n video.addEventListener(\"ended\", onEnded);\n const onTimeUpdate = () => options.onTimeUpdate?.(video.currentTime, video.duration);\n video.addEventListener(\"timeupdate\", onTimeUpdate);\n\n const isHlsUrl = activeUrl.includes(\".m3u8\") || activeUrl.includes(\"/playback/\");\n if (isHlsUrl && Hls.isSupported()) {\n class TokenAwareLoader extends Hls.DefaultConfig.loader {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n load(context: any, config: any, callbacks: any) {\n if (activeParams && context?.url) {\n try {\n const sourceUrl = new URL(activeUrl, document.baseURI);\n const childUrl = new URL(context.url, sourceUrl);\n if (childUrl.origin === sourceUrl.origin && !childUrl.searchParams.has(\"token\")) {\n new URLSearchParams(activeParams).forEach((value, key) => {\n if (!childUrl.searchParams.has(key)) childUrl.searchParams.set(key, value);\n });\n context.url = childUrl.toString();\n }\n } catch {\n // Leave an unparseable child URL unchanged.\n }\n }\n super.load(context, config, callbacks);\n }\n }\n hls = new Hls({ enableWorker: true, loader: TokenAwareLoader as any, fLoader: TokenAwareLoader as any, pLoader: TokenAwareLoader as any });\n const activeHls = hls;\n activeHls.on(Hls.Events.MANIFEST_PARSED, () => {\n if (hls !== activeHls || destroyed) return;\n const resume = resumeAfterRefresh;\n resumeAfterRefresh = null;\n signalReady();\n if (resume && Number.isFinite(resume.time)) video.currentTime = resume.time;\n if ((resume?.shouldPlay || options.autoPlay) && !destroyed) video.play().catch(() => {});\n });\n activeHls.on(Hls.Events.ERROR, async (_event, data) => {\n if (hls !== activeHls || destroyed) return;\n if ((data.response?.code === 401 || data.response?.code === 403) && options.refreshToken) {\n try {\n refreshPromise ??= options.refreshToken();\n const newToken = await refreshPromise;\n refreshPromise = null;\n if (newToken && hls === activeHls && !destroyed) {\n resumeAfterRefresh = { time: video.currentTime, shouldPlay: !video.paused };\n const url = new URL(activeUrl, document.baseURI);\n url.searchParams.set(\"token\", newToken);\n activeUrl = url.toString();\n activeParams = url.search;\n activeHls.loadSource(activeUrl);\n activeHls.startLoad();\n return;\n }\n } catch {\n refreshPromise = null;\n }\n }\n if (hls !== activeHls || destroyed) return;\n if (data.fatal) useFallback({ code: data.details || \"MEDIA_PLAYBACK_ERROR\", message: `Failed to stream video: ${data.details || \"network error\"}`, fatal: true });\n });\n activeHls.loadSource(activeUrl);\n activeHls.attachMedia(video);\n } else if (isHlsUrl && video.canPlayType(\"application/vnd.apple.mpegurl\")) {\n video.src = activeUrl;\n nativeTokenRefresh = startNativeTokenRefresh(video, activeUrl, options.refreshToken);\n nativeErrorListener = () => {\n void nativeTokenRefresh?.refreshNow().then((refreshed) => {\n if (refreshed || destroyed) return;\n video.removeEventListener(\"error\", nativeErrorListener!);\n useFallback({ code: \"NATIVE_HLS_ERROR\", message: \"Failed to play native HLS stream\", fatal: true });\n });\n };\n video.addEventListener(\"error\", nativeErrorListener);\n } else if (!isHlsUrl || options.fallbackSrc) {\n video.src = isHlsUrl ? options.fallbackSrc! : activeUrl;\n } else {\n useFallback({ code: \"HLS_UNSUPPORTED\", message: \"This browser does not support HLS playback.\", fatal: true });\n }\n\n return {\n play: () => video.play(),\n pause: () => video.pause(),\n seek: (seconds) => { video.currentTime = Math.max(0, Math.min(seconds, video.duration || seconds)); },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n nativeTokenRefresh?.stop();\n if (hls) hls.destroy();\n hls = null;\n if (watermarkTimer) clearInterval(watermarkTimer);\n container.removeEventListener(\"contextmenu\", onContextMenu);\n video.removeEventListener(\"contextmenu\", onContextMenu);\n video.removeEventListener(\"dragstart\", onDragStart);\n video.removeEventListener(\"loadedmetadata\", onLoaded);\n video.removeEventListener(\"loadeddata\", onLoaded);\n video.removeEventListener(\"play\", onPlay);\n video.removeEventListener(\"pause\", onPause);\n video.removeEventListener(\"ended\", onEnded);\n video.removeEventListener(\"timeupdate\", onTimeUpdate);\n if (nativeErrorListener) video.removeEventListener(\"error\", nativeErrorListener);\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n video.remove();\n watermarkElement?.remove();\n if (!hadPlayerClass) container.classList.remove(\"vexor-player\");\n if (options.protect && !hadProtectedClass) container.classList.remove(\"is-protected\");\n },\n getElement: () => video,\n };\n}\n","const TOKEN_REFRESH_INTERVAL_MS = 4 * 60 * 1000;\nconst TOKEN_REFRESH_RETRY_MS = 30 * 1000;\n\n/**\n * Refresh native-HLS playback before Vexor's five-minute token expires.\n * Browsers do not expose the native HLS request status to JavaScript, so this\n * timer complements the reactive 401/403 handling used by hls.js.\n */\nexport function startNativeTokenRefresh(\n video: HTMLVideoElement,\n initialUrl: string,\n refreshToken?: () => Promise<string>,\n): { refreshNow: () => Promise<boolean>; stop: () => void } {\n let currentUrl = initialUrl;\n let disposed = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let refreshInFlight: Promise<boolean> | null = null;\n let restoreListener: (() => void) | undefined;\n\n const schedule = (delay = TOKEN_REFRESH_INTERVAL_MS) => {\n if (!refreshToken || disposed) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void refreshNow(), delay);\n };\n\n const refreshNow = (): Promise<boolean> => {\n if (!refreshToken || disposed) return Promise.resolve(false);\n if (refreshInFlight) return refreshInFlight;\n\n refreshInFlight = (async () => {\n try {\n const token = await refreshToken();\n if (disposed || !token) return false;\n\n const nextUrl = new URL(currentUrl, document.baseURI);\n nextUrl.searchParams.set(\"token\", token);\n currentUrl = nextUrl.toString();\n\n const resumeTime = video.currentTime;\n const shouldResume = !video.paused;\n restoreListener = () => {\n restoreListener = undefined;\n if (disposed) return;\n if (Number.isFinite(resumeTime) && resumeTime > 0) {\n try {\n video.currentTime = resumeTime;\n } catch {\n // The new manifest may not have loaded enough media to seek yet.\n }\n }\n if (shouldResume) video.play().catch(() => {});\n };\n video.addEventListener(\"loadedmetadata\", restoreListener, { once: true });\n video.src = currentUrl;\n video.load();\n schedule();\n return true;\n } catch {\n // Retry soon; do not keep using a token that is about to expire.\n schedule(TOKEN_REFRESH_RETRY_MS);\n return false;\n } finally {\n refreshInFlight = null;\n }\n })();\n\n return refreshInFlight;\n };\n\n schedule();\n\n return {\n refreshNow,\n stop: () => {\n disposed = true;\n if (timer) clearTimeout(timer);\n if (restoreListener) video.removeEventListener(\"loadedmetadata\", restoreListener);\n },\n };\n}\n"],"mappings":"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","names":[]}