@uniai-fe/uds-templates 0.6.26 → 0.6.28
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 +23 -1
- package/package.json +7 -7
- package/src/cctv/apis/client.ts +173 -11
- package/src/cctv/components/Provider.tsx +66 -8
- package/src/cctv/components/cam-list/Item.tsx +6 -0
- package/src/cctv/components/pagination/list/Item.tsx +20 -0
- package/src/cctv/components/video/Template.tsx +3 -0
- package/src/cctv/components/viewer/desktop/Video.tsx +1 -0
- package/src/cctv/hooks/streamScheduler.ts +276 -0
- package/src/cctv/hooks/useRtcStream.ts +222 -20
- package/src/cctv/styles/cam-list.scss +28 -2
- package/src/cctv/styles/variables.scss +1 -0
- package/src/cctv/types/context.ts +21 -0
- package/src/cctv/types/hook.ts +24 -0
- package/src/cctv/types/props.ts +12 -1
- package/src/cctv/types/video-state.ts +18 -3
- package/src/cctv/utils/video-state.ts +3 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import type {
|
|
5
|
+
CctvRtcStreamOptions,
|
|
6
|
+
CctvRtcStreamScheduleStatus,
|
|
7
|
+
} from "../types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_STREAM_SCHEDULER_OPTIONS = {
|
|
10
|
+
batchSize: 4,
|
|
11
|
+
batchIntervalMs: 1200,
|
|
12
|
+
} satisfies Required<CctvRtcStreamOptions>;
|
|
13
|
+
|
|
14
|
+
type StreamSchedulerListener = () => void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CCTV; RTC stream scheduler contract
|
|
18
|
+
* @property {(identityKey: string) => () => void} request stream 시작 순번 요청 함수
|
|
19
|
+
* @property {(identityKey: string) => CctvRtcStreamScheduleStatus} getStatus stream 시작 순번 상태 조회 함수
|
|
20
|
+
* @property {(listener: StreamSchedulerListener) => () => void} subscribe scheduler 변경 구독 함수
|
|
21
|
+
* @property {() => void} clear scheduler 내부 상태 정리 함수
|
|
22
|
+
*/
|
|
23
|
+
export interface CctvRtcStreamScheduler {
|
|
24
|
+
/**
|
|
25
|
+
* stream 시작 순번 요청 함수
|
|
26
|
+
*/
|
|
27
|
+
request: (identityKey: string) => () => void;
|
|
28
|
+
/**
|
|
29
|
+
* stream 시작 순번 상태 조회 함수
|
|
30
|
+
*/
|
|
31
|
+
getStatus: (identityKey: string) => CctvRtcStreamScheduleStatus;
|
|
32
|
+
/**
|
|
33
|
+
* scheduler 변경 구독 함수
|
|
34
|
+
*/
|
|
35
|
+
subscribe: (listener: StreamSchedulerListener) => () => void;
|
|
36
|
+
/**
|
|
37
|
+
* scheduler 내부 상태 정리 함수
|
|
38
|
+
*/
|
|
39
|
+
clear: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* CCTV; RTC stream scheduler 옵션 정규화
|
|
44
|
+
* @param {CctvRtcStreamOptions} [options] Provider에서 전달한 stream lazy loading 옵션
|
|
45
|
+
* @returns {Required<CctvRtcStreamOptions>} scheduler 내부에서 사용할 확정 옵션
|
|
46
|
+
* @desc
|
|
47
|
+
* service가 잘못된 수치를 넘겨도 기본값과 최소 clamp만 적용해 scheduler가 멈추지 않게 한다.
|
|
48
|
+
*/
|
|
49
|
+
const getNormalizedSchedulerOptions = (
|
|
50
|
+
options?: CctvRtcStreamOptions,
|
|
51
|
+
): Required<CctvRtcStreamOptions> => {
|
|
52
|
+
// optional 값을 먼저 지역 변수로 좁혀 TypeScript가 number 판정을 안정적으로 추론하게 한다.
|
|
53
|
+
const batchSizeCandidate = options?.batchSize;
|
|
54
|
+
const batchIntervalCandidate = options?.batchIntervalMs;
|
|
55
|
+
|
|
56
|
+
// batchSize는 한 번에 grant할 stream 수라 1보다 작으면 scheduler가 진행되지 않는다.
|
|
57
|
+
const batchSize =
|
|
58
|
+
typeof batchSizeCandidate === "number" &&
|
|
59
|
+
Number.isFinite(batchSizeCandidate)
|
|
60
|
+
? Math.max(1, Math.floor(batchSizeCandidate))
|
|
61
|
+
: DEFAULT_STREAM_SCHEDULER_OPTIONS.batchSize;
|
|
62
|
+
|
|
63
|
+
// batchIntervalMs는 batch 사이 대기시간이며 0은 연속 flush 허용값으로 둔다.
|
|
64
|
+
const batchIntervalMs =
|
|
65
|
+
typeof batchIntervalCandidate === "number" &&
|
|
66
|
+
Number.isFinite(batchIntervalCandidate)
|
|
67
|
+
? Math.max(0, Math.floor(batchIntervalCandidate))
|
|
68
|
+
: DEFAULT_STREAM_SCHEDULER_OPTIONS.batchIntervalMs;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
batchSize,
|
|
72
|
+
batchIntervalMs,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* CCTV; RTC stream scheduler 생성
|
|
78
|
+
* @param {CctvRtcStreamOptions} [options] stream lazy loading 옵션
|
|
79
|
+
* @returns {CctvRtcStreamScheduler} stream 시작 순번 scheduler
|
|
80
|
+
* @desc
|
|
81
|
+
* item UI는 즉시 렌더하되 token/WHEP 시작만 batch 단위로 열어 초기 요청 burst를 줄인다.
|
|
82
|
+
*/
|
|
83
|
+
export function createCctvRtcStreamScheduler(
|
|
84
|
+
options?: CctvRtcStreamOptions,
|
|
85
|
+
): CctvRtcStreamScheduler {
|
|
86
|
+
// Provider option은 scheduler 생성 시점에 확정해 queue 처리 중 정책 drift를 막는다.
|
|
87
|
+
const { batchSize, batchIntervalMs } = getNormalizedSchedulerOptions(options);
|
|
88
|
+
|
|
89
|
+
// queue는 아직 token/WHEP 시작 권한을 받지 못한 stream identity 순서를 보관한다.
|
|
90
|
+
const queue: string[] = [];
|
|
91
|
+
|
|
92
|
+
// granted는 token query를 열 수 있는 stream identity 집합이다.
|
|
93
|
+
const granted = new Set<string>();
|
|
94
|
+
|
|
95
|
+
// requestCounts는 같은 stream identity를 여러 component가 동시에 쓰는 경우 premature release를 막는다.
|
|
96
|
+
const requestCounts = new Map<string, number>();
|
|
97
|
+
|
|
98
|
+
// listeners는 hook state와 scheduler 내부 상태를 동기화하는 구독자 집합이다.
|
|
99
|
+
const listeners = new Set<StreamSchedulerListener>();
|
|
100
|
+
|
|
101
|
+
// timer는 다음 batch flush 예약을 단일화해 중복 setTimeout을 방지한다.
|
|
102
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
103
|
+
|
|
104
|
+
const notify = () => {
|
|
105
|
+
// queue/grant 상태가 바뀌면 모든 hook subscriber가 status를 다시 읽는다.
|
|
106
|
+
listeners.forEach(listener => listener());
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const clearTimer = () => {
|
|
110
|
+
// 예약된 flush가 없으면 timer cleanup은 no-op으로 둔다.
|
|
111
|
+
if (!timer) return;
|
|
112
|
+
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
timer = null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const flush = () => {
|
|
118
|
+
// 현재 timer callback이 실행됐으므로 다음 예약이 가능하도록 비운다.
|
|
119
|
+
timer = null;
|
|
120
|
+
|
|
121
|
+
// FIFO 순서대로 이번 batch 크기만큼 grant한다.
|
|
122
|
+
const nextBatch = queue.splice(0, batchSize);
|
|
123
|
+
nextBatch.forEach(identityKey => {
|
|
124
|
+
granted.add(identityKey);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 실제 grant 변화가 있을 때만 subscriber를 깨운다.
|
|
128
|
+
if (nextBatch.length > 0) {
|
|
129
|
+
notify();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 아직 대기열이 남아 있으면 다음 batch를 interval 뒤에 예약한다.
|
|
133
|
+
if (queue.length > 0) {
|
|
134
|
+
timer = setTimeout(flush, batchIntervalMs);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const scheduleFlush = () => {
|
|
139
|
+
// 이미 예약된 flush가 있으면 같은 queue에 중복 timer를 만들지 않는다.
|
|
140
|
+
if (timer) return;
|
|
141
|
+
|
|
142
|
+
// 첫 batch는 즉시 열고, 이후 batch만 interval을 적용한다.
|
|
143
|
+
timer = setTimeout(flush, granted.size === 0 ? 0 : batchIntervalMs);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
request(identityKey) {
|
|
148
|
+
// 같은 stream identity를 여러 video가 요청하면 reference count만 증가시킨다.
|
|
149
|
+
const currentCount = requestCounts.get(identityKey) ?? 0;
|
|
150
|
+
requestCounts.set(identityKey, currentCount + 1);
|
|
151
|
+
|
|
152
|
+
// 이미 grant됐거나 queue에 있으면 중복 등록하지 않는다.
|
|
153
|
+
if (!granted.has(identityKey) && !queue.includes(identityKey)) {
|
|
154
|
+
queue.push(identityKey);
|
|
155
|
+
notify();
|
|
156
|
+
scheduleFlush();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
// cleanup은 hook unmount 또는 identity 변경 때 호출되며 reference count를 먼저 낮춘다.
|
|
161
|
+
const nextCount = (requestCounts.get(identityKey) ?? 1) - 1;
|
|
162
|
+
|
|
163
|
+
// 아직 같은 identity를 쓰는 component가 남아 있으면 grant/queue 상태를 유지한다.
|
|
164
|
+
if (nextCount > 0) {
|
|
165
|
+
requestCounts.set(identityKey, nextCount);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 마지막 subscriber가 사라지면 scheduler 상태에서 identity를 제거한다.
|
|
170
|
+
requestCounts.delete(identityKey);
|
|
171
|
+
granted.delete(identityKey);
|
|
172
|
+
|
|
173
|
+
// 아직 grant 전 queue에 남아 있던 identity라면 대기열에서도 제거한다.
|
|
174
|
+
const queuedIndex = queue.indexOf(identityKey);
|
|
175
|
+
if (queuedIndex > -1) {
|
|
176
|
+
queue.splice(queuedIndex, 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 더 이상 처리할 queue가 없으면 다음 flush 예약도 취소한다.
|
|
180
|
+
if (queue.length === 0) {
|
|
181
|
+
clearTimer();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// release 결과를 subscriber에게 알려 `queued -> idle` 또는 `granted -> idle` 전환을 반영한다.
|
|
185
|
+
notify();
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
getStatus(identityKey) {
|
|
189
|
+
// token query를 열 수 있는 상태를 최우선으로 반환한다.
|
|
190
|
+
if (granted.has(identityKey)) return "granted";
|
|
191
|
+
|
|
192
|
+
// queue에 들어갔지만 아직 grant되지 않은 상태는 overlay에서 대기 상태로 표시된다.
|
|
193
|
+
if (queue.includes(identityKey)) return "queued";
|
|
194
|
+
|
|
195
|
+
// 요청 전이거나 release 이후면 idle이다.
|
|
196
|
+
return "idle";
|
|
197
|
+
},
|
|
198
|
+
subscribe(listener) {
|
|
199
|
+
// hook이 scheduler 상태 변경을 React state로 동기화할 수 있게 listener를 등록한다.
|
|
200
|
+
listeners.add(listener);
|
|
201
|
+
|
|
202
|
+
return () => {
|
|
203
|
+
// hook cleanup 시 listener 누수를 막는다.
|
|
204
|
+
listeners.delete(listener);
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
clear() {
|
|
208
|
+
// Provider unmount 시 timer와 모든 mutable scheduler 상태를 비운다.
|
|
209
|
+
clearTimer();
|
|
210
|
+
queue.splice(0, queue.length);
|
|
211
|
+
granted.clear();
|
|
212
|
+
requestCounts.clear();
|
|
213
|
+
|
|
214
|
+
// 마지막 상태 변화를 알린 뒤 listener 집합도 비운다.
|
|
215
|
+
notify();
|
|
216
|
+
listeners.clear();
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* CCTV; RTC stream scheduler 상태 구독
|
|
223
|
+
* @hook
|
|
224
|
+
* @param {CctvRtcStreamScheduler} scheduler stream 시작 순번 scheduler
|
|
225
|
+
* @param {string} identityKey stream identity key
|
|
226
|
+
* @param {boolean} enabled scheduler 요청 활성화 여부
|
|
227
|
+
* @returns {CctvRtcStreamScheduleStatus} stream 시작 순번 상태
|
|
228
|
+
*/
|
|
229
|
+
export function useCctvRtcStreamScheduleStatus({
|
|
230
|
+
enabled,
|
|
231
|
+
identityKey,
|
|
232
|
+
scheduler,
|
|
233
|
+
}: {
|
|
234
|
+
/**
|
|
235
|
+
* scheduler 요청 활성화 여부
|
|
236
|
+
*/
|
|
237
|
+
enabled: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* stream identity key
|
|
240
|
+
*/
|
|
241
|
+
identityKey: string;
|
|
242
|
+
/**
|
|
243
|
+
* stream 시작 순번 scheduler
|
|
244
|
+
*/
|
|
245
|
+
scheduler: CctvRtcStreamScheduler;
|
|
246
|
+
}): CctvRtcStreamScheduleStatus {
|
|
247
|
+
const [status, setStatus] = useState<CctvRtcStreamScheduleStatus>("idle");
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
// 필수 identity가 없거나 offline이면 scheduler에 등록하지 않고 idle로 되돌린다.
|
|
251
|
+
if (!enabled || !identityKey) {
|
|
252
|
+
setStatus("idle");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// scheduler mutable state를 React state로 복사해 render가 status 변화를 감지하게 한다.
|
|
257
|
+
const syncStatus = () => {
|
|
258
|
+
setStatus(scheduler.getStatus(identityKey));
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// subscribe 이후 request해야 request 직후 notify도 놓치지 않는다.
|
|
262
|
+
const unsubscribe = scheduler.subscribe(syncStatus);
|
|
263
|
+
const release = scheduler.request(identityKey);
|
|
264
|
+
|
|
265
|
+
// request 직후 현재 상태를 즉시 반영해 queued/granted 표시 지연을 줄인다.
|
|
266
|
+
syncStatus();
|
|
267
|
+
|
|
268
|
+
return () => {
|
|
269
|
+
// cleanup 순서는 scheduler reference release 후 listener 제거로 둬 release notify를 받을 수 있게 한다.
|
|
270
|
+
release();
|
|
271
|
+
unsubscribe();
|
|
272
|
+
};
|
|
273
|
+
}, [enabled, identityKey, scheduler]);
|
|
274
|
+
|
|
275
|
+
return status;
|
|
276
|
+
}
|