@uniai-fe/uds-templates 0.10.0 → 0.10.2
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 +9 -9
- package/src/cctv/apis/client.ts +8 -4
- package/src/cctv/hooks/useContext.ts +3 -5
- package/src/cctv/hooks/useRtcStream.ts +47 -14
- package/src/cctv/types/api.ts +7 -2
- package/src/cctv/types/hook.ts +2 -2
- package/src/cctv/types/list.ts +2 -2
- package/src/cctv/utils/video-state.ts +2 -1
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.2",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@uniai-fe/util-next": "^0.3.0 || ^0.4.0 || ^0.5.0",
|
|
45
45
|
"@uniai-fe/util-rtc": "^0.1.4 || ^0.2.0",
|
|
46
46
|
"jotai": ">=2 <3",
|
|
47
|
-
"next": ">=
|
|
47
|
+
"next": ">=15.5.20 <17",
|
|
48
48
|
"react": ">=19 <20",
|
|
49
49
|
"react-dom": ">=19 <20",
|
|
50
50
|
"react-hook-form": ">=7 <8",
|
|
@@ -67,17 +67,17 @@
|
|
|
67
67
|
"react-hook-form": "^7.80.0",
|
|
68
68
|
"sass": "^1.101.0",
|
|
69
69
|
"typescript": "6.0.3",
|
|
70
|
-
"@uniai-fe/eslint-config": "0.4.0",
|
|
71
70
|
"@uniai-fe/next-devkit": "0.4.0",
|
|
71
|
+
"@uniai-fe/eslint-config": "0.4.1",
|
|
72
|
+
"@uniai-fe/util-api": "0.2.1",
|
|
72
73
|
"@uniai-fe/tsconfig": "0.2.0",
|
|
73
74
|
"@uniai-fe/react-hooks": "0.3.0",
|
|
74
|
-
"@uniai-fe/
|
|
75
|
-
"@uniai-fe/util-functions": "0.4.1",
|
|
76
|
-
"@uniai-fe/uds-foundation": "0.5.0",
|
|
75
|
+
"@uniai-fe/util-functions": "0.4.3",
|
|
77
76
|
"@uniai-fe/util-jotai": "0.3.0",
|
|
78
|
-
"@uniai-fe/
|
|
79
|
-
"@uniai-fe/
|
|
80
|
-
"@uniai-fe/util-rtc": "0.2.0"
|
|
77
|
+
"@uniai-fe/uds-foundation": "0.5.0",
|
|
78
|
+
"@uniai-fe/uds-primitives": "0.10.0",
|
|
79
|
+
"@uniai-fe/util-rtc": "0.2.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",
|
package/src/cctv/apis/client.ts
CHANGED
|
@@ -41,7 +41,7 @@ class CctvRtcTokenRequestError extends Error {
|
|
|
41
41
|
/**
|
|
42
42
|
* CCTV; RTC token direct response guard
|
|
43
43
|
* @param {unknown} payload 검사할 token response payload
|
|
44
|
-
* @returns {payload is API_Res_CctvRtcToken} direct `{ token }` 응답 여부
|
|
44
|
+
* @returns {payload is API_Res_CctvRtcToken} direct `{ token, session_ref }` 응답 여부
|
|
45
45
|
*/
|
|
46
46
|
const isCctvRtcTokenResponse = (
|
|
47
47
|
payload: unknown,
|
|
@@ -53,7 +53,10 @@ const isCctvRtcTokenResponse = (
|
|
|
53
53
|
const nextPayload = payload as Partial<API_Res_CctvRtcToken>;
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
-
typeof nextPayload.token === "string" &&
|
|
56
|
+
typeof nextPayload.token === "string" &&
|
|
57
|
+
nextPayload.token.trim() !== "" &&
|
|
58
|
+
typeof nextPayload.session_ref === "string" &&
|
|
59
|
+
nextPayload.session_ref.trim() !== ""
|
|
57
60
|
);
|
|
58
61
|
};
|
|
59
62
|
|
|
@@ -62,7 +65,8 @@ const isCctvRtcTokenResponse = (
|
|
|
62
65
|
* @param {unknown} payload service token route 응답 payload
|
|
63
66
|
* @returns {API_Res_CctvRtcToken | null} 사용 가능한 token 응답
|
|
64
67
|
* @desc
|
|
65
|
-
* service route가 direct `{ token }` 또는 API base
|
|
68
|
+
* service route가 direct `{ token, session_ref }` 또는 API base
|
|
69
|
+
* `{ data: { token, session_ref } }`를 반환하는 경우를 모두 소비한다.
|
|
66
70
|
*/
|
|
67
71
|
const getCctvRtcTokenResponse = (
|
|
68
72
|
payload: unknown,
|
|
@@ -181,7 +185,7 @@ export const postCctvRtcToken = async ({
|
|
|
181
185
|
|
|
182
186
|
if (!tokenResponse) {
|
|
183
187
|
throw new CctvRtcTokenRequestError(
|
|
184
|
-
"CCTV token response is missing token.",
|
|
188
|
+
"CCTV token response is missing token or session_ref.",
|
|
185
189
|
response.status,
|
|
186
190
|
);
|
|
187
191
|
}
|
|
@@ -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
|
);
|
|
@@ -238,27 +238,39 @@ export function useCctvRtcStream({
|
|
|
238
238
|
const endpointUsername =
|
|
239
239
|
whepUsername === undefined ? tokenUsername : whepUsername;
|
|
240
240
|
|
|
241
|
-
// 카메라의 RTC 엔드포인트와 사용자명을 조합해
|
|
242
|
-
const
|
|
241
|
+
// 카메라의 RTC 엔드포인트와 사용자명을 조합해 stable WHEP identity endpoint를 계산한다.
|
|
242
|
+
const streamIdentityEndpoint = (() => {
|
|
243
243
|
// cam_rtc가 없으면 WHEP endpoint를 만들 수 없어 이후 identity/token gate도 닫힌다.
|
|
244
244
|
if (!cam?.cam_rtc) return undefined;
|
|
245
245
|
|
|
246
|
+
// cam_rtc 뒤의 slash 중복을 제거하고 WHEP path를 붙인다.
|
|
247
|
+
const nextEndpoint = new URL(`${cam.cam_rtc.replace(/\/$/, "")}/whep`);
|
|
248
|
+
|
|
246
249
|
// WHEP username override가 있으면 token username과 별개로 endpoint query에만 반영한다.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
if (endpointUsername) {
|
|
251
|
+
nextEndpoint.searchParams.set("username", endpointUsername);
|
|
252
|
+
}
|
|
250
253
|
|
|
251
|
-
|
|
252
|
-
return `${cam.cam_rtc.replace(/\/$/, "")}/whep${query}`;
|
|
254
|
+
return nextEndpoint.toString();
|
|
253
255
|
})();
|
|
254
256
|
|
|
255
257
|
const streamIdentityKey = (() => {
|
|
256
258
|
// identity 필수값이 모두 있어야 scheduler와 registry identity cleanup이 가능하다.
|
|
257
|
-
if (
|
|
259
|
+
if (
|
|
260
|
+
!tokenUsername ||
|
|
261
|
+
!cam?.company_id ||
|
|
262
|
+
!cam.cam_id ||
|
|
263
|
+
!streamIdentityEndpoint
|
|
264
|
+
)
|
|
258
265
|
return "";
|
|
259
266
|
|
|
260
267
|
// token 갱신 시각은 제외해 같은 카메라 stream family를 하나의 identity로 묶는다.
|
|
261
|
-
return [
|
|
268
|
+
return [
|
|
269
|
+
tokenUsername,
|
|
270
|
+
cam.company_id,
|
|
271
|
+
cam.cam_id,
|
|
272
|
+
streamIdentityEndpoint,
|
|
273
|
+
].join("|");
|
|
262
274
|
})();
|
|
263
275
|
const hasCurrentStreamState = streamStateIdentityKey === streamIdentityKey;
|
|
264
276
|
const connectionState = hasCurrentStreamState ? connectionStateValue : "new";
|
|
@@ -289,9 +301,25 @@ export function useCctvRtcStream({
|
|
|
289
301
|
const isTokenEndpointMissing = isStreamStartGranted && !tokenEndpointUrl;
|
|
290
302
|
const isTokenError = tokenQuery.isError || isTokenEndpointMissing;
|
|
291
303
|
|
|
304
|
+
// token-specific ref는 실제 WHEP 요청 URL에만 붙이고 stream identity/key에서는 제외한다.
|
|
305
|
+
const requestEndpoint = (() => {
|
|
306
|
+
if (!streamIdentityEndpoint || !tokenQuery.data?.session_ref)
|
|
307
|
+
return undefined;
|
|
308
|
+
|
|
309
|
+
const nextEndpoint = new URL(streamIdentityEndpoint);
|
|
310
|
+
nextEndpoint.searchParams.set("session_ref", tokenQuery.data.session_ref);
|
|
311
|
+
|
|
312
|
+
return nextEndpoint.toString();
|
|
313
|
+
})();
|
|
314
|
+
|
|
292
315
|
const streamKeyCandidate = (() => {
|
|
293
316
|
// streamKey는 online 카메라와 endpoint/token이 모두 준비된 뒤에만 만들 수 있다.
|
|
294
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
!cam?.cam_id ||
|
|
319
|
+
!cam.cam_online ||
|
|
320
|
+
!streamIdentityEndpoint ||
|
|
321
|
+
!tokenQuery.data?.token
|
|
322
|
+
)
|
|
295
323
|
return "";
|
|
296
324
|
|
|
297
325
|
// token dataUpdatedAt을 포함해 명시 refetch 후에는 새 WHEP 연결 후보로 분리한다.
|
|
@@ -299,7 +327,7 @@ export function useCctvRtcStream({
|
|
|
299
327
|
tokenUsername,
|
|
300
328
|
cam.company_id,
|
|
301
329
|
cam.cam_id,
|
|
302
|
-
|
|
330
|
+
streamIdentityEndpoint,
|
|
303
331
|
tokenQuery.dataUpdatedAt,
|
|
304
332
|
].join("|");
|
|
305
333
|
})();
|
|
@@ -466,7 +494,12 @@ export function useCctvRtcStream({
|
|
|
466
494
|
}
|
|
467
495
|
|
|
468
496
|
// 필수 값이 없으면 스트림을 시작하지 않는다.
|
|
469
|
-
if (
|
|
497
|
+
if (
|
|
498
|
+
!streamKey ||
|
|
499
|
+
!tokenQuery.data?.token ||
|
|
500
|
+
!requestEndpoint ||
|
|
501
|
+
!currentVideo
|
|
502
|
+
)
|
|
470
503
|
return;
|
|
471
504
|
|
|
472
505
|
// 같은 identity에서 token dataUpdatedAt만 바뀌면 새 stream attach 이후 이전 stream을 닫는다.
|
|
@@ -497,7 +530,7 @@ export function useCctvRtcStream({
|
|
|
497
530
|
streamRegistry.start({
|
|
498
531
|
streamKey,
|
|
499
532
|
identityKey: streamIdentityKey,
|
|
500
|
-
endpoint,
|
|
533
|
+
endpoint: requestEndpoint,
|
|
501
534
|
token: tokenQuery.data.token,
|
|
502
535
|
video: currentVideo,
|
|
503
536
|
});
|
|
@@ -545,7 +578,7 @@ export function useCctvRtcStream({
|
|
|
545
578
|
detachVideo();
|
|
546
579
|
};
|
|
547
580
|
}, [
|
|
548
|
-
|
|
581
|
+
requestEndpoint,
|
|
549
582
|
streamIdentityKey,
|
|
550
583
|
streamKey,
|
|
551
584
|
streamRegistry,
|
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
|
}
|
|
@@ -316,6 +316,7 @@ export interface API_Req_CctvRtcToken {
|
|
|
316
316
|
/**
|
|
317
317
|
* CCTV; 실시간 스트리밍 토큰 응답
|
|
318
318
|
* @property {string} token 인증 토큰
|
|
319
|
+
* @property {string} session_ref WHEP 세션 연계용 단기 참조값
|
|
319
320
|
*/
|
|
320
321
|
export interface API_Res_CctvRtcToken {
|
|
321
322
|
/**
|
|
@@ -323,6 +324,10 @@ export interface API_Res_CctvRtcToken {
|
|
|
323
324
|
* Bearer
|
|
324
325
|
*/
|
|
325
326
|
token: string;
|
|
327
|
+
/**
|
|
328
|
+
* WHEP 요청 세션 연계용 단기 참조값
|
|
329
|
+
*/
|
|
330
|
+
session_ref: string;
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
/**
|
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;
|