@uniai-fe/uds-templates 0.5.6 → 0.5.8
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 +1 -1
- package/src/cctv/apis/client.ts +6 -0
- package/src/cctv/components/Provider.tsx +50 -15
- package/src/cctv/hooks/streamRegistry.ts +195 -0
- package/src/cctv/hooks/useContext.ts +2 -2
- package/src/cctv/hooks/useRtcStream.ts +91 -43
- package/src/cctv/types/context.ts +45 -6
- package/src/cctv/types/hook.ts +2 -2
package/package.json
CHANGED
package/src/cctv/apis/client.ts
CHANGED
|
@@ -51,4 +51,10 @@ export const useQueryCctvRtcToken = ({
|
|
|
51
51
|
useQuery({
|
|
52
52
|
queryKey: ["cctv_rtc_token", username, company_id, cam_id, url],
|
|
53
53
|
queryFn: () => postCctvRtcToken({ company_id, cam_id, username, url }),
|
|
54
|
+
enabled: Boolean(username && company_id && cam_id),
|
|
55
|
+
staleTime: Infinity,
|
|
56
|
+
gcTime: Infinity,
|
|
57
|
+
refetchOnMount: false,
|
|
58
|
+
refetchOnReconnect: false,
|
|
59
|
+
refetchOnWindowFocus: false,
|
|
54
60
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext } from "react";
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo } from "react";
|
|
4
4
|
import { Form } from "@uniai-fe/uds-primitives";
|
|
5
|
+
import type { DefaultValues } from "react-hook-form";
|
|
5
6
|
|
|
6
7
|
import type {
|
|
7
8
|
CctvApiUrlContext,
|
|
@@ -9,6 +10,10 @@ import type {
|
|
|
9
10
|
CctvProviderProps,
|
|
10
11
|
} from "../types";
|
|
11
12
|
import { CCTV_CONTEXT_DEFAULT_VALUES } from "../data";
|
|
13
|
+
import {
|
|
14
|
+
createCctvRtcStreamRegistry,
|
|
15
|
+
type CctvRtcStreamRegistry,
|
|
16
|
+
} from "../hooks/streamRegistry";
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* CCTV; API 경로 컨텍스트
|
|
@@ -20,6 +25,10 @@ const ApiUrlContext = createContext<CctvApiUrlContext>({
|
|
|
20
25
|
tokenUrl: undefined,
|
|
21
26
|
});
|
|
22
27
|
|
|
28
|
+
const RtcStreamRegistryContext = createContext<CctvRtcStreamRegistry | null>(
|
|
29
|
+
null,
|
|
30
|
+
);
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
33
|
* CCTV; API 경로 컨텍스트
|
|
25
34
|
* @return {CctvApiUrlContext}
|
|
@@ -31,6 +40,16 @@ export function useCctvApiUrl(): CctvApiUrlContext {
|
|
|
31
40
|
return useContext(ApiUrlContext);
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
export function useCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
44
|
+
const registry = useContext(RtcStreamRegistryContext);
|
|
45
|
+
if (!registry) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"CCTV.Provider 내부에서 useCctvRtcStreamRegistry를 사용해야 합니다.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return registry;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
/**
|
|
35
54
|
* CCTV; Provider props
|
|
36
55
|
* @component
|
|
@@ -39,30 +58,46 @@ export function useCctvApiUrl(): CctvApiUrlContext {
|
|
|
39
58
|
* @property {string} [listUrl] company-list API 요청 경로
|
|
40
59
|
* @property {string} [tokenUrl] token API 요청 경로
|
|
41
60
|
* @property {string} [username] CCTV 조회시 권한 확인을 위한 계정명
|
|
61
|
+
* @property {ContextExtension} [defaultValues] 서비스 확장 context 기본값
|
|
42
62
|
* @property {React.ReactNode} children
|
|
43
63
|
*/
|
|
44
|
-
export default function CCTVProvider({
|
|
64
|
+
export default function CCTVProvider<ContextExtension extends object = object>({
|
|
45
65
|
username,
|
|
46
66
|
company_id,
|
|
47
67
|
cam_id,
|
|
48
68
|
listUrl,
|
|
49
69
|
tokenUrl,
|
|
70
|
+
defaultValues,
|
|
50
71
|
children,
|
|
51
|
-
}: CctvProviderProps) {
|
|
72
|
+
}: CctvProviderProps<ContextExtension>) {
|
|
73
|
+
const streamRegistry = useMemo(() => createCctvRtcStreamRegistry(), []);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
return () => {
|
|
77
|
+
streamRegistry.closeAll();
|
|
78
|
+
};
|
|
79
|
+
}, [streamRegistry]);
|
|
80
|
+
|
|
81
|
+
const mergedDefaultValues = {
|
|
82
|
+
// 변경 설명: service 확장 context 기본값을 core CCTV form context와 한 RHF scope에서 병합한다.
|
|
83
|
+
...defaultValues,
|
|
84
|
+
...CCTV_CONTEXT_DEFAULT_VALUES,
|
|
85
|
+
username,
|
|
86
|
+
company_id,
|
|
87
|
+
cam_id,
|
|
88
|
+
} as DefaultValues<CctvContext<ContextExtension>>;
|
|
89
|
+
|
|
52
90
|
return (
|
|
53
91
|
<ApiUrlContext.Provider value={{ listUrl, tokenUrl }}>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
>
|
|
64
|
-
{children}
|
|
65
|
-
</Form.Provider>
|
|
92
|
+
<RtcStreamRegistryContext.Provider value={streamRegistry}>
|
|
93
|
+
<Form.Provider<CctvContext<ContextExtension>>
|
|
94
|
+
options={{
|
|
95
|
+
defaultValues: mergedDefaultValues,
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</Form.Provider>
|
|
100
|
+
</RtcStreamRegistryContext.Provider>
|
|
66
101
|
</ApiUrlContext.Provider>
|
|
67
102
|
);
|
|
68
103
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
4
|
+
|
|
5
|
+
interface CctvRtcStreamSnapshot {
|
|
6
|
+
connectionState: RTCPeerConnectionState;
|
|
7
|
+
isStreaming: boolean;
|
|
8
|
+
stream: MediaStream | null;
|
|
9
|
+
streamError: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CctvRtcStreamEntry extends CctvRtcStreamSnapshot {
|
|
13
|
+
attachedVideos: Set<HTMLVideoElement>;
|
|
14
|
+
controller: AbortController | null;
|
|
15
|
+
handle: WhepStreamHandle | null;
|
|
16
|
+
identityKey: string;
|
|
17
|
+
listeners: Set<(snapshot: CctvRtcStreamSnapshot) => void>;
|
|
18
|
+
streamKey: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CctvRtcStreamStartParams {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
identityKey: string;
|
|
24
|
+
streamKey: string;
|
|
25
|
+
token: string;
|
|
26
|
+
video: HTMLVideoElement;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getEntrySnapshot = (
|
|
30
|
+
entry?: CctvRtcStreamEntry,
|
|
31
|
+
): CctvRtcStreamSnapshot => ({
|
|
32
|
+
connectionState: entry?.connectionState ?? "new",
|
|
33
|
+
isStreaming: entry?.isStreaming ?? false,
|
|
34
|
+
stream: entry?.stream ?? null,
|
|
35
|
+
streamError: entry?.streamError ?? null,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const attachVideo = (video: HTMLVideoElement, stream: MediaStream | null) => {
|
|
39
|
+
video.playsInline = true;
|
|
40
|
+
video.autoplay = true;
|
|
41
|
+
video.srcObject = stream;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const notifyEntry = (entry: CctvRtcStreamEntry) => {
|
|
45
|
+
const snapshot = getEntrySnapshot(entry);
|
|
46
|
+
entry.listeners.forEach(listener => listener(snapshot));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const closeEntry = (entry: CctvRtcStreamEntry) => {
|
|
50
|
+
entry.controller?.abort();
|
|
51
|
+
entry.controller = null;
|
|
52
|
+
entry.handle?.close();
|
|
53
|
+
entry.handle = null;
|
|
54
|
+
entry.connectionState = "closed";
|
|
55
|
+
entry.isStreaming = false;
|
|
56
|
+
|
|
57
|
+
entry.attachedVideos.forEach(video => {
|
|
58
|
+
if (!entry.stream || video.srcObject === entry.stream) {
|
|
59
|
+
video.srcObject = null;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
notifyEntry(entry);
|
|
64
|
+
entry.attachedVideos.clear();
|
|
65
|
+
entry.listeners.clear();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface CctvRtcStreamRegistry {
|
|
69
|
+
attach: (streamKey: string, video: HTMLVideoElement) => () => void;
|
|
70
|
+
closeAll: () => void;
|
|
71
|
+
closeByIdentity: (identityKey: string, exceptStreamKey?: string) => void;
|
|
72
|
+
getSnapshot: (streamKey: string) => CctvRtcStreamSnapshot;
|
|
73
|
+
start: (params: CctvRtcStreamStartParams) => void;
|
|
74
|
+
subscribe: (
|
|
75
|
+
streamKey: string,
|
|
76
|
+
listener: (snapshot: CctvRtcStreamSnapshot) => void,
|
|
77
|
+
) => () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
|
|
81
|
+
const entries = new Map<string, CctvRtcStreamEntry>();
|
|
82
|
+
|
|
83
|
+
const deleteEntry = (entry: CctvRtcStreamEntry) => {
|
|
84
|
+
closeEntry(entry);
|
|
85
|
+
entries.delete(entry.streamKey);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
attach(streamKey, video) {
|
|
90
|
+
const entry = entries.get(streamKey);
|
|
91
|
+
if (!entry) {
|
|
92
|
+
attachVideo(video, null);
|
|
93
|
+
return () => {
|
|
94
|
+
if (video.srcObject) video.srcObject = null;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
entry.attachedVideos.add(video);
|
|
99
|
+
attachVideo(video, entry.stream);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
entry.attachedVideos.delete(video);
|
|
103
|
+
if (!entry.stream || video.srcObject === entry.stream) {
|
|
104
|
+
video.srcObject = null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
closeAll() {
|
|
109
|
+
entries.forEach(entry => closeEntry(entry));
|
|
110
|
+
entries.clear();
|
|
111
|
+
},
|
|
112
|
+
closeByIdentity(identityKey, exceptStreamKey) {
|
|
113
|
+
entries.forEach(entry => {
|
|
114
|
+
if (
|
|
115
|
+
entry.identityKey === identityKey &&
|
|
116
|
+
entry.streamKey !== exceptStreamKey
|
|
117
|
+
) {
|
|
118
|
+
deleteEntry(entry);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
getSnapshot(streamKey) {
|
|
123
|
+
return getEntrySnapshot(entries.get(streamKey));
|
|
124
|
+
},
|
|
125
|
+
start({ endpoint, identityKey, streamKey, token, video }) {
|
|
126
|
+
const existingEntry = entries.get(streamKey);
|
|
127
|
+
if (existingEntry) return;
|
|
128
|
+
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const entry: CctvRtcStreamEntry = {
|
|
131
|
+
attachedVideos: new Set([video]),
|
|
132
|
+
connectionState: "connecting",
|
|
133
|
+
controller,
|
|
134
|
+
handle: null,
|
|
135
|
+
identityKey,
|
|
136
|
+
isStreaming: true,
|
|
137
|
+
listeners: new Set(),
|
|
138
|
+
stream: null,
|
|
139
|
+
streamError: null,
|
|
140
|
+
streamKey,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
entries.set(streamKey, entry);
|
|
144
|
+
attachVideo(video, null);
|
|
145
|
+
|
|
146
|
+
startWhepStream({
|
|
147
|
+
endpoint,
|
|
148
|
+
token,
|
|
149
|
+
video,
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
onConnectionStateChange: state => {
|
|
152
|
+
entry.connectionState = state;
|
|
153
|
+
notifyEntry(entry);
|
|
154
|
+
},
|
|
155
|
+
onTrack: event => {
|
|
156
|
+
entry.stream = event.streams[0] ?? null;
|
|
157
|
+
entry.attachedVideos.forEach(attachedVideo =>
|
|
158
|
+
attachVideo(attachedVideo, entry.stream),
|
|
159
|
+
);
|
|
160
|
+
notifyEntry(entry);
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
.then(handle => {
|
|
164
|
+
entry.handle = handle;
|
|
165
|
+
entry.isStreaming = false;
|
|
166
|
+
notifyEntry(entry);
|
|
167
|
+
})
|
|
168
|
+
.catch(error => {
|
|
169
|
+
entry.controller = null;
|
|
170
|
+
entry.handle = null;
|
|
171
|
+
entry.isStreaming = false;
|
|
172
|
+
entry.stream = null;
|
|
173
|
+
entry.streamError =
|
|
174
|
+
error instanceof Error
|
|
175
|
+
? error.message
|
|
176
|
+
: "스트림 연결에 실패했습니다.";
|
|
177
|
+
entry.attachedVideos.forEach(attachedVideo =>
|
|
178
|
+
attachVideo(attachedVideo, null),
|
|
179
|
+
);
|
|
180
|
+
notifyEntry(entry);
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
subscribe(streamKey, listener) {
|
|
184
|
+
const entry = entries.get(streamKey);
|
|
185
|
+
if (!entry) return () => undefined;
|
|
186
|
+
|
|
187
|
+
entry.listeners.add(listener);
|
|
188
|
+
listener(getEntrySnapshot(entry));
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
entry.listeners.delete(listener);
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
CctvCompanyCameraList,
|
|
12
12
|
CctvCompanyGroup,
|
|
13
13
|
CctvCompanyGroupItems,
|
|
14
|
-
|
|
14
|
+
CctvBaseContext,
|
|
15
15
|
UseCctvContextReturn,
|
|
16
16
|
} from "../types";
|
|
17
17
|
import { initCompanyList } from "../utils/data";
|
|
@@ -49,7 +49,7 @@ export default function useCctvContext(): UseCctvContextReturn {
|
|
|
49
49
|
const setCtx = useSetAtom(cctvSelectedContext);
|
|
50
50
|
const liveRegistry = useAtomValue(cctvRtcLiveRegistryAtom);
|
|
51
51
|
// react-hook-form context에서 control/setValue 등을 꺼낸다.
|
|
52
|
-
const { control, setValue, ...context } = useFormContext<
|
|
52
|
+
const { control, setValue, ...context } = useFormContext<CctvBaseContext>();
|
|
53
53
|
|
|
54
54
|
// 폼 상태를 watch 하여 실시간 컨텍스트 값을 얻는다.
|
|
55
55
|
const username = useWatch({ control, name: "username" });
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// React 훅들을 활용해 WebRTC 스트림 라이프사이클을 제어한다.
|
|
4
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { useSetAtom } from "jotai";
|
|
6
|
-
// util-rtc 모듈에서 제공하는 WHEP(WebRTC-HTTP Egress Protocol) 헬퍼와 핸들 타입.
|
|
7
|
-
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
8
6
|
|
|
9
7
|
// 토큰 발급 쿼리와 API URL 컨텍스트 훅, 타입, react-hook-form 유틸.
|
|
10
8
|
import { useQueryCctvRtcToken } from "../apis/client";
|
|
11
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useCctvApiUrl,
|
|
11
|
+
useCctvRtcStreamRegistry,
|
|
12
|
+
} from "../components/Provider";
|
|
12
13
|
import type { UseCctvRtcStreamParams, UseCctvRtcStreamReturn } from "../types";
|
|
13
14
|
import { cctvRtcLiveRegistryAtom } from "../jotai/rtc";
|
|
14
15
|
import { getIsLive } from "../utils/video-state";
|
|
@@ -39,6 +40,7 @@ export function useCctvRtcStream({
|
|
|
39
40
|
}: UseCctvRtcStreamParams): UseCctvRtcStreamReturn {
|
|
40
41
|
// Provider를 통해 주입된 기본 토큰 발급 URL을 확보한다.
|
|
41
42
|
const { tokenUrl: contextTokenUrl } = useCctvApiUrl();
|
|
43
|
+
const streamRegistry = useCctvRtcStreamRegistry();
|
|
42
44
|
const setLiveRegistry = useSetAtom(cctvRtcLiveRegistryAtom);
|
|
43
45
|
const instanceKeyRef = useRef<string | null>(null);
|
|
44
46
|
if (!instanceKeyRef.current) {
|
|
@@ -47,6 +49,8 @@ export function useCctvRtcStream({
|
|
|
47
49
|
|
|
48
50
|
// WebRTC MediaStream을 연결할 video 요소 ref.
|
|
49
51
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
52
|
+
const activeStreamKeyRef = useRef<string | null>(null);
|
|
53
|
+
const activeStreamIdentityKeyRef = useRef<string | null>(null);
|
|
50
54
|
|
|
51
55
|
// RTCPeerConnectionState를 관찰해 UI에 노출하기 위한 상태값.
|
|
52
56
|
const [connectionState, setConnectionState] =
|
|
@@ -80,71 +84,115 @@ export function useCctvRtcStream({
|
|
|
80
84
|
return `${cam.cam_rtc.replace(/\/$/, "")}/whep${query}`;
|
|
81
85
|
}, [cam?.cam_rtc, username]);
|
|
82
86
|
|
|
87
|
+
const streamKey = useMemo(() => {
|
|
88
|
+
if (!cam?.cam_id || !cam.cam_online || !endpoint || !tokenQuery.data?.token)
|
|
89
|
+
return "";
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
username,
|
|
93
|
+
cam.company_id,
|
|
94
|
+
cam.cam_id,
|
|
95
|
+
endpoint,
|
|
96
|
+
tokenQuery.dataUpdatedAt,
|
|
97
|
+
].join("|");
|
|
98
|
+
}, [
|
|
99
|
+
cam?.cam_id,
|
|
100
|
+
cam?.cam_online,
|
|
101
|
+
cam?.company_id,
|
|
102
|
+
endpoint,
|
|
103
|
+
tokenQuery.data?.token,
|
|
104
|
+
tokenQuery.dataUpdatedAt,
|
|
105
|
+
username,
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const streamIdentityKey = useMemo(() => {
|
|
109
|
+
if (!cam?.cam_id || !endpoint) return "";
|
|
110
|
+
|
|
111
|
+
return [username, cam.company_id, cam.cam_id, endpoint].join("|");
|
|
112
|
+
}, [cam?.cam_id, cam?.company_id, endpoint, username]);
|
|
113
|
+
|
|
83
114
|
// 토큰과 endpoint가 준비되면 WebRTC 스트림을 연결한다.
|
|
84
115
|
useEffect(() => {
|
|
85
116
|
const currentVideo = videoRef.current;
|
|
86
117
|
|
|
87
118
|
// 토큰/카메라 에러로 전환되면 직전 MediaStream이 화면에 남지 않도록 비운다.
|
|
88
119
|
if (tokenQuery.isError || !cam?.cam_online) {
|
|
120
|
+
if (streamIdentityKey) {
|
|
121
|
+
streamRegistry.closeByIdentity(streamIdentityKey);
|
|
122
|
+
}
|
|
89
123
|
if (currentVideo) currentVideo.srcObject = null;
|
|
124
|
+
setConnectionState("new");
|
|
125
|
+
setStreamError(null);
|
|
126
|
+
setStreaming(false);
|
|
90
127
|
return;
|
|
91
128
|
}
|
|
92
129
|
|
|
93
130
|
// 필수 값이 없으면 스트림을 시작하지 않는다.
|
|
94
|
-
if (!tokenQuery.data?.token || !endpoint || !currentVideo)
|
|
95
|
-
|
|
96
|
-
let handle: WhepStreamHandle | null = null;
|
|
131
|
+
if (!streamKey || !tokenQuery.data?.token || !endpoint || !currentVideo)
|
|
132
|
+
return;
|
|
97
133
|
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
if (
|
|
135
|
+
activeStreamIdentityKeyRef.current === streamIdentityKey &&
|
|
136
|
+
activeStreamKeyRef.current !== streamKey
|
|
137
|
+
) {
|
|
138
|
+
streamRegistry.closeByIdentity(streamIdentityKey, streamKey);
|
|
139
|
+
}
|
|
100
140
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
setStreaming(true);
|
|
104
|
-
setConnectionState("connecting");
|
|
141
|
+
activeStreamKeyRef.current = streamKey;
|
|
142
|
+
activeStreamIdentityKeyRef.current = streamIdentityKey;
|
|
105
143
|
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
streamRegistry.start({
|
|
145
|
+
streamKey,
|
|
146
|
+
identityKey: streamIdentityKey,
|
|
108
147
|
endpoint,
|
|
109
148
|
token: tokenQuery.data.token,
|
|
110
149
|
video: currentVideo,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
setStreaming(false);
|
|
131
|
-
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const detachVideo = streamRegistry.attach(streamKey, currentVideo);
|
|
153
|
+
const unsubscribe = streamRegistry.subscribe(streamKey, snapshot => {
|
|
154
|
+
setConnectionState(snapshot.connectionState);
|
|
155
|
+
setStreamError(snapshot.streamError);
|
|
156
|
+
setStreaming(snapshot.isStreaming);
|
|
157
|
+
if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
|
|
158
|
+
currentVideo.srcObject = snapshot.stream;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const snapshot = streamRegistry.getSnapshot(streamKey);
|
|
162
|
+
setConnectionState(snapshot.connectionState);
|
|
163
|
+
setStreamError(snapshot.streamError);
|
|
164
|
+
setStreaming(snapshot.isStreaming);
|
|
165
|
+
|
|
166
|
+
if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
|
|
167
|
+
currentVideo.srcObject = snapshot.stream;
|
|
168
|
+
}
|
|
132
169
|
|
|
133
|
-
// effect cleanup:
|
|
170
|
+
// effect cleanup: current video attach만 해제하고 registry stream은 유지한다.
|
|
134
171
|
return () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
handle = null;
|
|
138
|
-
currentVideo.srcObject = null;
|
|
172
|
+
unsubscribe();
|
|
173
|
+
detachVideo();
|
|
139
174
|
};
|
|
140
175
|
}, [
|
|
141
176
|
endpoint,
|
|
177
|
+
streamIdentityKey,
|
|
178
|
+
streamKey,
|
|
179
|
+
streamRegistry,
|
|
142
180
|
tokenQuery.data?.token,
|
|
143
181
|
tokenQuery.isError,
|
|
144
|
-
cam?.cam_id,
|
|
145
182
|
cam?.cam_online,
|
|
146
183
|
]);
|
|
147
184
|
|
|
185
|
+
const refetchToken = useCallback<typeof tokenQuery.refetch>(
|
|
186
|
+
async options => {
|
|
187
|
+
const result = await tokenQuery.refetch(options);
|
|
188
|
+
if (result.isSuccess && streamIdentityKey) {
|
|
189
|
+
streamRegistry.closeByIdentity(streamIdentityKey);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
},
|
|
193
|
+
[streamIdentityKey, streamRegistry, tokenQuery],
|
|
194
|
+
);
|
|
195
|
+
|
|
148
196
|
const liveState = useMemo(
|
|
149
197
|
() =>
|
|
150
198
|
cam
|
|
@@ -202,6 +250,6 @@ export function useCctvRtcStream({
|
|
|
202
250
|
isStreaming,
|
|
203
251
|
isTokenLoading,
|
|
204
252
|
isTokenError,
|
|
205
|
-
refetchToken
|
|
253
|
+
refetchToken,
|
|
206
254
|
};
|
|
207
255
|
}
|
|
@@ -59,13 +59,17 @@ export interface CctvApiUrlContext {
|
|
|
59
59
|
* CCTV; 실시간 영상보기 컨텍스트
|
|
60
60
|
* @property {string} [company_id] 선택된 업체 id코드
|
|
61
61
|
* @property {string} [cam_id] 선택된 카메라 id코드
|
|
62
|
+
* @property {string} username CCTV 조회시 권한 확인을 위한 계정명
|
|
62
63
|
* @property {string} [search] 검색 키워드 (입력값)
|
|
63
64
|
* @property {string} [filter] 검색 키워드 (필터값)
|
|
64
65
|
* @property {API_Res_CctvCompanyGroup[]} rawData 원본 데이터 배열
|
|
65
66
|
* @property {boolean} isFetching 데이터 로딩 상태
|
|
66
67
|
* @property {boolean} isError 데이터 에러 상태
|
|
67
68
|
*/
|
|
68
|
-
export interface
|
|
69
|
+
export interface CctvBaseContext extends CctvSelectedContext {
|
|
70
|
+
/**
|
|
71
|
+
* CCTV 조회시 권한 확인을 위한 계정명
|
|
72
|
+
*/
|
|
69
73
|
username: string;
|
|
70
74
|
/**
|
|
71
75
|
* 검색 키워드 (입력값)
|
|
@@ -79,21 +83,56 @@ export interface CctvContext extends CctvSelectedContext {
|
|
|
79
83
|
* 원본 데이터 배열
|
|
80
84
|
*/
|
|
81
85
|
rawData: API_Res_CctvCompanyGroup[];
|
|
86
|
+
/**
|
|
87
|
+
* 데이터 로딩 상태
|
|
88
|
+
*/
|
|
82
89
|
isFetching: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* 데이터 에러 상태
|
|
92
|
+
*/
|
|
83
93
|
isError: boolean;
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
/**
|
|
97
|
+
* CCTV; Provider form context
|
|
98
|
+
* @template ContextExtension
|
|
99
|
+
* @typedef {CctvBaseContext & ContextExtension} CctvContext
|
|
100
|
+
* @property {string} [company_id] 선택된 업체 id코드
|
|
101
|
+
* @property {string} [cam_id] 선택된 카메라 id코드
|
|
102
|
+
* @property {string} username CCTV 조회시 권한 확인을 위한 계정명
|
|
103
|
+
* @property {string} search 검색 키워드 입력값
|
|
104
|
+
* @property {string} filter 검색 키워드 필터값
|
|
105
|
+
* @property {API_Res_CctvCompanyGroup[]} rawData 원본 데이터 배열
|
|
106
|
+
* @property {boolean} isFetching 데이터 로딩 상태
|
|
107
|
+
* @property {boolean} isError 데이터 에러 상태
|
|
108
|
+
*/
|
|
109
|
+
export type CctvContext<ContextExtension extends object = object> =
|
|
110
|
+
CctvBaseContext & ContextExtension;
|
|
111
|
+
|
|
86
112
|
/**
|
|
87
113
|
* CCTV; Provider props
|
|
114
|
+
* @template ContextExtension
|
|
88
115
|
* @property {string} [company_id] 선택된 업체 id코드
|
|
89
116
|
* @property {string} [cam_id] 선택된 카메라 id코드
|
|
90
117
|
* @property {string} [listUrl] company-list API 요청 경로
|
|
91
118
|
* @property {string} [tokenUrl] token API 요청 경로
|
|
92
119
|
* @property {string} [username] CCTV 조회시 권한 확인을 위한 계정명
|
|
120
|
+
* @property {ContextExtension} [defaultValues] 서비스 확장 context 기본값
|
|
93
121
|
* @property {React.ReactNode} children
|
|
94
122
|
*/
|
|
95
|
-
export type CctvProviderProps =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
export type CctvProviderProps<ContextExtension extends object = object> =
|
|
124
|
+
CctvSelectedContext &
|
|
125
|
+
CctvApiUrlContext & {
|
|
126
|
+
/**
|
|
127
|
+
* CCTV 조회시 권한 확인을 위한 계정명
|
|
128
|
+
*/
|
|
129
|
+
username?: string;
|
|
130
|
+
/**
|
|
131
|
+
* 서비스 확장 context 기본값
|
|
132
|
+
*/
|
|
133
|
+
defaultValues?: ContextExtension;
|
|
134
|
+
/**
|
|
135
|
+
* Provider 하위 콘텐츠
|
|
136
|
+
*/
|
|
137
|
+
children: React.ReactNode;
|
|
138
|
+
};
|
package/src/cctv/types/hook.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
CctvCompanyGroupItems,
|
|
8
8
|
} from "./list";
|
|
9
9
|
import type { UseFormReturn } from "react-hook-form";
|
|
10
|
-
import type {
|
|
10
|
+
import type { CctvBaseContext } from "./context";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* CCTV; useCctvRtcStream params
|
|
@@ -135,7 +135,7 @@ export interface UseCctvCompanyDataReturn extends Omit<
|
|
|
135
135
|
* @property {(cam_id: string) => void} onOpenCamera 카메라 선택 함수
|
|
136
136
|
* @property {() => void} onCloseCamera 카메라 선택 해제 함수
|
|
137
137
|
*/
|
|
138
|
-
export interface UseCctvContextReturn extends UseFormReturn<
|
|
138
|
+
export interface UseCctvContextReturn extends UseFormReturn<CctvBaseContext> {
|
|
139
139
|
/**
|
|
140
140
|
* 데이터 로딩 중 상태
|
|
141
141
|
*/
|