@uniai-fe/uds-templates 0.6.13 → 0.6.15

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.13",
3
+ "version": "0.6.15",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -49,6 +49,15 @@ const attachVideo = (video: HTMLVideoElement, stream: MediaStream | null) => {
49
49
  video.srcObject = stream;
50
50
  };
51
51
 
52
+ const clearVideoIfAttachedStream = (
53
+ video: HTMLVideoElement,
54
+ stream: MediaStream | null,
55
+ ) => {
56
+ if (stream && video.srcObject === stream) {
57
+ video.srcObject = null;
58
+ }
59
+ };
60
+
52
61
  const notifyEntry = (entry: CctvRtcStreamEntry) => {
53
62
  const snapshot = getEntrySnapshot(entry);
54
63
  entry.listeners.forEach(listener => listener(snapshot));
@@ -125,9 +134,7 @@ const closeEntry = (entry: CctvRtcStreamEntry) => {
125
134
  entry.isStreaming = false;
126
135
 
127
136
  entry.attachedVideos.forEach(video => {
128
- if (!entry.stream || video.srcObject === entry.stream) {
129
- video.srcObject = null;
130
- }
137
+ clearVideoIfAttachedStream(video, entry.stream);
131
138
  });
132
139
 
133
140
  notifyEntry(entry);
@@ -166,13 +173,13 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
166
173
  }
167
174
 
168
175
  entry.attachedVideos.add(video);
169
- attachVideo(video, entry.stream);
176
+ if (entry.stream || !video.srcObject) {
177
+ attachVideo(video, entry.stream);
178
+ }
170
179
 
171
180
  return () => {
172
181
  entry.attachedVideos.delete(video);
173
- if (!entry.stream || video.srcObject === entry.stream) {
174
- video.srcObject = null;
175
- }
182
+ clearVideoIfAttachedStream(video, entry.stream);
176
183
  };
177
184
  },
178
185
  closeAll() {
@@ -233,7 +240,12 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
233
240
  };
234
241
 
235
242
  entries.set(streamKey, entry);
236
- attachVideo(video, null);
243
+ if (!video.srcObject) {
244
+ attachVideo(video, null);
245
+ } else {
246
+ video.playsInline = true;
247
+ video.autoplay = true;
248
+ }
237
249
  logCctvDebugEvent({
238
250
  event: "start:create-entry",
239
251
  payload: {
@@ -328,7 +340,7 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
328
340
  source: "streamRegistry",
329
341
  });
330
342
  entry.attachedVideos.forEach(attachedVideo =>
331
- attachVideo(attachedVideo, null),
343
+ clearVideoIfAttachedStream(attachedVideo, entry.stream),
332
344
  );
333
345
  notifyEntry(entry);
334
346
  });
@@ -138,10 +138,10 @@ const canUseTokenForNewConnection = ({
138
138
  * @desc
139
139
  * return {
140
140
  * videoRef, // WebRTC MediaStream을 연결할 video 요소 ref
141
- * connectionState, // RTCPeerConnectionState 상태값
141
+ * connectionState, // UI 표시 기준 RTCPeerConnectionState 상태값
142
142
  * streamError, // 스트림 시도 중 발생한 오류 메시지
143
- * isStreaming, // 현재 스트림 연결 절차가 진행 중인지 여부
144
- * isTokenLoading, // 토큰 발급 요청이 진행 중인지 여부
143
+ * isStreaming, // UI 표시 기준 스트림 연결 절차 진행 여부
144
+ * isTokenLoading, // UI 표시 기준 토큰 발급 요청 진행 여부
145
145
  * isTokenError, // 토큰 발급 요청이 실패했는지 여부
146
146
  * canReconnect, // UDS 내부 grace/stagger 이후 재연결 호출이 가능한지 여부
147
147
  * refetchToken, // 토큰 발급을 재시도하는 함수
@@ -328,16 +328,34 @@ export function useCctvRtcStream({
328
328
  streamIdentityKey,
329
329
  ]);
330
330
 
331
- const shouldPreserveConnectedDisplay =
331
+ const isPostConnectedRecoverableState =
332
332
  hasConnected &&
333
- !isPostConnectedReconnectReady &&
333
+ Boolean(streamIdentityKey) &&
334
+ !isTokenError &&
335
+ !streamError &&
334
336
  DISPLAY_CONNECTED_DURING_RECOVERY_STATES.has(connectionState);
335
337
 
338
+ const isPostConnectedReplacementLoading =
339
+ hasConnected &&
340
+ Boolean(streamIdentityKey) &&
341
+ !isTokenError &&
342
+ !streamError &&
343
+ (isTokenLoading || isStreaming);
344
+
345
+ const shouldPreserveConnectedDisplay =
346
+ isPostConnectedReplacementLoading || isPostConnectedRecoverableState;
347
+
336
348
  // 반환 state는 CamList/Viewer/overlay의 live/error/message 계산에 직접 쓰인다.
337
- // post-connected disconnected는 자동 재연결 대신 기존 화면을 유지해 UI reset 파동을 막는다.
349
+ // post-connected recovery/replacement 중에는 canReconnect만 열고, 기존 화면은 유지해 UI reset 파동을 막는다.
338
350
  const displayConnectionState = shouldPreserveConnectedDisplay
339
351
  ? "connected"
340
352
  : connectionState;
353
+ const displayIsStreaming = shouldPreserveConnectedDisplay
354
+ ? false
355
+ : isStreaming;
356
+ const displayIsTokenLoading = shouldPreserveConnectedDisplay
357
+ ? false
358
+ : isTokenLoading;
341
359
 
342
360
  const skippedDisconnectedDebugKeyRef = useRef<string | null>(null);
343
361
 
@@ -468,20 +486,39 @@ export function useCctvRtcStream({
468
486
  if (!streamKey || !tokenQuery.data?.token || !endpoint || !currentVideo)
469
487
  return;
470
488
 
471
- if (
489
+ const previousStreamKey =
472
490
  activeStreamIdentityKeyRef.current === streamIdentityKey &&
473
491
  activeStreamKeyRef.current !== streamKey
474
- ) {
492
+ ? activeStreamKeyRef.current
493
+ : null;
494
+ let didClosePreviousStream = false;
495
+
496
+ const closePreviousStreamAfterTrack = () => {
497
+ if (!previousStreamKey || didClosePreviousStream) return;
498
+
499
+ didClosePreviousStream = true;
475
500
  logCctvDebugEvent({
476
- event: "stream-effect:close-previous-identity",
501
+ event: "stream-effect:close-previous-after-track",
477
502
  payload: {
478
503
  ...debugBasePayload,
479
- activeStreamKey: getCctvDebugKeyLabel(activeStreamKeyRef.current),
480
- nextStreamKey: getCctvDebugKeyLabel(streamKey),
504
+ previousStreamKey: getCctvDebugKeyLabel(previousStreamKey),
505
+ streamKey: getCctvDebugKeyLabel(streamKey),
481
506
  },
482
507
  source: "useRtcStream",
483
508
  });
484
509
  streamRegistry.closeByIdentity(streamIdentityKey, streamKey);
510
+ };
511
+
512
+ if (previousStreamKey) {
513
+ logCctvDebugEvent({
514
+ event: "stream-effect:prepare-replacement",
515
+ payload: {
516
+ ...debugBasePayload,
517
+ previousStreamKey: getCctvDebugKeyLabel(previousStreamKey),
518
+ streamKey: getCctvDebugKeyLabel(streamKey),
519
+ },
520
+ source: "useRtcStream",
521
+ });
485
522
  }
486
523
 
487
524
  activeStreamKeyRef.current = streamKey;
@@ -520,8 +557,11 @@ export function useCctvRtcStream({
520
557
  });
521
558
  setHasConnected(true);
522
559
  }
523
- if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
524
- currentVideo.srcObject = snapshot.stream;
560
+ if (snapshot.stream) {
561
+ if (currentVideo.srcObject !== snapshot.stream) {
562
+ currentVideo.srcObject = snapshot.stream;
563
+ }
564
+ closePreviousStreamAfterTrack();
525
565
  }
526
566
  });
527
567
  const snapshot = streamRegistry.getSnapshot(streamKey);
@@ -530,8 +570,11 @@ export function useCctvRtcStream({
530
570
  setStreaming(snapshot.isStreaming);
531
571
  if (snapshot.connectionState === "connected") setHasConnected(true);
532
572
 
533
- if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
534
- currentVideo.srcObject = snapshot.stream;
573
+ if (snapshot.stream) {
574
+ if (currentVideo.srcObject !== snapshot.stream) {
575
+ currentVideo.srcObject = snapshot.stream;
576
+ }
577
+ closePreviousStreamAfterTrack();
535
578
  }
536
579
 
537
580
  // effect cleanup: current video attach만 해제하고 registry stream은 유지한다.
@@ -602,17 +645,9 @@ export function useCctvRtcStream({
602
645
  },
603
646
  source: "useRtcStream",
604
647
  });
605
- if (result.isSuccess && streamIdentityKey) {
606
- logCctvDebugEvent({
607
- event: "reconnect-stream:close-identity",
608
- payload: resultDebugState.debugBasePayload,
609
- source: "useRtcStream",
610
- });
611
- streamRegistry.closeByIdentity(streamIdentityKey);
612
- }
613
648
  return result;
614
649
  },
615
- [refetchRtcToken, streamIdentityKey, streamRegistry],
650
+ [refetchRtcToken],
616
651
  );
617
652
 
618
653
  const canReconnect = useMemo(() => {
@@ -681,18 +716,18 @@ export function useCctvRtcStream({
681
716
  ? getIsLive({
682
717
  cam,
683
718
  connectionState: displayConnectionState,
684
- isTokenLoading,
719
+ isTokenLoading: displayIsTokenLoading,
685
720
  isTokenError,
686
- isStreaming,
721
+ isStreaming: displayIsStreaming,
687
722
  streamError,
688
723
  })
689
724
  : false,
690
725
  [
691
726
  cam,
692
727
  displayConnectionState,
693
- isTokenLoading,
728
+ displayIsTokenLoading,
694
729
  isTokenError,
695
- isStreaming,
730
+ displayIsStreaming,
696
731
  streamError,
697
732
  ],
698
733
  );
@@ -706,7 +741,11 @@ export function useCctvRtcStream({
706
741
  connectionState,
707
742
  displayConnectionState,
708
743
  hasConnected,
744
+ isPostConnectedReplacementLoading,
745
+ isPostConnectedRecoverableState,
709
746
  isPostConnectedReconnectReady,
747
+ displayIsStreaming,
748
+ displayIsTokenLoading,
710
749
  isStreaming,
711
750
  isTokenError,
712
751
  isTokenLoading,
@@ -721,7 +760,11 @@ export function useCctvRtcStream({
721
760
  connectionState,
722
761
  debugBasePayload,
723
762
  displayConnectionState,
763
+ displayIsStreaming,
764
+ displayIsTokenLoading,
724
765
  hasConnected,
766
+ isPostConnectedReplacementLoading,
767
+ isPostConnectedRecoverableState,
725
768
  isPostConnectedReconnectReady,
726
769
  isStreaming,
727
770
  isTokenError,
@@ -770,8 +813,8 @@ export function useCctvRtcStream({
770
813
  videoRef,
771
814
  connectionState: displayConnectionState,
772
815
  streamError,
773
- isStreaming,
774
- isTokenLoading,
816
+ isStreaming: displayIsStreaming,
817
+ isTokenLoading: displayIsTokenLoading,
775
818
  isTokenError,
776
819
  canReconnect,
777
820
  refetchToken,
@@ -59,23 +59,33 @@ export interface UseCctvRtcStreamError {
59
59
 
60
60
  /**
61
61
  * CCTV; useCctvRtcStream 연결 상태
62
- * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
63
- * @property {boolean} isStreaming startWhepStream 진행 여부
64
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
62
+ * @property {RTCPeerConnectionState} connectionState UI 표시 기준 WebRTC 연결 상태
63
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
64
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
65
65
  * @property {boolean} isTokenError 토큰 발급 실패 여부
66
66
  * @property {string | null} streamError 스트림 오류 메시지
67
67
  */
68
68
  export interface UseCctvRtcStreamState extends UseCctvRtcStreamError {
69
69
  /**
70
- * WebRTC 연결 상태
70
+ * UI 표시 기준 WebRTC 연결 상태
71
+ * @desc
72
+ * 최초 연결 전에는 실제 PeerConnection 상태를 나타낸다. 이미 한 번 연결된 stream이
73
+ * recoverable `disconnected`/`failed` 상태에 들어가면 기존 화면과 live count 유지를 위해
74
+ * `connected`로 smoothing될 수 있다.
71
75
  */
72
76
  connectionState: RTCPeerConnectionState;
73
77
  /**
74
- * startWhepStream 진행 여부
78
+ * UI 표시 기준 startWhepStream 진행 여부
79
+ * @desc
80
+ * 최초 연결 전에는 실제 진행 상태를 나타낸다. 이미 한 번 연결된 stream을 교체하는 동안에는
81
+ * 기존 화면과 live count를 유지하기 위해 false로 smoothing될 수 있다.
75
82
  */
76
83
  isStreaming: boolean;
77
84
  /**
78
- * 토큰 발급 요청 진행 여부
85
+ * UI 표시 기준 토큰 발급 요청 진행 여부
86
+ * @desc
87
+ * 최초 연결 전에는 실제 요청 상태를 나타낸다. 이미 한 번 연결된 stream의 재연결 token
88
+ * 재발급 중에는 기존 화면과 live count를 유지하기 위해 false로 smoothing될 수 있다.
79
89
  */
80
90
  isTokenLoading: boolean;
81
91
  }
@@ -83,10 +93,10 @@ export interface UseCctvRtcStreamState extends UseCctvRtcStreamError {
83
93
  /**
84
94
  * CCTV; useCctvRtcStream return
85
95
  * @property {React.RefObject<HTMLVideoElement>} videoRef <video /> ref
86
- * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
96
+ * @property {RTCPeerConnectionState} connectionState UI 표시 기준 WebRTC 연결 상태
87
97
  * @property {string | null} streamError 스트림 오류 메시지
88
- * @property {boolean} isStreaming startWhepStream 진행 여부
89
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
98
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
99
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
90
100
  * @property {boolean} isTokenError 토큰 발급 실패 여부
91
101
  * @property {boolean} canReconnect UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
92
102
  * @property {() => Promise<void>} refetchToken 토큰 재발급 함수
@@ -104,6 +114,7 @@ export interface UseCctvRtcStreamReturn extends UseCctvRtcStreamState {
104
114
  * 서비스 overlay가 reconnectStream()을 호출해도 되는 시점에 true가 된다.
105
115
  * post-connected `disconnected`는 자동 재연결 사유가 아니며,
106
116
  * token/stream 오류 또는 `failed`처럼 명확한 복구 사유만 true 전환 후보가 된다.
117
+ * recoverable `failed`에서는 화면이 live로 유지되는 동안에도 true가 될 수 있다.
107
118
  */
108
119
  canReconnect: boolean;
109
120
  /**
@@ -6,12 +6,13 @@ import type { UseCctvRtcStreamError, UseCctvRtcStreamState } from "./hook";
6
6
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
7
7
  * @property {boolean} [hasCamProp] 직접 cam prop을 받은 경우 true
8
8
  * @property {boolean} [isFetching] 서버에서 cam 리스트를 가져오는 중인지 여부
9
- * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
10
- * @property {boolean} isStreaming startWhepStream 진행 여부
11
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
9
+ * @property {RTCPeerConnectionState} connectionState UI 표시 기준 WebRTC 연결 상태
10
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
11
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
12
12
  * @property {boolean} isTokenError 토큰 발급 실패 여부
13
13
  * @property {string | null} streamError 스트림 오류 메시지
14
- * @property {boolean} [canReconnect] UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
14
+ * @property {boolean} [canReconnect] UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부.
15
+ * 오류 스타일 판정에는 직접 사용하지 않는다.
15
16
  */
16
17
  export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
17
18
  /**
@@ -28,6 +29,8 @@ export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
28
29
  isFetching?: boolean;
29
30
  /**
30
31
  * UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
32
+ * @desc
33
+ * 재연결 trigger gate이며, 기본 error style 판정에는 직접 사용하지 않는다.
31
34
  */
32
35
  canReconnect?: boolean;
33
36
  }
@@ -35,9 +38,9 @@ export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
35
38
  /**
36
39
  * CCTV; getIsLive() params
37
40
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
38
- * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
39
- * @property {boolean} isStreaming startWhepStream 진행 여부
40
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
41
+ * @property {RTCPeerConnectionState} connectionState UI 표시 기준 WebRTC 연결 상태
42
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
43
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
41
44
  * @property {boolean} isTokenError 토큰 발급 실패 여부
42
45
  * @property {string | null} streamError 스트림 오류 메시지
43
46
  */
@@ -53,7 +56,8 @@ export interface CctvVideoLiveParams extends UseCctvRtcStreamState {
53
56
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
54
57
  * @property {boolean} isTokenError 토큰 발급 실패 여부
55
58
  * @property {string | null} streamError 스트림 오류 메시지
56
- * @property {boolean} [canReconnect] UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
59
+ * @property {boolean} [canReconnect] UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부.
60
+ * 오류 스타일 판정에는 직접 사용하지 않는다.
57
61
  */
58
62
  export interface CctvVideoErrorParams extends UseCctvRtcStreamError {
59
63
  /**
@@ -62,6 +66,8 @@ export interface CctvVideoErrorParams extends UseCctvRtcStreamError {
62
66
  cam?: CctvCompanyCameraData;
63
67
  /**
64
68
  * UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
69
+ * @desc
70
+ * 재연결 trigger gate이며, 기본 error style 판정에는 직접 사용하지 않는다.
65
71
  */
66
72
  canReconnect?: boolean;
67
73
  }
@@ -31,9 +31,9 @@ const RTC_SESSION_ENDED_RECONNECT_STATES = new Set<RTCPeerConnectionState>([
31
31
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
32
32
  * @property {boolean} [hasCamProp] 직접 cam prop을 받은 경우 true
33
33
  * @property {boolean} [isFetching] 서버에서 cam 리스트를 가져오는 중인지 여부
34
- * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
35
- * @property {boolean} isStreaming startWhepStream 진행 여부
36
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
34
+ * @property {RTCPeerConnectionState} connectionState UI 표시 기준 WebRTC 연결 상태
35
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
36
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
37
37
  * @property {boolean} isTokenError 토큰 발급 실패 여부
38
38
  * @property {string | null} streamError 스트림 오류 메시지
39
39
  * @returns {string | null} 상태에 따른 메시지 반환 (문제가 없으면 null)
@@ -72,9 +72,9 @@ export function getOverlayMessage({
72
72
  * CCTV; 스트리밍 라이브 상태 판단
73
73
  * @param {CctvVideoLiveParams} params 상태 파라미터
74
74
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
75
- * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
76
- * @property {boolean} isStreaming startWhepStream 진행 여부
77
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
75
+ * @property {RTCPeerConnectionState} connectionState UI 표시 기준 WebRTC 연결 상태
76
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
77
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
78
78
  * @property {boolean} isTokenError 토큰 발급 실패 여부
79
79
  * @property {string | null} streamError 스트림 오류 메시지
80
80
  * @returns {boolean} 라이브 상태 여부 반환
@@ -102,17 +102,16 @@ export function getIsLive({
102
102
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
103
103
  * @property {boolean} isTokenError 토큰 발급 실패 여부
104
104
  * @property {string | null} streamError 스트림 오류 메시지
105
+ * @property {boolean} [canReconnect] 재연결 trigger 가능 여부. 오류 표시 여부에는 직접 사용하지 않는다.
105
106
  * @returns {boolean} 에러 상태 여부 반환
106
107
  */
107
108
  export function getIsError({
108
109
  cam,
109
- canReconnect,
110
110
  isTokenError,
111
111
  streamError,
112
112
  }: CctvVideoErrorParams): boolean {
113
113
  if (!cam) return true;
114
114
  if (!cam.cam_online) return true;
115
- if (canReconnect) return true;
116
115
  if (isTokenError) return true;
117
116
  if (streamError) return true;
118
117
  return false;