@uniai-fe/uds-templates 0.6.15 → 0.6.16
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 +1 -1
- package/package.json +1 -1
- package/src/cctv/hooks/streamRegistry.ts +0 -151
- package/src/cctv/hooks/useRtcStream.ts +3 -251
- package/src/cctv/styles/video.scss +1 -1
- package/src/cctv/utils/debug.ts +0 -461
package/dist/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
4
|
-
import {
|
|
5
|
-
getCctvDebugJwtMetadata,
|
|
6
|
-
getCctvDebugKeyLabel,
|
|
7
|
-
getCctvDebugUrlLabel,
|
|
8
|
-
logCctvDebugEvent,
|
|
9
|
-
} from "../utils/debug";
|
|
10
4
|
|
|
11
5
|
interface CctvRtcStreamSnapshot {
|
|
12
6
|
connectionState: RTCPeerConnectionState;
|
|
@@ -32,8 +26,6 @@ interface CctvRtcStreamStartParams {
|
|
|
32
26
|
video: HTMLVideoElement;
|
|
33
27
|
}
|
|
34
28
|
|
|
35
|
-
const WHEP_ERROR_BODY_PREVIEW_LIMIT = 1000;
|
|
36
|
-
|
|
37
29
|
const getEntrySnapshot = (
|
|
38
30
|
entry?: CctvRtcStreamEntry,
|
|
39
31
|
): CctvRtcStreamSnapshot => ({
|
|
@@ -63,69 +55,7 @@ const notifyEntry = (entry: CctvRtcStreamEntry) => {
|
|
|
63
55
|
entry.listeners.forEach(listener => listener(snapshot));
|
|
64
56
|
};
|
|
65
57
|
|
|
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
|
-
|
|
116
58
|
const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
117
|
-
logCctvDebugEvent({
|
|
118
|
-
event: "entry:close",
|
|
119
|
-
payload: {
|
|
120
|
-
attachedVideoCount: entry.attachedVideos.size,
|
|
121
|
-
connectionState: entry.connectionState,
|
|
122
|
-
identityKey: getCctvDebugKeyLabel(entry.identityKey),
|
|
123
|
-
isStreaming: entry.isStreaming,
|
|
124
|
-
streamKey: getCctvDebugKeyLabel(entry.streamKey),
|
|
125
|
-
streamTrackCount: entry.stream?.getTracks().length ?? 0,
|
|
126
|
-
},
|
|
127
|
-
source: "streamRegistry",
|
|
128
|
-
});
|
|
129
59
|
entry.controller?.abort();
|
|
130
60
|
entry.controller = null;
|
|
131
61
|
entry.handle?.close();
|
|
@@ -187,15 +117,6 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
187
117
|
entries.clear();
|
|
188
118
|
},
|
|
189
119
|
closeByIdentity(identityKey, exceptStreamKey) {
|
|
190
|
-
logCctvDebugEvent({
|
|
191
|
-
event: "identity:close",
|
|
192
|
-
payload: {
|
|
193
|
-
entryCount: entries.size,
|
|
194
|
-
exceptStreamKey: getCctvDebugKeyLabel(exceptStreamKey),
|
|
195
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
196
|
-
},
|
|
197
|
-
source: "streamRegistry",
|
|
198
|
-
});
|
|
199
120
|
entries.forEach(entry => {
|
|
200
121
|
if (
|
|
201
122
|
entry.identityKey === identityKey &&
|
|
@@ -211,21 +132,10 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
211
132
|
start({ endpoint, identityKey, streamKey, token, video }) {
|
|
212
133
|
const existingEntry = entries.get(streamKey);
|
|
213
134
|
if (existingEntry) {
|
|
214
|
-
logCctvDebugEvent({
|
|
215
|
-
event: "start:reuse-existing",
|
|
216
|
-
payload: {
|
|
217
|
-
connectionState: existingEntry.connectionState,
|
|
218
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
219
|
-
isStreaming: existingEntry.isStreaming,
|
|
220
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
221
|
-
},
|
|
222
|
-
source: "streamRegistry",
|
|
223
|
-
});
|
|
224
135
|
return;
|
|
225
136
|
}
|
|
226
137
|
|
|
227
138
|
const controller = new AbortController();
|
|
228
|
-
const startedAt = Date.now();
|
|
229
139
|
const entry: CctvRtcStreamEntry = {
|
|
230
140
|
attachedVideos: new Set([video]),
|
|
231
141
|
connectionState: "connecting",
|
|
@@ -246,58 +156,18 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
246
156
|
video.playsInline = true;
|
|
247
157
|
video.autoplay = true;
|
|
248
158
|
}
|
|
249
|
-
logCctvDebugEvent({
|
|
250
|
-
event: "start:create-entry",
|
|
251
|
-
payload: {
|
|
252
|
-
attachedVideoCount: entry.attachedVideos.size,
|
|
253
|
-
endpoint: getCctvDebugUrlLabel(endpoint),
|
|
254
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
255
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
256
|
-
token: getCctvDebugJwtMetadata(token),
|
|
257
|
-
},
|
|
258
|
-
source: "streamRegistry",
|
|
259
|
-
});
|
|
260
159
|
|
|
261
160
|
startWhepStream({
|
|
262
161
|
endpoint,
|
|
263
|
-
fetcher: createCctvDebugWhepFetcher({
|
|
264
|
-
endpoint,
|
|
265
|
-
identityKey,
|
|
266
|
-
streamKey,
|
|
267
|
-
token,
|
|
268
|
-
}),
|
|
269
162
|
token,
|
|
270
163
|
video,
|
|
271
164
|
signal: controller.signal,
|
|
272
165
|
onConnectionStateChange: state => {
|
|
273
166
|
entry.connectionState = state;
|
|
274
|
-
logCctvDebugEvent({
|
|
275
|
-
event: "connection-state:change",
|
|
276
|
-
payload: {
|
|
277
|
-
elapsedMs: Date.now() - startedAt,
|
|
278
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
279
|
-
state,
|
|
280
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
281
|
-
},
|
|
282
|
-
source: "streamRegistry",
|
|
283
|
-
});
|
|
284
167
|
notifyEntry(entry);
|
|
285
168
|
},
|
|
286
169
|
onTrack: event => {
|
|
287
170
|
entry.stream = event.streams[0] ?? null;
|
|
288
|
-
logCctvDebugEvent({
|
|
289
|
-
event: "track:received",
|
|
290
|
-
payload: {
|
|
291
|
-
elapsedMs: Date.now() - startedAt,
|
|
292
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
293
|
-
streamId: entry.stream?.id ?? null,
|
|
294
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
295
|
-
trackId: event.track.id,
|
|
296
|
-
trackKind: event.track.kind,
|
|
297
|
-
trackReadyState: event.track.readyState,
|
|
298
|
-
},
|
|
299
|
-
source: "streamRegistry",
|
|
300
|
-
});
|
|
301
171
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
302
172
|
attachVideo(attachedVideo, entry.stream),
|
|
303
173
|
);
|
|
@@ -307,16 +177,6 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
307
177
|
.then(handle => {
|
|
308
178
|
entry.handle = handle;
|
|
309
179
|
entry.isStreaming = false;
|
|
310
|
-
logCctvDebugEvent({
|
|
311
|
-
event: "start:resolved",
|
|
312
|
-
level: "info",
|
|
313
|
-
payload: {
|
|
314
|
-
elapsedMs: Date.now() - startedAt,
|
|
315
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
316
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
317
|
-
},
|
|
318
|
-
source: "streamRegistry",
|
|
319
|
-
});
|
|
320
180
|
notifyEntry(entry);
|
|
321
181
|
})
|
|
322
182
|
.catch(error => {
|
|
@@ -328,17 +188,6 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
328
188
|
error instanceof Error
|
|
329
189
|
? error.message
|
|
330
190
|
: "스트림 연결에 실패했습니다.";
|
|
331
|
-
logCctvDebugEvent({
|
|
332
|
-
event: "start:rejected",
|
|
333
|
-
level: "error",
|
|
334
|
-
payload: {
|
|
335
|
-
elapsedMs: Date.now() - startedAt,
|
|
336
|
-
errorMessage: entry.streamError,
|
|
337
|
-
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
338
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
339
|
-
},
|
|
340
|
-
source: "streamRegistry",
|
|
341
|
-
});
|
|
342
191
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
343
192
|
clearVideoIfAttachedStream(attachedVideo, entry.stream),
|
|
344
193
|
);
|
|
@@ -19,11 +19,6 @@ import type {
|
|
|
19
19
|
UseCctvRtcStreamReturn,
|
|
20
20
|
} from "../types";
|
|
21
21
|
import { cctvRtcLiveRegistryAtom } from "../jotai/rtc";
|
|
22
|
-
import {
|
|
23
|
-
getCctvDebugKeyLabel,
|
|
24
|
-
getCctvDebugStackTrace,
|
|
25
|
-
logCctvDebugEvent,
|
|
26
|
-
} from "../utils/debug";
|
|
27
22
|
import { getIsLive } from "../utils/video-state";
|
|
28
23
|
import { useFormContext, useWatch } from "react-hook-form";
|
|
29
24
|
|
|
@@ -236,23 +231,9 @@ export function useCctvRtcStream({
|
|
|
236
231
|
return [tokenUsername, cam.company_id, cam.cam_id, endpoint].join("|");
|
|
237
232
|
}, [cam?.cam_id, cam?.company_id, endpoint, tokenUsername]);
|
|
238
233
|
|
|
239
|
-
const debugBasePayload = useMemo(
|
|
240
|
-
() => ({
|
|
241
|
-
camId: cam?.cam_id,
|
|
242
|
-
companyId: cam?.company_id,
|
|
243
|
-
streamIdentityKey: getCctvDebugKeyLabel(streamIdentityKey),
|
|
244
|
-
}),
|
|
245
|
-
[cam?.cam_id, cam?.company_id, streamIdentityKey],
|
|
246
|
-
);
|
|
247
|
-
|
|
248
234
|
useEffect(() => {
|
|
249
|
-
logCctvDebugEvent({
|
|
250
|
-
event: "identity:reset-has-connected",
|
|
251
|
-
payload: debugBasePayload,
|
|
252
|
-
source: "useRtcStream",
|
|
253
|
-
});
|
|
254
235
|
setHasConnected(false);
|
|
255
|
-
}, [
|
|
236
|
+
}, [streamIdentityKey]);
|
|
256
237
|
|
|
257
238
|
const reconnectReason = useMemo(
|
|
258
239
|
() =>
|
|
@@ -282,45 +263,14 @@ export function useCctvRtcStream({
|
|
|
282
263
|
`${streamIdentityKey}|${reconnectReason}`,
|
|
283
264
|
);
|
|
284
265
|
|
|
285
|
-
logCctvDebugEvent({
|
|
286
|
-
event: "reconnect-gate:timer-start",
|
|
287
|
-
payload: {
|
|
288
|
-
...debugBasePayload,
|
|
289
|
-
isStreaming,
|
|
290
|
-
isTokenLoading,
|
|
291
|
-
reconnectDelayMs,
|
|
292
|
-
reconnectReason,
|
|
293
|
-
},
|
|
294
|
-
source: "useRtcStream",
|
|
295
|
-
});
|
|
296
|
-
|
|
297
266
|
const timeout = setTimeout(() => {
|
|
298
267
|
setPostConnectedReconnectReady(true);
|
|
299
|
-
logCctvDebugEvent({
|
|
300
|
-
event: "reconnect-gate:ready",
|
|
301
|
-
payload: {
|
|
302
|
-
...debugBasePayload,
|
|
303
|
-
reconnectDelayMs,
|
|
304
|
-
reconnectReason,
|
|
305
|
-
},
|
|
306
|
-
source: "useRtcStream",
|
|
307
|
-
});
|
|
308
268
|
}, reconnectDelayMs);
|
|
309
269
|
|
|
310
270
|
return () => {
|
|
311
271
|
clearTimeout(timeout);
|
|
312
|
-
logCctvDebugEvent({
|
|
313
|
-
event: "reconnect-gate:timer-cancel",
|
|
314
|
-
payload: {
|
|
315
|
-
...debugBasePayload,
|
|
316
|
-
reconnectDelayMs,
|
|
317
|
-
reconnectReason,
|
|
318
|
-
},
|
|
319
|
-
source: "useRtcStream",
|
|
320
|
-
});
|
|
321
272
|
};
|
|
322
273
|
}, [
|
|
323
|
-
debugBasePayload,
|
|
324
274
|
hasConnected,
|
|
325
275
|
isStreaming,
|
|
326
276
|
isTokenLoading,
|
|
@@ -357,36 +307,6 @@ export function useCctvRtcStream({
|
|
|
357
307
|
? false
|
|
358
308
|
: isTokenLoading;
|
|
359
309
|
|
|
360
|
-
const skippedDisconnectedDebugKeyRef = useRef<string | null>(null);
|
|
361
|
-
|
|
362
|
-
useEffect(() => {
|
|
363
|
-
if (
|
|
364
|
-
connectionState !== "disconnected" ||
|
|
365
|
-
!hasConnected ||
|
|
366
|
-
!streamIdentityKey
|
|
367
|
-
) {
|
|
368
|
-
if (connectionState !== "disconnected") {
|
|
369
|
-
skippedDisconnectedDebugKeyRef.current = null;
|
|
370
|
-
}
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const skipKey = `${streamIdentityKey}|${activeStreamKeyRef.current ?? ""}`;
|
|
375
|
-
if (skippedDisconnectedDebugKeyRef.current === skipKey) return;
|
|
376
|
-
|
|
377
|
-
skippedDisconnectedDebugKeyRef.current = skipKey;
|
|
378
|
-
logCctvDebugEvent({
|
|
379
|
-
event: "reconnect-gate:skip-disconnected",
|
|
380
|
-
payload: {
|
|
381
|
-
...debugBasePayload,
|
|
382
|
-
connectionState,
|
|
383
|
-
reason: "disconnected-observed-only",
|
|
384
|
-
streamKey: getCctvDebugKeyLabel(activeStreamKeyRef.current),
|
|
385
|
-
},
|
|
386
|
-
source: "useRtcStream",
|
|
387
|
-
});
|
|
388
|
-
}, [connectionState, debugBasePayload, hasConnected, streamIdentityKey]);
|
|
389
|
-
|
|
390
310
|
const hasReusableRegistryStream = useMemo(() => {
|
|
391
311
|
if (!streamKeyCandidate) return false;
|
|
392
312
|
|
|
@@ -436,19 +356,9 @@ export function useCctvRtcStream({
|
|
|
436
356
|
}
|
|
437
357
|
|
|
438
358
|
staleTokenRefreshKeyRef.current = streamKeyCandidate;
|
|
439
|
-
logCctvDebugEvent({
|
|
440
|
-
event: "token:stale-refetch",
|
|
441
|
-
payload: {
|
|
442
|
-
...debugBasePayload,
|
|
443
|
-
dataUpdatedAt: tokenQuery.dataUpdatedAt,
|
|
444
|
-
streamKey: getCctvDebugKeyLabel(streamKeyCandidate),
|
|
445
|
-
},
|
|
446
|
-
source: "useRtcStream",
|
|
447
|
-
});
|
|
448
359
|
void refetchRtcToken();
|
|
449
360
|
}, [
|
|
450
361
|
canUseTokenForStream,
|
|
451
|
-
debugBasePayload,
|
|
452
362
|
refetchRtcToken,
|
|
453
363
|
streamKeyCandidate,
|
|
454
364
|
tokenQuery.data?.token,
|
|
@@ -463,15 +373,6 @@ export function useCctvRtcStream({
|
|
|
463
373
|
|
|
464
374
|
// 토큰/카메라 에러로 전환되면 직전 MediaStream이 화면에 남지 않도록 비운다.
|
|
465
375
|
if (tokenQuery.isError || !cam?.cam_online) {
|
|
466
|
-
logCctvDebugEvent({
|
|
467
|
-
event: "stream-effect:cleanup-token-or-offline",
|
|
468
|
-
payload: {
|
|
469
|
-
...debugBasePayload,
|
|
470
|
-
camOnline: cam?.cam_online,
|
|
471
|
-
isTokenError: tokenQuery.isError,
|
|
472
|
-
},
|
|
473
|
-
source: "useRtcStream",
|
|
474
|
-
});
|
|
475
376
|
if (streamIdentityKey) {
|
|
476
377
|
streamRegistry.closeByIdentity(streamIdentityKey);
|
|
477
378
|
}
|
|
@@ -497,42 +398,12 @@ export function useCctvRtcStream({
|
|
|
497
398
|
if (!previousStreamKey || didClosePreviousStream) return;
|
|
498
399
|
|
|
499
400
|
didClosePreviousStream = true;
|
|
500
|
-
logCctvDebugEvent({
|
|
501
|
-
event: "stream-effect:close-previous-after-track",
|
|
502
|
-
payload: {
|
|
503
|
-
...debugBasePayload,
|
|
504
|
-
previousStreamKey: getCctvDebugKeyLabel(previousStreamKey),
|
|
505
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
506
|
-
},
|
|
507
|
-
source: "useRtcStream",
|
|
508
|
-
});
|
|
509
401
|
streamRegistry.closeByIdentity(streamIdentityKey, streamKey);
|
|
510
402
|
};
|
|
511
403
|
|
|
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
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
404
|
activeStreamKeyRef.current = streamKey;
|
|
525
405
|
activeStreamIdentityKeyRef.current = streamIdentityKey;
|
|
526
406
|
|
|
527
|
-
logCctvDebugEvent({
|
|
528
|
-
event: "stream-effect:start",
|
|
529
|
-
payload: {
|
|
530
|
-
...debugBasePayload,
|
|
531
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
532
|
-
},
|
|
533
|
-
source: "useRtcStream",
|
|
534
|
-
});
|
|
535
|
-
|
|
536
407
|
streamRegistry.start({
|
|
537
408
|
streamKey,
|
|
538
409
|
identityKey: streamIdentityKey,
|
|
@@ -547,14 +418,6 @@ export function useCctvRtcStream({
|
|
|
547
418
|
setStreamError(snapshot.streamError);
|
|
548
419
|
setStreaming(snapshot.isStreaming);
|
|
549
420
|
if (snapshot.connectionState === "connected") {
|
|
550
|
-
logCctvDebugEvent({
|
|
551
|
-
event: "stream-effect:has-connected",
|
|
552
|
-
payload: {
|
|
553
|
-
...debugBasePayload,
|
|
554
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
555
|
-
},
|
|
556
|
-
source: "useRtcStream",
|
|
557
|
-
});
|
|
558
421
|
setHasConnected(true);
|
|
559
422
|
}
|
|
560
423
|
if (snapshot.stream) {
|
|
@@ -579,19 +442,10 @@ export function useCctvRtcStream({
|
|
|
579
442
|
|
|
580
443
|
// effect cleanup: current video attach만 해제하고 registry stream은 유지한다.
|
|
581
444
|
return () => {
|
|
582
|
-
logCctvDebugEvent({
|
|
583
|
-
event: "stream-effect:detach-video",
|
|
584
|
-
payload: {
|
|
585
|
-
...debugBasePayload,
|
|
586
|
-
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
587
|
-
},
|
|
588
|
-
source: "useRtcStream",
|
|
589
|
-
});
|
|
590
445
|
unsubscribe();
|
|
591
446
|
detachVideo();
|
|
592
447
|
};
|
|
593
448
|
}, [
|
|
594
|
-
debugBasePayload,
|
|
595
449
|
endpoint,
|
|
596
450
|
streamIdentityKey,
|
|
597
451
|
streamKey,
|
|
@@ -601,52 +455,8 @@ export function useCctvRtcStream({
|
|
|
601
455
|
cam?.cam_online,
|
|
602
456
|
]);
|
|
603
457
|
|
|
604
|
-
const debugReconnectStateRef = useRef({
|
|
605
|
-
connectionState,
|
|
606
|
-
debugBasePayload,
|
|
607
|
-
isPostConnectedReconnectReady,
|
|
608
|
-
reconnectReason,
|
|
609
|
-
streamError,
|
|
610
|
-
});
|
|
611
|
-
debugReconnectStateRef.current = {
|
|
612
|
-
connectionState,
|
|
613
|
-
debugBasePayload,
|
|
614
|
-
isPostConnectedReconnectReady,
|
|
615
|
-
reconnectReason,
|
|
616
|
-
streamError,
|
|
617
|
-
};
|
|
618
|
-
|
|
619
458
|
const reconnectStream = useCallback<CctvRtcReconnectTrigger>(
|
|
620
|
-
|
|
621
|
-
const debugState = debugReconnectStateRef.current;
|
|
622
|
-
logCctvDebugEvent({
|
|
623
|
-
event: "reconnect-stream:start",
|
|
624
|
-
payload: {
|
|
625
|
-
...debugState.debugBasePayload,
|
|
626
|
-
connectionState: debugState.connectionState,
|
|
627
|
-
callerStack: getCctvDebugStackTrace(),
|
|
628
|
-
isPostConnectedReconnectReady:
|
|
629
|
-
debugState.isPostConnectedReconnectReady,
|
|
630
|
-
reconnectReason: debugState.reconnectReason,
|
|
631
|
-
streamError: debugState.streamError,
|
|
632
|
-
},
|
|
633
|
-
source: "useRtcStream",
|
|
634
|
-
});
|
|
635
|
-
const result = await refetchRtcToken(options);
|
|
636
|
-
const resultDebugState = debugReconnectStateRef.current;
|
|
637
|
-
logCctvDebugEvent({
|
|
638
|
-
event: "reconnect-stream:token-result",
|
|
639
|
-
level: result.isSuccess ? "info" : "warn",
|
|
640
|
-
payload: {
|
|
641
|
-
...resultDebugState.debugBasePayload,
|
|
642
|
-
isError: result.isError,
|
|
643
|
-
isSuccess: result.isSuccess,
|
|
644
|
-
status: result.status,
|
|
645
|
-
},
|
|
646
|
-
source: "useRtcStream",
|
|
647
|
-
});
|
|
648
|
-
return result;
|
|
649
|
-
},
|
|
459
|
+
options => refetchRtcToken(options),
|
|
650
460
|
[refetchRtcToken],
|
|
651
461
|
);
|
|
652
462
|
|
|
@@ -682,16 +492,6 @@ export function useCctvRtcStream({
|
|
|
682
492
|
return;
|
|
683
493
|
|
|
684
494
|
lastAutoReconnectAtRef.current = now;
|
|
685
|
-
logCctvDebugEvent({
|
|
686
|
-
event: "focus:auto-reconnect",
|
|
687
|
-
payload: {
|
|
688
|
-
...debugBasePayload,
|
|
689
|
-
canReconnect,
|
|
690
|
-
connectionState,
|
|
691
|
-
reconnectReason,
|
|
692
|
-
},
|
|
693
|
-
source: "useRtcStream",
|
|
694
|
-
});
|
|
695
495
|
void reconnectStream();
|
|
696
496
|
};
|
|
697
497
|
|
|
@@ -702,13 +502,7 @@ export function useCctvRtcStream({
|
|
|
702
502
|
window.removeEventListener("focus", reconnectOnFocus);
|
|
703
503
|
document.removeEventListener("visibilitychange", reconnectOnFocus);
|
|
704
504
|
};
|
|
705
|
-
}, [
|
|
706
|
-
canReconnect,
|
|
707
|
-
connectionState,
|
|
708
|
-
debugBasePayload,
|
|
709
|
-
reconnectReason,
|
|
710
|
-
reconnectStream,
|
|
711
|
-
]);
|
|
505
|
+
}, [canReconnect, reconnectStream]);
|
|
712
506
|
|
|
713
507
|
const liveState = useMemo(
|
|
714
508
|
() =>
|
|
@@ -732,48 +526,6 @@ export function useCctvRtcStream({
|
|
|
732
526
|
],
|
|
733
527
|
);
|
|
734
528
|
|
|
735
|
-
useEffect(() => {
|
|
736
|
-
logCctvDebugEvent({
|
|
737
|
-
event: "hook-state:update",
|
|
738
|
-
payload: {
|
|
739
|
-
...debugBasePayload,
|
|
740
|
-
canReconnect,
|
|
741
|
-
connectionState,
|
|
742
|
-
displayConnectionState,
|
|
743
|
-
hasConnected,
|
|
744
|
-
isPostConnectedReplacementLoading,
|
|
745
|
-
isPostConnectedRecoverableState,
|
|
746
|
-
isPostConnectedReconnectReady,
|
|
747
|
-
displayIsStreaming,
|
|
748
|
-
displayIsTokenLoading,
|
|
749
|
-
isStreaming,
|
|
750
|
-
isTokenError,
|
|
751
|
-
isTokenLoading,
|
|
752
|
-
liveState,
|
|
753
|
-
reconnectReason,
|
|
754
|
-
streamError,
|
|
755
|
-
},
|
|
756
|
-
source: "useRtcStream",
|
|
757
|
-
});
|
|
758
|
-
}, [
|
|
759
|
-
canReconnect,
|
|
760
|
-
connectionState,
|
|
761
|
-
debugBasePayload,
|
|
762
|
-
displayConnectionState,
|
|
763
|
-
displayIsStreaming,
|
|
764
|
-
displayIsTokenLoading,
|
|
765
|
-
hasConnected,
|
|
766
|
-
isPostConnectedReplacementLoading,
|
|
767
|
-
isPostConnectedRecoverableState,
|
|
768
|
-
isPostConnectedReconnectReady,
|
|
769
|
-
isStreaming,
|
|
770
|
-
isTokenError,
|
|
771
|
-
isTokenLoading,
|
|
772
|
-
liveState,
|
|
773
|
-
reconnectReason,
|
|
774
|
-
streamError,
|
|
775
|
-
]);
|
|
776
|
-
|
|
777
529
|
useEffect(() => {
|
|
778
530
|
const camId = cam?.cam_id;
|
|
779
531
|
if (!camId) return;
|
package/src/cctv/utils/debug.ts
DELETED
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
export type CctvDebugEventLevel = "debug" | "info" | "warn" | "error";
|
|
4
|
-
|
|
5
|
-
export interface CctvDebugEvent {
|
|
6
|
-
at: string;
|
|
7
|
-
event: string;
|
|
8
|
-
level: CctvDebugEventLevel;
|
|
9
|
-
payload?: Record<string, unknown>;
|
|
10
|
-
seq: number;
|
|
11
|
-
source: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface CctvDebugReconnectStartSummary {
|
|
15
|
-
at: string;
|
|
16
|
-
caller: string | null;
|
|
17
|
-
camId: string | null;
|
|
18
|
-
connectionState: string | null;
|
|
19
|
-
isPostConnectedReconnectReady: boolean | null;
|
|
20
|
-
reconnectReason: string | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CctvDebugSummary {
|
|
24
|
-
connectionStateCounts: Record<string, number>;
|
|
25
|
-
eventCounts: Record<string, number>;
|
|
26
|
-
first: string | null;
|
|
27
|
-
generatedAt: string;
|
|
28
|
-
last: string | null;
|
|
29
|
-
reconnectCallers: Record<string, number>;
|
|
30
|
-
reconnectReasons: Record<string, number>;
|
|
31
|
-
reconnectStarts: CctvDebugReconnectStartSummary[];
|
|
32
|
-
total: number;
|
|
33
|
-
}
|
|
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
|
-
|
|
44
|
-
export interface CctvDebugBuffer {
|
|
45
|
-
clear: () => void;
|
|
46
|
-
dump: () => CctvDebugEvent[];
|
|
47
|
-
enabled: true;
|
|
48
|
-
events: CctvDebugEvent[];
|
|
49
|
-
filter: (keywords?: readonly string[]) => CctvDebugEvent[];
|
|
50
|
-
limit: number;
|
|
51
|
-
sequence: number;
|
|
52
|
-
summary: () => CctvDebugSummary;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
declare global {
|
|
56
|
-
interface Window {
|
|
57
|
-
__UDS_CCTV_DEBUG__?: CctvDebugBuffer;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const DEBUG_QUERY_KEYS = ["udsCctvDebug", "cctvDebug"] as const;
|
|
62
|
-
const DEBUG_STORAGE_KEYS = ["uds:cctv:debug", "UDS_CCTV_DEBUG"] as const;
|
|
63
|
-
const DEBUG_CONSOLE_QUERY_KEYS = [
|
|
64
|
-
"udsCctvDebugConsole",
|
|
65
|
-
"cctvDebugConsole",
|
|
66
|
-
] as const;
|
|
67
|
-
const DEBUG_CONSOLE_STORAGE_KEYS = [
|
|
68
|
-
"uds:cctv:debug:console",
|
|
69
|
-
"UDS_CCTV_DEBUG_CONSOLE",
|
|
70
|
-
] as const;
|
|
71
|
-
const DEBUG_ENABLED_VALUES = new Set(["1", "true", "yes", "on", "debug"]);
|
|
72
|
-
const DEBUG_DISABLED_VALUES = new Set(["0", "false", "no", "off"]);
|
|
73
|
-
const DEBUG_BUFFER_LIMIT = 1000;
|
|
74
|
-
const DEBUG_DEFAULT_FILTER_KEYWORDS = [
|
|
75
|
-
"reconnect",
|
|
76
|
-
"close",
|
|
77
|
-
"rejected",
|
|
78
|
-
"connection-state:change",
|
|
79
|
-
"track:received",
|
|
80
|
-
] as const;
|
|
81
|
-
|
|
82
|
-
const isBrowser = (): boolean =>
|
|
83
|
-
typeof window !== "undefined" && typeof document !== "undefined";
|
|
84
|
-
|
|
85
|
-
const getDebugValueOverride = (value: string | null): boolean | null => {
|
|
86
|
-
if (value === "") return true;
|
|
87
|
-
if (typeof value !== "string") return null;
|
|
88
|
-
|
|
89
|
-
const normalizedValue = value.toLowerCase();
|
|
90
|
-
if (DEBUG_ENABLED_VALUES.has(normalizedValue)) return true;
|
|
91
|
-
if (DEBUG_DISABLED_VALUES.has(normalizedValue)) return false;
|
|
92
|
-
return null;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const getDebugQueryOverride = (keys: readonly string[]): boolean | null => {
|
|
96
|
-
const query = new URLSearchParams(window.location.search);
|
|
97
|
-
for (const key of keys) {
|
|
98
|
-
if (!query.has(key)) continue;
|
|
99
|
-
|
|
100
|
-
const override = getDebugValueOverride(query.get(key));
|
|
101
|
-
if (override !== null) return override;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return null;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const getDebugStorageOverride = (keys: readonly string[]): boolean | null => {
|
|
108
|
-
for (const key of keys) {
|
|
109
|
-
try {
|
|
110
|
-
const override = getDebugValueOverride(window.localStorage.getItem(key));
|
|
111
|
-
if (override !== null) return override;
|
|
112
|
-
} catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return null;
|
|
118
|
-
};
|
|
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
|
-
|
|
127
|
-
const isLocalhostDebugDefaultEnabled = (): boolean => {
|
|
128
|
-
const { hostname } = window.location;
|
|
129
|
-
return (
|
|
130
|
-
hostname === "localhost" ||
|
|
131
|
-
hostname === "127.0.0.1" ||
|
|
132
|
-
hostname === "[::1]" ||
|
|
133
|
-
hostname === "::1" ||
|
|
134
|
-
hostname.endsWith(".localhost")
|
|
135
|
-
);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const getPayloadString = (
|
|
139
|
-
payload: Record<string, unknown> | undefined,
|
|
140
|
-
key: string,
|
|
141
|
-
): string | null => {
|
|
142
|
-
const value = payload?.[key];
|
|
143
|
-
return typeof value === "string" ? value : null;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const getPayloadBoolean = (
|
|
147
|
-
payload: Record<string, unknown> | undefined,
|
|
148
|
-
key: string,
|
|
149
|
-
): boolean | null => {
|
|
150
|
-
const value = payload?.[key];
|
|
151
|
-
return typeof value === "boolean" ? value : null;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const getReconnectCaller = (
|
|
155
|
-
payload: Record<string, unknown> | undefined,
|
|
156
|
-
): string | null => {
|
|
157
|
-
const callerStack = payload?.callerStack;
|
|
158
|
-
const stackLines = Array.isArray(callerStack)
|
|
159
|
-
? callerStack.filter((line): line is string => typeof line === "string")
|
|
160
|
-
: typeof callerStack === "string"
|
|
161
|
-
? callerStack.split("\n")
|
|
162
|
-
: [];
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
stackLines
|
|
166
|
-
.find(
|
|
167
|
-
line =>
|
|
168
|
-
line.includes("CCTVManagerVideoOverlay") ||
|
|
169
|
-
line.includes("reconnectOnFocus"),
|
|
170
|
-
)
|
|
171
|
-
?.trim() ?? null
|
|
172
|
-
);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const countBy = <T extends string>(
|
|
176
|
-
target: Record<T, number>,
|
|
177
|
-
key: T | null,
|
|
178
|
-
): void => {
|
|
179
|
-
if (!key) return;
|
|
180
|
-
target[key] = (target[key] ?? 0) + 1;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const filterCctvDebugEvents = (
|
|
184
|
-
events: readonly CctvDebugEvent[],
|
|
185
|
-
keywords: readonly string[] = DEBUG_DEFAULT_FILTER_KEYWORDS,
|
|
186
|
-
): CctvDebugEvent[] =>
|
|
187
|
-
events.filter(entry =>
|
|
188
|
-
keywords.some(keyword => entry.event.includes(keyword)),
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const summarizeCctvDebugEvents = (
|
|
192
|
-
events: readonly CctvDebugEvent[],
|
|
193
|
-
): CctvDebugSummary => {
|
|
194
|
-
const eventCounts: Record<string, number> = {};
|
|
195
|
-
const connectionStateCounts: Record<string, number> = {};
|
|
196
|
-
const reconnectReasons: Record<string, number> = {};
|
|
197
|
-
const reconnectCallers: Record<string, number> = {};
|
|
198
|
-
const reconnectStarts: CctvDebugReconnectStartSummary[] = [];
|
|
199
|
-
|
|
200
|
-
for (const entry of events) {
|
|
201
|
-
countBy(eventCounts, entry.event);
|
|
202
|
-
|
|
203
|
-
if (entry.event === "connection-state:change") {
|
|
204
|
-
countBy(connectionStateCounts, getPayloadString(entry.payload, "state"));
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (entry.event !== "reconnect-stream:start") continue;
|
|
208
|
-
|
|
209
|
-
const reconnectReason = getPayloadString(entry.payload, "reconnectReason");
|
|
210
|
-
const caller = getReconnectCaller(entry.payload);
|
|
211
|
-
|
|
212
|
-
countBy(reconnectReasons, reconnectReason ?? "none");
|
|
213
|
-
countBy(reconnectCallers, caller ?? "unknown");
|
|
214
|
-
reconnectStarts.push({
|
|
215
|
-
at: entry.at,
|
|
216
|
-
caller,
|
|
217
|
-
camId: getPayloadString(entry.payload, "camId"),
|
|
218
|
-
connectionState: getPayloadString(entry.payload, "connectionState"),
|
|
219
|
-
isPostConnectedReconnectReady: getPayloadBoolean(
|
|
220
|
-
entry.payload,
|
|
221
|
-
"isPostConnectedReconnectReady",
|
|
222
|
-
),
|
|
223
|
-
reconnectReason,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
connectionStateCounts,
|
|
229
|
-
eventCounts,
|
|
230
|
-
first: events[0]?.at ?? null,
|
|
231
|
-
generatedAt: new Date().toISOString(),
|
|
232
|
-
last: events[events.length - 1]?.at ?? null,
|
|
233
|
-
reconnectCallers,
|
|
234
|
-
reconnectReasons,
|
|
235
|
-
reconnectStarts,
|
|
236
|
-
total: events.length,
|
|
237
|
-
};
|
|
238
|
-
};
|
|
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
|
-
|
|
291
|
-
/**
|
|
292
|
-
* identity/stream key를 원문 대신 추적 가능한 hash label로 바꾼다.
|
|
293
|
-
*/
|
|
294
|
-
export const getCctvDebugKeyLabel = (
|
|
295
|
-
value: string | null | undefined,
|
|
296
|
-
): string | null => {
|
|
297
|
-
if (!value) return null;
|
|
298
|
-
|
|
299
|
-
let hash = 0;
|
|
300
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
301
|
-
hash = (hash * 31 + value.charCodeAt(i)) >>> 0;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return `hash:${hash.toString(36)}|len:${value.length}`;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* endpoint는 query string을 제거한 origin/path만 기록한다.
|
|
309
|
-
*/
|
|
310
|
-
export const getCctvDebugUrlLabel = (
|
|
311
|
-
value: string | null | undefined,
|
|
312
|
-
): string | null => {
|
|
313
|
-
if (!value) return null;
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
const url = new URL(value);
|
|
317
|
-
return `${url.origin}${url.pathname}`;
|
|
318
|
-
} catch {
|
|
319
|
-
return value.split("?")[0] ?? null;
|
|
320
|
-
}
|
|
321
|
-
};
|
|
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
|
-
|
|
349
|
-
/**
|
|
350
|
-
* CCTV debug logging 활성화 여부를 확인한다.
|
|
351
|
-
* @desc
|
|
352
|
-
* production default는 off, localhost default는 ring buffer on이다.
|
|
353
|
-
* console 출력은 별도 console flag를 켠 경우에만 활성화한다.
|
|
354
|
-
*/
|
|
355
|
-
export const isCctvDebugEnabled = (): boolean => {
|
|
356
|
-
if (!isBrowser()) return false;
|
|
357
|
-
|
|
358
|
-
const queryOverride = getDebugQueryOverride(DEBUG_QUERY_KEYS);
|
|
359
|
-
if (queryOverride !== null) return queryOverride;
|
|
360
|
-
|
|
361
|
-
const consoleOverride = getDebugConsoleOverride();
|
|
362
|
-
if (consoleOverride === true) return true;
|
|
363
|
-
|
|
364
|
-
const storageOverride = getDebugStorageOverride(DEBUG_STORAGE_KEYS);
|
|
365
|
-
if (storageOverride !== null) return storageOverride;
|
|
366
|
-
|
|
367
|
-
return isLocalhostDebugDefaultEnabled();
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
const isCctvDebugConsoleEnabled = (): boolean => {
|
|
371
|
-
if (!isBrowser()) return false;
|
|
372
|
-
|
|
373
|
-
return getDebugConsoleOverride() === true;
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* reconnect 호출 주체를 좁히기 위한 stack trace 일부를 만든다.
|
|
378
|
-
*/
|
|
379
|
-
export const getCctvDebugStackTrace = (): string[] | null => {
|
|
380
|
-
if (!isCctvDebugEnabled()) return null;
|
|
381
|
-
|
|
382
|
-
const stack = new Error().stack;
|
|
383
|
-
if (!stack) return null;
|
|
384
|
-
|
|
385
|
-
return stack
|
|
386
|
-
.split("\n")
|
|
387
|
-
.slice(2, 8)
|
|
388
|
-
.map(line => line.trim());
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const getCctvDebugBuffer = (): CctvDebugBuffer | null => {
|
|
392
|
-
if (!isCctvDebugEnabled()) return null;
|
|
393
|
-
|
|
394
|
-
const existingBuffer = window.__UDS_CCTV_DEBUG__;
|
|
395
|
-
if (existingBuffer) return existingBuffer;
|
|
396
|
-
|
|
397
|
-
const buffer: CctvDebugBuffer = {
|
|
398
|
-
enabled: true,
|
|
399
|
-
events: [],
|
|
400
|
-
limit: DEBUG_BUFFER_LIMIT,
|
|
401
|
-
sequence: 0,
|
|
402
|
-
clear: () => {
|
|
403
|
-
buffer.events.splice(0, buffer.events.length);
|
|
404
|
-
buffer.sequence = 0;
|
|
405
|
-
},
|
|
406
|
-
dump: () => [...buffer.events],
|
|
407
|
-
filter: keywords => filterCctvDebugEvents(buffer.events, keywords),
|
|
408
|
-
summary: () => summarizeCctvDebugEvents(buffer.events),
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
window.__UDS_CCTV_DEBUG__ = buffer;
|
|
412
|
-
return buffer;
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* CCTV runtime 진단 이벤트를 ring buffer와 console에 남긴다.
|
|
417
|
-
* @desc
|
|
418
|
-
* 토큰 값은 payload에 넣지 않는다. 이 logger는 debug flag가 켜진 경우에만 동작한다.
|
|
419
|
-
*/
|
|
420
|
-
export const logCctvDebugEvent = ({
|
|
421
|
-
event,
|
|
422
|
-
level = "debug",
|
|
423
|
-
payload,
|
|
424
|
-
source,
|
|
425
|
-
}: {
|
|
426
|
-
event: string;
|
|
427
|
-
level?: CctvDebugEventLevel;
|
|
428
|
-
payload?: Record<string, unknown>;
|
|
429
|
-
source: string;
|
|
430
|
-
}): void => {
|
|
431
|
-
const buffer = getCctvDebugBuffer();
|
|
432
|
-
if (!buffer) return;
|
|
433
|
-
|
|
434
|
-
const entry: CctvDebugEvent = {
|
|
435
|
-
at: new Date().toISOString(),
|
|
436
|
-
event,
|
|
437
|
-
level,
|
|
438
|
-
payload,
|
|
439
|
-
seq: buffer.sequence + 1,
|
|
440
|
-
source,
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
buffer.sequence = entry.seq;
|
|
444
|
-
buffer.events.push(entry);
|
|
445
|
-
if (buffer.events.length > buffer.limit) {
|
|
446
|
-
buffer.events.splice(0, buffer.events.length - buffer.limit);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (!isCctvDebugConsoleEnabled()) return;
|
|
450
|
-
|
|
451
|
-
const logger =
|
|
452
|
-
level === "error"
|
|
453
|
-
? console.error
|
|
454
|
-
: level === "warn"
|
|
455
|
-
? console.warn
|
|
456
|
-
: level === "info"
|
|
457
|
-
? console.info
|
|
458
|
-
: console.debug;
|
|
459
|
-
|
|
460
|
-
logger.call(console, `[UDS:CCTV] ${stringifyCctvDebugEvent(entry)}`);
|
|
461
|
-
};
|