@uniai-fe/uds-templates 0.6.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css
CHANGED
|
@@ -1796,6 +1796,22 @@
|
|
|
1796
1796
|
flex-direction: column;
|
|
1797
1797
|
align-items: center;
|
|
1798
1798
|
justify-content: center;
|
|
1799
|
+
gap: 2px;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.cctv-video-loading-icon {
|
|
1803
|
+
flex: 0 0 auto;
|
|
1804
|
+
width: 24px;
|
|
1805
|
+
height: 24px;
|
|
1806
|
+
display: flex;
|
|
1807
|
+
color: var(--cctv-error-text-color);
|
|
1808
|
+
}
|
|
1809
|
+
.cctv-video-loading-icon .alternate-loading-icon {
|
|
1810
|
+
width: 24px;
|
|
1811
|
+
height: 24px;
|
|
1812
|
+
}
|
|
1813
|
+
.cctv-video-loading-icon svg stop:first-child {
|
|
1814
|
+
stop-color: currentColor;
|
|
1799
1815
|
}
|
|
1800
1816
|
|
|
1801
1817
|
.cctv-video-error-icon {
|
|
@@ -1806,11 +1822,17 @@
|
|
|
1806
1822
|
|
|
1807
1823
|
.cctv-video-error-message {
|
|
1808
1824
|
font-size: 13px;
|
|
1809
|
-
color: var(--cctv-
|
|
1825
|
+
color: var(--cctv-error-text-color);
|
|
1810
1826
|
line-height: 1.5em;
|
|
1811
1827
|
font-weight: 500;
|
|
1812
1828
|
}
|
|
1813
1829
|
|
|
1830
|
+
.cctv-video-overlay-body.is-error .cctv-video-error {
|
|
1831
|
+
gap: 0;
|
|
1832
|
+
}
|
|
1833
|
+
.cctv-video-overlay-body.is-error .cctv-video-loading-icon {
|
|
1834
|
+
display: none;
|
|
1835
|
+
}
|
|
1814
1836
|
.cctv-video-overlay-body.is-error .cctv-video-error-icon {
|
|
1815
1837
|
display: block;
|
|
1816
1838
|
}
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Alternate } from "@uniai-fe/uds-primitives";
|
|
1
2
|
import CCTVErrorIcon from "../../../../img/error.svg";
|
|
2
3
|
|
|
3
4
|
export default function CCTVVideoError({
|
|
@@ -7,6 +8,9 @@ export default function CCTVVideoError({
|
|
|
7
8
|
}) {
|
|
8
9
|
return (
|
|
9
10
|
<div className="cctv-video-error">
|
|
11
|
+
<span className="cctv-video-loading-icon" aria-hidden="true">
|
|
12
|
+
<Alternate.LoadingIcon size="small" />
|
|
13
|
+
</span>
|
|
10
14
|
<figure className="cctv-video-error-icon">
|
|
11
15
|
<CCTVErrorIcon width={24} height={24} />
|
|
12
16
|
</figure>
|
|
@@ -23,8 +23,24 @@ import { getIsLive } from "../utils/video-state";
|
|
|
23
23
|
import { useFormContext, useWatch } from "react-hook-form";
|
|
24
24
|
|
|
25
25
|
const AUTO_RECONNECT_INTERVAL_MS = 3000;
|
|
26
|
+
const DISCONNECTED_RECONNECT_GRACE_MS = 5000;
|
|
27
|
+
const DISCONNECTED_RECONNECT_STAGGER_MS = 5000;
|
|
26
28
|
const TOKEN_EXPIRY_SAFETY_MS = 30 * 1000;
|
|
27
29
|
|
|
30
|
+
const getStableStaggerMs = (value: string): number => {
|
|
31
|
+
let hash = 0;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
34
|
+
hash =
|
|
35
|
+
(hash * 31 + value.charCodeAt(i)) % DISCONNECTED_RECONNECT_STAGGER_MS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return hash;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getDisconnectedReconnectDelayMs = (identityKey: string): number =>
|
|
42
|
+
DISCONNECTED_RECONNECT_GRACE_MS + getStableStaggerMs(identityKey);
|
|
43
|
+
|
|
28
44
|
const decodeBase64Url = (value: string): string | null => {
|
|
29
45
|
try {
|
|
30
46
|
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -121,6 +137,8 @@ export function useCctvRtcStream({
|
|
|
121
137
|
// 현재 스트림 연결 절차가 진행 중인지 여부.
|
|
122
138
|
const [isStreaming, setStreaming] = useState(false);
|
|
123
139
|
const [hasConnected, setHasConnected] = useState(false);
|
|
140
|
+
const [isDisconnectedReconnectReady, setDisconnectedReconnectReady] =
|
|
141
|
+
useState(false);
|
|
124
142
|
|
|
125
143
|
// react-hook-form 컨텍스트에서 token username과 WHEP username override를 추적한다.
|
|
126
144
|
const { control } = useFormContext();
|
|
@@ -181,6 +199,34 @@ export function useCctvRtcStream({
|
|
|
181
199
|
setHasConnected(false);
|
|
182
200
|
}, [streamIdentityKey]);
|
|
183
201
|
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (
|
|
204
|
+
connectionState !== "disconnected" ||
|
|
205
|
+
!hasConnected ||
|
|
206
|
+
!streamIdentityKey
|
|
207
|
+
) {
|
|
208
|
+
setDisconnectedReconnectReady(false);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setDisconnectedReconnectReady(false);
|
|
213
|
+
|
|
214
|
+
const timeout = setTimeout(() => {
|
|
215
|
+
setDisconnectedReconnectReady(true);
|
|
216
|
+
}, getDisconnectedReconnectDelayMs(streamIdentityKey));
|
|
217
|
+
|
|
218
|
+
return () => {
|
|
219
|
+
clearTimeout(timeout);
|
|
220
|
+
};
|
|
221
|
+
}, [connectionState, hasConnected, streamIdentityKey]);
|
|
222
|
+
|
|
223
|
+
const displayConnectionState =
|
|
224
|
+
connectionState === "disconnected" &&
|
|
225
|
+
hasConnected &&
|
|
226
|
+
!isDisconnectedReconnectReady
|
|
227
|
+
? "connected"
|
|
228
|
+
: connectionState;
|
|
229
|
+
|
|
184
230
|
const hasReusableRegistryStream = useMemo(() => {
|
|
185
231
|
if (!streamKeyCandidate) return false;
|
|
186
232
|
|
|
@@ -331,13 +377,14 @@ export function useCctvRtcStream({
|
|
|
331
377
|
if (isTokenLoading || isStreaming) return false;
|
|
332
378
|
if (isTokenError || streamError) return true;
|
|
333
379
|
if (connectionState === "failed") return true;
|
|
334
|
-
if (connectionState === "disconnected") return
|
|
380
|
+
if (connectionState === "disconnected") return isDisconnectedReconnectReady;
|
|
335
381
|
if (connectionState === "closed") return true;
|
|
336
382
|
return false;
|
|
337
383
|
}, [
|
|
338
384
|
cam?.cam_online,
|
|
339
385
|
connectionState,
|
|
340
386
|
hasConnected,
|
|
387
|
+
isDisconnectedReconnectReady,
|
|
341
388
|
isStreaming,
|
|
342
389
|
isTokenError,
|
|
343
390
|
isTokenLoading,
|
|
@@ -377,7 +424,7 @@ export function useCctvRtcStream({
|
|
|
377
424
|
cam
|
|
378
425
|
? getIsLive({
|
|
379
426
|
cam,
|
|
380
|
-
connectionState,
|
|
427
|
+
connectionState: displayConnectionState,
|
|
381
428
|
isTokenLoading,
|
|
382
429
|
isTokenError,
|
|
383
430
|
isStreaming,
|
|
@@ -386,7 +433,7 @@ export function useCctvRtcStream({
|
|
|
386
433
|
: false,
|
|
387
434
|
[
|
|
388
435
|
cam,
|
|
389
|
-
|
|
436
|
+
displayConnectionState,
|
|
390
437
|
isTokenLoading,
|
|
391
438
|
isTokenError,
|
|
392
439
|
isStreaming,
|
|
@@ -431,7 +478,7 @@ export function useCctvRtcStream({
|
|
|
431
478
|
// 호출자에게 video ref와 상태값, 토큰 쿼리 상태를 전달한다.
|
|
432
479
|
return {
|
|
433
480
|
videoRef,
|
|
434
|
-
connectionState,
|
|
481
|
+
connectionState: displayConnectionState,
|
|
435
482
|
streamError,
|
|
436
483
|
isStreaming,
|
|
437
484
|
isTokenLoading,
|
|
@@ -137,6 +137,23 @@
|
|
|
137
137
|
flex-direction: column;
|
|
138
138
|
align-items: center;
|
|
139
139
|
justify-content: center;
|
|
140
|
+
gap: 2px;
|
|
141
|
+
}
|
|
142
|
+
.cctv-video-loading-icon {
|
|
143
|
+
flex: 0 0 auto;
|
|
144
|
+
width: 24px;
|
|
145
|
+
height: 24px;
|
|
146
|
+
display: flex;
|
|
147
|
+
color: var(--cctv-error-text-color);
|
|
148
|
+
|
|
149
|
+
.alternate-loading-icon {
|
|
150
|
+
width: 24px;
|
|
151
|
+
height: 24px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
svg stop:first-child {
|
|
155
|
+
stop-color: currentColor;
|
|
156
|
+
}
|
|
140
157
|
}
|
|
141
158
|
.cctv-video-error-icon {
|
|
142
159
|
display: none;
|
|
@@ -145,12 +162,20 @@
|
|
|
145
162
|
}
|
|
146
163
|
.cctv-video-error-message {
|
|
147
164
|
font-size: 13px;
|
|
148
|
-
color: var(--cctv-
|
|
165
|
+
color: var(--cctv-error-text-color);
|
|
149
166
|
line-height: 1.5em;
|
|
150
167
|
font-weight: 500;
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
.cctv-video-overlay-body.is-error {
|
|
171
|
+
.cctv-video-error {
|
|
172
|
+
gap: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.cctv-video-loading-icon {
|
|
176
|
+
display: none;
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
.cctv-video-error-icon {
|
|
155
180
|
display: block;
|
|
156
181
|
}
|