@uniai-fe/uds-templates 0.10.0 → 0.10.1

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.0",
3
+ "version": "0.10.1",
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": ">=16 <17",
47
+ "next": ">=15.5.20 <17",
48
48
  "react": ">=19 <20",
49
49
  "react-dom": ">=19 <20",
50
50
  "react-hook-form": ">=7 <8",
@@ -69,15 +69,15 @@
69
69
  "typescript": "6.0.3",
70
70
  "@uniai-fe/eslint-config": "0.4.0",
71
71
  "@uniai-fe/next-devkit": "0.4.0",
72
- "@uniai-fe/tsconfig": "0.2.0",
73
72
  "@uniai-fe/react-hooks": "0.3.0",
74
- "@uniai-fe/uds-primitives": "0.10.0",
75
- "@uniai-fe/util-functions": "0.4.1",
73
+ "@uniai-fe/tsconfig": "0.2.0",
76
74
  "@uniai-fe/uds-foundation": "0.5.0",
77
- "@uniai-fe/util-jotai": "0.3.0",
78
75
  "@uniai-fe/util-api": "0.2.1",
76
+ "@uniai-fe/util-functions": "0.4.1",
77
+ "@uniai-fe/util-jotai": "0.3.0",
79
78
  "@uniai-fe/util-next": "0.5.0",
80
- "@uniai-fe/util-rtc": "0.2.0"
79
+ "@uniai-fe/util-rtc": "0.2.0",
80
+ "@uniai-fe/uds-primitives": "0.10.0"
81
81
  },
82
82
  "scripts": {
83
83
  "check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
@@ -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" && nextPayload.token.trim() !== ""
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 `{ data: { token } }`를 반환하는 경우를 모두 소비한다.
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
  }
@@ -238,27 +238,39 @@ export function useCctvRtcStream({
238
238
  const endpointUsername =
239
239
  whepUsername === undefined ? tokenUsername : whepUsername;
240
240
 
241
- // 카메라의 RTC 엔드포인트와 사용자명을 조합해 실제 WHEP endpoint를 계산한다.
242
- const endpoint = (() => {
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
- const query = endpointUsername
248
- ? `?username=${encodeURIComponent(endpointUsername)}`
249
- : "";
250
+ if (endpointUsername) {
251
+ nextEndpoint.searchParams.set("username", endpointUsername);
252
+ }
250
253
 
251
- // cam_rtc 뒤의 slash 중복을 제거하고 WHEP path를 붙인다.
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 (!tokenUsername || !cam?.company_id || !cam.cam_id || !endpoint)
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 [tokenUsername, cam.company_id, cam.cam_id, endpoint].join("|");
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 (!cam?.cam_id || !cam.cam_online || !endpoint || !tokenQuery.data?.token)
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
- endpoint,
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 (!streamKey || !tokenQuery.data?.token || !endpoint || !currentVideo)
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
- endpoint,
581
+ requestEndpoint,
549
582
  streamIdentityKey,
550
583
  streamKey,
551
584
  streamRegistry,
@@ -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
  /**