@xhub-short/ui 0.1.0-beta.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.
@@ -0,0 +1,1110 @@
1
+ import { cn } from './chunk-WKX2WBVO.js';
2
+ import { injectComponentCSS } from './chunk-UXMA4KJZ.js';
3
+ import { useRef, useCallback, useEffect, useState, useInsertionEffect, useMemo } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ // src/components/VideoPlayer/VideoPlayer.css.ts
7
+ var VIDEO_PLAYER_CSS = (
8
+ /* css */
9
+ `
10
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11
+ VideoPlayer Container
12
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
13
+
14
+ .sv-video-player {
15
+ position: relative;
16
+ width: 100%;
17
+ height: 100%;
18
+ overflow: hidden;
19
+ background: var(--sv-bg-primary, #000);
20
+ /* CSS variable z-index defaults */
21
+ --sv-player-video-z: 1;
22
+ --sv-player-poster-z: 2;
23
+ --sv-player-loading-z: 3;
24
+ --sv-player-error-z: 4;
25
+ }
26
+
27
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
28
+ Video Element Wrapper
29
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
30
+
31
+ .sv-video-player__video-wrapper {
32
+ position: absolute;
33
+ inset: 0;
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ z-index: var(--sv-player-video-z);
38
+ }
39
+
40
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
41
+ Video Element
42
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
43
+
44
+ .sv-video-player__video {
45
+ width: 100%;
46
+ height: 100%;
47
+ object-fit: var(--sv-player-object-fit, contain);
48
+ background: transparent;
49
+ /* Prevent iOS controls */
50
+ -webkit-playsinline: true;
51
+ }
52
+
53
+ /* Hide video when loading/error for smooth poster display */
54
+ .sv-video-player--loading .sv-video-player__video,
55
+ .sv-video-player--error .sv-video-player__video {
56
+ opacity: 0;
57
+ }
58
+
59
+ /* Smooth transition for video visibility */
60
+ .sv-video-player__video {
61
+ transition: opacity var(--sv-transition-duration, 200ms) ease-out;
62
+ }
63
+
64
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
65
+ Poster Overlay
66
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
67
+
68
+ .sv-video-player__poster {
69
+ position: absolute;
70
+ inset: 0;
71
+ z-index: var(--sv-player-poster-z);
72
+ background-size: cover;
73
+ background-position: center;
74
+ background-repeat: no-repeat;
75
+ transition: opacity var(--sv-transition-duration, 200ms) ease-out;
76
+ }
77
+
78
+ /**
79
+ * Hide poster when video is ready OR playing.
80
+ *
81
+ * Poster visibility rules:
82
+ * - SHOW: During initial loading (--loading)
83
+ * - HIDE: Once video is ready (--ready) - even if paused
84
+ * - HIDE: When playing (--playing)
85
+ * - STAY HIDDEN: During buffering (--buffering) - video already visible
86
+ *
87
+ * pointer-events: none ensures poster doesn't block clicks on video.
88
+ */
89
+ .sv-video-player--ready .sv-video-player__poster,
90
+ .sv-video-player--playing .sv-video-player__poster {
91
+ opacity: 0;
92
+ pointer-events: none;
93
+ }
94
+
95
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
96
+ Loading Spinner
97
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
98
+
99
+ .sv-video-player__loading {
100
+ position: absolute;
101
+ inset: 0;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ z-index: var(--sv-player-loading-z);
106
+ background: var(--sv-loading-bg, rgba(0, 0, 0, 0.3));
107
+ opacity: 0;
108
+ pointer-events: none;
109
+ transition: opacity var(--sv-transition-duration, 200ms) ease-out;
110
+ }
111
+
112
+ .sv-video-player--loading .sv-video-player__loading {
113
+ opacity: 1;
114
+ pointer-events: auto;
115
+ }
116
+
117
+ /* Default spinner animation */
118
+ .sv-video-player__spinner {
119
+ width: var(--sv-spinner-size, 40px);
120
+ height: var(--sv-spinner-size, 40px);
121
+ border: 3px solid var(--sv-spinner-color, rgba(255, 255, 255, 0.3));
122
+ border-top-color: var(--sv-spinner-active-color, #fff);
123
+ border-radius: 50%;
124
+ animation: sv-player-spin 0.8s linear infinite;
125
+ }
126
+
127
+ @keyframes sv-player-spin {
128
+ from {
129
+ transform: rotate(0deg);
130
+ }
131
+ to {
132
+ transform: rotate(360deg);
133
+ }
134
+ }
135
+
136
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
137
+ Error State
138
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
139
+
140
+ .sv-video-player__error {
141
+ position: absolute;
142
+ inset: 0;
143
+ display: flex;
144
+ flex-direction: column;
145
+ align-items: center;
146
+ justify-content: center;
147
+ gap: var(--sv-spacing-md, 12px);
148
+ z-index: var(--sv-player-error-z);
149
+ background: var(--sv-error-bg, rgba(0, 0, 0, 0.8));
150
+ color: var(--sv-error-color, #fff);
151
+ opacity: 0;
152
+ pointer-events: none;
153
+ transition: opacity var(--sv-transition-duration, 200ms) ease-out;
154
+ }
155
+
156
+ .sv-video-player--error .sv-video-player__error {
157
+ opacity: 1;
158
+ pointer-events: auto;
159
+ }
160
+
161
+ .sv-video-player__error-icon {
162
+ width: var(--sv-error-icon-size, 48px);
163
+ height: var(--sv-error-icon-size, 48px);
164
+ color: var(--sv-error-icon-color, rgba(255, 255, 255, 0.7));
165
+ }
166
+
167
+ .sv-video-player__error-message {
168
+ font-size: var(--sv-error-font-size, 14px);
169
+ color: var(--sv-error-text-color, rgba(255, 255, 255, 0.8));
170
+ text-align: center;
171
+ max-width: 80%;
172
+ }
173
+
174
+ .sv-video-player__retry-btn {
175
+ padding: var(--sv-spacing-sm, 8px) var(--sv-spacing-lg, 16px);
176
+ background: var(--sv-btn-bg, rgba(255, 255, 255, 0.2));
177
+ border: 1px solid var(--sv-btn-border, rgba(255, 255, 255, 0.3));
178
+ border-radius: var(--sv-btn-radius, 6px);
179
+ color: var(--sv-btn-color, #fff);
180
+ font-size: var(--sv-btn-font-size, 14px);
181
+ cursor: pointer;
182
+ transition: background var(--sv-transition-duration, 200ms) ease;
183
+ }
184
+
185
+ .sv-video-player__retry-btn:hover {
186
+ background: var(--sv-btn-hover-bg, rgba(255, 255, 255, 0.3));
187
+ }
188
+
189
+ .sv-video-player__retry-btn:active {
190
+ transform: scale(0.98);
191
+ }
192
+
193
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
194
+ Accessibility - Reduced Motion
195
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
196
+
197
+ @media (prefers-reduced-motion: reduce) {
198
+ .sv-video-player__video,
199
+ .sv-video-player__poster,
200
+ .sv-video-player__loading,
201
+ .sv-video-player__error {
202
+ transition: none;
203
+ }
204
+
205
+ .sv-video-player__spinner {
206
+ animation: none;
207
+ border-top-color: var(--sv-spinner-active-color, #fff);
208
+ }
209
+ }
210
+ `
211
+ );
212
+
213
+ // src/components/VideoPlayer/constants.ts
214
+ var VIDEO_TYPE_ATTR = "data-video-type";
215
+ var PLAYBACK_STATE_ATTR = "data-playback-state";
216
+ var LOADING_STATE_ATTR = "data-loading";
217
+ var PLAYER_CLASS = "sv-video-player";
218
+ var VIDEO_WRAPPER_CLASS = "sv-video-player__video-wrapper";
219
+ var VIDEO_CLASS = "sv-video-player__video";
220
+ var POSTER_CLASS = "sv-video-player__poster";
221
+ var LOADING_CLASS = "sv-video-player__loading";
222
+ var ERROR_CLASS = "sv-video-player__error";
223
+ var PLAYING_CLASS = "sv-video-player--playing";
224
+ var PAUSED_CLASS = "sv-video-player--paused";
225
+ var LOADING_STATE_CLASS = "sv-video-player--loading";
226
+ var BUFFERING_STATE_CLASS = "sv-video-player--buffering";
227
+ var ERROR_STATE_CLASS = "sv-video-player--error";
228
+ var READY_CLASS = "sv-video-player--ready";
229
+ var ENDED_CLASS = "sv-video-player--ended";
230
+ var Z_INDEX = {
231
+ VIDEO: 1,
232
+ POSTER: 2,
233
+ LOADING: 3,
234
+ ERROR: 4
235
+ };
236
+ var Z_INDEX_CSS_VARS = {
237
+ VIDEO: "--sv-player-video-z",
238
+ POSTER: "--sv-player-poster-z",
239
+ LOADING: "--sv-player-loading-z",
240
+ ERROR: "--sv-player-error-z"
241
+ };
242
+ var DEFAULT_PRELOAD = "metadata";
243
+ var DEFAULT_OBJECT_FIT = "cover";
244
+ var FIRST_FRAME_QUALITY = 0.8;
245
+ var FIRST_FRAME_MAX_WIDTH = 360;
246
+
247
+ // src/services/FirstFrameCache.ts
248
+ var DEFAULT_CONFIG = {
249
+ maxSize: 10,
250
+ quality: 0.8,
251
+ maxWidth: 360,
252
+ debugCORS: false
253
+ };
254
+ var FirstFrameCacheService = class {
255
+ constructor(config = {}) {
256
+ this.cache = /* @__PURE__ */ new Map();
257
+ // In-flight captures to prevent duplicate work
258
+ this.inFlight = /* @__PURE__ */ new Map();
259
+ // Stats tracking
260
+ this._hits = 0;
261
+ this._misses = 0;
262
+ this.config = { ...DEFAULT_CONFIG, ...config };
263
+ }
264
+ /**
265
+ * Get cache statistics
266
+ */
267
+ get stats() {
268
+ return {
269
+ hits: this._hits,
270
+ misses: this._misses,
271
+ size: this.cache.size,
272
+ inFlight: this.inFlight.size
273
+ };
274
+ }
275
+ /**
276
+ * Capture the first frame of a video element
277
+ *
278
+ * Features:
279
+ * - Deduplicates concurrent capture requests for same video
280
+ * - Waits for video data if not ready
281
+ * - Resizes to maxWidth maintaining aspect ratio
282
+ *
283
+ * @param videoId - Unique identifier for the video
284
+ * @param videoElement - The video element to capture from
285
+ * @returns Data URL of the captured frame, or null if capture failed
286
+ */
287
+ async capture(videoId, videoElement) {
288
+ if (typeof document === "undefined" || typeof window === "undefined") {
289
+ return null;
290
+ }
291
+ const existing = this.get(videoId);
292
+ if (existing) {
293
+ return existing;
294
+ }
295
+ const inFlightPromise = this.inFlight.get(videoId);
296
+ if (inFlightPromise) {
297
+ return inFlightPromise;
298
+ }
299
+ const capturePromise = this.doCapture(videoId, videoElement);
300
+ this.inFlight.set(videoId, capturePromise);
301
+ try {
302
+ return await capturePromise;
303
+ } finally {
304
+ this.inFlight.delete(videoId);
305
+ }
306
+ }
307
+ /**
308
+ * Internal capture implementation
309
+ */
310
+ async doCapture(videoId, videoElement) {
311
+ try {
312
+ if (videoElement.readyState < 2) {
313
+ await this.waitForVideoData(videoElement);
314
+ }
315
+ const { videoWidth, videoHeight } = videoElement;
316
+ if (!videoWidth || !videoHeight) {
317
+ return null;
318
+ }
319
+ const aspectRatio = videoHeight / videoWidth;
320
+ const width = Math.min(videoWidth, this.config.maxWidth);
321
+ const height = Math.round(width * aspectRatio);
322
+ const canvas = document.createElement("canvas");
323
+ canvas.width = width;
324
+ canvas.height = height;
325
+ const ctx = canvas.getContext("2d");
326
+ if (!ctx) {
327
+ return null;
328
+ }
329
+ ctx.drawImage(videoElement, 0, 0, width, height);
330
+ const dataUrl = canvas.toDataURL("image/jpeg", this.config.quality);
331
+ this.set(videoId, dataUrl);
332
+ return dataUrl;
333
+ } catch (error) {
334
+ if (this.config.debugCORS && error instanceof DOMException && error.name === "SecurityError") {
335
+ console.warn(
336
+ `[FirstFrameCache] CORS error capturing frame for "${videoId}".`,
337
+ 'Ensure video has crossorigin="anonymous" and server sends CORS headers.',
338
+ error
339
+ );
340
+ }
341
+ return null;
342
+ }
343
+ }
344
+ /**
345
+ * Get a cached frame
346
+ *
347
+ * Note: This updates LRU access time (get = read = access)
348
+ *
349
+ * @param videoId - Video identifier
350
+ * @returns Data URL or null if not cached
351
+ */
352
+ get(videoId) {
353
+ const entry = this.cache.get(videoId);
354
+ if (!entry) {
355
+ this._misses++;
356
+ return null;
357
+ }
358
+ this._hits++;
359
+ this.cache.delete(videoId);
360
+ this.cache.set(videoId, entry);
361
+ return entry.dataUrl;
362
+ }
363
+ /**
364
+ * Check if a frame is cached
365
+ *
366
+ * Note: This does NOT update LRU access time (check-only operation).
367
+ * Use get() if you want to mark the entry as accessed.
368
+ *
369
+ * @param videoId - Video identifier
370
+ */
371
+ has(videoId) {
372
+ return this.cache.has(videoId);
373
+ }
374
+ /**
375
+ * Manually set a cached frame
376
+ *
377
+ * @param videoId - Video identifier
378
+ * @param dataUrl - Data URL of the frame
379
+ */
380
+ set(videoId, dataUrl) {
381
+ if (this.cache.has(videoId)) {
382
+ this.cache.delete(videoId);
383
+ }
384
+ if (this.cache.size >= this.config.maxSize) {
385
+ this.evictLRU();
386
+ }
387
+ this.cache.set(videoId, { dataUrl });
388
+ }
389
+ /**
390
+ * Remove a cached frame
391
+ *
392
+ * @param videoId - Video identifier
393
+ */
394
+ delete(videoId) {
395
+ return this.cache.delete(videoId);
396
+ }
397
+ /**
398
+ * Clear all cached frames
399
+ */
400
+ clear() {
401
+ this.cache.clear();
402
+ this._hits = 0;
403
+ this._misses = 0;
404
+ }
405
+ /**
406
+ * Get current cache size
407
+ */
408
+ get size() {
409
+ return this.cache.size;
410
+ }
411
+ /**
412
+ * Get all cached video IDs
413
+ */
414
+ keys() {
415
+ return Array.from(this.cache.keys());
416
+ }
417
+ /**
418
+ * Update configuration
419
+ */
420
+ configure(config) {
421
+ this.config = { ...this.config, ...config };
422
+ while (this.cache.size > this.config.maxSize) {
423
+ this.evictLRU();
424
+ }
425
+ }
426
+ /**
427
+ * Evict least recently used entry
428
+ *
429
+ * Uses Map insertion order: first item = oldest (least recently used)
430
+ * Time complexity: O(1)
431
+ */
432
+ evictLRU() {
433
+ const firstKey = this.cache.keys().next().value;
434
+ if (firstKey !== void 0) {
435
+ this.cache.delete(firstKey);
436
+ }
437
+ }
438
+ /**
439
+ * Wait for video to have loadeddata
440
+ */
441
+ waitForVideoData(videoElement, timeout = 5e3) {
442
+ return new Promise((resolve, reject) => {
443
+ if (videoElement.readyState >= 2) {
444
+ resolve();
445
+ return;
446
+ }
447
+ const timeoutId = setTimeout(() => {
448
+ cleanup();
449
+ reject(new Error("Timeout waiting for video data"));
450
+ }, timeout);
451
+ const handleLoadedData = () => {
452
+ cleanup();
453
+ resolve();
454
+ };
455
+ const handleError = () => {
456
+ cleanup();
457
+ reject(new Error("Video load error"));
458
+ };
459
+ const cleanup = () => {
460
+ clearTimeout(timeoutId);
461
+ videoElement.removeEventListener("loadeddata", handleLoadedData);
462
+ videoElement.removeEventListener("error", handleError);
463
+ };
464
+ videoElement.addEventListener("loadeddata", handleLoadedData);
465
+ videoElement.addEventListener("error", handleError);
466
+ });
467
+ }
468
+ };
469
+ var firstFrameCache = new FirstFrameCacheService();
470
+ function createFirstFrameCache(config) {
471
+ return new FirstFrameCacheService(config);
472
+ }
473
+
474
+ // src/components/VideoPlayer/useFirstFrameCapture.ts
475
+ function useFirstFrameCapture(config) {
476
+ const { videoId, enabled = true, onCapture, onError } = config;
477
+ const captureAttemptedRef = useRef(null);
478
+ const capture = useCallback(
479
+ async (videoElement) => {
480
+ if (!enabled) return null;
481
+ if (captureAttemptedRef.current === videoId) return null;
482
+ const cached = firstFrameCache.get(videoId);
483
+ if (cached) {
484
+ onCapture?.(cached);
485
+ return cached;
486
+ }
487
+ captureAttemptedRef.current = videoId;
488
+ try {
489
+ const dataUrl = await firstFrameCache.capture(videoId, videoElement);
490
+ if (dataUrl) onCapture?.(dataUrl);
491
+ return dataUrl;
492
+ } catch (error) {
493
+ onError?.(error instanceof Error ? error : new Error("First frame capture failed"));
494
+ return null;
495
+ }
496
+ },
497
+ [videoId, enabled, onCapture, onError]
498
+ );
499
+ const getCached = useCallback(() => {
500
+ return firstFrameCache.get(videoId);
501
+ }, [videoId]);
502
+ const isCached = useCallback(() => {
503
+ return firstFrameCache.has(videoId);
504
+ }, [videoId]);
505
+ const clearCache = useCallback(() => {
506
+ firstFrameCache.delete(videoId);
507
+ captureAttemptedRef.current = null;
508
+ }, [videoId]);
509
+ return {
510
+ capture,
511
+ getCached,
512
+ isCached,
513
+ clearCache
514
+ };
515
+ }
516
+ function useAutoFirstFrameCapture(config) {
517
+ const { videoId, videoRef, enabled = true, onCapture, onError } = config;
518
+ const { capture, getCached, isCached } = useFirstFrameCapture({
519
+ videoId,
520
+ enabled,
521
+ onCapture,
522
+ onError
523
+ });
524
+ const capturedFrameRef = useRef(null);
525
+ useEffect(() => {
526
+ const video = videoRef.current;
527
+ if (!video || !enabled) return;
528
+ capturedFrameRef.current = null;
529
+ if (isCached()) {
530
+ capturedFrameRef.current = getCached();
531
+ return;
532
+ }
533
+ const handleLoadedData = async () => {
534
+ if (!capturedFrameRef.current) {
535
+ const dataUrl = await capture(video);
536
+ if (dataUrl) {
537
+ capturedFrameRef.current = dataUrl;
538
+ }
539
+ }
540
+ };
541
+ if (video.readyState >= 2) {
542
+ handleLoadedData();
543
+ } else {
544
+ video.addEventListener("loadeddata", handleLoadedData, { once: true });
545
+ }
546
+ return () => {
547
+ video.removeEventListener("loadeddata", handleLoadedData);
548
+ };
549
+ }, [videoRef, enabled, capture, getCached, isCached]);
550
+ return capturedFrameRef.current || getCached();
551
+ }
552
+ var VideoElementError = class extends Error {
553
+ constructor(message, origin, options) {
554
+ super(message);
555
+ this.name = "VideoElementError";
556
+ this.origin = origin;
557
+ this.mediaErrorCode = options?.mediaErrorCode;
558
+ this.recoverable = options?.recoverable ?? origin !== "setup";
559
+ }
560
+ /** Check if error is from HLS.js */
561
+ isHlsError() {
562
+ return this.origin === "hls";
563
+ }
564
+ /** Check if error is from media element */
565
+ isMediaError() {
566
+ return this.origin === "media";
567
+ }
568
+ /** Check if error is network-related */
569
+ isNetworkError() {
570
+ return this.mediaErrorCode === 2;
571
+ }
572
+ };
573
+ var HlsModuleCache = null;
574
+ var hlsLoadPromise = null;
575
+ var nativeHlsSupportCache = null;
576
+ async function loadHls() {
577
+ if (HlsModuleCache) {
578
+ return HlsModuleCache;
579
+ }
580
+ if (!hlsLoadPromise) {
581
+ hlsLoadPromise = import('hls.js').then((mod) => {
582
+ HlsModuleCache = mod;
583
+ return HlsModuleCache;
584
+ }).catch(() => {
585
+ return null;
586
+ });
587
+ }
588
+ return hlsLoadPromise;
589
+ }
590
+ function supportsNativeHLS() {
591
+ if (nativeHlsSupportCache !== null) {
592
+ return nativeHlsSupportCache;
593
+ }
594
+ if (typeof document === "undefined") {
595
+ return false;
596
+ }
597
+ const video = document.createElement("video");
598
+ nativeHlsSupportCache = video.canPlayType("application/vnd.apple.mpegurl") !== "";
599
+ return nativeHlsSupportCache;
600
+ }
601
+ function isHlsErrorData(d) {
602
+ return typeof d === "object" && d !== null && "fatal" in d;
603
+ }
604
+ async function setupHlsSource(video, src, hlsRef, onHlsError) {
605
+ if (supportsNativeHLS()) {
606
+ video.src = src;
607
+ return;
608
+ }
609
+ const Hls = await loadHls();
610
+ if (!Hls?.isSupported?.()) {
611
+ video.src = src;
612
+ return;
613
+ }
614
+ const hls = new Hls.default();
615
+ hlsRef.current = hls;
616
+ hls.loadSource(src);
617
+ hls.attachMedia(video);
618
+ hls.on("hlsError", (_event, data) => {
619
+ if (isHlsErrorData(data) && data.fatal) {
620
+ onHlsError(
621
+ new VideoElementError(`HLS Error: ${data.type || "Unknown"}`, "hls", {
622
+ recoverable: true
623
+ })
624
+ );
625
+ }
626
+ });
627
+ }
628
+ function useVideoElement(config) {
629
+ const {
630
+ src,
631
+ type,
632
+ autoPlay = true,
633
+ loop = true,
634
+ muted = true,
635
+ volume = 1,
636
+ onCanPlay,
637
+ onPlay,
638
+ onPlaying,
639
+ onPause,
640
+ onEnded,
641
+ onTimeUpdate,
642
+ onDurationChange,
643
+ onWaiting,
644
+ onSeeking,
645
+ onSeeked,
646
+ onError,
647
+ onLoadedData
648
+ } = config;
649
+ const videoRef = useRef(null);
650
+ const hlsRef = useRef(null);
651
+ const [isLoading, setIsLoading] = useState(true);
652
+ const [isBuffering, setIsBuffering] = useState(false);
653
+ const [isReady, setIsReady] = useState(false);
654
+ const [error, setError] = useState(null);
655
+ const [isPlaying, setIsPlaying] = useState(false);
656
+ const [isPaused, setIsPaused] = useState(true);
657
+ const [isEnded, setIsEnded] = useState(false);
658
+ const currentSrcRef = useRef(null);
659
+ const retryCountRef = useRef(0);
660
+ const [retryTrigger, setRetryTrigger] = useState(0);
661
+ useEffect(() => {
662
+ const video = videoRef.current;
663
+ if (!video || !src) return;
664
+ const isRetry = retryTrigger > 0 && currentSrcRef.current === src;
665
+ if (currentSrcRef.current === src && !isRetry) return;
666
+ currentSrcRef.current = src;
667
+ setIsLoading(true);
668
+ setIsBuffering(false);
669
+ setIsReady(false);
670
+ setIsPlaying(false);
671
+ setIsPaused(true);
672
+ setIsEnded(false);
673
+ setError(null);
674
+ if (hlsRef.current) {
675
+ hlsRef.current.destroy();
676
+ hlsRef.current = null;
677
+ }
678
+ const setupSource = async () => {
679
+ try {
680
+ if (type === "hls") {
681
+ await setupHlsSource(video, src, hlsRef, (hlsError) => {
682
+ setError(hlsError);
683
+ setIsLoading(false);
684
+ onError?.(hlsError);
685
+ });
686
+ } else {
687
+ video.src = src;
688
+ }
689
+ video.loop = loop;
690
+ video.muted = muted;
691
+ video.volume = volume;
692
+ video.playsInline = true;
693
+ video.load();
694
+ } catch (err) {
695
+ const message = err instanceof Error ? err.message : "Failed to setup video";
696
+ const setupError = new VideoElementError(message, "setup", {
697
+ recoverable: false
698
+ });
699
+ setError(setupError);
700
+ setIsLoading(false);
701
+ onError?.(setupError);
702
+ }
703
+ };
704
+ setupSource();
705
+ return () => {
706
+ if (hlsRef.current) {
707
+ hlsRef.current.destroy();
708
+ hlsRef.current = null;
709
+ }
710
+ };
711
+ }, [src, type, loop, muted, volume, onError, retryTrigger]);
712
+ useEffect(() => {
713
+ const video = videoRef.current;
714
+ if (!video) return;
715
+ const handleCanPlay = () => {
716
+ setIsLoading(false);
717
+ setIsBuffering(false);
718
+ setIsReady(true);
719
+ onCanPlay?.();
720
+ if (autoPlay) {
721
+ video.play().catch(() => {
722
+ });
723
+ }
724
+ };
725
+ const handlePlay = () => {
726
+ setIsPlaying(true);
727
+ setIsPaused(false);
728
+ setIsEnded(false);
729
+ setIsBuffering(false);
730
+ onPlay?.();
731
+ };
732
+ const handlePause = () => {
733
+ setIsPlaying(false);
734
+ setIsPaused(true);
735
+ onPause?.();
736
+ };
737
+ const handleEnded = () => {
738
+ setIsPlaying(false);
739
+ setIsPaused(true);
740
+ setIsEnded(true);
741
+ onEnded?.();
742
+ };
743
+ const handleTimeUpdate = () => {
744
+ onTimeUpdate?.(video.currentTime);
745
+ };
746
+ const handleDurationChange = () => {
747
+ onDurationChange?.(video.duration);
748
+ };
749
+ const handleWaiting = () => {
750
+ setIsBuffering(true);
751
+ onWaiting?.();
752
+ };
753
+ const handlePlaying = () => {
754
+ setIsBuffering(false);
755
+ onPlaying?.();
756
+ };
757
+ const handleSeeking = () => {
758
+ setIsBuffering(true);
759
+ onSeeking?.();
760
+ };
761
+ const handleSeeked = () => {
762
+ setIsBuffering(false);
763
+ setIsEnded(false);
764
+ onSeeked?.();
765
+ };
766
+ const handleLoadedData = () => {
767
+ onLoadedData?.();
768
+ };
769
+ const handleError = () => {
770
+ const mediaError = video.error;
771
+ const code = mediaError?.code;
772
+ const message = mediaError?.message || getMediaErrorMessage(code);
773
+ const err = new VideoElementError(message, "media", {
774
+ mediaErrorCode: code,
775
+ // Network errors are recoverable
776
+ recoverable: code === 2
777
+ });
778
+ setError(err);
779
+ setIsLoading(false);
780
+ setIsBuffering(false);
781
+ onError?.(err);
782
+ };
783
+ video.addEventListener("canplay", handleCanPlay);
784
+ video.addEventListener("play", handlePlay);
785
+ video.addEventListener("playing", handlePlaying);
786
+ video.addEventListener("pause", handlePause);
787
+ video.addEventListener("ended", handleEnded);
788
+ video.addEventListener("timeupdate", handleTimeUpdate);
789
+ video.addEventListener("durationchange", handleDurationChange);
790
+ video.addEventListener("waiting", handleWaiting);
791
+ video.addEventListener("seeking", handleSeeking);
792
+ video.addEventListener("seeked", handleSeeked);
793
+ video.addEventListener("loadeddata", handleLoadedData);
794
+ video.addEventListener("error", handleError);
795
+ return () => {
796
+ video.removeEventListener("canplay", handleCanPlay);
797
+ video.removeEventListener("play", handlePlay);
798
+ video.removeEventListener("playing", handlePlaying);
799
+ video.removeEventListener("pause", handlePause);
800
+ video.removeEventListener("ended", handleEnded);
801
+ video.removeEventListener("timeupdate", handleTimeUpdate);
802
+ video.removeEventListener("durationchange", handleDurationChange);
803
+ video.removeEventListener("waiting", handleWaiting);
804
+ video.removeEventListener("seeking", handleSeeking);
805
+ video.removeEventListener("seeked", handleSeeked);
806
+ video.removeEventListener("loadeddata", handleLoadedData);
807
+ video.removeEventListener("error", handleError);
808
+ };
809
+ }, [
810
+ autoPlay,
811
+ onCanPlay,
812
+ onPlay,
813
+ onPlaying,
814
+ onPause,
815
+ onEnded,
816
+ onTimeUpdate,
817
+ onDurationChange,
818
+ onWaiting,
819
+ onSeeking,
820
+ onSeeked,
821
+ onLoadedData,
822
+ onError
823
+ ]);
824
+ const play = useCallback(async () => {
825
+ const video = videoRef.current;
826
+ if (!video) return;
827
+ try {
828
+ await video.play();
829
+ } catch (err) {
830
+ const message = err instanceof Error ? err.message : "Play failed";
831
+ const playError = new VideoElementError(message, "media", {
832
+ recoverable: true
833
+ });
834
+ onError?.(playError);
835
+ }
836
+ }, [onError]);
837
+ const pause = useCallback(() => {
838
+ const video = videoRef.current;
839
+ if (!video) return;
840
+ video.pause();
841
+ }, []);
842
+ const seek = useCallback((time) => {
843
+ const video = videoRef.current;
844
+ if (!video) return;
845
+ video.currentTime = Math.max(0, Math.min(time, video.duration || 0));
846
+ }, []);
847
+ const toggle = useCallback(async () => {
848
+ const video = videoRef.current;
849
+ if (!video) return;
850
+ if (video.paused) {
851
+ await play();
852
+ } else {
853
+ pause();
854
+ }
855
+ }, [play, pause]);
856
+ const setVolume = useCallback((vol) => {
857
+ const video = videoRef.current;
858
+ if (!video) return;
859
+ video.volume = Math.max(0, Math.min(1, vol));
860
+ }, []);
861
+ const setMuted = useCallback((mute) => {
862
+ const video = videoRef.current;
863
+ if (!video) return;
864
+ video.muted = mute;
865
+ }, []);
866
+ const retry = useCallback(() => {
867
+ const video = videoRef.current;
868
+ if (!video) return;
869
+ if (hlsRef.current) {
870
+ hlsRef.current.destroy();
871
+ hlsRef.current = null;
872
+ }
873
+ setError(null);
874
+ setIsLoading(true);
875
+ setIsBuffering(false);
876
+ setIsReady(false);
877
+ retryCountRef.current += 1;
878
+ setRetryTrigger(retryCountRef.current);
879
+ }, []);
880
+ return {
881
+ videoRef,
882
+ isLoading,
883
+ isBuffering,
884
+ isReady,
885
+ isPlaying,
886
+ isPaused,
887
+ isEnded,
888
+ error,
889
+ play,
890
+ pause,
891
+ seek,
892
+ toggle,
893
+ setVolume,
894
+ setMuted,
895
+ retry
896
+ };
897
+ }
898
+ function getMediaErrorMessage(code) {
899
+ switch (code) {
900
+ case 1:
901
+ return "Video loading was aborted";
902
+ case 2:
903
+ return "Network error while loading video";
904
+ case 3:
905
+ return "Video decoding error";
906
+ case 4:
907
+ return "Video format not supported";
908
+ default:
909
+ return "Unknown video error";
910
+ }
911
+ }
912
+ function VideoPlayerHeadless({
913
+ src,
914
+ type,
915
+ poster,
916
+ autoPlay = true,
917
+ loop = true,
918
+ muted = true,
919
+ volume = 1,
920
+ className,
921
+ videoRef: externalVideoRef,
922
+ videoId,
923
+ objectFit = "contain",
924
+ showErrorUI = true,
925
+ showLoadingUI = true,
926
+ showPoster = true,
927
+ loadingComponent,
928
+ errorComponent,
929
+ onCanPlay,
930
+ onPlay,
931
+ onPlaying,
932
+ onPause,
933
+ onEnded,
934
+ onTimeUpdate,
935
+ onDurationChange,
936
+ onWaiting,
937
+ onError,
938
+ onFirstFrameCapture
939
+ }) {
940
+ useInsertionEffect(() => {
941
+ return injectComponentCSS("video-player", VIDEO_PLAYER_CSS);
942
+ }, []);
943
+ const { videoRef, isLoading, isBuffering, isReady, isPlaying, isPaused, isEnded, error, retry } = useVideoElement({
944
+ src,
945
+ type,
946
+ autoPlay,
947
+ loop,
948
+ muted,
949
+ volume,
950
+ onCanPlay,
951
+ onPlay,
952
+ onPlaying,
953
+ onPause,
954
+ onEnded,
955
+ onTimeUpdate,
956
+ onDurationChange,
957
+ onWaiting,
958
+ onError,
959
+ onLoadedData: handleLoadedData
960
+ });
961
+ const shouldShowLoadingSpinner = isLoading || isBuffering;
962
+ const effectiveVideoId = videoId || src;
963
+ const { capture, getCached } = useFirstFrameCapture({
964
+ videoId: effectiveVideoId,
965
+ enabled: !!effectiveVideoId,
966
+ onCapture: onFirstFrameCapture
967
+ });
968
+ function handleLoadedData() {
969
+ const video = videoRef.current;
970
+ if (video && effectiveVideoId) {
971
+ capture(video);
972
+ }
973
+ }
974
+ const cachedFirstFrame = showPoster ? getCached() : null;
975
+ const setVideoRef = useCallback(
976
+ (el) => {
977
+ videoRef.current = el;
978
+ if (externalVideoRef) {
979
+ if (typeof externalVideoRef === "function") {
980
+ externalVideoRef(el);
981
+ } else {
982
+ externalVideoRef.current = el;
983
+ }
984
+ }
985
+ },
986
+ [externalVideoRef, videoRef]
987
+ );
988
+ const stateClasses = useMemo(
989
+ () => ({
990
+ [LOADING_STATE_CLASS]: isLoading,
991
+ [BUFFERING_STATE_CLASS]: isBuffering,
992
+ [ERROR_STATE_CLASS]: !!error,
993
+ [READY_CLASS]: isReady,
994
+ [PLAYING_CLASS]: isPlaying,
995
+ [PAUSED_CLASS]: isPaused,
996
+ [ENDED_CLASS]: isEnded
997
+ }),
998
+ [isLoading, isBuffering, error, isReady, isPlaying, isPaused, isEnded]
999
+ );
1000
+ const effectivePoster = cachedFirstFrame || poster;
1001
+ const playbackState = getPlaybackState({
1002
+ error,
1003
+ isLoading,
1004
+ isBuffering,
1005
+ isPlaying,
1006
+ isEnded
1007
+ });
1008
+ return /* @__PURE__ */ jsxs(
1009
+ "div",
1010
+ {
1011
+ className: cn(PLAYER_CLASS, stateClasses, className),
1012
+ ...{ [VIDEO_TYPE_ATTR]: type },
1013
+ ...{ [PLAYBACK_STATE_ATTR]: playbackState },
1014
+ style: {
1015
+ ["--sv-player-object-fit"]: objectFit
1016
+ },
1017
+ children: [
1018
+ /* @__PURE__ */ jsx("div", { className: VIDEO_WRAPPER_CLASS, children: /* @__PURE__ */ jsx(
1019
+ "video",
1020
+ {
1021
+ ref: setVideoRef,
1022
+ className: VIDEO_CLASS,
1023
+ playsInline: true,
1024
+ crossOrigin: "anonymous",
1025
+ preload: "metadata",
1026
+ "aria-label": "Video player"
1027
+ }
1028
+ ) }),
1029
+ showPoster && effectivePoster && /* @__PURE__ */ jsx(
1030
+ "div",
1031
+ {
1032
+ className: POSTER_CLASS,
1033
+ style: { backgroundImage: `url(${effectivePoster})` },
1034
+ "aria-hidden": "true"
1035
+ }
1036
+ ),
1037
+ showLoadingUI && shouldShowLoadingSpinner && /* @__PURE__ */ jsx("div", { className: LOADING_CLASS, "aria-label": "Loading video", children: loadingComponent || /* @__PURE__ */ jsx(DefaultLoadingSpinner, {}) }),
1038
+ showErrorUI && error && /* @__PURE__ */ jsx("div", { className: ERROR_CLASS, role: "alert", children: errorComponent || /* @__PURE__ */ jsx(DefaultErrorUI, { error, onRetry: retry }) })
1039
+ ]
1040
+ }
1041
+ );
1042
+ }
1043
+ function getPlaybackState(params) {
1044
+ const { error, isLoading, isBuffering, isEnded, isPlaying } = params;
1045
+ if (error) return "error";
1046
+ if (isLoading) return "loading";
1047
+ if (isBuffering) return "buffering";
1048
+ if (isEnded) return "ended";
1049
+ if (isPlaying) return "playing";
1050
+ return "paused";
1051
+ }
1052
+ function DefaultLoadingSpinner() {
1053
+ return /* @__PURE__ */ jsx("div", { className: "sv-video-player__spinner", "aria-hidden": "true" });
1054
+ }
1055
+ function DefaultErrorUI({
1056
+ error,
1057
+ onRetry
1058
+ }) {
1059
+ const friendlyMessage = getUserFriendlyErrorMessage(error.message);
1060
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1061
+ /* @__PURE__ */ jsx("div", { className: "sv-video-player__error-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsxs(
1062
+ "svg",
1063
+ {
1064
+ "aria-hidden": "true",
1065
+ viewBox: "0 0 24 24",
1066
+ fill: "none",
1067
+ stroke: "currentColor",
1068
+ strokeWidth: 2,
1069
+ children: [
1070
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1071
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
1072
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
1073
+ ]
1074
+ }
1075
+ ) }),
1076
+ /* @__PURE__ */ jsx("p", { className: "sv-video-player__error-message", children: friendlyMessage }),
1077
+ /* @__PURE__ */ jsx(
1078
+ "button",
1079
+ {
1080
+ className: "sv-video-player__retry-btn",
1081
+ onClick: onRetry,
1082
+ type: "button",
1083
+ "aria-label": "Retry loading video",
1084
+ children: "Retry"
1085
+ }
1086
+ )
1087
+ ] });
1088
+ }
1089
+ function getUserFriendlyErrorMessage(message) {
1090
+ const lowercaseMessage = message.toLowerCase();
1091
+ if (lowercaseMessage.includes("network") || lowercaseMessage.includes("fetch")) {
1092
+ return "Unable to load video. Please check your internet connection.";
1093
+ }
1094
+ if (lowercaseMessage.includes("decode") || lowercaseMessage.includes("codec")) {
1095
+ return "Video format is not supported on this device.";
1096
+ }
1097
+ if (lowercaseMessage.includes("abort")) {
1098
+ return "Video loading was interrupted.";
1099
+ }
1100
+ if (lowercaseMessage.includes("not supported") || lowercaseMessage.includes("src_not_supported")) {
1101
+ return "This video format is not supported.";
1102
+ }
1103
+ if (lowercaseMessage.includes("hls")) {
1104
+ return "Unable to stream video. Please try again.";
1105
+ }
1106
+ return "Failed to load video. Please try again.";
1107
+ }
1108
+ VideoPlayerHeadless.displayName = "VideoPlayerHeadless";
1109
+
1110
+ export { BUFFERING_STATE_CLASS, DEFAULT_OBJECT_FIT, DEFAULT_PRELOAD, ENDED_CLASS, ERROR_CLASS, ERROR_STATE_CLASS, FIRST_FRAME_MAX_WIDTH, FIRST_FRAME_QUALITY, LOADING_CLASS, LOADING_STATE_ATTR, LOADING_STATE_CLASS, PAUSED_CLASS, PLAYBACK_STATE_ATTR, PLAYER_CLASS, PLAYING_CLASS, POSTER_CLASS, READY_CLASS, VIDEO_CLASS, VIDEO_PLAYER_CSS, VIDEO_TYPE_ATTR, VIDEO_WRAPPER_CLASS, VideoElementError, VideoPlayerHeadless, Z_INDEX, Z_INDEX_CSS_VARS, createFirstFrameCache, firstFrameCache, useAutoFirstFrameCapture, useFirstFrameCapture, useVideoElement };