@uniai-fe/uds-templates 0.6.8 → 0.6.9
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 +1 -1
- package/src/cctv/hooks/streamRegistry.ts +94 -1
- package/src/cctv/hooks/useRtcStream.ts +211 -10
- package/src/cctv/utils/debug.ts +179 -0
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
4
|
+
import {
|
|
5
|
+
getCctvDebugKeyLabel,
|
|
6
|
+
getCctvDebugUrlLabel,
|
|
7
|
+
logCctvDebugEvent,
|
|
8
|
+
} from "../utils/debug";
|
|
4
9
|
|
|
5
10
|
interface CctvRtcStreamSnapshot {
|
|
6
11
|
connectionState: RTCPeerConnectionState;
|
|
@@ -47,6 +52,18 @@ const notifyEntry = (entry: CctvRtcStreamEntry) => {
|
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
55
|
+
logCctvDebugEvent({
|
|
56
|
+
event: "entry:close",
|
|
57
|
+
payload: {
|
|
58
|
+
attachedVideoCount: entry.attachedVideos.size,
|
|
59
|
+
connectionState: entry.connectionState,
|
|
60
|
+
identityKey: getCctvDebugKeyLabel(entry.identityKey),
|
|
61
|
+
isStreaming: entry.isStreaming,
|
|
62
|
+
streamKey: getCctvDebugKeyLabel(entry.streamKey),
|
|
63
|
+
streamTrackCount: entry.stream?.getTracks().length ?? 0,
|
|
64
|
+
},
|
|
65
|
+
source: "streamRegistry",
|
|
66
|
+
});
|
|
50
67
|
entry.controller?.abort();
|
|
51
68
|
entry.controller = null;
|
|
52
69
|
entry.handle?.close();
|
|
@@ -110,6 +127,15 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
110
127
|
entries.clear();
|
|
111
128
|
},
|
|
112
129
|
closeByIdentity(identityKey, exceptStreamKey) {
|
|
130
|
+
logCctvDebugEvent({
|
|
131
|
+
event: "identity:close",
|
|
132
|
+
payload: {
|
|
133
|
+
entryCount: entries.size,
|
|
134
|
+
exceptStreamKey: getCctvDebugKeyLabel(exceptStreamKey),
|
|
135
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
136
|
+
},
|
|
137
|
+
source: "streamRegistry",
|
|
138
|
+
});
|
|
113
139
|
entries.forEach(entry => {
|
|
114
140
|
if (
|
|
115
141
|
entry.identityKey === identityKey &&
|
|
@@ -124,9 +150,22 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
124
150
|
},
|
|
125
151
|
start({ endpoint, identityKey, streamKey, token, video }) {
|
|
126
152
|
const existingEntry = entries.get(streamKey);
|
|
127
|
-
if (existingEntry)
|
|
153
|
+
if (existingEntry) {
|
|
154
|
+
logCctvDebugEvent({
|
|
155
|
+
event: "start:reuse-existing",
|
|
156
|
+
payload: {
|
|
157
|
+
connectionState: existingEntry.connectionState,
|
|
158
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
159
|
+
isStreaming: existingEntry.isStreaming,
|
|
160
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
161
|
+
},
|
|
162
|
+
source: "streamRegistry",
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
128
166
|
|
|
129
167
|
const controller = new AbortController();
|
|
168
|
+
const startedAt = Date.now();
|
|
130
169
|
const entry: CctvRtcStreamEntry = {
|
|
131
170
|
attachedVideos: new Set([video]),
|
|
132
171
|
connectionState: "connecting",
|
|
@@ -142,6 +181,16 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
142
181
|
|
|
143
182
|
entries.set(streamKey, entry);
|
|
144
183
|
attachVideo(video, null);
|
|
184
|
+
logCctvDebugEvent({
|
|
185
|
+
event: "start:create-entry",
|
|
186
|
+
payload: {
|
|
187
|
+
attachedVideoCount: entry.attachedVideos.size,
|
|
188
|
+
endpoint: getCctvDebugUrlLabel(endpoint),
|
|
189
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
190
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
191
|
+
},
|
|
192
|
+
source: "streamRegistry",
|
|
193
|
+
});
|
|
145
194
|
|
|
146
195
|
startWhepStream({
|
|
147
196
|
endpoint,
|
|
@@ -150,10 +199,33 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
150
199
|
signal: controller.signal,
|
|
151
200
|
onConnectionStateChange: state => {
|
|
152
201
|
entry.connectionState = state;
|
|
202
|
+
logCctvDebugEvent({
|
|
203
|
+
event: "connection-state:change",
|
|
204
|
+
payload: {
|
|
205
|
+
elapsedMs: Date.now() - startedAt,
|
|
206
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
207
|
+
state,
|
|
208
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
209
|
+
},
|
|
210
|
+
source: "streamRegistry",
|
|
211
|
+
});
|
|
153
212
|
notifyEntry(entry);
|
|
154
213
|
},
|
|
155
214
|
onTrack: event => {
|
|
156
215
|
entry.stream = event.streams[0] ?? null;
|
|
216
|
+
logCctvDebugEvent({
|
|
217
|
+
event: "track:received",
|
|
218
|
+
payload: {
|
|
219
|
+
elapsedMs: Date.now() - startedAt,
|
|
220
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
221
|
+
streamId: entry.stream?.id ?? null,
|
|
222
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
223
|
+
trackId: event.track.id,
|
|
224
|
+
trackKind: event.track.kind,
|
|
225
|
+
trackReadyState: event.track.readyState,
|
|
226
|
+
},
|
|
227
|
+
source: "streamRegistry",
|
|
228
|
+
});
|
|
157
229
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
158
230
|
attachVideo(attachedVideo, entry.stream),
|
|
159
231
|
);
|
|
@@ -163,6 +235,16 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
163
235
|
.then(handle => {
|
|
164
236
|
entry.handle = handle;
|
|
165
237
|
entry.isStreaming = false;
|
|
238
|
+
logCctvDebugEvent({
|
|
239
|
+
event: "start:resolved",
|
|
240
|
+
level: "info",
|
|
241
|
+
payload: {
|
|
242
|
+
elapsedMs: Date.now() - startedAt,
|
|
243
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
244
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
245
|
+
},
|
|
246
|
+
source: "streamRegistry",
|
|
247
|
+
});
|
|
166
248
|
notifyEntry(entry);
|
|
167
249
|
})
|
|
168
250
|
.catch(error => {
|
|
@@ -174,6 +256,17 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
174
256
|
error instanceof Error
|
|
175
257
|
? error.message
|
|
176
258
|
: "스트림 연결에 실패했습니다.";
|
|
259
|
+
logCctvDebugEvent({
|
|
260
|
+
event: "start:rejected",
|
|
261
|
+
level: "error",
|
|
262
|
+
payload: {
|
|
263
|
+
elapsedMs: Date.now() - startedAt,
|
|
264
|
+
errorMessage: entry.streamError,
|
|
265
|
+
identityKey: getCctvDebugKeyLabel(identityKey),
|
|
266
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
267
|
+
},
|
|
268
|
+
source: "streamRegistry",
|
|
269
|
+
});
|
|
177
270
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
178
271
|
attachVideo(attachedVideo, null),
|
|
179
272
|
);
|
|
@@ -19,6 +19,11 @@ 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";
|
|
22
27
|
import { getIsLive } from "../utils/video-state";
|
|
23
28
|
import { useFormContext, useWatch } from "react-hook-form";
|
|
24
29
|
|
|
@@ -236,9 +241,23 @@ export function useCctvRtcStream({
|
|
|
236
241
|
return [tokenUsername, cam.company_id, cam.cam_id, endpoint].join("|");
|
|
237
242
|
}, [cam?.cam_id, cam?.company_id, endpoint, tokenUsername]);
|
|
238
243
|
|
|
244
|
+
const debugBasePayload = useMemo(
|
|
245
|
+
() => ({
|
|
246
|
+
camId: cam?.cam_id,
|
|
247
|
+
companyId: cam?.company_id,
|
|
248
|
+
streamIdentityKey: getCctvDebugKeyLabel(streamIdentityKey),
|
|
249
|
+
}),
|
|
250
|
+
[cam?.cam_id, cam?.company_id, streamIdentityKey],
|
|
251
|
+
);
|
|
252
|
+
|
|
239
253
|
useEffect(() => {
|
|
254
|
+
logCctvDebugEvent({
|
|
255
|
+
event: "identity:reset-has-connected",
|
|
256
|
+
payload: debugBasePayload,
|
|
257
|
+
source: "useRtcStream",
|
|
258
|
+
});
|
|
240
259
|
setHasConnected(false);
|
|
241
|
-
}, [streamIdentityKey]);
|
|
260
|
+
}, [debugBasePayload, streamIdentityKey]);
|
|
242
261
|
|
|
243
262
|
const reconnectReason = useMemo(
|
|
244
263
|
() =>
|
|
@@ -264,20 +283,49 @@ export function useCctvRtcStream({
|
|
|
264
283
|
}
|
|
265
284
|
|
|
266
285
|
setPostConnectedReconnectReady(false);
|
|
286
|
+
const reconnectDelayMs = getPostConnectedReconnectDelayMs(
|
|
287
|
+
`${streamIdentityKey}|${reconnectReason}`,
|
|
288
|
+
);
|
|
267
289
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
290
|
+
logCctvDebugEvent({
|
|
291
|
+
event: "reconnect-gate:timer-start",
|
|
292
|
+
payload: {
|
|
293
|
+
...debugBasePayload,
|
|
294
|
+
isStreaming,
|
|
295
|
+
isTokenLoading,
|
|
296
|
+
reconnectDelayMs,
|
|
297
|
+
reconnectReason,
|
|
271
298
|
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
299
|
+
source: "useRtcStream",
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const timeout = setTimeout(() => {
|
|
303
|
+
setPostConnectedReconnectReady(true);
|
|
304
|
+
logCctvDebugEvent({
|
|
305
|
+
event: "reconnect-gate:ready",
|
|
306
|
+
payload: {
|
|
307
|
+
...debugBasePayload,
|
|
308
|
+
reconnectDelayMs,
|
|
309
|
+
reconnectReason,
|
|
310
|
+
},
|
|
311
|
+
source: "useRtcStream",
|
|
312
|
+
});
|
|
313
|
+
}, reconnectDelayMs);
|
|
276
314
|
|
|
277
315
|
return () => {
|
|
278
316
|
clearTimeout(timeout);
|
|
317
|
+
logCctvDebugEvent({
|
|
318
|
+
event: "reconnect-gate:timer-cancel",
|
|
319
|
+
payload: {
|
|
320
|
+
...debugBasePayload,
|
|
321
|
+
reconnectDelayMs,
|
|
322
|
+
reconnectReason,
|
|
323
|
+
},
|
|
324
|
+
source: "useRtcStream",
|
|
325
|
+
});
|
|
279
326
|
};
|
|
280
327
|
}, [
|
|
328
|
+
debugBasePayload,
|
|
281
329
|
hasConnected,
|
|
282
330
|
isStreaming,
|
|
283
331
|
isTokenLoading,
|
|
@@ -343,12 +391,23 @@ export function useCctvRtcStream({
|
|
|
343
391
|
}
|
|
344
392
|
|
|
345
393
|
staleTokenRefreshKeyRef.current = streamKeyCandidate;
|
|
394
|
+
logCctvDebugEvent({
|
|
395
|
+
event: "token:stale-refetch",
|
|
396
|
+
payload: {
|
|
397
|
+
...debugBasePayload,
|
|
398
|
+
dataUpdatedAt: tokenQuery.dataUpdatedAt,
|
|
399
|
+
streamKey: getCctvDebugKeyLabel(streamKeyCandidate),
|
|
400
|
+
},
|
|
401
|
+
source: "useRtcStream",
|
|
402
|
+
});
|
|
346
403
|
void refetchRtcToken();
|
|
347
404
|
}, [
|
|
348
405
|
canUseTokenForStream,
|
|
406
|
+
debugBasePayload,
|
|
349
407
|
refetchRtcToken,
|
|
350
408
|
streamKeyCandidate,
|
|
351
409
|
tokenQuery.data?.token,
|
|
410
|
+
tokenQuery.dataUpdatedAt,
|
|
352
411
|
tokenQuery.isError,
|
|
353
412
|
tokenQuery.isFetching,
|
|
354
413
|
]);
|
|
@@ -359,6 +418,15 @@ export function useCctvRtcStream({
|
|
|
359
418
|
|
|
360
419
|
// 토큰/카메라 에러로 전환되면 직전 MediaStream이 화면에 남지 않도록 비운다.
|
|
361
420
|
if (tokenQuery.isError || !cam?.cam_online) {
|
|
421
|
+
logCctvDebugEvent({
|
|
422
|
+
event: "stream-effect:cleanup-token-or-offline",
|
|
423
|
+
payload: {
|
|
424
|
+
...debugBasePayload,
|
|
425
|
+
camOnline: cam?.cam_online,
|
|
426
|
+
isTokenError: tokenQuery.isError,
|
|
427
|
+
},
|
|
428
|
+
source: "useRtcStream",
|
|
429
|
+
});
|
|
362
430
|
if (streamIdentityKey) {
|
|
363
431
|
streamRegistry.closeByIdentity(streamIdentityKey);
|
|
364
432
|
}
|
|
@@ -377,12 +445,30 @@ export function useCctvRtcStream({
|
|
|
377
445
|
activeStreamIdentityKeyRef.current === streamIdentityKey &&
|
|
378
446
|
activeStreamKeyRef.current !== streamKey
|
|
379
447
|
) {
|
|
448
|
+
logCctvDebugEvent({
|
|
449
|
+
event: "stream-effect:close-previous-identity",
|
|
450
|
+
payload: {
|
|
451
|
+
...debugBasePayload,
|
|
452
|
+
activeStreamKey: getCctvDebugKeyLabel(activeStreamKeyRef.current),
|
|
453
|
+
nextStreamKey: getCctvDebugKeyLabel(streamKey),
|
|
454
|
+
},
|
|
455
|
+
source: "useRtcStream",
|
|
456
|
+
});
|
|
380
457
|
streamRegistry.closeByIdentity(streamIdentityKey, streamKey);
|
|
381
458
|
}
|
|
382
459
|
|
|
383
460
|
activeStreamKeyRef.current = streamKey;
|
|
384
461
|
activeStreamIdentityKeyRef.current = streamIdentityKey;
|
|
385
462
|
|
|
463
|
+
logCctvDebugEvent({
|
|
464
|
+
event: "stream-effect:start",
|
|
465
|
+
payload: {
|
|
466
|
+
...debugBasePayload,
|
|
467
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
468
|
+
},
|
|
469
|
+
source: "useRtcStream",
|
|
470
|
+
});
|
|
471
|
+
|
|
386
472
|
streamRegistry.start({
|
|
387
473
|
streamKey,
|
|
388
474
|
identityKey: streamIdentityKey,
|
|
@@ -396,7 +482,17 @@ export function useCctvRtcStream({
|
|
|
396
482
|
setConnectionState(snapshot.connectionState);
|
|
397
483
|
setStreamError(snapshot.streamError);
|
|
398
484
|
setStreaming(snapshot.isStreaming);
|
|
399
|
-
if (snapshot.connectionState === "connected")
|
|
485
|
+
if (snapshot.connectionState === "connected") {
|
|
486
|
+
logCctvDebugEvent({
|
|
487
|
+
event: "stream-effect:has-connected",
|
|
488
|
+
payload: {
|
|
489
|
+
...debugBasePayload,
|
|
490
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
491
|
+
},
|
|
492
|
+
source: "useRtcStream",
|
|
493
|
+
});
|
|
494
|
+
setHasConnected(true);
|
|
495
|
+
}
|
|
400
496
|
if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
|
|
401
497
|
currentVideo.srcObject = snapshot.stream;
|
|
402
498
|
}
|
|
@@ -413,10 +509,19 @@ export function useCctvRtcStream({
|
|
|
413
509
|
|
|
414
510
|
// effect cleanup: current video attach만 해제하고 registry stream은 유지한다.
|
|
415
511
|
return () => {
|
|
512
|
+
logCctvDebugEvent({
|
|
513
|
+
event: "stream-effect:detach-video",
|
|
514
|
+
payload: {
|
|
515
|
+
...debugBasePayload,
|
|
516
|
+
streamKey: getCctvDebugKeyLabel(streamKey),
|
|
517
|
+
},
|
|
518
|
+
source: "useRtcStream",
|
|
519
|
+
});
|
|
416
520
|
unsubscribe();
|
|
417
521
|
detachVideo();
|
|
418
522
|
};
|
|
419
523
|
}, [
|
|
524
|
+
debugBasePayload,
|
|
420
525
|
endpoint,
|
|
421
526
|
streamIdentityKey,
|
|
422
527
|
streamKey,
|
|
@@ -426,10 +531,56 @@ export function useCctvRtcStream({
|
|
|
426
531
|
cam?.cam_online,
|
|
427
532
|
]);
|
|
428
533
|
|
|
534
|
+
const debugReconnectStateRef = useRef({
|
|
535
|
+
connectionState,
|
|
536
|
+
debugBasePayload,
|
|
537
|
+
isPostConnectedReconnectReady,
|
|
538
|
+
reconnectReason,
|
|
539
|
+
streamError,
|
|
540
|
+
});
|
|
541
|
+
debugReconnectStateRef.current = {
|
|
542
|
+
connectionState,
|
|
543
|
+
debugBasePayload,
|
|
544
|
+
isPostConnectedReconnectReady,
|
|
545
|
+
reconnectReason,
|
|
546
|
+
streamError,
|
|
547
|
+
};
|
|
548
|
+
|
|
429
549
|
const reconnectStream = useCallback<CctvRtcReconnectTrigger>(
|
|
430
550
|
async options => {
|
|
551
|
+
const debugState = debugReconnectStateRef.current;
|
|
552
|
+
logCctvDebugEvent({
|
|
553
|
+
event: "reconnect-stream:start",
|
|
554
|
+
payload: {
|
|
555
|
+
...debugState.debugBasePayload,
|
|
556
|
+
connectionState: debugState.connectionState,
|
|
557
|
+
callerStack: getCctvDebugStackTrace(),
|
|
558
|
+
isPostConnectedReconnectReady:
|
|
559
|
+
debugState.isPostConnectedReconnectReady,
|
|
560
|
+
reconnectReason: debugState.reconnectReason,
|
|
561
|
+
streamError: debugState.streamError,
|
|
562
|
+
},
|
|
563
|
+
source: "useRtcStream",
|
|
564
|
+
});
|
|
431
565
|
const result = await refetchRtcToken(options);
|
|
566
|
+
const resultDebugState = debugReconnectStateRef.current;
|
|
567
|
+
logCctvDebugEvent({
|
|
568
|
+
event: "reconnect-stream:token-result",
|
|
569
|
+
level: result.isSuccess ? "info" : "warn",
|
|
570
|
+
payload: {
|
|
571
|
+
...resultDebugState.debugBasePayload,
|
|
572
|
+
isError: result.isError,
|
|
573
|
+
isSuccess: result.isSuccess,
|
|
574
|
+
status: result.status,
|
|
575
|
+
},
|
|
576
|
+
source: "useRtcStream",
|
|
577
|
+
});
|
|
432
578
|
if (result.isSuccess && streamIdentityKey) {
|
|
579
|
+
logCctvDebugEvent({
|
|
580
|
+
event: "reconnect-stream:close-identity",
|
|
581
|
+
payload: resultDebugState.debugBasePayload,
|
|
582
|
+
source: "useRtcStream",
|
|
583
|
+
});
|
|
433
584
|
streamRegistry.closeByIdentity(streamIdentityKey);
|
|
434
585
|
}
|
|
435
586
|
return result;
|
|
@@ -469,6 +620,16 @@ export function useCctvRtcStream({
|
|
|
469
620
|
return;
|
|
470
621
|
|
|
471
622
|
lastAutoReconnectAtRef.current = now;
|
|
623
|
+
logCctvDebugEvent({
|
|
624
|
+
event: "focus:auto-reconnect",
|
|
625
|
+
payload: {
|
|
626
|
+
...debugBasePayload,
|
|
627
|
+
canReconnect,
|
|
628
|
+
connectionState,
|
|
629
|
+
reconnectReason,
|
|
630
|
+
},
|
|
631
|
+
source: "useRtcStream",
|
|
632
|
+
});
|
|
472
633
|
void reconnectStream();
|
|
473
634
|
};
|
|
474
635
|
|
|
@@ -479,7 +640,13 @@ export function useCctvRtcStream({
|
|
|
479
640
|
window.removeEventListener("focus", reconnectOnFocus);
|
|
480
641
|
document.removeEventListener("visibilitychange", reconnectOnFocus);
|
|
481
642
|
};
|
|
482
|
-
}, [
|
|
643
|
+
}, [
|
|
644
|
+
canReconnect,
|
|
645
|
+
connectionState,
|
|
646
|
+
debugBasePayload,
|
|
647
|
+
reconnectReason,
|
|
648
|
+
reconnectStream,
|
|
649
|
+
]);
|
|
483
650
|
|
|
484
651
|
const liveState = useMemo(
|
|
485
652
|
() =>
|
|
@@ -503,6 +670,40 @@ export function useCctvRtcStream({
|
|
|
503
670
|
],
|
|
504
671
|
);
|
|
505
672
|
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
logCctvDebugEvent({
|
|
675
|
+
event: "hook-state:update",
|
|
676
|
+
payload: {
|
|
677
|
+
...debugBasePayload,
|
|
678
|
+
canReconnect,
|
|
679
|
+
connectionState,
|
|
680
|
+
displayConnectionState,
|
|
681
|
+
hasConnected,
|
|
682
|
+
isPostConnectedReconnectReady,
|
|
683
|
+
isStreaming,
|
|
684
|
+
isTokenError,
|
|
685
|
+
isTokenLoading,
|
|
686
|
+
liveState,
|
|
687
|
+
reconnectReason,
|
|
688
|
+
streamError,
|
|
689
|
+
},
|
|
690
|
+
source: "useRtcStream",
|
|
691
|
+
});
|
|
692
|
+
}, [
|
|
693
|
+
canReconnect,
|
|
694
|
+
connectionState,
|
|
695
|
+
debugBasePayload,
|
|
696
|
+
displayConnectionState,
|
|
697
|
+
hasConnected,
|
|
698
|
+
isPostConnectedReconnectReady,
|
|
699
|
+
isStreaming,
|
|
700
|
+
isTokenError,
|
|
701
|
+
isTokenLoading,
|
|
702
|
+
liveState,
|
|
703
|
+
reconnectReason,
|
|
704
|
+
streamError,
|
|
705
|
+
]);
|
|
706
|
+
|
|
506
707
|
useEffect(() => {
|
|
507
708
|
const camId = cam?.cam_id;
|
|
508
709
|
if (!camId) return;
|
|
@@ -0,0 +1,179 @@
|
|
|
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 CctvDebugBuffer {
|
|
15
|
+
clear: () => void;
|
|
16
|
+
dump: () => CctvDebugEvent[];
|
|
17
|
+
enabled: true;
|
|
18
|
+
events: CctvDebugEvent[];
|
|
19
|
+
limit: number;
|
|
20
|
+
sequence: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
__UDS_CCTV_DEBUG__?: CctvDebugBuffer;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEBUG_QUERY_KEYS = ["udsCctvDebug", "cctvDebug"] as const;
|
|
30
|
+
const DEBUG_STORAGE_KEYS = ["uds:cctv:debug", "UDS_CCTV_DEBUG"] as const;
|
|
31
|
+
const DEBUG_ENABLED_VALUES = new Set(["1", "true", "yes", "on", "debug"]);
|
|
32
|
+
const DEBUG_BUFFER_LIMIT = 1000;
|
|
33
|
+
|
|
34
|
+
const isBrowser = (): boolean =>
|
|
35
|
+
typeof window !== "undefined" && typeof document !== "undefined";
|
|
36
|
+
|
|
37
|
+
const isDebugValueEnabled = (value: string | null): boolean =>
|
|
38
|
+
value === "" ||
|
|
39
|
+
(typeof value === "string" && DEBUG_ENABLED_VALUES.has(value));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* identity/stream key를 원문 대신 추적 가능한 hash label로 바꾼다.
|
|
43
|
+
*/
|
|
44
|
+
export const getCctvDebugKeyLabel = (
|
|
45
|
+
value: string | null | undefined,
|
|
46
|
+
): string | null => {
|
|
47
|
+
if (!value) return null;
|
|
48
|
+
|
|
49
|
+
let hash = 0;
|
|
50
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
51
|
+
hash = (hash * 31 + value.charCodeAt(i)) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `hash:${hash.toString(36)}|len:${value.length}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* endpoint는 query string을 제거한 origin/path만 기록한다.
|
|
59
|
+
*/
|
|
60
|
+
export const getCctvDebugUrlLabel = (
|
|
61
|
+
value: string | null | undefined,
|
|
62
|
+
): string | null => {
|
|
63
|
+
if (!value) return null;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(value);
|
|
67
|
+
return `${url.origin}${url.pathname}`;
|
|
68
|
+
} catch {
|
|
69
|
+
return value.split("?")[0] ?? null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* CCTV debug logging 활성화 여부를 확인한다.
|
|
75
|
+
* @desc
|
|
76
|
+
* production default는 off다. `?udsCctvDebug=1` 또는
|
|
77
|
+
* `localStorage.setItem("uds:cctv:debug", "1")`로 runtime에서 켤 수 있다.
|
|
78
|
+
*/
|
|
79
|
+
export const isCctvDebugEnabled = (): boolean => {
|
|
80
|
+
if (!isBrowser()) return false;
|
|
81
|
+
|
|
82
|
+
const query = new URLSearchParams(window.location.search);
|
|
83
|
+
for (const key of DEBUG_QUERY_KEYS) {
|
|
84
|
+
if (query.has(key) && isDebugValueEnabled(query.get(key))) return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const key of DEBUG_STORAGE_KEYS) {
|
|
88
|
+
try {
|
|
89
|
+
if (isDebugValueEnabled(window.localStorage.getItem(key))) return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* reconnect 호출 주체를 좁히기 위한 stack trace 일부를 만든다.
|
|
100
|
+
*/
|
|
101
|
+
export const getCctvDebugStackTrace = (): string[] | null => {
|
|
102
|
+
if (!isCctvDebugEnabled()) return null;
|
|
103
|
+
|
|
104
|
+
const stack = new Error().stack;
|
|
105
|
+
if (!stack) return null;
|
|
106
|
+
|
|
107
|
+
return stack
|
|
108
|
+
.split("\n")
|
|
109
|
+
.slice(2, 8)
|
|
110
|
+
.map(line => line.trim());
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const getCctvDebugBuffer = (): CctvDebugBuffer | null => {
|
|
114
|
+
if (!isCctvDebugEnabled()) return null;
|
|
115
|
+
|
|
116
|
+
const existingBuffer = window.__UDS_CCTV_DEBUG__;
|
|
117
|
+
if (existingBuffer) return existingBuffer;
|
|
118
|
+
|
|
119
|
+
const buffer: CctvDebugBuffer = {
|
|
120
|
+
enabled: true,
|
|
121
|
+
events: [],
|
|
122
|
+
limit: DEBUG_BUFFER_LIMIT,
|
|
123
|
+
sequence: 0,
|
|
124
|
+
clear: () => {
|
|
125
|
+
buffer.events.splice(0, buffer.events.length);
|
|
126
|
+
buffer.sequence = 0;
|
|
127
|
+
},
|
|
128
|
+
dump: () => [...buffer.events],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
window.__UDS_CCTV_DEBUG__ = buffer;
|
|
132
|
+
return buffer;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* CCTV runtime 진단 이벤트를 ring buffer와 console에 남긴다.
|
|
137
|
+
* @desc
|
|
138
|
+
* 토큰 값은 payload에 넣지 않는다. 이 logger는 debug flag가 켜진 경우에만 동작한다.
|
|
139
|
+
*/
|
|
140
|
+
export const logCctvDebugEvent = ({
|
|
141
|
+
event,
|
|
142
|
+
level = "debug",
|
|
143
|
+
payload,
|
|
144
|
+
source,
|
|
145
|
+
}: {
|
|
146
|
+
event: string;
|
|
147
|
+
level?: CctvDebugEventLevel;
|
|
148
|
+
payload?: Record<string, unknown>;
|
|
149
|
+
source: string;
|
|
150
|
+
}): void => {
|
|
151
|
+
const buffer = getCctvDebugBuffer();
|
|
152
|
+
if (!buffer) return;
|
|
153
|
+
|
|
154
|
+
const entry: CctvDebugEvent = {
|
|
155
|
+
at: new Date().toISOString(),
|
|
156
|
+
event,
|
|
157
|
+
level,
|
|
158
|
+
payload,
|
|
159
|
+
seq: buffer.sequence + 1,
|
|
160
|
+
source,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
buffer.sequence = entry.seq;
|
|
164
|
+
buffer.events.push(entry);
|
|
165
|
+
if (buffer.events.length > buffer.limit) {
|
|
166
|
+
buffer.events.splice(0, buffer.events.length - buffer.limit);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const logger =
|
|
170
|
+
level === "error"
|
|
171
|
+
? console.error
|
|
172
|
+
: level === "warn"
|
|
173
|
+
? console.warn
|
|
174
|
+
: level === "info"
|
|
175
|
+
? console.info
|
|
176
|
+
: console.debug;
|
|
177
|
+
|
|
178
|
+
logger.call(console, "[UDS:CCTV]", entry);
|
|
179
|
+
};
|