@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-status-text-color);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -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 true;
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
- connectionState,
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-status-text-color);
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
  }