@uniai-fe/uds-templates 0.6.12 → 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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
4
4
|
import {
|
|
5
|
+
getCctvDebugJwtMetadata,
|
|
5
6
|
getCctvDebugKeyLabel,
|
|
6
7
|
getCctvDebugUrlLabel,
|
|
7
8
|
logCctvDebugEvent,
|
|
@@ -31,6 +32,8 @@ interface CctvRtcStreamStartParams {
|
|
|
31
32
|
video: HTMLVideoElement;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
const WHEP_ERROR_BODY_PREVIEW_LIMIT = 1000;
|
|
36
|
+
|
|
34
37
|
const getEntrySnapshot = (
|
|
35
38
|
entry?: CctvRtcStreamEntry,
|
|
36
39
|
): CctvRtcStreamSnapshot => ({
|
|
@@ -46,11 +49,70 @@ const attachVideo = (video: HTMLVideoElement, stream: MediaStream | null) => {
|
|
|
46
49
|
video.srcObject = stream;
|
|
47
50
|
};
|
|
48
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
|
+
|
|
49
61
|
const notifyEntry = (entry: CctvRtcStreamEntry) => {
|
|
50
62
|
const snapshot = getEntrySnapshot(entry);
|
|
51
63
|
entry.listeners.forEach(listener => listener(snapshot));
|
|
52
64
|
};
|
|
53
65
|
|
|
66
|
+
const getWhepResponseBodyPreview = async (
|
|
67
|
+
response: Response,
|
|
68
|
+
): Promise<string | null> => {
|
|
69
|
+
try {
|
|
70
|
+
const text = await response.clone().text();
|
|
71
|
+
return text.slice(0, WHEP_ERROR_BODY_PREVIEW_LIMIT);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* WHEP 실패 body를 startWhepStream 외부에서 관찰하기 위한 fetch wrapper.
|
|
79
|
+
* @desc
|
|
80
|
+
* 성공 응답은 그대로 통과시키고, 실패 응답에서만 clone body를 읽어 debug log에 남긴다.
|
|
81
|
+
*/
|
|
82
|
+
const createCctvDebugWhepFetcher =
|
|
83
|
+
({
|
|
84
|
+
endpoint,
|
|
85
|
+
identityKey,
|
|
86
|
+
streamKey,
|
|
87
|
+
token,
|
|
88
|
+
}: {
|
|
89
|
+
endpoint: string;
|
|
90
|
+
identityKey: string;
|
|
91
|
+
streamKey: string;
|
|
92
|
+
token: string;
|
|
93
|
+
}): typeof fetch =>
|
|
94
|
+
async (input, init) => {
|
|
95
|
+
const response = await fetch(input, init);
|
|
96
|
+
if (response.ok) return response;
|
|
97
|
+
|
|
98
|
+
logCctvDebugEvent({
|
|
99
|
+
event: "whep:response-error",
|
|
100
|
+
level: "error",
|
|
101
|
+
payload: {
|
|
102
|
+
endpoint: getCctvDebugUrlLabel(endpoint),
|
|
103
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
104
|
+
responseBody: await getWhepResponseBodyPreview(response),
|
|
105
|
+
status: response.status,
|
|
106
|
+
statusText: response.statusText,
|
|
107
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
108
|
+
token: getCctvDebugJwtMetadata(token),
|
|
109
|
+
},
|
|
110
|
+
source: "streamRegistry",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return response;
|
|
114
|
+
};
|
|
115
|
+
|
|
54
116
|
const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
55
117
|
logCctvDebugEvent({
|
|
56
118
|
event: "entry:close",
|
|
@@ -72,9 +134,7 @@ const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
|
72
134
|
entry.isStreaming = false;
|
|
73
135
|
|
|
74
136
|
entry.attachedVideos.forEach(video => {
|
|
75
|
-
|
|
76
|
-
video.srcObject = null;
|
|
77
|
-
}
|
|
137
|
+
clearVideoIfAttachedStream(video, entry.stream);
|
|
78
138
|
});
|
|
79
139
|
|
|
80
140
|
notifyEntry(entry);
|
|
@@ -113,13 +173,13 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
113
173
|
}
|
|
114
174
|
|
|
115
175
|
entry.attachedVideos.add(video);
|
|
116
|
-
|
|
176
|
+
if (entry.stream || !video.srcObject) {
|
|
177
|
+
attachVideo(video, entry.stream);
|
|
178
|
+
}
|
|
117
179
|
|
|
118
180
|
return () => {
|
|
119
181
|
entry.attachedVideos.delete(video);
|
|
120
|
-
|
|
121
|
-
video.srcObject = null;
|
|
122
|
-
}
|
|
182
|
+
clearVideoIfAttachedStream(video, entry.stream);
|
|
123
183
|
};
|
|
124
184
|
},
|
|
125
185
|
closeAll() {
|
|
@@ -180,7 +240,12 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
180
240
|
};
|
|
181
241
|
|
|
182
242
|
entries.set(streamKey, entry);
|
|
183
|
-
|
|
243
|
+
if (!video.srcObject) {
|
|
244
|
+
attachVideo(video, null);
|
|
245
|
+
} else {
|
|
246
|
+
video.playsInline = true;
|
|
247
|
+
video.autoplay = true;
|
|
248
|
+
}
|
|
184
249
|
logCctvDebugEvent({
|
|
185
250
|
event: "start:create-entry",
|
|
186
251
|
payload: {
|
|
@@ -188,12 +253,19 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
188
253
|
endpoint: getCctvDebugUrlLabel(endpoint),
|
|
189
254
|
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
190
255
|
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
256
|
+
token: getCctvDebugJwtMetadata(token),
|
|
191
257
|
},
|
|
192
258
|
source: "streamRegistry",
|
|
193
259
|
});
|
|
194
260
|
|
|
195
261
|
startWhepStream({
|
|
196
262
|
endpoint,
|
|
263
|
+
fetcher: createCctvDebugWhepFetcher({
|
|
264
|
+
endpoint,
|
|
265
|
+
identityKey,
|
|
266
|
+
streamKey,
|
|
267
|
+
token,
|
|
268
|
+
}),
|
|
197
269
|
token,
|
|
198
270
|
video,
|
|
199
271
|
signal: controller.signal,
|
|
@@ -268,7 +340,7 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
268
340
|
source: "streamRegistry",
|
|
269
341
|
});
|
|
270
342
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
271
|
-
|
|
343
|
+
clearVideoIfAttachedStream(attachedVideo, entry.stream),
|
|
272
344
|
);
|
|
273
345
|
notifyEntry(entry);
|
|
274
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
|
|
331
|
+
const isPostConnectedReplacementLoading =
|
|
332
332
|
hasConnected &&
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
497
|
+
event: "stream-effect:close-previous-after-track",
|
|
477
498
|
payload: {
|
|
478
499
|
...debugBasePayload,
|
|
479
|
-
|
|
480
|
-
|
|
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
|
|
524
|
-
currentVideo.srcObject
|
|
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
|
|
534
|
-
currentVideo.srcObject
|
|
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
|
|
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
|
-
|
|
724
|
+
displayIsTokenLoading,
|
|
694
725
|
isTokenError,
|
|
695
|
-
|
|
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,
|
package/src/cctv/types/hook.ts
CHANGED
|
@@ -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
|
*/
|
package/src/cctv/utils/debug.ts
CHANGED
|
@@ -32,6 +32,15 @@ export interface CctvDebugSummary {
|
|
|
32
32
|
total: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface CctvDebugJwtMetadata {
|
|
36
|
+
expiresAt: string | null;
|
|
37
|
+
expiresInSec: number | null;
|
|
38
|
+
headerAlg: string | null;
|
|
39
|
+
headerKid: string | null;
|
|
40
|
+
tokenLabel: string | null;
|
|
41
|
+
tokenPartCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
export interface CctvDebugBuffer {
|
|
36
45
|
clear: () => void;
|
|
37
46
|
dump: () => CctvDebugEvent[];
|
|
@@ -108,6 +117,13 @@ const getDebugStorageOverride = (keys: readonly string[]): boolean | null => {
|
|
|
108
117
|
return null;
|
|
109
118
|
};
|
|
110
119
|
|
|
120
|
+
const getDebugConsoleOverride = (): boolean | null => {
|
|
121
|
+
const queryOverride = getDebugQueryOverride(DEBUG_CONSOLE_QUERY_KEYS);
|
|
122
|
+
if (queryOverride !== null) return queryOverride;
|
|
123
|
+
|
|
124
|
+
return getDebugStorageOverride(DEBUG_CONSOLE_STORAGE_KEYS);
|
|
125
|
+
};
|
|
126
|
+
|
|
111
127
|
const isLocalhostDebugDefaultEnabled = (): boolean => {
|
|
112
128
|
const { hostname } = window.location;
|
|
113
129
|
return (
|
|
@@ -221,6 +237,57 @@ const summarizeCctvDebugEvents = (
|
|
|
221
237
|
};
|
|
222
238
|
};
|
|
223
239
|
|
|
240
|
+
const decodeBase64UrlJson = (value: string): Record<string, unknown> | null => {
|
|
241
|
+
try {
|
|
242
|
+
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
243
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
244
|
+
const parsed = JSON.parse(atob(padded));
|
|
245
|
+
|
|
246
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
247
|
+
? (parsed as Record<string, unknown>)
|
|
248
|
+
: null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const getJwtStringClaim = (
|
|
255
|
+
claims: Record<string, unknown> | null,
|
|
256
|
+
key: string,
|
|
257
|
+
): string | null => {
|
|
258
|
+
const value = claims?.[key];
|
|
259
|
+
return typeof value === "string" ? value : null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const getJwtExpiresAt = (
|
|
263
|
+
claims: Record<string, unknown> | null,
|
|
264
|
+
): { expiresAt: string | null; expiresInSec: number | null } => {
|
|
265
|
+
const exp = claims?.exp;
|
|
266
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) {
|
|
267
|
+
return { expiresAt: null, expiresInSec: null };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const expiresAtMs = exp * 1000;
|
|
271
|
+
return {
|
|
272
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
273
|
+
expiresInSec: Math.floor((expiresAtMs - Date.now()) / 1000),
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const stringifyCctvDebugEvent = (entry: CctvDebugEvent): string => {
|
|
278
|
+
try {
|
|
279
|
+
return JSON.stringify(entry);
|
|
280
|
+
} catch {
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
at: entry.at,
|
|
283
|
+
event: entry.event,
|
|
284
|
+
level: entry.level,
|
|
285
|
+
seq: entry.seq,
|
|
286
|
+
source: entry.source,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
224
291
|
/**
|
|
225
292
|
* identity/stream key를 원문 대신 추적 가능한 hash label로 바꾼다.
|
|
226
293
|
*/
|
|
@@ -253,11 +320,37 @@ export const getCctvDebugUrlLabel = (
|
|
|
253
320
|
}
|
|
254
321
|
};
|
|
255
322
|
|
|
323
|
+
/**
|
|
324
|
+
* JWT 원문 대신 헤더/만료시각과 hash label만 기록한다.
|
|
325
|
+
* @desc
|
|
326
|
+
* token signature mismatch를 추적하기 위한 메타 정보이며, Bearer token 원문과 payload claim은
|
|
327
|
+
* debug log에 남기지 않는다.
|
|
328
|
+
*/
|
|
329
|
+
export const getCctvDebugJwtMetadata = (
|
|
330
|
+
token: string | null | undefined,
|
|
331
|
+
): CctvDebugJwtMetadata | null => {
|
|
332
|
+
if (!token) return null;
|
|
333
|
+
|
|
334
|
+
const [headerPart, payloadPart] = token.split(".");
|
|
335
|
+
const header = headerPart ? decodeBase64UrlJson(headerPart) : null;
|
|
336
|
+
const payload = payloadPart ? decodeBase64UrlJson(payloadPart) : null;
|
|
337
|
+
const { expiresAt, expiresInSec } = getJwtExpiresAt(payload);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
expiresAt,
|
|
341
|
+
expiresInSec,
|
|
342
|
+
headerAlg: getJwtStringClaim(header, "alg"),
|
|
343
|
+
headerKid: getJwtStringClaim(header, "kid"),
|
|
344
|
+
tokenLabel: getCctvDebugKeyLabel(token),
|
|
345
|
+
tokenPartCount: token.split(".").length,
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
256
349
|
/**
|
|
257
350
|
* CCTV debug logging 활성화 여부를 확인한다.
|
|
258
351
|
* @desc
|
|
259
|
-
* production default는 off
|
|
260
|
-
*
|
|
352
|
+
* production default는 off, localhost default는 ring buffer on이다.
|
|
353
|
+
* console 출력은 별도 console flag를 켠 경우에만 활성화한다.
|
|
261
354
|
*/
|
|
262
355
|
export const isCctvDebugEnabled = (): boolean => {
|
|
263
356
|
if (!isBrowser()) return false;
|
|
@@ -265,6 +358,9 @@ export const isCctvDebugEnabled = (): boolean => {
|
|
|
265
358
|
const queryOverride = getDebugQueryOverride(DEBUG_QUERY_KEYS);
|
|
266
359
|
if (queryOverride !== null) return queryOverride;
|
|
267
360
|
|
|
361
|
+
const consoleOverride = getDebugConsoleOverride();
|
|
362
|
+
if (consoleOverride === true) return true;
|
|
363
|
+
|
|
268
364
|
const storageOverride = getDebugStorageOverride(DEBUG_STORAGE_KEYS);
|
|
269
365
|
if (storageOverride !== null) return storageOverride;
|
|
270
366
|
|
|
@@ -274,19 +370,7 @@ export const isCctvDebugEnabled = (): boolean => {
|
|
|
274
370
|
const isCctvDebugConsoleEnabled = (): boolean => {
|
|
275
371
|
if (!isBrowser()) return false;
|
|
276
372
|
|
|
277
|
-
|
|
278
|
-
if (consoleQueryOverride !== null) return consoleQueryOverride;
|
|
279
|
-
|
|
280
|
-
const consoleStorageOverride = getDebugStorageOverride(
|
|
281
|
-
DEBUG_CONSOLE_STORAGE_KEYS,
|
|
282
|
-
);
|
|
283
|
-
if (consoleStorageOverride !== null) return consoleStorageOverride;
|
|
284
|
-
|
|
285
|
-
return (
|
|
286
|
-
getDebugQueryOverride(DEBUG_QUERY_KEYS) === true ||
|
|
287
|
-
getDebugStorageOverride(DEBUG_STORAGE_KEYS) === true ||
|
|
288
|
-
isLocalhostDebugDefaultEnabled()
|
|
289
|
-
);
|
|
373
|
+
return getDebugConsoleOverride() === true;
|
|
290
374
|
};
|
|
291
375
|
|
|
292
376
|
/**
|
|
@@ -373,5 +457,5 @@ export const logCctvDebugEvent = ({
|
|
|
373
457
|
? console.info
|
|
374
458
|
: console.debug;
|
|
375
459
|
|
|
376
|
-
logger.call(console,
|
|
460
|
+
logger.call(console, `[UDS:CCTV] ${stringifyCctvDebugEvent(entry)}`);
|
|
377
461
|
};
|
|
@@ -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} 라이브 상태 여부 반환
|