@uniai-fe/uds-templates 0.10.2 → 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 +12 -12
- package/src/cctv/hooks/streamRegistry.ts +230 -0
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/next-devkit": "0.4.0",
|
|
71
70
|
"@uniai-fe/eslint-config": "0.4.1",
|
|
72
|
-
"@uniai-fe/
|
|
71
|
+
"@uniai-fe/next-devkit": "0.4.0",
|
|
73
72
|
"@uniai-fe/tsconfig": "0.2.0",
|
|
74
73
|
"@uniai-fe/react-hooks": "0.3.0",
|
|
75
|
-
"@uniai-fe/util-functions": "0.4.3",
|
|
76
|
-
"@uniai-fe/util-jotai": "0.3.0",
|
|
77
74
|
"@uniai-fe/uds-foundation": "0.5.0",
|
|
78
75
|
"@uniai-fe/uds-primitives": "0.10.0",
|
|
76
|
+
"@uniai-fe/util-functions": "0.4.3",
|
|
77
|
+
"@uniai-fe/util-api": "0.2.1",
|
|
78
|
+
"@uniai-fe/util-next": "0.5.0",
|
|
79
79
|
"@uniai-fe/util-rtc": "0.2.0",
|
|
80
|
-
"@uniai-fe/util-
|
|
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
|
|