@uniai-fe/uds-templates 0.6.7 → 0.6.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -23,23 +23,64 @@ 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
+ const POST_CONNECTED_RECONNECT_GRACE_MS = 5000;
27
+ const POST_CONNECTED_RECONNECT_STAGGER_MS = 5000;
28
28
  const TOKEN_EXPIRY_SAFETY_MS = 30 * 1000;
29
29
 
30
+ type CctvRtcReconnectReason =
31
+ | "tokenError"
32
+ | "streamError"
33
+ | "failed"
34
+ | "disconnected";
35
+
36
+ const DISPLAY_CONNECTED_DURING_GRACE_STATES = new Set<RTCPeerConnectionState>([
37
+ "disconnected",
38
+ "failed",
39
+ ]);
40
+
41
+ /**
42
+ * 같은 회사/카메라 묶음이 동시에 재연결하지 않도록 identity 기반 지연값을 만든다.
43
+ * @desc
44
+ * 이 값은 보안용 난수가 아니라 UI/네트워크 부하를 분산하기 위한 안정 해시다.
45
+ */
30
46
  const getStableStaggerMs = (value: string): number => {
31
47
  let hash = 0;
32
48
 
33
49
  for (let i = 0; i < value.length; i += 1) {
34
50
  hash =
35
- (hash * 31 + value.charCodeAt(i)) % DISCONNECTED_RECONNECT_STAGGER_MS;
51
+ (hash * 31 + value.charCodeAt(i)) % POST_CONNECTED_RECONNECT_STAGGER_MS;
36
52
  }
37
53
 
38
54
  return hash;
39
55
  };
40
56
 
41
- const getDisconnectedReconnectDelayMs = (identityKey: string): number =>
42
- DISCONNECTED_RECONNECT_GRACE_MS + getStableStaggerMs(identityKey);
57
+ /**
58
+ * 최초 연결 이후 끊김/실패가 감지됐을 때 자동 재연결 노출 전 대기시간을 계산한다.
59
+ */
60
+ const getPostConnectedReconnectDelayMs = (delayKey: string): number =>
61
+ POST_CONNECTED_RECONNECT_GRACE_MS + getStableStaggerMs(delayKey);
62
+
63
+ /**
64
+ * 재연결이 필요한 장애 사유를 UDS 내부 기준으로 정규화한다.
65
+ * @desc
66
+ * `closed`는 `streamRegistry.closeByIdentity()`처럼 UDS가 의도적으로 닫을 때도 발생하므로
67
+ * 자동 재연결 사유에 포함하지 않는다.
68
+ */
69
+ const getPostConnectedReconnectReason = ({
70
+ connectionState,
71
+ isTokenError,
72
+ streamError,
73
+ }: {
74
+ connectionState: RTCPeerConnectionState;
75
+ isTokenError: boolean;
76
+ streamError: string | null;
77
+ }): CctvRtcReconnectReason | null => {
78
+ if (isTokenError) return "tokenError";
79
+ if (streamError) return "streamError";
80
+ if (connectionState === "failed") return "failed";
81
+ if (connectionState === "disconnected") return "disconnected";
82
+ return null;
83
+ };
43
84
 
44
85
  const decodeBase64Url = (value: string): string | null => {
45
86
  try {
@@ -102,7 +143,7 @@ const canUseTokenForNewConnection = ({
102
143
  * isStreaming, // 현재 스트림 연결 절차가 진행 중인지 여부
103
144
  * isTokenLoading, // 토큰 발급 요청이 진행 중인지 여부
104
145
  * isTokenError, // 토큰 발급 요청이 실패했는지 여부
105
- * canReconnect, // 재연결 가능한 종료 상태 여부
146
+ * canReconnect, // UDS 내부 grace/stagger 이후 재연결 호출이 가능한지 여부
106
147
  * refetchToken, // 토큰 발급을 재시도하는 함수
107
148
  * reconnectStream, // 재연결 trigger 함수
108
149
  * }
@@ -137,7 +178,7 @@ export function useCctvRtcStream({
137
178
  // 현재 스트림 연결 절차가 진행 중인지 여부.
138
179
  const [isStreaming, setStreaming] = useState(false);
139
180
  const [hasConnected, setHasConnected] = useState(false);
140
- const [isDisconnectedReconnectReady, setDisconnectedReconnectReady] =
181
+ const [isPostConnectedReconnectReady, setPostConnectedReconnectReady] =
141
182
  useState(false);
142
183
 
143
184
  // react-hook-form 컨텍스트에서 token username과 WHEP username override를 추적한다.
@@ -199,33 +240,59 @@ export function useCctvRtcStream({
199
240
  setHasConnected(false);
200
241
  }, [streamIdentityKey]);
201
242
 
243
+ const reconnectReason = useMemo(
244
+ () =>
245
+ getPostConnectedReconnectReason({
246
+ connectionState,
247
+ isTokenError,
248
+ streamError,
249
+ }),
250
+ [connectionState, isTokenError, streamError],
251
+ );
252
+
202
253
  useEffect(() => {
203
- if (
204
- connectionState !== "disconnected" ||
205
- !hasConnected ||
206
- !streamIdentityKey
207
- ) {
208
- setDisconnectedReconnectReady(false);
254
+ const canStartReconnectTimer =
255
+ hasConnected &&
256
+ Boolean(streamIdentityKey) &&
257
+ Boolean(reconnectReason) &&
258
+ !isTokenLoading &&
259
+ !isStreaming;
260
+
261
+ if (!canStartReconnectTimer) {
262
+ setPostConnectedReconnectReady(false);
209
263
  return;
210
264
  }
211
265
 
212
- setDisconnectedReconnectReady(false);
266
+ setPostConnectedReconnectReady(false);
213
267
 
214
- const timeout = setTimeout(() => {
215
- setDisconnectedReconnectReady(true);
216
- }, getDisconnectedReconnectDelayMs(streamIdentityKey));
268
+ const timeout = setTimeout(
269
+ () => {
270
+ setPostConnectedReconnectReady(true);
271
+ },
272
+ getPostConnectedReconnectDelayMs(
273
+ `${streamIdentityKey}|${reconnectReason}`,
274
+ ),
275
+ );
217
276
 
218
277
  return () => {
219
278
  clearTimeout(timeout);
220
279
  };
221
- }, [connectionState, hasConnected, streamIdentityKey]);
280
+ }, [
281
+ hasConnected,
282
+ isStreaming,
283
+ isTokenLoading,
284
+ reconnectReason,
285
+ streamIdentityKey,
286
+ ]);
222
287
 
223
- const displayConnectionState =
224
- connectionState === "disconnected" &&
288
+ const shouldPreserveConnectedDisplay =
225
289
  hasConnected &&
226
- !isDisconnectedReconnectReady
227
- ? "connected"
228
- : connectionState;
290
+ !isPostConnectedReconnectReady &&
291
+ DISPLAY_CONNECTED_DURING_GRACE_STATES.has(connectionState);
292
+
293
+ const displayConnectionState = shouldPreserveConnectedDisplay
294
+ ? "connected"
295
+ : connectionState;
229
296
 
230
297
  const hasReusableRegistryStream = useMemo(() => {
231
298
  if (!streamKeyCandidate) return false;
@@ -375,20 +442,15 @@ export function useCctvRtcStream({
375
442
  if (!hasConnected) return false;
376
443
  if (!streamIdentityKey) return false;
377
444
  if (isTokenLoading || isStreaming) return false;
378
- if (isTokenError || streamError) return true;
379
- if (connectionState === "failed") return true;
380
- if (connectionState === "disconnected") return isDisconnectedReconnectReady;
381
- if (connectionState === "closed") return true;
382
- return false;
445
+ if (!reconnectReason) return false;
446
+ return isPostConnectedReconnectReady;
383
447
  }, [
384
448
  cam?.cam_online,
385
- connectionState,
386
449
  hasConnected,
387
- isDisconnectedReconnectReady,
450
+ isPostConnectedReconnectReady,
388
451
  isStreaming,
389
- isTokenError,
390
452
  isTokenLoading,
391
- streamError,
453
+ reconnectReason,
392
454
  streamIdentityKey,
393
455
  ]);
394
456
 
@@ -88,7 +88,7 @@ export interface UseCctvRtcStreamState extends UseCctvRtcStreamError {
88
88
  * @property {boolean} isStreaming startWhepStream 진행 여부
89
89
  * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
90
90
  * @property {boolean} isTokenError 토큰 발급 실패 여부
91
- * @property {boolean} canReconnect 재연결 가능한 종료 상태 여부
91
+ * @property {boolean} canReconnect UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
92
92
  * @property {() => Promise<void>} refetchToken 토큰 재발급 함수
93
93
  * @property {() => Promise<QueryObserverResult<API_Res_CctvRtcToken>>} reconnectStream 재연결 trigger 함수
94
94
  */
@@ -98,7 +98,10 @@ export interface UseCctvRtcStreamReturn extends UseCctvRtcStreamState {
98
98
  */
99
99
  videoRef: React.RefObject<HTMLVideoElement | null>;
100
100
  /**
101
- * 재연결 가능한 종료 상태 여부
101
+ * UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
102
+ * @desc
103
+ * 스트림 장애 감지 즉시 true가 되는 플래그가 아니라,
104
+ * 서비스 overlay가 reconnectStream()을 호출해도 되는 시점에 true가 된다.
102
105
  */
103
106
  canReconnect: boolean;
104
107
  /**
@@ -9,7 +9,7 @@ import type { CctvRtcReconnectTrigger } from "./hook";
9
9
  * @property {boolean | undefined} [isError] 영상 에러 상태 여부
10
10
  * @property {boolean | undefined} [isLive] 영상 live 상태 여부
11
11
  * @property {React.ReactNode} [overlayMessage] 기본 안내/에러 메시지 콘텐츠
12
- * @property {boolean | undefined} [canReconnect] 재연결 가능한 종료 상태 여부
12
+ * @property {boolean | undefined} [canReconnect] UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
13
13
  * @property {CctvRtcReconnectTrigger | undefined} [reconnectStream] 재연결 trigger 함수
14
14
  */
15
15
  export interface CctvVideoRenderOverlayContext {
@@ -38,7 +38,10 @@ export interface CctvVideoRenderOverlayContext {
38
38
  */
39
39
  overlayMessage?: React.ReactNode;
40
40
  /**
41
- * 재연결 가능한 종료 상태 여부
41
+ * UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
42
+ * @desc
43
+ * custom overlay에서 자동 재연결을 구현할 때 이 값이 true인 경우에만
44
+ * reconnectStream()을 호출해야 한다.
42
45
  */
43
46
  canReconnect?: boolean;
44
47
  /**
@@ -126,7 +129,7 @@ export interface CctvVideoOverlayFooterProps {
126
129
  * @property {boolean} [isError] 에러 상태 여부
127
130
  * @property {boolean} [isLive] 영상이 정상적으로 재생 중인지 여부
128
131
  * @property {React.ReactNode} [overlayMessage] 에러/안내 메시지 콘텐츠
129
- * @property {boolean} [canReconnect] 재연결 가능한 종료 상태 여부
132
+ * @property {boolean} [canReconnect] UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
130
133
  * @property {CctvRtcReconnectTrigger} [reconnectStream] 재연결 trigger 함수
131
134
  */
132
135
  export interface CctvVideoStateProps {
@@ -143,7 +146,7 @@ export interface CctvVideoStateProps {
143
146
  */
144
147
  overlayMessage?: React.ReactNode;
145
148
  /**
146
- * 재연결 가능한 종료 상태 여부
149
+ * UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
147
150
  */
148
151
  canReconnect?: boolean;
149
152
  /**
@@ -11,7 +11,7 @@ import type { UseCctvRtcStreamError, UseCctvRtcStreamState } from "./hook";
11
11
  * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
12
12
  * @property {boolean} isTokenError 토큰 발급 실패 여부
13
13
  * @property {string | null} streamError 스트림 오류 메시지
14
- * @property {boolean} [canReconnect] 재연결 가능한 종료 상태 여부
14
+ * @property {boolean} [canReconnect] UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
15
15
  */
16
16
  export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
17
17
  /**
@@ -27,7 +27,7 @@ export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
27
27
  */
28
28
  isFetching?: boolean;
29
29
  /**
30
- * 재연결 가능한 종료 상태 여부
30
+ * UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
31
31
  */
32
32
  canReconnect?: boolean;
33
33
  }
@@ -53,7 +53,7 @@ export interface CctvVideoLiveParams extends UseCctvRtcStreamState {
53
53
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
54
54
  * @property {boolean} isTokenError 토큰 발급 실패 여부
55
55
  * @property {string | null} streamError 스트림 오류 메시지
56
- * @property {boolean} [canReconnect] 재연결 가능한 종료 상태 여부
56
+ * @property {boolean} [canReconnect] UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
57
57
  */
58
58
  export interface CctvVideoErrorParams extends UseCctvRtcStreamError {
59
59
  /**
@@ -61,7 +61,7 @@ export interface CctvVideoErrorParams extends UseCctvRtcStreamError {
61
61
  */
62
62
  cam?: CctvCompanyCameraData;
63
63
  /**
64
- * 재연결 가능한 종료 상태 여부
64
+ * UDS 내부 grace/stagger를 통과해 재연결 호출이 가능한지 여부
65
65
  */
66
66
  canReconnect?: boolean;
67
67
  }
@@ -62,6 +62,10 @@ export function getOverlayMessage({
62
62
  if (canReconnect && RTC_SESSION_ENDED_RECONNECT_STATES.has(connectionState)) {
63
63
  return CCTV_MESSAGE.sessionEnded;
64
64
  }
65
+ // grace/stagger가 끝난 disconnected는 더 이상 준비 상태가 아니라 재연결 대상 장애다.
66
+ if (canReconnect && connectionState === "disconnected") {
67
+ return CCTV_MESSAGE.offline;
68
+ }
65
69
  if (isTokenLoading || isStreaming) return CCTV_MESSAGE.preparing;
66
70
  if (RTC_PREPARING_STATES.has(connectionState)) return CCTV_MESSAGE.preparing;
67
71
  return null;