@uniai-fe/uds-templates 0.5.16 → 0.5.17
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/components/cam-list/Item.tsx +7 -1
- package/src/cctv/components/pagination/list/Item.tsx +7 -1
- package/src/cctv/components/video/Template.tsx +6 -0
- package/src/cctv/components/viewer/desktop/Video.tsx +7 -1
- package/src/cctv/hooks/useRtcStream.ts +71 -4
- package/src/cctv/types/hook.ts +23 -2
- package/src/cctv/types/props.ts +21 -0
- package/src/cctv/types/video-state.ts +10 -0
- package/src/cctv/utils/video-state.ts +6 -0
package/package.json
CHANGED
|
@@ -57,7 +57,13 @@ export default function CCTVCamListItem({
|
|
|
57
57
|
footerOptions={{ activeTitle: true, activeOpenButton: true, cam }}
|
|
58
58
|
// 변경 설명: list item custom overlay는 template seam 하나만 따라간다.
|
|
59
59
|
renderOverlay={renderOverlay}
|
|
60
|
-
{...{
|
|
60
|
+
{...{
|
|
61
|
+
isError,
|
|
62
|
+
overlayMessage,
|
|
63
|
+
isLive,
|
|
64
|
+
canReconnect: rtcCtx.canReconnect,
|
|
65
|
+
reconnectStream: rtcCtx.reconnectStream,
|
|
66
|
+
}}
|
|
61
67
|
/>
|
|
62
68
|
</li>
|
|
63
69
|
);
|
|
@@ -59,7 +59,13 @@ export default function CCTVPaginationListItem({
|
|
|
59
59
|
title: cam.cam_name,
|
|
60
60
|
}}
|
|
61
61
|
footerOptions={{ cam }}
|
|
62
|
-
{...{
|
|
62
|
+
{...{
|
|
63
|
+
isError,
|
|
64
|
+
overlayMessage,
|
|
65
|
+
isLive,
|
|
66
|
+
canReconnect: rtcCtx.canReconnect,
|
|
67
|
+
reconnectStream: rtcCtx.reconnectStream,
|
|
68
|
+
}}
|
|
63
69
|
/>
|
|
64
70
|
</button>
|
|
65
71
|
</li>
|
|
@@ -18,6 +18,8 @@ import type { CctvVideoTemplateProps } from "../../types/props";
|
|
|
18
18
|
* @property {React.ReactNode} [overlayMessage]
|
|
19
19
|
* @property {boolean} [isLive]
|
|
20
20
|
* @property {CctvCompanyCameraData} [cam]
|
|
21
|
+
* @property {boolean} [canReconnect]
|
|
22
|
+
* @property {CctvRtcReconnectTrigger} [reconnectStream]
|
|
21
23
|
* @property {CctvVideoRenderOverlay} [renderOverlay]
|
|
22
24
|
* @property {React.Ref<HTMLVideoElement>} ref
|
|
23
25
|
*/
|
|
@@ -31,6 +33,8 @@ const CCTVVideoTemplate = forwardRef<HTMLVideoElement, CctvVideoTemplateProps>(
|
|
|
31
33
|
isError,
|
|
32
34
|
isLive,
|
|
33
35
|
overlayMessage,
|
|
36
|
+
canReconnect,
|
|
37
|
+
reconnectStream,
|
|
34
38
|
renderOverlay,
|
|
35
39
|
},
|
|
36
40
|
ref,
|
|
@@ -56,6 +60,8 @@ const CCTVVideoTemplate = forwardRef<HTMLVideoElement, CctvVideoTemplateProps>(
|
|
|
56
60
|
isError,
|
|
57
61
|
isLive,
|
|
58
62
|
overlayMessage,
|
|
63
|
+
canReconnect,
|
|
64
|
+
reconnectStream,
|
|
59
65
|
})
|
|
60
66
|
: defaultOverlay;
|
|
61
67
|
|
|
@@ -57,7 +57,13 @@ export default function CCTVViewerDesktopVideo({
|
|
|
57
57
|
footerOptions={{ activeTitle: false }}
|
|
58
58
|
// 변경 설명: viewer video는 service custom overlay를 직접 수용하는 첫 public entry다.
|
|
59
59
|
renderOverlay={renderOverlay}
|
|
60
|
-
{...{
|
|
60
|
+
{...{
|
|
61
|
+
isError,
|
|
62
|
+
overlayMessage,
|
|
63
|
+
isLive,
|
|
64
|
+
canReconnect: rtcCtx.canReconnect,
|
|
65
|
+
reconnectStream: rtcCtx.reconnectStream,
|
|
66
|
+
}}
|
|
61
67
|
/>
|
|
62
68
|
);
|
|
63
69
|
}
|
|
@@ -10,11 +10,17 @@ import {
|
|
|
10
10
|
useCctvApiUrl,
|
|
11
11
|
useCctvRtcStreamRegistry,
|
|
12
12
|
} from "../components/Provider";
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
CctvRtcReconnectTrigger,
|
|
15
|
+
UseCctvRtcStreamParams,
|
|
16
|
+
UseCctvRtcStreamReturn,
|
|
17
|
+
} from "../types";
|
|
14
18
|
import { cctvRtcLiveRegistryAtom } from "../jotai/rtc";
|
|
15
19
|
import { getIsLive } from "../utils/video-state";
|
|
16
20
|
import { useFormContext, useWatch } from "react-hook-form";
|
|
17
21
|
|
|
22
|
+
const AUTO_RECONNECT_INTERVAL_MS = 3000;
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* CCTV 영상 스트림을 WebRTC로 연결하는 커스텀 훅.
|
|
20
26
|
* @hook
|
|
@@ -31,7 +37,9 @@ import { useFormContext, useWatch } from "react-hook-form";
|
|
|
31
37
|
* isStreaming, // 현재 스트림 연결 절차가 진행 중인지 여부
|
|
32
38
|
* isTokenLoading, // 토큰 발급 요청이 진행 중인지 여부
|
|
33
39
|
* isTokenError, // 토큰 발급 요청이 실패했는지 여부
|
|
40
|
+
* canReconnect, // 재연결 가능한 종료 상태 여부
|
|
34
41
|
* refetchToken, // 토큰 발급을 재시도하는 함수
|
|
42
|
+
* reconnectStream, // 재연결 trigger 함수
|
|
35
43
|
* }
|
|
36
44
|
*/
|
|
37
45
|
export function useCctvRtcStream({
|
|
@@ -51,6 +59,7 @@ export function useCctvRtcStream({
|
|
|
51
59
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
52
60
|
const activeStreamKeyRef = useRef<string | null>(null);
|
|
53
61
|
const activeStreamIdentityKeyRef = useRef<string | null>(null);
|
|
62
|
+
const lastAutoReconnectAtRef = useRef(0);
|
|
54
63
|
|
|
55
64
|
// RTCPeerConnectionState를 관찰해 UI에 노출하기 위한 상태값.
|
|
56
65
|
const [connectionState, setConnectionState] =
|
|
@@ -61,6 +70,7 @@ export function useCctvRtcStream({
|
|
|
61
70
|
|
|
62
71
|
// 현재 스트림 연결 절차가 진행 중인지 여부.
|
|
63
72
|
const [isStreaming, setStreaming] = useState(false);
|
|
73
|
+
const [hasConnected, setHasConnected] = useState(false);
|
|
64
74
|
|
|
65
75
|
// react-hook-form 컨텍스트에서 username을 추적한다.
|
|
66
76
|
const { control } = useFormContext();
|
|
@@ -74,6 +84,7 @@ export function useCctvRtcStream({
|
|
|
74
84
|
url: tokenUrl ?? contextTokenUrl,
|
|
75
85
|
});
|
|
76
86
|
|
|
87
|
+
const { refetch: refetchRtcToken } = tokenQuery;
|
|
77
88
|
const isTokenLoading = tokenQuery.isFetching;
|
|
78
89
|
const isTokenError = tokenQuery.isError;
|
|
79
90
|
|
|
@@ -111,6 +122,10 @@ export function useCctvRtcStream({
|
|
|
111
122
|
return [username, cam.company_id, cam.cam_id, endpoint].join("|");
|
|
112
123
|
}, [cam?.cam_id, cam?.company_id, endpoint, username]);
|
|
113
124
|
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
setHasConnected(false);
|
|
127
|
+
}, [streamIdentityKey]);
|
|
128
|
+
|
|
114
129
|
// 토큰과 endpoint가 준비되면 WebRTC 스트림을 연결한다.
|
|
115
130
|
useEffect(() => {
|
|
116
131
|
const currentVideo = videoRef.current;
|
|
@@ -154,6 +169,7 @@ export function useCctvRtcStream({
|
|
|
154
169
|
setConnectionState(snapshot.connectionState);
|
|
155
170
|
setStreamError(snapshot.streamError);
|
|
156
171
|
setStreaming(snapshot.isStreaming);
|
|
172
|
+
if (snapshot.connectionState === "connected") setHasConnected(true);
|
|
157
173
|
if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
|
|
158
174
|
currentVideo.srcObject = snapshot.stream;
|
|
159
175
|
}
|
|
@@ -162,6 +178,7 @@ export function useCctvRtcStream({
|
|
|
162
178
|
setConnectionState(snapshot.connectionState);
|
|
163
179
|
setStreamError(snapshot.streamError);
|
|
164
180
|
setStreaming(snapshot.isStreaming);
|
|
181
|
+
if (snapshot.connectionState === "connected") setHasConnected(true);
|
|
165
182
|
|
|
166
183
|
if (snapshot.stream && currentVideo.srcObject !== snapshot.stream) {
|
|
167
184
|
currentVideo.srcObject = snapshot.stream;
|
|
@@ -182,17 +199,65 @@ export function useCctvRtcStream({
|
|
|
182
199
|
cam?.cam_online,
|
|
183
200
|
]);
|
|
184
201
|
|
|
185
|
-
const
|
|
202
|
+
const reconnectStream = useCallback<CctvRtcReconnectTrigger>(
|
|
186
203
|
async options => {
|
|
187
|
-
const result = await
|
|
204
|
+
const result = await refetchRtcToken(options);
|
|
188
205
|
if (result.isSuccess && streamIdentityKey) {
|
|
189
206
|
streamRegistry.closeByIdentity(streamIdentityKey);
|
|
190
207
|
}
|
|
191
208
|
return result;
|
|
192
209
|
},
|
|
193
|
-
[streamIdentityKey, streamRegistry
|
|
210
|
+
[refetchRtcToken, streamIdentityKey, streamRegistry],
|
|
194
211
|
);
|
|
195
212
|
|
|
213
|
+
const canReconnect = useMemo(() => {
|
|
214
|
+
if (!cam?.cam_online) return false;
|
|
215
|
+
if (!hasConnected) return false;
|
|
216
|
+
if (!streamIdentityKey) return false;
|
|
217
|
+
if (isTokenLoading || isStreaming) return false;
|
|
218
|
+
if (isTokenError || streamError) return true;
|
|
219
|
+
if (connectionState === "failed") return true;
|
|
220
|
+
if (connectionState === "disconnected") return true;
|
|
221
|
+
if (connectionState === "closed") return true;
|
|
222
|
+
return false;
|
|
223
|
+
}, [
|
|
224
|
+
cam?.cam_online,
|
|
225
|
+
connectionState,
|
|
226
|
+
hasConnected,
|
|
227
|
+
isStreaming,
|
|
228
|
+
isTokenError,
|
|
229
|
+
isTokenLoading,
|
|
230
|
+
streamError,
|
|
231
|
+
streamIdentityKey,
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const refetchToken = reconnectStream;
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
238
|
+
return;
|
|
239
|
+
|
|
240
|
+
const reconnectOnFocus = () => {
|
|
241
|
+
if (document.visibilityState !== "visible") return;
|
|
242
|
+
if (!canReconnect) return;
|
|
243
|
+
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
if (now - lastAutoReconnectAtRef.current < AUTO_RECONNECT_INTERVAL_MS)
|
|
246
|
+
return;
|
|
247
|
+
|
|
248
|
+
lastAutoReconnectAtRef.current = now;
|
|
249
|
+
void reconnectStream();
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
window.addEventListener("focus", reconnectOnFocus);
|
|
253
|
+
document.addEventListener("visibilitychange", reconnectOnFocus);
|
|
254
|
+
|
|
255
|
+
return () => {
|
|
256
|
+
window.removeEventListener("focus", reconnectOnFocus);
|
|
257
|
+
document.removeEventListener("visibilitychange", reconnectOnFocus);
|
|
258
|
+
};
|
|
259
|
+
}, [canReconnect, reconnectStream]);
|
|
260
|
+
|
|
196
261
|
const liveState = useMemo(
|
|
197
262
|
() =>
|
|
198
263
|
cam
|
|
@@ -257,6 +322,8 @@ export function useCctvRtcStream({
|
|
|
257
322
|
isStreaming,
|
|
258
323
|
isTokenLoading,
|
|
259
324
|
isTokenError,
|
|
325
|
+
canReconnect,
|
|
260
326
|
refetchToken,
|
|
327
|
+
reconnectStream,
|
|
261
328
|
};
|
|
262
329
|
}
|
package/src/cctv/types/hook.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
QueryObserverResult,
|
|
3
|
+
RefetchOptions,
|
|
4
|
+
UseQueryResult,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
2
6
|
import type { API_Res_CctvCompany, API_Res_CctvRtcToken } from "./api";
|
|
3
7
|
import type {
|
|
4
8
|
CctvCompanyCameraData,
|
|
@@ -9,6 +13,13 @@ import type {
|
|
|
9
13
|
import type { UseFormReturn } from "react-hook-form";
|
|
10
14
|
import type { CctvBaseContext } from "./context";
|
|
11
15
|
|
|
16
|
+
/**
|
|
17
|
+
* CCTV; RTC stream 재연결 trigger
|
|
18
|
+
*/
|
|
19
|
+
export type CctvRtcReconnectTrigger = (
|
|
20
|
+
options?: RefetchOptions,
|
|
21
|
+
) => Promise<QueryObserverResult<API_Res_CctvRtcToken>>;
|
|
22
|
+
|
|
12
23
|
/**
|
|
13
24
|
* CCTV; useCctvRtcStream params
|
|
14
25
|
* @property {CctvCompanyCameraData} [cam] 스트리밍 카메라 정보
|
|
@@ -77,17 +88,27 @@ export interface UseCctvRtcStreamState extends UseCctvRtcStreamError {
|
|
|
77
88
|
* @property {boolean} isStreaming startWhepStream 진행 여부
|
|
78
89
|
* @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
|
|
79
90
|
* @property {boolean} isTokenError 토큰 발급 실패 여부
|
|
91
|
+
* @property {boolean} canReconnect 재연결 가능한 종료 상태 여부
|
|
80
92
|
* @property {() => Promise<void>} refetchToken 토큰 재발급 함수
|
|
93
|
+
* @property {() => Promise<QueryObserverResult<API_Res_CctvRtcToken>>} reconnectStream 재연결 trigger 함수
|
|
81
94
|
*/
|
|
82
95
|
export interface UseCctvRtcStreamReturn extends UseCctvRtcStreamState {
|
|
83
96
|
/**
|
|
84
97
|
* <video /> ref
|
|
85
98
|
*/
|
|
86
99
|
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
100
|
+
/**
|
|
101
|
+
* 재연결 가능한 종료 상태 여부
|
|
102
|
+
*/
|
|
103
|
+
canReconnect: boolean;
|
|
87
104
|
/**
|
|
88
105
|
* 토큰 재발급 함수
|
|
89
106
|
*/
|
|
90
|
-
refetchToken:
|
|
107
|
+
refetchToken: CctvRtcReconnectTrigger;
|
|
108
|
+
/**
|
|
109
|
+
* 재연결 trigger 함수
|
|
110
|
+
*/
|
|
111
|
+
reconnectStream: CctvRtcReconnectTrigger;
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
/**
|
package/src/cctv/types/props.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CctvCompanyCameraData, CctvCompanyCameraList } from "./list";
|
|
2
|
+
import type { CctvRtcReconnectTrigger } from "./hook";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* CCTV; custom overlay render context
|
|
@@ -8,6 +9,8 @@ import type { CctvCompanyCameraData, CctvCompanyCameraList } from "./list";
|
|
|
8
9
|
* @property {boolean | undefined} [isError] 영상 에러 상태 여부
|
|
9
10
|
* @property {boolean | undefined} [isLive] 영상 live 상태 여부
|
|
10
11
|
* @property {React.ReactNode} [overlayMessage] 기본 안내/에러 메시지 콘텐츠
|
|
12
|
+
* @property {boolean | undefined} [canReconnect] 재연결 가능한 종료 상태 여부
|
|
13
|
+
* @property {CctvRtcReconnectTrigger | undefined} [reconnectStream] 재연결 trigger 함수
|
|
11
14
|
*/
|
|
12
15
|
export interface CctvVideoRenderOverlayContext {
|
|
13
16
|
/**
|
|
@@ -34,6 +37,14 @@ export interface CctvVideoRenderOverlayContext {
|
|
|
34
37
|
* 기본 안내/에러 메시지 콘텐츠
|
|
35
38
|
*/
|
|
36
39
|
overlayMessage?: React.ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* 재연결 가능한 종료 상태 여부
|
|
42
|
+
*/
|
|
43
|
+
canReconnect?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* 재연결 trigger 함수
|
|
46
|
+
*/
|
|
47
|
+
reconnectStream?: CctvRtcReconnectTrigger;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
@@ -115,6 +126,8 @@ export interface CctvVideoOverlayFooterProps {
|
|
|
115
126
|
* @property {boolean} [isError] 에러 상태 여부
|
|
116
127
|
* @property {boolean} [isLive] 영상이 정상적으로 재생 중인지 여부
|
|
117
128
|
* @property {React.ReactNode} [overlayMessage] 에러/안내 메시지 콘텐츠
|
|
129
|
+
* @property {boolean} [canReconnect] 재연결 가능한 종료 상태 여부
|
|
130
|
+
* @property {CctvRtcReconnectTrigger} [reconnectStream] 재연결 trigger 함수
|
|
118
131
|
*/
|
|
119
132
|
export interface CctvVideoStateProps {
|
|
120
133
|
/**
|
|
@@ -129,6 +142,14 @@ export interface CctvVideoStateProps {
|
|
|
129
142
|
* 오버레이 메시지 콘텐츠
|
|
130
143
|
*/
|
|
131
144
|
overlayMessage?: React.ReactNode;
|
|
145
|
+
/**
|
|
146
|
+
* 재연결 가능한 종료 상태 여부
|
|
147
|
+
*/
|
|
148
|
+
canReconnect?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* 재연결 trigger 함수
|
|
151
|
+
*/
|
|
152
|
+
reconnectStream?: CctvRtcReconnectTrigger;
|
|
132
153
|
}
|
|
133
154
|
|
|
134
155
|
/**
|
|
@@ -11,6 +11,7 @@ import type { UseCctvRtcStreamError, UseCctvRtcStreamState } from "./hook";
|
|
|
11
11
|
* @property {boolean} isTokenLoading 토큰 발급 요청 진행 여부
|
|
12
12
|
* @property {boolean} isTokenError 토큰 발급 실패 여부
|
|
13
13
|
* @property {string | null} streamError 스트림 오류 메시지
|
|
14
|
+
* @property {boolean} [canReconnect] 재연결 가능한 종료 상태 여부
|
|
14
15
|
*/
|
|
15
16
|
export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
|
|
16
17
|
/**
|
|
@@ -25,6 +26,10 @@ export interface CctvVideoOverlayMessageParams extends UseCctvRtcStreamState {
|
|
|
25
26
|
* 서버에서 cam 리스트를 가져오는 중인지 여부
|
|
26
27
|
*/
|
|
27
28
|
isFetching?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* 재연결 가능한 종료 상태 여부
|
|
31
|
+
*/
|
|
32
|
+
canReconnect?: boolean;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -48,10 +53,15 @@ export interface CctvVideoLiveParams extends UseCctvRtcStreamState {
|
|
|
48
53
|
* @property {CctvCompanyCameraData} [cam] 카메라 데이터
|
|
49
54
|
* @property {boolean} isTokenError 토큰 발급 실패 여부
|
|
50
55
|
* @property {string | null} streamError 스트림 오류 메시지
|
|
56
|
+
* @property {boolean} [canReconnect] 재연결 가능한 종료 상태 여부
|
|
51
57
|
*/
|
|
52
58
|
export interface CctvVideoErrorParams extends UseCctvRtcStreamError {
|
|
53
59
|
/**
|
|
54
60
|
* 카메라 데이터
|
|
55
61
|
*/
|
|
56
62
|
cam?: CctvCompanyCameraData;
|
|
63
|
+
/**
|
|
64
|
+
* 재연결 가능한 종료 상태 여부
|
|
65
|
+
*/
|
|
66
|
+
canReconnect?: boolean;
|
|
57
67
|
}
|
|
@@ -8,6 +8,8 @@ export const CCTV_MESSAGE = {
|
|
|
8
8
|
selectCam: "카메라를 선택하세요.",
|
|
9
9
|
fetching: "CCTV 데이터를 불러오는 중입니다.",
|
|
10
10
|
preparing: "스트림을 준비하고 있습니다.",
|
|
11
|
+
sessionEnded:
|
|
12
|
+
"장시간 미사용으로 연결이 종료되었습니다. 계속 확인하시려면 다시 연결해 주세요.",
|
|
11
13
|
tokenError: "토큰을 발급하지 못했습니다.",
|
|
12
14
|
offline: "CCTV 연결 오류",
|
|
13
15
|
} as const;
|
|
@@ -34,6 +36,7 @@ const RTC_PREPARING_STATES = new Set<RTCPeerConnectionState>([
|
|
|
34
36
|
*/
|
|
35
37
|
export function getOverlayMessage({
|
|
36
38
|
cam,
|
|
39
|
+
canReconnect,
|
|
37
40
|
connectionState,
|
|
38
41
|
hasCamProp = false,
|
|
39
42
|
isFetching = false,
|
|
@@ -48,6 +51,7 @@ export function getOverlayMessage({
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
if (!cam.cam_online) return CCTV_MESSAGE.offline;
|
|
54
|
+
if (canReconnect) return CCTV_MESSAGE.sessionEnded;
|
|
51
55
|
// 에러 상태가 준비 상태와 겹칠 때는 에러 문구가 최종 표시 계약을 우선한다.
|
|
52
56
|
if (isTokenError) return CCTV_MESSAGE.tokenError;
|
|
53
57
|
if (streamError) return streamError;
|
|
@@ -95,11 +99,13 @@ export function getIsLive({
|
|
|
95
99
|
*/
|
|
96
100
|
export function getIsError({
|
|
97
101
|
cam,
|
|
102
|
+
canReconnect,
|
|
98
103
|
isTokenError,
|
|
99
104
|
streamError,
|
|
100
105
|
}: CctvVideoErrorParams): boolean {
|
|
101
106
|
if (!cam) return true;
|
|
102
107
|
if (!cam.cam_online) return true;
|
|
108
|
+
if (canReconnect) return true;
|
|
103
109
|
if (isTokenError) return true;
|
|
104
110
|
if (streamError) return true;
|
|
105
111
|
return false;
|