@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.
- package/README.md +194 -0
- package/dist/chunk-4MN72OZH.js +148 -0
- package/dist/chunk-MMTAPG2C.js +201 -0
- package/dist/chunk-OXY5JHVJ.js +524 -0
- package/dist/chunk-QKQUXR3H.js +1318 -0
- package/dist/chunk-SSJDO24Q.js +204 -0
- package/dist/chunk-UECU42WC.js +1110 -0
- package/dist/chunk-UXMA4KJZ.js +45 -0
- package/dist/chunk-WKX2WBVO.js +98 -0
- package/dist/chunk-YW23IBKF.js +530 -0
- package/dist/chunk-ZZDQKP4R.js +418 -0
- package/dist/components/ActionBar/index.d.ts +244 -0
- package/dist/components/ActionBar/index.js +1 -0
- package/dist/components/ErrorBoundary/index.d.ts +81 -0
- package/dist/components/ErrorBoundary/index.js +1 -0
- package/dist/components/ProgressBar/index.d.ts +84 -0
- package/dist/components/ProgressBar/index.js +1 -0
- package/dist/components/Skeleton/index.d.ts +62 -0
- package/dist/components/Skeleton/index.js +1 -0
- package/dist/components/VideoFeed/index.d.ts +274 -0
- package/dist/components/VideoFeed/index.js +1 -0
- package/dist/components/VideoPlayer/index.d.ts +457 -0
- package/dist/components/VideoPlayer/index.js +1 -0
- package/dist/components/VideoSlot/index.d.ts +592 -0
- package/dist/components/VideoSlot/index.js +1 -0
- package/dist/components/icons/index.d.ts +58 -0
- package/dist/components/icons/index.js +1 -0
- package/dist/index.d.ts +595 -0
- package/dist/index.js +687 -0
- package/dist/use-gesture-react.esm-3SV4QLEJ.js +1893 -0
- package/package.json +65 -0
|
@@ -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 };
|