@uniai-fe/uds-templates 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -56,28 +56,28 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@svgr/webpack": "^8.1.0",
|
|
59
|
-
"@tanstack/react-query": "^5.101.
|
|
60
|
-
"@types/node": "^24.
|
|
59
|
+
"@tanstack/react-query": "^5.101.2",
|
|
60
|
+
"@types/node": "^24.13.2",
|
|
61
61
|
"@types/react": "^19.2.17",
|
|
62
62
|
"@types/react-dom": "^19.2.3",
|
|
63
|
-
"eslint": "^9.39.
|
|
64
|
-
"jotai": "^2.20.
|
|
63
|
+
"eslint": "^9.39.4",
|
|
64
|
+
"jotai": "^2.20.2",
|
|
65
65
|
"next": "16.2.10",
|
|
66
|
-
"prettier": "^3.8.
|
|
67
|
-
"react-hook-form": "^7.
|
|
66
|
+
"prettier": "^3.8.5",
|
|
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.
|
|
70
|
+
"@uniai-fe/eslint-config": "0.4.1",
|
|
71
71
|
"@uniai-fe/next-devkit": "0.4.0",
|
|
72
|
-
"@uniai-fe/react-hooks": "0.3.0",
|
|
73
72
|
"@uniai-fe/tsconfig": "0.2.0",
|
|
73
|
+
"@uniai-fe/react-hooks": "0.3.0",
|
|
74
74
|
"@uniai-fe/uds-foundation": "0.5.0",
|
|
75
|
+
"@uniai-fe/uds-primitives": "0.10.0",
|
|
76
|
+
"@uniai-fe/util-functions": "0.4.3",
|
|
75
77
|
"@uniai-fe/util-api": "0.2.1",
|
|
76
|
-
"@uniai-fe/util-functions": "0.4.1",
|
|
77
|
-
"@uniai-fe/util-jotai": "0.3.0",
|
|
78
78
|
"@uniai-fe/util-next": "0.5.0",
|
|
79
79
|
"@uniai-fe/util-rtc": "0.2.0",
|
|
80
|
-
"@uniai-fe/
|
|
80
|
+
"@uniai-fe/util-jotai": "0.3.0"
|
|
81
81
|
},
|
|
82
82
|
"scripts": {
|
|
83
83
|
"check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
|
|
@@ -2,30 +2,106 @@
|
|
|
2
2
|
|
|
3
3
|
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* CCTV Stream Registry Types; 외부 구독 snapshot
|
|
7
|
+
* @property {RTCPeerConnectionState} connectionState PeerConnection 연결 상태
|
|
8
|
+
* @property {boolean} isStreaming WHEP 시작 promise 진행 여부
|
|
9
|
+
* @property {MediaStream | null} stream 현재 수신 중인 MediaStream
|
|
10
|
+
* @property {string | null} streamError 마지막 WHEP 시작 오류 메시지
|
|
11
|
+
*/
|
|
5
12
|
interface CctvRtcStreamSnapshot {
|
|
13
|
+
/**
|
|
14
|
+
* PeerConnection 연결 상태
|
|
15
|
+
*/
|
|
6
16
|
connectionState: RTCPeerConnectionState;
|
|
17
|
+
/**
|
|
18
|
+
* WHEP 시작 promise 진행 여부
|
|
19
|
+
*/
|
|
7
20
|
isStreaming: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* 현재 수신 중인 MediaStream
|
|
23
|
+
*/
|
|
8
24
|
stream: MediaStream | null;
|
|
25
|
+
/**
|
|
26
|
+
* 마지막 WHEP 시작 오류 메시지
|
|
27
|
+
*/
|
|
9
28
|
streamError: string | null;
|
|
10
29
|
}
|
|
11
30
|
|
|
31
|
+
/**
|
|
32
|
+
* CCTV Stream Registry Types; Provider scope stream entry
|
|
33
|
+
* @property {Set<HTMLVideoElement>} attachedVideos 같은 stream을 표시하는 video 집합
|
|
34
|
+
* @property {AbortController | null} controller 진행 중이거나 연결된 WHEP cleanup controller
|
|
35
|
+
* @property {WhepStreamHandle | null} handle 연결 완료 뒤 PeerConnection cleanup handle
|
|
36
|
+
* @property {string} identityKey camera와 endpoint 기준 stream family key
|
|
37
|
+
* @property {Set<(snapshot: CctvRtcStreamSnapshot) => void>} listeners snapshot 구독자 집합
|
|
38
|
+
* @property {string} streamKey token generation까지 포함한 stream key
|
|
39
|
+
*/
|
|
12
40
|
interface CctvRtcStreamEntry extends CctvRtcStreamSnapshot {
|
|
41
|
+
/**
|
|
42
|
+
* 같은 stream을 표시하는 video element 집합
|
|
43
|
+
*/
|
|
13
44
|
attachedVideos: Set<HTMLVideoElement>;
|
|
45
|
+
/**
|
|
46
|
+
* 진행 중이거나 연결된 WHEP를 중단하는 controller
|
|
47
|
+
*/
|
|
14
48
|
controller: AbortController | null;
|
|
49
|
+
/**
|
|
50
|
+
* 연결 완료 뒤 PeerConnection을 닫는 handle
|
|
51
|
+
*/
|
|
15
52
|
handle: WhepStreamHandle | null;
|
|
53
|
+
/**
|
|
54
|
+
* camera와 endpoint 기준 stream family key
|
|
55
|
+
*/
|
|
16
56
|
identityKey: string;
|
|
57
|
+
/**
|
|
58
|
+
* entry snapshot 구독자 집합
|
|
59
|
+
*/
|
|
17
60
|
listeners: Set<(snapshot: CctvRtcStreamSnapshot) => void>;
|
|
61
|
+
/**
|
|
62
|
+
* token generation까지 포함한 stream key
|
|
63
|
+
*/
|
|
18
64
|
streamKey: string;
|
|
19
65
|
}
|
|
20
66
|
|
|
67
|
+
/**
|
|
68
|
+
* CCTV Stream Registry Types; RTC stream 시작 파라미터
|
|
69
|
+
* @property {string} endpoint token-specific WHEP 요청 endpoint
|
|
70
|
+
* @property {string} identityKey camera와 endpoint 기준 stream family key
|
|
71
|
+
* @property {string} streamKey token generation까지 포함한 stream key
|
|
72
|
+
* @property {string} token WHEP Bearer token
|
|
73
|
+
* @property {HTMLVideoElement} video 최초 stream attach 대상 video
|
|
74
|
+
*/
|
|
21
75
|
interface CctvRtcStreamStartParams {
|
|
76
|
+
/**
|
|
77
|
+
* token-specific WHEP 요청 endpoint
|
|
78
|
+
*/
|
|
22
79
|
endpoint: string;
|
|
80
|
+
/**
|
|
81
|
+
* camera와 endpoint 기준 stream family key
|
|
82
|
+
*/
|
|
23
83
|
identityKey: string;
|
|
84
|
+
/**
|
|
85
|
+
* token generation까지 포함한 stream key
|
|
86
|
+
*/
|
|
24
87
|
streamKey: string;
|
|
88
|
+
/**
|
|
89
|
+
* WHEP Bearer token
|
|
90
|
+
*/
|
|
25
91
|
token: string;
|
|
92
|
+
/**
|
|
93
|
+
* 최초 stream attach 대상 video element
|
|
94
|
+
*/
|
|
26
95
|
video: HTMLVideoElement;
|
|
27
96
|
}
|
|
28
97
|
|
|
98
|
+
/**
|
|
99
|
+
* CCTV Stream Registry Util; 외부 구독용 immutable snapshot 생성
|
|
100
|
+
* @util
|
|
101
|
+
* @param {CctvRtcStreamEntry} [entry] snapshot을 만들 registry entry
|
|
102
|
+
* @desc Map에 entry가 없을 때는 새 연결 전 기본 상태를 반환한다.
|
|
103
|
+
* @return {CctvRtcStreamSnapshot} 현재 entry 상태 또는 기본 idle 상태
|
|
104
|
+
*/
|
|
29
105
|
const getEntrySnapshot = (
|
|
30
106
|
entry?: CctvRtcStreamEntry,
|
|
31
107
|
): CctvRtcStreamSnapshot => ({
|
|
@@ -35,12 +111,26 @@ const getEntrySnapshot = (
|
|
|
35
111
|
streamError: entry?.streamError ?? null,
|
|
36
112
|
});
|
|
37
113
|
|
|
114
|
+
/**
|
|
115
|
+
* CCTV Stream Registry Util; video element에 RTC stream 연결
|
|
116
|
+
* @util
|
|
117
|
+
* @param {HTMLVideoElement} video stream attach 대상 video
|
|
118
|
+
* @param {MediaStream | null} stream 표시할 MediaStream 또는 초기화용 null
|
|
119
|
+
* @desc inline autoplay 속성과 srcObject를 함께 적용한다.
|
|
120
|
+
*/
|
|
38
121
|
const attachVideo = (video: HTMLVideoElement, stream: MediaStream | null) => {
|
|
39
122
|
video.playsInline = true;
|
|
40
123
|
video.autoplay = true;
|
|
41
124
|
video.srcObject = stream;
|
|
42
125
|
};
|
|
43
126
|
|
|
127
|
+
/**
|
|
128
|
+
* CCTV Stream Registry Util; 지정 stream의 video 연결 해제
|
|
129
|
+
* @util
|
|
130
|
+
* @param {HTMLVideoElement} video cleanup 대상 video
|
|
131
|
+
* @param {MediaStream | null} stream 제거 대상 MediaStream
|
|
132
|
+
* @desc replacement stream이 이미 붙은 video를 이전 entry cleanup이 비우지 않게 한다.
|
|
133
|
+
*/
|
|
44
134
|
const clearVideoIfAttachedStream = (
|
|
45
135
|
video: HTMLVideoElement,
|
|
46
136
|
stream: MediaStream | null,
|
|
@@ -50,12 +140,25 @@ const clearVideoIfAttachedStream = (
|
|
|
50
140
|
}
|
|
51
141
|
};
|
|
52
142
|
|
|
143
|
+
/**
|
|
144
|
+
* CCTV Stream Registry Util; entry snapshot 구독자 통지
|
|
145
|
+
* @util
|
|
146
|
+
* @param {CctvRtcStreamEntry} entry 변경된 registry entry
|
|
147
|
+
* @desc 현재 entry의 immutable snapshot을 생성해 등록된 listener에 전달한다.
|
|
148
|
+
*/
|
|
53
149
|
const notifyEntry = (entry: CctvRtcStreamEntry) => {
|
|
54
150
|
const snapshot = getEntrySnapshot(entry);
|
|
55
151
|
entry.listeners.forEach(listener => listener(snapshot));
|
|
56
152
|
};
|
|
57
153
|
|
|
154
|
+
/**
|
|
155
|
+
* CCTV Stream Registry Util; 단일 registry entry resource 정리
|
|
156
|
+
* @util
|
|
157
|
+
* @param {CctvRtcStreamEntry} entry 종료할 registry entry
|
|
158
|
+
* @desc AbortController와 WHEP handle cleanup은 각 구현의 idempotent 계약을 재사용한다.
|
|
159
|
+
*/
|
|
58
160
|
const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
161
|
+
// pending WHEP와 연결 완료 handle을 모두 닫되 다음 close가 중복 실행되지 않게 참조를 비운다.
|
|
59
162
|
entry.controller?.abort();
|
|
60
163
|
entry.controller = null;
|
|
61
164
|
entry.handle?.close();
|
|
@@ -67,41 +170,95 @@ const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
|
67
170
|
clearVideoIfAttachedStream(video, entry.stream);
|
|
68
171
|
});
|
|
69
172
|
|
|
173
|
+
// 마지막 closed snapshot을 전달한 뒤 entry가 외부 객체를 붙잡지 않도록 집합을 비운다.
|
|
70
174
|
notifyEntry(entry);
|
|
71
175
|
entry.attachedVideos.clear();
|
|
72
176
|
entry.listeners.clear();
|
|
73
177
|
};
|
|
74
178
|
|
|
179
|
+
/**
|
|
180
|
+
* CCTV Stream Registry Types; Provider scope registry 계약
|
|
181
|
+
* @property {(streamKey: string, video: HTMLVideoElement) => () => void} attach video attach와 detach cleanup 등록
|
|
182
|
+
* @property {() => void} closeAll Provider scope의 모든 stream 종료
|
|
183
|
+
* @property {(identityKey: string, exceptStreamKey?: string) => void} closeByIdentity 같은 camera family의 stream 종료
|
|
184
|
+
* @property {(streamKey: string) => CctvRtcStreamSnapshot} getSnapshot stream snapshot 조회
|
|
185
|
+
* @property {(params: CctvRtcStreamStartParams) => void} start streamKey 단위 WHEP 시작
|
|
186
|
+
* @property {(streamKey: string, listener: (snapshot: CctvRtcStreamSnapshot) => void) => () => void} subscribe stream snapshot 구독
|
|
187
|
+
*/
|
|
75
188
|
export interface CctvRtcStreamRegistry {
|
|
189
|
+
/**
|
|
190
|
+
* video attach와 detach cleanup 등록
|
|
191
|
+
*/
|
|
76
192
|
attach: (streamKey: string, video: HTMLVideoElement) => () => void;
|
|
193
|
+
/**
|
|
194
|
+
* Provider scope의 모든 stream 종료
|
|
195
|
+
*/
|
|
77
196
|
closeAll: () => void;
|
|
197
|
+
/**
|
|
198
|
+
* 같은 camera family의 stream을 optional 예외 key 외에는 모두 종료
|
|
199
|
+
*/
|
|
78
200
|
closeByIdentity: (identityKey: string, exceptStreamKey?: string) => void;
|
|
201
|
+
/**
|
|
202
|
+
* streamKey의 현재 외부 구독 snapshot 조회
|
|
203
|
+
*/
|
|
79
204
|
getSnapshot: (streamKey: string) => CctvRtcStreamSnapshot;
|
|
205
|
+
/**
|
|
206
|
+
* streamKey 단위 WHEP 시작
|
|
207
|
+
*/
|
|
80
208
|
start: (params: CctvRtcStreamStartParams) => void;
|
|
209
|
+
/**
|
|
210
|
+
* streamKey snapshot 구독과 unsubscribe cleanup 등록
|
|
211
|
+
*/
|
|
81
212
|
subscribe: (
|
|
82
213
|
streamKey: string,
|
|
83
214
|
listener: (snapshot: CctvRtcStreamSnapshot) => void,
|
|
84
215
|
) => () => void;
|
|
85
216
|
}
|
|
86
217
|
|
|
218
|
+
/**
|
|
219
|
+
* CCTV Stream Registry Util; Provider scope stream registry 생성
|
|
220
|
+
* @util
|
|
221
|
+
* @desc 같은 streamKey의 중복 WHEP start를 막고 camera identity별 replacement cleanup을 관리한다.
|
|
222
|
+
* @return {CctvRtcStreamRegistry} 독립된 Provider scope stream registry
|
|
223
|
+
* @example
|
|
224
|
+
* const registry = createCctvRtcStreamRegistry();
|
|
225
|
+
* registry.start({ endpoint, identityKey, streamKey, token, video });
|
|
226
|
+
*/
|
|
87
227
|
export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
228
|
+
// Map은 token generation이 포함된 streamKey마다 현재 lifecycle owner를 하나만 유지한다.
|
|
88
229
|
const entries = new Map<string, CctvRtcStreamEntry>();
|
|
89
230
|
|
|
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
|
+
*/
|
|
90
237
|
const deleteEntry = (entry: CctvRtcStreamEntry) => {
|
|
91
238
|
closeEntry(entry);
|
|
92
239
|
entries.delete(entry.streamKey);
|
|
93
240
|
};
|
|
94
241
|
|
|
95
242
|
return {
|
|
243
|
+
/**
|
|
244
|
+
* CCTV Stream Registry Util; streamKey entry에 video 연결
|
|
245
|
+
* @util
|
|
246
|
+
* @param {string} streamKey attach할 stream key
|
|
247
|
+
* @param {HTMLVideoElement} video attach 대상 video
|
|
248
|
+
* @desc 현재 stream을 video에 연결하고 attach 집합에 등록한다.
|
|
249
|
+
* @return {() => void} 현재 video attach만 해제하는 cleanup
|
|
250
|
+
*/
|
|
96
251
|
attach(streamKey, video) {
|
|
97
252
|
const entry = entries.get(streamKey);
|
|
98
253
|
if (!entry) {
|
|
254
|
+
// 아직 시작되지 않은 key는 stale srcObject 없이 빈 video 상태로 정규화한다.
|
|
99
255
|
attachVideo(video, null);
|
|
100
256
|
return () => {
|
|
101
257
|
if (video.srcObject) video.srcObject = null;
|
|
102
258
|
};
|
|
103
259
|
}
|
|
104
260
|
|
|
261
|
+
// 기존 MediaStream이 있으면 즉시 reattach하고 pending entry면 현재 화면을 보존한다.
|
|
105
262
|
entry.attachedVideos.add(video);
|
|
106
263
|
if (entry.stream || !video.srcObject) {
|
|
107
264
|
attachVideo(video, entry.stream);
|
|
@@ -112,11 +269,25 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
112
269
|
clearVideoIfAttachedStream(video, entry.stream);
|
|
113
270
|
};
|
|
114
271
|
},
|
|
272
|
+
/**
|
|
273
|
+
* CCTV Stream Registry Util; Provider scope의 모든 stream 정리
|
|
274
|
+
* @util
|
|
275
|
+
* @desc 모든 entry resource를 닫고 registry Map을 비운다.
|
|
276
|
+
*/
|
|
115
277
|
closeAll() {
|
|
278
|
+
// Provider unmount에서 모든 entry를 한 번 닫은 뒤 registry owner Map을 비운다.
|
|
116
279
|
entries.forEach(entry => closeEntry(entry));
|
|
117
280
|
entries.clear();
|
|
118
281
|
},
|
|
282
|
+
/**
|
|
283
|
+
* CCTV Stream Registry Util; 같은 identity의 이전 stream 정리
|
|
284
|
+
* @util
|
|
285
|
+
* @param {string} identityKey cleanup할 camera stream family key
|
|
286
|
+
* @param {string} [exceptStreamKey] 유지할 replacement stream key
|
|
287
|
+
* @desc optional 예외 key를 제외한 같은 camera family entry를 닫는다.
|
|
288
|
+
*/
|
|
119
289
|
closeByIdentity(identityKey, exceptStreamKey) {
|
|
290
|
+
// replacement track이 붙은 뒤 같은 camera family의 이전 generation만 선택적으로 닫는다.
|
|
120
291
|
entries.forEach(entry => {
|
|
121
292
|
if (
|
|
122
293
|
entry.identityKey === identityKey &&
|
|
@@ -126,15 +297,36 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
126
297
|
}
|
|
127
298
|
});
|
|
128
299
|
},
|
|
300
|
+
/**
|
|
301
|
+
* CCTV Stream Registry Util; streamKey snapshot 조회
|
|
302
|
+
* @util
|
|
303
|
+
* @param {string} streamKey 조회할 stream key
|
|
304
|
+
* @desc 내부 mutable entry를 노출하지 않고 외부 구독용 snapshot만 반환한다.
|
|
305
|
+
* @return {CctvRtcStreamSnapshot} 현재 entry 상태 또는 기본 idle 상태
|
|
306
|
+
*/
|
|
129
307
|
getSnapshot(streamKey) {
|
|
308
|
+
// 외부 consumer가 내부 mutable entry를 직접 소유하지 않도록 snapshot만 반환한다.
|
|
130
309
|
return getEntrySnapshot(entries.get(streamKey));
|
|
131
310
|
},
|
|
311
|
+
/**
|
|
312
|
+
* CCTV Stream Registry Util; streamKey 단위 WHEP 연결 시작
|
|
313
|
+
* @util
|
|
314
|
+
* @param {CctvRtcStreamStartParams} params WHEP 시작 파라미터
|
|
315
|
+
* @property {string} params.endpoint token-specific WHEP 요청 endpoint
|
|
316
|
+
* @property {string} params.identityKey camera와 endpoint 기준 stream family key
|
|
317
|
+
* @property {string} params.streamKey token generation까지 포함한 stream key
|
|
318
|
+
* @property {string} params.token WHEP Bearer token
|
|
319
|
+
* @property {HTMLVideoElement} params.video 최초 stream attach 대상 video element
|
|
320
|
+
* @desc 같은 streamKey의 중복 start를 막고 현재 entry의 lifecycle만 갱신한다.
|
|
321
|
+
*/
|
|
132
322
|
start({ endpoint, identityKey, streamKey, token, video }) {
|
|
133
323
|
const existingEntry = entries.get(streamKey);
|
|
134
324
|
if (existingEntry) {
|
|
325
|
+
// 같은 token generation의 concurrent attach는 기존 WHEP start 하나를 공유한다.
|
|
135
326
|
return;
|
|
136
327
|
}
|
|
137
328
|
|
|
329
|
+
// WHEP promise보다 entry를 먼저 등록해 같은 tick의 중복 start를 차단한다.
|
|
138
330
|
const controller = new AbortController();
|
|
139
331
|
const entry: CctvRtcStreamEntry = {
|
|
140
332
|
attachedVideos: new Set([video]),
|
|
@@ -150,7 +342,17 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
150
342
|
};
|
|
151
343
|
|
|
152
344
|
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
|
+
*/
|
|
351
|
+
const isCurrentEntry = () =>
|
|
352
|
+
entries.get(streamKey) === entry && !controller.signal.aborted;
|
|
353
|
+
|
|
153
354
|
if (!video.srcObject) {
|
|
355
|
+
// cold start는 빈 video를 초기화하고 replacement는 기존 화면을 그대로 유지한다.
|
|
154
356
|
attachVideo(video, null);
|
|
155
357
|
} else {
|
|
156
358
|
video.playsInline = true;
|
|
@@ -163,10 +365,16 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
163
365
|
video,
|
|
164
366
|
signal: controller.signal,
|
|
165
367
|
onConnectionStateChange: state => {
|
|
368
|
+
// Provider cleanup 또는 같은 key owner 교체 뒤 늦은 state event는 반영하지 않는다.
|
|
369
|
+
if (!isCurrentEntry()) return;
|
|
370
|
+
|
|
166
371
|
entry.connectionState = state;
|
|
167
372
|
notifyEntry(entry);
|
|
168
373
|
},
|
|
169
374
|
onTrack: event => {
|
|
375
|
+
// 닫힌 entry의 late track이 video와 snapshot을 되살리지 않게 막는다.
|
|
376
|
+
if (!isCurrentEntry()) return;
|
|
377
|
+
|
|
170
378
|
entry.stream = event.streams[0] ?? null;
|
|
171
379
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
172
380
|
attachVideo(attachedVideo, entry.stream),
|
|
@@ -175,11 +383,22 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
175
383
|
},
|
|
176
384
|
})
|
|
177
385
|
.then(handle => {
|
|
386
|
+
// pending 중 close된 start가 늦게 resolve되면 새 handle을 즉시 회수한다.
|
|
387
|
+
if (!isCurrentEntry()) {
|
|
388
|
+
handle.close();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
178
392
|
entry.handle = handle;
|
|
179
393
|
entry.isStreaming = false;
|
|
180
394
|
notifyEntry(entry);
|
|
181
395
|
})
|
|
182
396
|
.catch(error => {
|
|
397
|
+
// abort/delete된 이전 owner의 rejection은 현재 entry 상태를 덮어쓰지 않는다.
|
|
398
|
+
if (entries.get(streamKey) !== entry) return;
|
|
399
|
+
|
|
400
|
+
// 실패 원인이 어느 negotiation 단계이든 util-rtc 내부 PeerConnection cleanup을 요청한다.
|
|
401
|
+
entry.controller?.abort();
|
|
183
402
|
entry.controller = null;
|
|
184
403
|
entry.handle = null;
|
|
185
404
|
entry.isStreaming = false;
|
|
@@ -191,13 +410,24 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
|
191
410
|
entry.attachedVideos.forEach(attachedVideo =>
|
|
192
411
|
clearVideoIfAttachedStream(attachedVideo, entry.stream),
|
|
193
412
|
);
|
|
413
|
+
// subscriber에는 terminal error를 전달하되 실패 entry는 다음 명시적 start를 막지 않는다.
|
|
194
414
|
notifyEntry(entry);
|
|
415
|
+
entries.delete(streamKey);
|
|
195
416
|
});
|
|
196
417
|
},
|
|
418
|
+
/**
|
|
419
|
+
* CCTV Stream Registry Util; streamKey snapshot listener 등록
|
|
420
|
+
* @util
|
|
421
|
+
* @param {string} streamKey 구독할 stream key
|
|
422
|
+
* @param {(snapshot: CctvRtcStreamSnapshot) => void} listener snapshot listener
|
|
423
|
+
* @desc 등록 직후 현재 snapshot을 전달하고 이후 상태 변경을 구독한다.
|
|
424
|
+
* @return {() => void} 현재 listener만 제거하는 unsubscribe
|
|
425
|
+
*/
|
|
197
426
|
subscribe(streamKey, listener) {
|
|
198
427
|
const entry = entries.get(streamKey);
|
|
199
428
|
if (!entry) return () => undefined;
|
|
200
429
|
|
|
430
|
+
// 구독 직후 현재 snapshot을 전달해 registry reattach의 첫 render 지연을 줄인다.
|
|
201
431
|
entry.listeners.add(listener);
|
|
202
432
|
listener(getEntrySnapshot(entry));
|
|
203
433
|
|
|
@@ -35,7 +35,7 @@ import { cctvRtcLiveRegistryAtom } from "../jotai/rtc";
|
|
|
35
35
|
* selectedCompany, // 선택된 회사 데이터
|
|
36
36
|
* selectedCam, // 선택된 카메라 데이터
|
|
37
37
|
* groups, // 회사 그룹/회사/카메라 리스트 구조 데이터
|
|
38
|
-
* companyValidList, //
|
|
38
|
+
* companyValidList, // 카메라가 하나라도 있는 회사 ID 리스트
|
|
39
39
|
* cams, // 선택된 회사의 카메라 리스트
|
|
40
40
|
* onToggleCamera(cam_id), // 카메라 선택 토글 함수
|
|
41
41
|
* onOpenCamera(cam_id), // 카메라 선택 함수
|
|
@@ -159,14 +159,12 @@ export default function useCctvContext(): UseCctvContextReturn {
|
|
|
159
159
|
[selectedCamId, selectedCompanyId, groups],
|
|
160
160
|
);
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// cam_online 상태와 무관하게 카메라가 하나라도 있는 company_id를 반환한다.
|
|
163
163
|
const companyValidList = useMemo(
|
|
164
164
|
() =>
|
|
165
165
|
groups
|
|
166
166
|
.flatMap(({ list }) => list)
|
|
167
|
-
.filter(
|
|
168
|
-
({ cam_list }) => !cam_list.every(({ cam_online }) => !cam_online),
|
|
169
|
-
)
|
|
167
|
+
.filter(({ cam_list }) => cam_list.length > 0)
|
|
170
168
|
.map(({ company_id }) => company_id),
|
|
171
169
|
[groups],
|
|
172
170
|
);
|
package/src/cctv/types/api.ts
CHANGED
|
@@ -144,7 +144,7 @@ export interface API_Res_CctvCameraData
|
|
|
144
144
|
* CCTV: 업체에 설치된 카메라 목록
|
|
145
145
|
* @property {string} cam_id 카메라 id코드
|
|
146
146
|
* @property {string} cam_name 카메라 별칭
|
|
147
|
-
* @property {boolean} cam_online 카메라
|
|
147
|
+
* @property {boolean} cam_online 카메라 네트워크 연결 상태
|
|
148
148
|
* @property {string} cam_rtc 카메라 스트리밍 WebRTC 경로
|
|
149
149
|
* @property {string} cam_rtcp 카메라 RTCP 경로
|
|
150
150
|
* @property {string} cam_rtsp 카메라 RTSP 경로
|
|
@@ -156,7 +156,7 @@ export interface API_Res_CctvCameraData
|
|
|
156
156
|
export interface API_Res_CctvCompanyCameraList extends API_Res_CctvCameraData {
|
|
157
157
|
// API 응답은 데이터-only 구조이므로 UI 전용 콜백은 포함하지 않는다.
|
|
158
158
|
/**
|
|
159
|
-
* 카메라
|
|
159
|
+
* 카메라 네트워크 연결 상태
|
|
160
160
|
*/
|
|
161
161
|
cam_online: boolean;
|
|
162
162
|
}
|
package/src/cctv/types/hook.ts
CHANGED
|
@@ -195,7 +195,7 @@ export interface UseCctvCompanyDataReturn extends Omit<
|
|
|
195
195
|
* @property {CctvCompanyGroupItems | undefined} selectedCompany 선택된 업체 데이터
|
|
196
196
|
* @property {CctvCompanyCameraData | undefined} selectedCam 선택된 카메라 데이터
|
|
197
197
|
* @property {CctvCompanyGroup[]} groups 업체 그룹/업체/카메라 리스트 구조 데이터
|
|
198
|
-
* @property {string[]} companyValidList
|
|
198
|
+
* @property {string[]} companyValidList 카메라가 하나라도 있는 업체 ID 리스트
|
|
199
199
|
* @property {CctvCompanyCameraList[]} cams 선택된 업체의 카메라 리스트
|
|
200
200
|
* @property {number} liveCamCount 실시간 스트리밍 중인 카메라 수
|
|
201
201
|
* @property {(cam_id: string) => void} onToggleCamera 카메라 선택 토글 함수
|
|
@@ -244,7 +244,7 @@ export interface UseCctvContextReturn extends UseFormReturn<CctvBaseContext> {
|
|
|
244
244
|
*/
|
|
245
245
|
groups: CctvCompanyGroup[];
|
|
246
246
|
/**
|
|
247
|
-
*
|
|
247
|
+
* 카메라가 하나라도 있는 업체 목록
|
|
248
248
|
*/
|
|
249
249
|
companyValidList: string[];
|
|
250
250
|
/**
|
package/src/cctv/types/list.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
* @property {Function} [onSelect] 카메라 선택 콜백 이벤트
|
|
11
11
|
* @property {string} cam_id 카메라 id코드
|
|
12
12
|
* @property {string} cam_name 카메라 별칭
|
|
13
|
-
* @property {boolean} cam_online 카메라
|
|
13
|
+
* @property {boolean} cam_online 카메라 네트워크 연결 상태
|
|
14
14
|
* @property {string} [cam_rtc] 카메라 스트리밍 WebRTC 경로
|
|
15
15
|
* @property {string} [cam_rtcp] 카메라 RTCP 경로
|
|
16
16
|
* @property {string} [cam_rtsp] 카메라 RTSP 경로
|
|
@@ -46,7 +46,7 @@ export interface CctvCompanyCameraData extends API_Res_CctvCompanyCameraList {
|
|
|
46
46
|
* @property {Function} [onSelect] 카메라 선택 콜백 이벤트
|
|
47
47
|
* @property {string} cam_id 카메라 id코드
|
|
48
48
|
* @property {string} cam_name 카메라 별칭
|
|
49
|
-
* @property {boolean} cam_online 카메라
|
|
49
|
+
* @property {boolean} cam_online 카메라 네트워크 연결 상태
|
|
50
50
|
* @property {string} [cam_rtc] 카메라 스트리밍 WebRTC 경로
|
|
51
51
|
* @property {string} [cam_rtcp] 카메라 RTCP 경로
|
|
52
52
|
* @property {string} [cam_rtsp] 카메라 RTSP 경로
|
|
@@ -13,6 +13,7 @@ export const CCTV_MESSAGE = {
|
|
|
13
13
|
"장시간 미사용으로 연결이 종료되었습니다. 계속 확인하시려면 다시 연결해 주세요.",
|
|
14
14
|
tokenError: "토큰을 발급하지 못했습니다.",
|
|
15
15
|
offline: "CCTV 연결 오류",
|
|
16
|
+
networkDisconnected: "네트워크 연결이 끊어졌습니다.",
|
|
16
17
|
} as const;
|
|
17
18
|
|
|
18
19
|
const RTC_PREPARING_STATES = new Set<RTCPeerConnectionState>([
|
|
@@ -56,7 +57,7 @@ export function getOverlayMessage({
|
|
|
56
57
|
return isFetching ? CCTV_MESSAGE.fetching : CCTV_MESSAGE.selectCam;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
if (!cam.cam_online) return CCTV_MESSAGE.
|
|
60
|
+
if (!cam.cam_online) return CCTV_MESSAGE.networkDisconnected;
|
|
60
61
|
// 에러 상태가 준비 상태와 겹칠 때는 에러 문구가 최종 표시 계약을 우선한다.
|
|
61
62
|
if (isTokenError) return CCTV_MESSAGE.tokenError;
|
|
62
63
|
if (streamError) return streamError;
|