@uniai-fe/uds-templates 0.6.13 → 0.6.14

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.14",
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
  });
@@ -140,8 +140,8 @@ const canUseTokenForNewConnection = ({
140
140
  * videoRef, // WebRTC MediaStream을 연결할 video 요소 ref
141
141
  * connectionState, // 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,30 @@ export function useCctvRtcStream({
328
328
  streamIdentityKey,
329
329
  ]);
330
330
 
331
- const shouldPreserveConnectedDisplay =
331
+ const isPostConnectedReplacementLoading =
332
332
  hasConnected &&
333
- !isPostConnectedReconnectReady &&
334
- DISPLAY_CONNECTED_DURING_RECOVERY_STATES.has(connectionState);
333
+ Boolean(streamIdentityKey) &&
334
+ !isTokenError &&
335
+ !streamError &&
336
+ (isTokenLoading || isStreaming);
337
+
338
+ const shouldPreserveConnectedDisplay =
339
+ isPostConnectedReplacementLoading ||
340
+ (hasConnected &&
341
+ !isPostConnectedReconnectReady &&
342
+ DISPLAY_CONNECTED_DURING_RECOVERY_STATES.has(connectionState));
335
343
 
336
344
  // 반환 state는 CamList/Viewer/overlay의 live/error/message 계산에 직접 쓰인다.
337
- // post-connected disconnected는 자동 재연결 대신 기존 화면을 유지해 UI reset 파동을 막는다.
345
+ // post-connected recovery/replacement 중에는 기존 화면을 유지해 UI reset 파동을 막는다.
338
346
  const displayConnectionState = shouldPreserveConnectedDisplay
339
347
  ? "connected"
340
348
  : connectionState;
349
+ const displayIsStreaming = shouldPreserveConnectedDisplay
350
+ ? false
351
+ : isStreaming;
352
+ const displayIsTokenLoading = shouldPreserveConnectedDisplay
353
+ ? false
354
+ : isTokenLoading;
341
355
 
342
356
  const skippedDisconnectedDebugKeyRef = useRef<string | null>(null);
343
357
 
@@ -468,20 +482,39 @@ export function useCctvRtcStream({
468
482
  if (!streamKey || !tokenQuery.data?.token || !endpoint || !currentVideo)
469
483
  return;
470
484
 
471
- if (
485
+ const previousStreamKey =
472
486
  activeStreamIdentityKeyRef.current === streamIdentityKey &&
473
487
  activeStreamKeyRef.current !== streamKey
474
- ) {
488
+ ? activeStreamKeyRef.current
489
+ : null;
490
+ let didClosePreviousStream = false;
491
+
492
+ const closePreviousStreamAfterTrack = () => {
493
+ if (!previousStreamKey || didClosePreviousStream) return;
494
+
495
+ didClosePreviousStream = true;
475
496
  logCctvDebugEvent({
476
- event: "stream-effect:close-previous-identity",
497
+ event: "stream-effect:close-previous-after-track",
477
498
  payload: {
478
499
  ...debugBasePayload,
479
- activeStreamKey: getCctvDebugKeyLabel(activeStreamKeyRef.current),
480
- nextStreamKey: getCctvDebugKeyLabel(streamKey),
500
+ previousStreamKey: getCctvDebugKeyLabel(previousStreamKey),
501
+ streamKey: getCctvDebugKeyLabel(streamKey),
481
502
  },
482
503
  source: "useRtcStream",
483
504
  });
484
505
  streamRegistry.closeByIdentity(streamIdentityKey, streamKey);
506
+ };
507
+
508
+ if (previousStreamKey) {
509
+ logCctvDebugEvent({
510
+ event: "stream-effect:prepare-replacement",
511
+ payload: {
512
+ ...debugBasePayload,
513
+ previousStreamKey: getCctvDebugKeyLabel(previousStreamKey),
514
+ streamKey: getCctvDebugKeyLabel(streamKey),
515
+ },
516
+ source: "useRtcStream",
517
+ });
485
518
  }
486
519
 
487
520
  activeStreamKeyRef.current = streamKey;
@@ -520,8 +553,11 @@ export function useCctvRtcStream({
520
553
  });
521
554
  setHasConnected(true);
522
555
  }
523
- if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
524
- currentVideo.srcObject = snapshot.stream;
556
+ if (snapshot.stream) {
557
+ if (currentVideo.srcObject !== snapshot.stream) {
558
+ currentVideo.srcObject = snapshot.stream;
559
+ }
560
+ closePreviousStreamAfterTrack();
525
561
  }
526
562
  });
527
563
  const snapshot = streamRegistry.getSnapshot(streamKey);
@@ -530,8 +566,11 @@ export function useCctvRtcStream({
530
566
  setStreaming(snapshot.isStreaming);
531
567
  if (snapshot.connectionState === "connected") setHasConnected(true);
532
568
 
533
- if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
534
- currentVideo.srcObject = snapshot.stream;
569
+ if (snapshot.stream) {
570
+ if (currentVideo.srcObject !== snapshot.stream) {
571
+ currentVideo.srcObject = snapshot.stream;
572
+ }
573
+ closePreviousStreamAfterTrack();
535
574
  }
536
575
 
537
576
  // effect cleanup: current video attach만 해제하고 registry stream은 유지한다.
@@ -602,17 +641,9 @@ export function useCctvRtcStream({
602
641
  },
603
642
  source: "useRtcStream",
604
643
  });
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
644
  return result;
614
645
  },
615
- [refetchRtcToken, streamIdentityKey, streamRegistry],
646
+ [refetchRtcToken],
616
647
  );
617
648
 
618
649
  const canReconnect = useMemo(() => {
@@ -681,18 +712,18 @@ export function useCctvRtcStream({
681
712
  ? getIsLive({
682
713
  cam,
683
714
  connectionState: displayConnectionState,
684
- isTokenLoading,
715
+ isTokenLoading: displayIsTokenLoading,
685
716
  isTokenError,
686
- isStreaming,
717
+ isStreaming: displayIsStreaming,
687
718
  streamError,
688
719
  })
689
720
  : false,
690
721
  [
691
722
  cam,
692
723
  displayConnectionState,
693
- isTokenLoading,
724
+ displayIsTokenLoading,
694
725
  isTokenError,
695
- isStreaming,
726
+ displayIsStreaming,
696
727
  streamError,
697
728
  ],
698
729
  );
@@ -706,7 +737,10 @@ export function useCctvRtcStream({
706
737
  connectionState,
707
738
  displayConnectionState,
708
739
  hasConnected,
740
+ isPostConnectedReplacementLoading,
709
741
  isPostConnectedReconnectReady,
742
+ displayIsStreaming,
743
+ displayIsTokenLoading,
710
744
  isStreaming,
711
745
  isTokenError,
712
746
  isTokenLoading,
@@ -721,7 +755,10 @@ export function useCctvRtcStream({
721
755
  connectionState,
722
756
  debugBasePayload,
723
757
  displayConnectionState,
758
+ displayIsStreaming,
759
+ displayIsTokenLoading,
724
760
  hasConnected,
761
+ isPostConnectedReplacementLoading,
725
762
  isPostConnectedReconnectReady,
726
763
  isStreaming,
727
764
  isTokenError,
@@ -770,8 +807,8 @@ export function useCctvRtcStream({
770
807
  videoRef,
771
808
  connectionState: displayConnectionState,
772
809
  streamError,
773
- isStreaming,
774
- isTokenLoading,
810
+ isStreaming: displayIsStreaming,
811
+ isTokenLoading: displayIsTokenLoading,
775
812
  isTokenError,
776
813
  canReconnect,
777
814
  refetchToken,
@@ -60,8 +60,8 @@ export interface UseCctvRtcStreamError {
60
60
  /**
61
61
  * CCTV; useCctvRtcStream 연결 상태
62
62
  * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
63
- * @property {boolean} isStreaming startWhepStream 진행 여부
64
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
63
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
64
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
65
65
  * @property {boolean} isTokenError 토큰 발급 실패 여부
66
66
  * @property {string | null} streamError 스트림 오류 메시지
67
67
  */
@@ -71,11 +71,17 @@ export interface UseCctvRtcStreamState extends UseCctvRtcStreamError {
71
71
  */
72
72
  connectionState: RTCPeerConnectionState;
73
73
  /**
74
- * startWhepStream 진행 여부
74
+ * UI 표시 기준 startWhepStream 진행 여부
75
+ * @desc
76
+ * 최초 연결 전에는 실제 진행 상태를 나타낸다. 이미 한 번 연결된 stream을 교체하는 동안에는
77
+ * 기존 화면과 live count를 유지하기 위해 false로 smoothing될 수 있다.
75
78
  */
76
79
  isStreaming: boolean;
77
80
  /**
78
- * 토큰 발급 요청 진행 여부
81
+ * UI 표시 기준 토큰 발급 요청 진행 여부
82
+ * @desc
83
+ * 최초 연결 전에는 실제 요청 상태를 나타낸다. 이미 한 번 연결된 stream의 재연결 token
84
+ * 재발급 중에는 기존 화면과 live count를 유지하기 위해 false로 smoothing될 수 있다.
79
85
  */
80
86
  isTokenLoading: boolean;
81
87
  }
@@ -85,8 +91,8 @@ export interface UseCctvRtcStreamState extends UseCctvRtcStreamError {
85
91
  * @property {React.RefObject<HTMLVideoElement>} videoRef <video /> ref
86
92
  * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
87
93
  * @property {string | null} streamError 스트림 오류 메시지
88
- * @property {boolean} isStreaming startWhepStream 진행 여부
89
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
94
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
95
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
90
96
  * @property {boolean} isTokenError 토큰 발급 실패 여부
91
97
  * @property {boolean} canReconnect UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
92
98
  * @property {() => Promise<void>} refetchToken 토큰 재발급 함수
@@ -7,8 +7,8 @@ import type { UseCctvRtcStreamError, UseCctvRtcStreamState } from "./hook";
7
7
  * @property {boolean} [hasCamProp] 직접 cam prop을 받은 경우 true
8
8
  * @property {boolean} [isFetching] 서버에서 cam 리스트를 가져오는 중인지 여부
9
9
  * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
10
- * @property {boolean} isStreaming startWhepStream 진행 여부
11
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
10
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
11
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
12
12
  * @property {boolean} isTokenError 토큰 발급 실패 여부
13
13
  * @property {string | null} streamError 스트림 오류 메시지
14
14
  * @property {boolean} [canReconnect] UDS 내부 허용 사유와 grace/stagger를 통과해 재연결 호출이 가능한지 여부
@@ -36,8 +36,8 @@ export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
36
36
  * CCTV; getIsLive() params
37
37
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
38
38
  * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
39
- * @property {boolean} isStreaming startWhepStream 진행 여부
40
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
39
+ * @property {boolean} isStreaming UI 표시 기준 startWhepStream 진행 여부
40
+ * @property {boolean} isTokenLoading UI 표시 기준 토큰 발급 요청 진행 여부
41
41
  * @property {boolean} isTokenError 토큰 발급 실패 여부
42
42
  * @property {string | null} streamError 스트림 오류 메시지
43
43
  */
@@ -32,8 +32,8 @@ const RTC_SESSION_ENDED_RECONNECT_STATES = new Set<RTCPeerConnectionState>([
32
32
  * @property {boolean} [hasCamProp] 직접 cam prop을 받은 경우 true
33
33
  * @property {boolean} [isFetching] 서버에서 cam 리스트를 가져오는 중인지 여부
34
34
  * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
35
- * @property {boolean} isStreaming startWhepStream 진행 여부
36
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
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)
@@ -73,8 +73,8 @@ export function getOverlayMessage({
73
73
  * @param {CctvVideoLiveParams} params 상태 파라미터
74
74
  * @property {CctvCompanyCameraData} [cam] 카메라 데이터
75
75
  * @property {RTCPeerConnectionState} connectionState WebRTC 연결 상태
76
- * @property {boolean} isStreaming startWhepStream 진행 여부
77
- * @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
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} 라이브 상태 여부 반환