@uniai-fe/uds-templates 0.10.3 → 0.10.4
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 +7 -7
- package/src/cctv/hooks/streamRegistry.ts +69 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -67,17 +67,17 @@
|
|
|
67
67
|
"react-hook-form": "^7.81.0",
|
|
68
68
|
"sass": "^1.101.0",
|
|
69
69
|
"typescript": "6.0.3",
|
|
70
|
-
"@uniai-fe/eslint-config": "0.4.1",
|
|
71
70
|
"@uniai-fe/next-devkit": "0.4.0",
|
|
71
|
+
"@uniai-fe/eslint-config": "0.4.1",
|
|
72
72
|
"@uniai-fe/tsconfig": "0.2.0",
|
|
73
73
|
"@uniai-fe/react-hooks": "0.3.0",
|
|
74
|
-
"@uniai-fe/
|
|
74
|
+
"@uniai-fe/util-api": "0.2.1",
|
|
75
75
|
"@uniai-fe/uds-primitives": "0.10.0",
|
|
76
76
|
"@uniai-fe/util-functions": "0.4.3",
|
|
77
|
-
"@uniai-fe/util-
|
|
78
|
-
"@uniai-fe/util-
|
|
79
|
-
"@uniai-fe/
|
|
80
|
-
"@uniai-fe/util-
|
|
77
|
+
"@uniai-fe/util-jotai": "0.3.0",
|
|
78
|
+
"@uniai-fe/util-rtc": "0.2.1",
|
|
79
|
+
"@uniai-fe/uds-foundation": "0.5.0",
|
|
80
|
+
"@uniai-fe/util-next": "0.5.0"
|
|
81
81
|
},
|
|
82
82
|
"scripts": {
|
|
83
83
|
"check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
|
|
@@ -8,6 +8,7 @@ import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
|
8
8
|
* @property {boolean} isStreaming WHEP 시작 promise 진행 여부
|
|
9
9
|
* @property {MediaStream | null} stream 현재 수신 중인 MediaStream
|
|
10
10
|
* @property {string | null} streamError 마지막 WHEP 시작 오류 메시지
|
|
11
|
+
* @desc 외부 subscriber에 노출하는 immutable RTC stream 상태 계약
|
|
11
12
|
*/
|
|
12
13
|
interface CctvRtcStreamSnapshot {
|
|
13
14
|
/**
|
|
@@ -35,7 +36,10 @@ interface CctvRtcStreamSnapshot {
|
|
|
35
36
|
* @property {WhepStreamHandle | null} handle 연결 완료 뒤 PeerConnection cleanup handle
|
|
36
37
|
* @property {string} identityKey camera와 endpoint 기준 stream family key
|
|
37
38
|
* @property {Set<(snapshot: CctvRtcStreamSnapshot) => void>} listeners snapshot 구독자 집합
|
|
39
|
+
* @property {MediaStream | null} pendingStream handle 반환 전 수신한 replacement stream
|
|
40
|
+
* @property {Map<HTMLVideoElement, MediaProvider | null>} previousVideoSources replacement 실패 시 복원할 video source
|
|
38
41
|
* @property {string} streamKey token generation까지 포함한 stream key
|
|
42
|
+
* @desc Provider scope에서 단일 stream lifecycle과 replacement rollback resource를 소유하는 내부 entry 계약
|
|
39
43
|
*/
|
|
40
44
|
interface CctvRtcStreamEntry extends CctvRtcStreamSnapshot {
|
|
41
45
|
/**
|
|
@@ -58,6 +62,14 @@ interface CctvRtcStreamEntry extends CctvRtcStreamSnapshot {
|
|
|
58
62
|
* entry snapshot 구독자 집합
|
|
59
63
|
*/
|
|
60
64
|
listeners: Set<(snapshot: CctvRtcStreamSnapshot) => void>;
|
|
65
|
+
/**
|
|
66
|
+
* handle 반환 전 수신한 replacement MediaStream
|
|
67
|
+
*/
|
|
68
|
+
pendingStream: MediaStream | null;
|
|
69
|
+
/**
|
|
70
|
+
* replacement 실패 시 복원할 video별 기존 source
|
|
71
|
+
*/
|
|
72
|
+
previousVideoSources: Map<HTMLVideoElement, MediaProvider | null>;
|
|
61
73
|
/**
|
|
62
74
|
* token generation까지 포함한 stream key
|
|
63
75
|
*/
|
|
@@ -71,6 +83,7 @@ interface CctvRtcStreamEntry extends CctvRtcStreamSnapshot {
|
|
|
71
83
|
* @property {string} streamKey token generation까지 포함한 stream key
|
|
72
84
|
* @property {string} token WHEP Bearer token
|
|
73
85
|
* @property {HTMLVideoElement} video 최초 stream attach 대상 video
|
|
86
|
+
* @desc registry가 token generation별 WHEP lifecycle을 시작하는 입력 계약
|
|
74
87
|
*/
|
|
75
88
|
interface CctvRtcStreamStartParams {
|
|
76
89
|
/**
|
|
@@ -167,8 +180,11 @@ const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
|
167
180
|
entry.isStreaming = false;
|
|
168
181
|
|
|
169
182
|
entry.attachedVideos.forEach(video => {
|
|
170
|
-
|
|
183
|
+
// committed 전 replacement도 현재 entry가 video에 붙인 resource로 보고 선택적으로 제거한다.
|
|
184
|
+
clearVideoIfAttachedStream(video, entry.pendingStream ?? entry.stream);
|
|
171
185
|
});
|
|
186
|
+
entry.pendingStream = null;
|
|
187
|
+
entry.previousVideoSources.clear();
|
|
172
188
|
|
|
173
189
|
// 마지막 closed snapshot을 전달한 뒤 entry가 외부 객체를 붙잡지 않도록 집합을 비운다.
|
|
174
190
|
notifyEntry(entry);
|
|
@@ -184,6 +200,7 @@ const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
|
184
200
|
* @property {(streamKey: string) => CctvRtcStreamSnapshot} getSnapshot stream snapshot 조회
|
|
185
201
|
* @property {(params: CctvRtcStreamStartParams) => void} start streamKey 단위 WHEP 시작
|
|
186
202
|
* @property {(streamKey: string, listener: (snapshot: CctvRtcStreamSnapshot) => void) => () => void} subscribe stream snapshot 구독
|
|
203
|
+
* @desc Provider scope에서 WHEP stream 시작, 연결, 구독과 cleanup을 관리하는 공개 registry 계약
|
|
187
204
|
*/
|
|
188
205
|
export interface CctvRtcStreamRegistry {
|
|
189
206
|
/**
|
|
@@ -228,12 +245,7 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
228
245
|
// Map은 token generation이 포함된 streamKey마다 현재 lifecycle owner를 하나만 유지한다.
|
|
229
246
|
const entries = new Map<string, CctvRtcStreamEntry>();
|
|
230
247
|
|
|
231
|
-
|
|
232
|
-
* CCTV Stream Registry Util; entry resource와 Map owner 제거
|
|
233
|
-
* @util
|
|
234
|
-
* @param {CctvRtcStreamEntry} entry 삭제할 registry entry
|
|
235
|
-
* @desc entry cleanup을 수행한 뒤 streamKey의 Map owner를 제거한다.
|
|
236
|
-
*/
|
|
248
|
+
// entry resource를 정리한 뒤 streamKey의 Map owner를 제거한다.
|
|
237
249
|
const deleteEntry = (entry: CctvRtcStreamEntry) => {
|
|
238
250
|
closeEntry(entry);
|
|
239
251
|
entries.delete(entry.streamKey);
|
|
@@ -253,6 +265,7 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
253
265
|
if (!entry) {
|
|
254
266
|
// 아직 시작되지 않은 key는 stale srcObject 없이 빈 video 상태로 정규화한다.
|
|
255
267
|
attachVideo(video, null);
|
|
268
|
+
// 소유할 registry entry가 없으므로 Map mutation 없이 현재 video만 비우는 cleanup을 반환한다.
|
|
256
269
|
return () => {
|
|
257
270
|
if (video.srcObject) video.srcObject = null;
|
|
258
271
|
};
|
|
@@ -260,13 +273,21 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
260
273
|
|
|
261
274
|
// 기존 MediaStream이 있으면 즉시 reattach하고 pending entry면 현재 화면을 보존한다.
|
|
262
275
|
entry.attachedVideos.add(video);
|
|
263
|
-
if (entry.
|
|
264
|
-
|
|
276
|
+
if (!entry.previousVideoSources.has(video)) {
|
|
277
|
+
// pending replacement 실패 시 이 attach 직전 source로 돌아갈 수 있게 최초 값만 보관한다.
|
|
278
|
+
entry.previousVideoSources.set(video, video.srcObject);
|
|
279
|
+
}
|
|
280
|
+
// handle 성공 전 track도 새 subscriber video에는 보이되 committed snapshot으로 승격하지 않는다.
|
|
281
|
+
const attachedStream = entry.stream ?? entry.pendingStream;
|
|
282
|
+
if (attachedStream || !video.srcObject) {
|
|
283
|
+
attachVideo(video, attachedStream);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
return () => {
|
|
287
|
+
// detach된 video는 이후 실패 복원과 entry cleanup의 소유 대상에서 제외한다.
|
|
268
288
|
entry.attachedVideos.delete(video);
|
|
269
|
-
|
|
289
|
+
entry.previousVideoSources.delete(video);
|
|
290
|
+
clearVideoIfAttachedStream(video, entry.pendingStream ?? entry.stream);
|
|
270
291
|
};
|
|
271
292
|
},
|
|
272
293
|
/**
|
|
@@ -311,7 +332,6 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
311
332
|
/**
|
|
312
333
|
* CCTV Stream Registry Util; streamKey 단위 WHEP 연결 시작
|
|
313
334
|
* @util
|
|
314
|
-
* @param {CctvRtcStreamStartParams} params WHEP 시작 파라미터
|
|
315
335
|
* @property {string} params.endpoint token-specific WHEP 요청 endpoint
|
|
316
336
|
* @property {string} params.identityKey camera와 endpoint 기준 stream family key
|
|
317
337
|
* @property {string} params.streamKey token generation까지 포함한 stream key
|
|
@@ -336,18 +356,16 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
336
356
|
identityKey,
|
|
337
357
|
isStreaming: true,
|
|
338
358
|
listeners: new Set(),
|
|
359
|
+
pendingStream: null,
|
|
360
|
+
// 최초 video가 이미 표시하던 connected stream을 replacement 실패 rollback 값으로 보존한다.
|
|
361
|
+
previousVideoSources: new Map([[video, video.srcObject]]),
|
|
339
362
|
stream: null,
|
|
340
363
|
streamError: null,
|
|
341
364
|
streamKey,
|
|
342
365
|
};
|
|
343
366
|
|
|
344
367
|
entries.set(streamKey, entry);
|
|
345
|
-
|
|
346
|
-
* CCTV Stream Registry Util; 현재 entry의 lifecycle owner 여부 확인
|
|
347
|
-
* @util
|
|
348
|
-
* @desc Map owner 일치와 AbortSignal 상태를 함께 확인한다.
|
|
349
|
-
* @return {boolean} callback과 settlement 반영 가능 여부
|
|
350
|
-
*/
|
|
368
|
+
// Map owner 일치와 AbortSignal 상태로 현재 entry의 lifecycle 소유권을 확인한다.
|
|
351
369
|
const isCurrentEntry = () =>
|
|
352
370
|
entries.get(streamKey) === entry && !controller.signal.aborted;
|
|
353
371
|
|
|
@@ -365,51 +383,69 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
365
383
|
video,
|
|
366
384
|
signal: controller.signal,
|
|
367
385
|
onConnectionStateChange: state => {
|
|
368
|
-
|
|
369
|
-
|
|
386
|
+
if (!isCurrentEntry()) {
|
|
387
|
+
// Provider cleanup 또는 같은 key owner 교체 뒤 늦은 state event는 반영하지 않는다.
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
370
390
|
|
|
371
391
|
entry.connectionState = state;
|
|
372
392
|
notifyEntry(entry);
|
|
373
393
|
},
|
|
374
394
|
onTrack: event => {
|
|
375
|
-
|
|
376
|
-
|
|
395
|
+
if (!isCurrentEntry()) {
|
|
396
|
+
// 닫힌 entry의 late track이 video와 snapshot을 되살리지 않게 막는다.
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
377
399
|
|
|
378
|
-
|
|
400
|
+
// track만으로 성공을 확정하지 않고 handle 반환 전까지 pending resource로 유지한다.
|
|
401
|
+
entry.pendingStream = event.streams[0] ?? null;
|
|
379
402
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
380
|
-
attachVideo(attachedVideo, entry.
|
|
403
|
+
attachVideo(attachedVideo, entry.pendingStream),
|
|
381
404
|
);
|
|
382
|
-
notifyEntry(entry);
|
|
383
405
|
},
|
|
384
406
|
})
|
|
385
407
|
.then(handle => {
|
|
386
408
|
// pending 중 close된 start가 늦게 resolve되면 새 handle을 즉시 회수한다.
|
|
387
409
|
if (!isCurrentEntry()) {
|
|
388
410
|
handle.close();
|
|
411
|
+
// 회수한 stale handle을 현재 entry의 성공 상태로 다시 기록하지 않는다.
|
|
389
412
|
return;
|
|
390
413
|
}
|
|
391
414
|
|
|
392
415
|
entry.handle = handle;
|
|
393
416
|
entry.isStreaming = false;
|
|
417
|
+
// remote SDP까지 완료된 handle만 committed snapshot으로 승격해 이전 generation을 닫게 한다.
|
|
418
|
+
entry.stream = entry.pendingStream;
|
|
419
|
+
entry.pendingStream = null;
|
|
394
420
|
notifyEntry(entry);
|
|
421
|
+
entry.previousVideoSources.clear();
|
|
395
422
|
})
|
|
396
423
|
.catch(error => {
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
if (entries.get(streamKey) !== entry) {
|
|
425
|
+
// abort/delete된 이전 owner의 rejection은 현재 entry 상태를 덮어쓰지 않는다.
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
399
428
|
|
|
400
429
|
// 실패 원인이 어느 negotiation 단계이든 util-rtc 내부 PeerConnection cleanup을 요청한다.
|
|
401
430
|
entry.controller?.abort();
|
|
402
431
|
entry.controller = null;
|
|
403
432
|
entry.handle = null;
|
|
404
433
|
entry.isStreaming = false;
|
|
434
|
+
const failedStream = entry.pendingStream ?? entry.stream;
|
|
435
|
+
entry.attachedVideos.forEach(attachedVideo => {
|
|
436
|
+
// 외부에서 다른 source로 교체한 video는 건드리지 않고 이 entry의 실패 stream만 rollback한다.
|
|
437
|
+
if (failedStream && attachedVideo.srcObject === failedStream) {
|
|
438
|
+
attachedVideo.srcObject =
|
|
439
|
+
entry.previousVideoSources.get(attachedVideo) ?? null;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
entry.pendingStream = null;
|
|
405
443
|
entry.stream = null;
|
|
444
|
+
entry.previousVideoSources.clear();
|
|
406
445
|
entry.streamError =
|
|
407
446
|
error instanceof Error
|
|
408
447
|
? error.message
|
|
409
448
|
: "스트림 연결에 실패했습니다.";
|
|
410
|
-
entry.attachedVideos.forEach(attachedVideo =>
|
|
411
|
-
clearVideoIfAttachedStream(attachedVideo, entry.stream),
|
|
412
|
-
);
|
|
413
449
|
// subscriber에는 terminal error를 전달하되 실패 entry는 다음 명시적 start를 막지 않는다.
|
|
414
450
|
notifyEntry(entry);
|
|
415
451
|
entries.delete(streamKey);
|
|
@@ -425,7 +461,10 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
425
461
|
*/
|
|
426
462
|
subscribe(streamKey, listener) {
|
|
427
463
|
const entry = entries.get(streamKey);
|
|
428
|
-
if (!entry)
|
|
464
|
+
if (!entry) {
|
|
465
|
+
// 구독할 lifecycle owner가 없으므로 listener를 저장하지 않고 no-op cleanup을 반환한다.
|
|
466
|
+
return () => undefined;
|
|
467
|
+
}
|
|
429
468
|
|
|
430
469
|
// 구독 직후 현재 snapshot을 전달해 registry reattach의 첫 render 지연을 줄인다.
|
|
431
470
|
entry.listeners.add(listener);
|