@uniai-fe/uds-templates 0.6.5 → 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
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
--cctv-live-state-dot-off: var(--color-border-strong);
|
|
111
111
|
--cctv-live-state-text-off: var(--color-label-disabled);
|
|
112
112
|
/* Error */
|
|
113
|
+
--cctv-status-text-color: var(--color-common-99);
|
|
113
114
|
--cctv-error-text-color: var(--color-label-disabled);
|
|
114
115
|
--cctv-error-icon-color: var(--color-label-disabled);
|
|
115
116
|
/* Pagination */
|
|
@@ -1795,9 +1796,26 @@
|
|
|
1795
1796
|
flex-direction: column;
|
|
1796
1797
|
align-items: center;
|
|
1797
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;
|
|
1798
1815
|
}
|
|
1799
1816
|
|
|
1800
1817
|
.cctv-video-error-icon {
|
|
1818
|
+
display: none;
|
|
1801
1819
|
margin-bottom: 2px;
|
|
1802
1820
|
fill: var(--cctv-error-icon-color);
|
|
1803
1821
|
}
|
|
@@ -1806,6 +1824,20 @@
|
|
|
1806
1824
|
font-size: 13px;
|
|
1807
1825
|
color: var(--cctv-error-text-color);
|
|
1808
1826
|
line-height: 1.5em;
|
|
1827
|
+
font-weight: 500;
|
|
1828
|
+
}
|
|
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
|
+
}
|
|
1836
|
+
.cctv-video-overlay-body.is-error .cctv-video-error-icon {
|
|
1837
|
+
display: block;
|
|
1838
|
+
}
|
|
1839
|
+
.cctv-video-overlay-body.is-error .cctv-video-error-message {
|
|
1840
|
+
color: var(--cctv-error-text-color);
|
|
1809
1841
|
font-weight: 600;
|
|
1810
1842
|
}
|
|
1811
1843
|
|
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,8 +137,26 @@
|
|
|
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 {
|
|
159
|
+
display: none;
|
|
142
160
|
margin-bottom: 2px;
|
|
143
161
|
fill: var(--cctv-error-icon-color);
|
|
144
162
|
}
|
|
@@ -146,7 +164,26 @@
|
|
|
146
164
|
font-size: 13px;
|
|
147
165
|
color: var(--cctv-error-text-color);
|
|
148
166
|
line-height: 1.5em;
|
|
149
|
-
font-weight:
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
}
|
|
169
|
+
|
|
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
|
+
|
|
179
|
+
.cctv-video-error-icon {
|
|
180
|
+
display: block;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.cctv-video-error-message {
|
|
184
|
+
color: var(--cctv-error-text-color);
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
}
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
.cctv-video-status-text {
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
export const CCTV_MESSAGE = {
|
|
8
8
|
selectCam: "카메라를 선택하세요.",
|
|
9
9
|
fetching: "CCTV 데이터를 불러오는 중입니다.",
|
|
10
|
-
preparing: "
|
|
10
|
+
preparing: "CCTV 영상을 연결하고 있습니다.",
|
|
11
11
|
sessionEnded:
|
|
12
12
|
"장시간 미사용으로 연결이 종료되었습니다. 계속 확인하시려면 다시 연결해 주세요.",
|
|
13
13
|
tokenError: "토큰을 발급하지 못했습니다.",
|
|
@@ -21,6 +21,10 @@ const RTC_PREPARING_STATES = new Set<RTCPeerConnectionState>([
|
|
|
21
21
|
"closed",
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
+
const RTC_SESSION_ENDED_RECONNECT_STATES = new Set<RTCPeerConnectionState>([
|
|
25
|
+
"closed",
|
|
26
|
+
]);
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
29
|
* CCTV; 스트리밍 상태에 따른 메시지 추출
|
|
26
30
|
* @param {CctvVideoOverlayMessageParams} params 상태 파라미터
|
|
@@ -51,12 +55,14 @@ export function getOverlayMessage({
|
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
if (!cam.cam_online) return CCTV_MESSAGE.offline;
|
|
54
|
-
if (canReconnect) return CCTV_MESSAGE.sessionEnded;
|
|
55
58
|
// 에러 상태가 준비 상태와 겹칠 때는 에러 문구가 최종 표시 계약을 우선한다.
|
|
56
59
|
if (isTokenError) return CCTV_MESSAGE.tokenError;
|
|
57
60
|
if (streamError) return streamError;
|
|
58
|
-
if (isTokenLoading || isStreaming) return CCTV_MESSAGE.preparing;
|
|
59
61
|
if (connectionState === "failed") return CCTV_MESSAGE.offline;
|
|
62
|
+
if (canReconnect && RTC_SESSION_ENDED_RECONNECT_STATES.has(connectionState)) {
|
|
63
|
+
return CCTV_MESSAGE.sessionEnded;
|
|
64
|
+
}
|
|
65
|
+
if (isTokenLoading || isStreaming) return CCTV_MESSAGE.preparing;
|
|
60
66
|
if (RTC_PREPARING_STATES.has(connectionState)) return CCTV_MESSAGE.preparing;
|
|
61
67
|
return null;
|
|
62
68
|
}
|