@apocaliss92/scrypted-reolink-native 0.3.17 → 0.4.0
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/.vscode/settings.json +1 -1
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +863 -792
- package/src/camera.ts +3897 -2790
- package/src/intercom.ts +496 -476
- package/src/main.ts +378 -409
- package/src/multiFocal.ts +297 -265
- package/src/nvr.ts +588 -477
- package/src/stream-utils.ts +478 -427
- package/src/utils.ts +384 -1009
package/src/baichuan-base.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BaichuanClientOptions,
|
|
3
|
+
ReolinkBaichuanApi,
|
|
4
|
+
ReolinkSimpleEvent,
|
|
5
|
+
} from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
6
|
import { ScryptedDeviceBase } from "@scrypted/sdk";
|
|
3
7
|
import { createBaichuanApi, type BaichuanTransport } from "./connect";
|
|
4
8
|
import { StreamManager } from "./stream-utils";
|
|
5
9
|
|
|
6
10
|
export interface BaichuanConnectionConfig {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
host: string;
|
|
12
|
+
username: string;
|
|
13
|
+
password: string;
|
|
14
|
+
uid?: string;
|
|
15
|
+
transport: BaichuanTransport;
|
|
16
|
+
debugOptions?: any;
|
|
17
|
+
udpDiscoveryMethod?: BaichuanClientOptions["udpDiscoveryMethod"];
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export interface BaichuanConnectionCallbacks {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
onError?: (err: unknown) => void;
|
|
22
|
+
onClose?: () => void | Promise<void>;
|
|
23
|
+
onSimpleEvent?: (ev: ReolinkSimpleEvent) => void;
|
|
24
|
+
getEventSubscriptionEnabled?: () => boolean;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -25,134 +29,138 @@ export interface BaichuanConnectionCallbacks {
|
|
|
25
29
|
* Implements Console interface to be compatible with Baichuan API
|
|
26
30
|
*/
|
|
27
31
|
export class BaichuanLogger implements Console {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
32
|
+
private baseLogger: Console;
|
|
33
|
+
private deviceName: string;
|
|
34
|
+
private isDebugEnabledCallback: () => boolean;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
baseLogger: Console,
|
|
38
|
+
deviceName: string,
|
|
39
|
+
isDebugEnabledCallback: () => boolean,
|
|
40
|
+
) {
|
|
41
|
+
this.baseLogger = baseLogger;
|
|
42
|
+
this.deviceName = deviceName;
|
|
43
|
+
this.isDebugEnabledCallback = isDebugEnabledCallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private formatMessage(level: string, ...args: any[]): string {
|
|
47
|
+
const timestamp = new Date().toLocaleString();
|
|
48
|
+
const prefix = `[${this.deviceName}] [${timestamp}] [${level}]`;
|
|
49
|
+
return `${prefix} ${args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" ")}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log(...args: any[]): void {
|
|
53
|
+
this.baseLogger.log(this.formatMessage("LOG", ...args));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
error(...args: any[]): void {
|
|
57
|
+
this.baseLogger.error(this.formatMessage("ERROR", ...args));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
warn(...args: any[]): void {
|
|
61
|
+
this.baseLogger.warn(this.formatMessage("WARN", ...args));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
debug(...args: any[]): void {
|
|
65
|
+
if (this.isDebugEnabledCallback()) {
|
|
66
|
+
this.baseLogger.debug(this.formatMessage("DEBUG", ...args));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Console interface implementation - delegate to baseLogger
|
|
71
|
+
assert(condition?: boolean, ...data: any[]): void {
|
|
72
|
+
this.baseLogger.assert(condition, ...data);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clear(): void {
|
|
76
|
+
this.baseLogger.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
count(label?: string): void {
|
|
80
|
+
this.baseLogger.count(label);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
countReset(label?: string): void {
|
|
84
|
+
this.baseLogger.countReset(label);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dir(item?: any, options?: any): void {
|
|
88
|
+
this.baseLogger.dir(item, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dirxml(...data: any[]): void {
|
|
92
|
+
this.baseLogger.dirxml(...data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
group(...data: any[]): void {
|
|
96
|
+
this.baseLogger.group(...data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
groupCollapsed(...data: any[]): void {
|
|
100
|
+
this.baseLogger.groupCollapsed(...data);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
groupEnd(): void {
|
|
104
|
+
this.baseLogger.groupEnd();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
info(...data: any[]): void {
|
|
108
|
+
this.baseLogger.info(this.formatMessage("INFO", ...data));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
table(tabularData?: any, properties?: string[]): void {
|
|
112
|
+
this.baseLogger.table(tabularData, properties);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
time(label?: string): void {
|
|
116
|
+
this.baseLogger.time(label);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
timeEnd(label?: string): void {
|
|
120
|
+
this.baseLogger.timeEnd(label);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
timeLog(label?: string, ...data: any[]): void {
|
|
124
|
+
this.baseLogger.timeLog(label, ...data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
trace(...data: any[]): void {
|
|
128
|
+
this.baseLogger.trace(...data);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Console properties
|
|
132
|
+
get memory(): any {
|
|
133
|
+
return (this.baseLogger as any).memory;
|
|
134
|
+
}
|
|
86
135
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
groupCollapsed(...data: any[]): void {
|
|
92
|
-
this.baseLogger.groupCollapsed(...data);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
groupEnd(): void {
|
|
96
|
-
this.baseLogger.groupEnd();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
info(...data: any[]): void {
|
|
100
|
-
this.baseLogger.info(this.formatMessage('INFO', ...data));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
table(tabularData?: any, properties?: string[]): void {
|
|
104
|
-
this.baseLogger.table(tabularData, properties);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
time(label?: string): void {
|
|
108
|
-
this.baseLogger.time(label);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
timeEnd(label?: string): void {
|
|
112
|
-
this.baseLogger.timeEnd(label);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
timeLog(label?: string, ...data: any[]): void {
|
|
116
|
-
this.baseLogger.timeLog(label, ...data);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
trace(...data: any[]): void {
|
|
120
|
-
this.baseLogger.trace(...data);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Console properties
|
|
124
|
-
get memory(): any {
|
|
125
|
-
return (this.baseLogger as any).memory;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
get Console(): any {
|
|
129
|
-
return (this.baseLogger as any).Console;
|
|
130
|
-
}
|
|
136
|
+
get Console(): any {
|
|
137
|
+
return (this.baseLogger as any).Console;
|
|
138
|
+
}
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
140
|
+
// Node.js specific
|
|
141
|
+
profile(label?: string): void {
|
|
142
|
+
if (typeof (this.baseLogger as any).profile === "function") {
|
|
143
|
+
(this.baseLogger as any).profile(label);
|
|
137
144
|
}
|
|
145
|
+
}
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
147
|
+
profileEnd(label?: string): void {
|
|
148
|
+
if (typeof (this.baseLogger as any).profileEnd === "function") {
|
|
149
|
+
(this.baseLogger as any).profileEnd(label);
|
|
143
150
|
}
|
|
151
|
+
}
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
153
|
+
timeStamp(label?: string): void {
|
|
154
|
+
if (typeof (this.baseLogger as any).timeStamp === "function") {
|
|
155
|
+
(this.baseLogger as any).timeStamp(label);
|
|
149
156
|
}
|
|
157
|
+
}
|
|
150
158
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
159
|
+
context(...data: any[]): void {
|
|
160
|
+
if (typeof (this.baseLogger as any).context === "function") {
|
|
161
|
+
(this.baseLogger as any).context(...data);
|
|
155
162
|
}
|
|
163
|
+
}
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
/**
|
|
@@ -160,712 +168,775 @@ export class BaichuanLogger implements Console {
|
|
|
160
168
|
* listener management, and event subscription handling.
|
|
161
169
|
*/
|
|
162
170
|
export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
171
|
+
protected baichuanApi: ReolinkBaichuanApi | undefined;
|
|
172
|
+
protected ensureClientPromise: Promise<ReolinkBaichuanApi> | undefined;
|
|
173
|
+
protected connectionTime: number | undefined;
|
|
174
|
+
transport: BaichuanTransport;
|
|
175
|
+
|
|
176
|
+
constructor(nativeId: string, transport: BaichuanTransport) {
|
|
177
|
+
super(nativeId);
|
|
178
|
+
this.transport = transport;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private errorListener?: (err: unknown) => void;
|
|
182
|
+
private closeListener?: () => void;
|
|
183
|
+
private lastDisconnectTime: number = 0;
|
|
184
|
+
private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
|
|
185
|
+
private eventSubscriptionActive: boolean = false;
|
|
186
|
+
private lastEventTime: number = 0;
|
|
187
|
+
private pingInterval?: NodeJS.Timeout;
|
|
188
|
+
private autoRenewInterval?: NodeJS.Timeout;
|
|
189
|
+
private eventCheckInterval?: NodeJS.Timeout;
|
|
190
|
+
private consecutivePingFailures: number = 0;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the connection configuration for this instance
|
|
194
|
+
*/
|
|
195
|
+
protected abstract getConnectionConfig(): BaichuanConnectionConfig;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get callbacks for connection events
|
|
199
|
+
*/
|
|
200
|
+
protected abstract getConnectionCallbacks(): BaichuanConnectionCallbacks;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if debug logging is enabled
|
|
204
|
+
*/
|
|
205
|
+
protected abstract isDebugEnabled(): boolean;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the device name for logging
|
|
209
|
+
*/
|
|
210
|
+
protected abstract getDeviceName(): string;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get StreamManager if available (optional, only for devices that support streaming)
|
|
214
|
+
* Override in subclasses that have a StreamManager
|
|
215
|
+
*/
|
|
216
|
+
protected getStreamManager?(): StreamManager | undefined;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get a Baichuan logger instance with formatting and debug control
|
|
220
|
+
* This logger implements Console interface and can be used everywhere
|
|
221
|
+
*/
|
|
222
|
+
public getBaichuanLogger(): BaichuanLogger {
|
|
223
|
+
return new BaichuanLogger(this.console, this.getDeviceName(), () =>
|
|
224
|
+
this.isDebugEnabled(),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Cleanup any additional resources (called before closing connection)
|
|
230
|
+
*/
|
|
231
|
+
protected async onBeforeCleanup(): Promise<void> {
|
|
232
|
+
// Override in subclasses if needed
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Ensure Baichuan client is connected and ready
|
|
237
|
+
*/
|
|
238
|
+
async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
|
|
239
|
+
// Prevent concurrent login storms - check promise first
|
|
240
|
+
if (this.ensureClientPromise) return await this.ensureClientPromise;
|
|
241
|
+
|
|
242
|
+
// Reuse existing client if socket is still connected and logged in
|
|
243
|
+
// Check this AFTER checking the promise to avoid race conditions
|
|
244
|
+
if (this.baichuanApi) {
|
|
245
|
+
const isConnected = this.baichuanApi.client.isSocketConnected();
|
|
246
|
+
const isLoggedIn = this.baichuanApi.client.loggedIn;
|
|
247
|
+
|
|
248
|
+
// Only reuse if both conditions are true
|
|
249
|
+
if (isConnected && isLoggedIn) {
|
|
250
|
+
return this.baichuanApi;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// If socket is not connected or not logged in, cleanup the stale client
|
|
254
|
+
// This prevents leaking connections when the socket appears connected but isn't
|
|
255
|
+
const logger = this.getBaichuanLogger();
|
|
256
|
+
logger.log(
|
|
257
|
+
`Stale client detected: connected=${isConnected}, loggedIn=${isLoggedIn}, cleaning up`,
|
|
258
|
+
);
|
|
259
|
+
await this.cleanupBaichuanApi();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Apply backoff to avoid aggressive reconnection after disconnection
|
|
263
|
+
if (this.lastDisconnectTime > 0) {
|
|
264
|
+
const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
|
|
265
|
+
if (timeSinceDisconnect < this.reconnectBackoffMs) {
|
|
266
|
+
const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
|
|
267
|
+
const logger = this.getBaichuanLogger();
|
|
268
|
+
logger.log(`Waiting ${waitTime}ms before reconnection (backoff)`);
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
270
|
+
}
|
|
232
271
|
}
|
|
233
272
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
*/
|
|
237
|
-
async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
|
|
238
|
-
// Prevent concurrent login storms - check promise first
|
|
239
|
-
if (this.ensureClientPromise) return await this.ensureClientPromise;
|
|
240
|
-
|
|
241
|
-
// Reuse existing client if socket is still connected and logged in
|
|
242
|
-
// Check this AFTER checking the promise to avoid race conditions
|
|
243
|
-
if (this.baichuanApi) {
|
|
244
|
-
const isConnected = this.baichuanApi.client.isSocketConnected();
|
|
245
|
-
const isLoggedIn = this.baichuanApi.client.loggedIn;
|
|
246
|
-
|
|
247
|
-
// Only reuse if both conditions are true
|
|
248
|
-
if (isConnected && isLoggedIn) {
|
|
249
|
-
return this.baichuanApi;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// If socket is not connected or not logged in, cleanup the stale client
|
|
253
|
-
// This prevents leaking connections when the socket appears connected but isn't
|
|
254
|
-
const logger = this.getBaichuanLogger();
|
|
255
|
-
logger.log(`Stale client detected: connected=${isConnected}, loggedIn=${isLoggedIn}, cleaning up`);
|
|
256
|
-
await this.cleanupBaichuanApi();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Apply backoff to avoid aggressive reconnection after disconnection
|
|
260
|
-
if (this.lastDisconnectTime > 0) {
|
|
261
|
-
const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
|
|
262
|
-
if (timeSinceDisconnect < this.reconnectBackoffMs) {
|
|
263
|
-
const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
|
|
264
|
-
const logger = this.getBaichuanLogger();
|
|
265
|
-
logger.log(`Waiting ${waitTime}ms before reconnection (backoff)`);
|
|
266
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
273
|
+
this.ensureClientPromise = (async () => {
|
|
274
|
+
const config = this.getConnectionConfig();
|
|
269
275
|
|
|
270
|
-
|
|
271
|
-
|
|
276
|
+
// Clean up old client if exists
|
|
277
|
+
if (this.baichuanApi) {
|
|
278
|
+
await this.cleanupBaichuanApi();
|
|
279
|
+
}
|
|
272
280
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
// Create new Baichuan client
|
|
282
|
+
// BaichuanLogger implements Console, so it can be used directly
|
|
283
|
+
const logger = this.getBaichuanLogger();
|
|
284
|
+
try {
|
|
285
|
+
const api = await createBaichuanApi({
|
|
286
|
+
inputs: {
|
|
287
|
+
host: config.host,
|
|
288
|
+
username: config.username,
|
|
289
|
+
password: config.password,
|
|
290
|
+
uid: config.uid,
|
|
291
|
+
logger,
|
|
292
|
+
debugOptions: config.debugOptions,
|
|
293
|
+
udpDiscoveryMethod: config.udpDiscoveryMethod,
|
|
294
|
+
},
|
|
295
|
+
transport: config.transport,
|
|
296
|
+
});
|
|
277
297
|
|
|
278
|
-
|
|
279
|
-
// BaichuanLogger implements Console, so it can be used directly
|
|
280
|
-
const logger = this.getBaichuanLogger();
|
|
281
|
-
try {
|
|
282
|
-
const api = await createBaichuanApi({
|
|
283
|
-
inputs: {
|
|
284
|
-
host: config.host,
|
|
285
|
-
username: config.username,
|
|
286
|
-
password: config.password,
|
|
287
|
-
uid: config.uid,
|
|
288
|
-
logger,
|
|
289
|
-
debugOptions: config.debugOptions,
|
|
290
|
-
udpDiscoveryMethod: config.udpDiscoveryMethod,
|
|
291
|
-
},
|
|
292
|
-
transport: config.transport,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
await api.login();
|
|
296
|
-
|
|
297
|
-
// Verify socket is connected before returning
|
|
298
|
-
if (!api.client.isSocketConnected()) {
|
|
299
|
-
throw new Error('Socket not connected after login');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Attach listeners
|
|
303
|
-
this.attachBaichuanListeners(api);
|
|
304
|
-
|
|
305
|
-
this.baichuanApi = api;
|
|
306
|
-
this.connectionTime = Date.now();
|
|
307
|
-
|
|
308
|
-
// Start ping and auto-renewal for TCP connections
|
|
309
|
-
if (this.transport === 'tcp') {
|
|
310
|
-
// this.startConnectionMaintenance(api);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Start event check for all connections
|
|
314
|
-
this.startEventCheck(api);
|
|
315
|
-
|
|
316
|
-
return api;
|
|
317
|
-
}
|
|
318
|
-
catch (e) {
|
|
319
|
-
// Apply backoff for connection failures too, otherwise multiple callers can hammer connect().
|
|
320
|
-
this.lastDisconnectTime = Date.now();
|
|
321
|
-
// Ensure state is reset so next attempt is clean.
|
|
322
|
-
await this.cleanupBaichuanApi();
|
|
323
|
-
throw e;
|
|
324
|
-
}
|
|
325
|
-
})();
|
|
298
|
+
await api.login();
|
|
326
299
|
|
|
327
|
-
|
|
328
|
-
|
|
300
|
+
// Verify socket is connected before returning
|
|
301
|
+
if (!api.client.isSocketConnected()) {
|
|
302
|
+
throw new Error("Socket not connected after login");
|
|
329
303
|
}
|
|
330
|
-
finally {
|
|
331
|
-
// Allow future reconnects and avoid pinning rejected promises
|
|
332
|
-
this.ensureClientPromise = undefined;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Attach error and close listeners to Baichuan API
|
|
338
|
-
*/
|
|
339
|
-
private attachBaichuanListeners(api: ReolinkBaichuanApi): void {
|
|
340
|
-
const logger = this.getBaichuanLogger();
|
|
341
|
-
const callbacks = this.getConnectionCallbacks();
|
|
342
|
-
|
|
343
|
-
// Error listener
|
|
344
|
-
this.errorListener = (err: unknown) => {
|
|
345
|
-
const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
346
|
-
|
|
347
|
-
// Only log if it's not a recoverable error to avoid spam
|
|
348
|
-
if (typeof msg === 'string' && (
|
|
349
|
-
msg.includes('Baichuan socket closed') ||
|
|
350
|
-
msg.includes('Baichuan UDP stream closed') ||
|
|
351
|
-
msg.includes('Not running')
|
|
352
|
-
)) {
|
|
353
|
-
logger.debug(`error (recoverable): ${msg}`);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
logger.error(`error: ${msg}`);
|
|
357
|
-
|
|
358
|
-
// Call custom error handler if provided
|
|
359
|
-
if (callbacks.onError) {
|
|
360
|
-
try {
|
|
361
|
-
callbacks.onError(err);
|
|
362
|
-
} catch {
|
|
363
|
-
// ignore
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// Close listener
|
|
369
|
-
this.closeListener = async () => {
|
|
370
|
-
// Prevent multiple concurrent cleanup operations
|
|
371
|
-
if (!this.baichuanApi || this.baichuanApi !== api) {
|
|
372
|
-
// This close event is for a different/old client, ignore it
|
|
373
|
-
logger.debug('Close event for stale client, ignoring');
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const wasConnected = api.client.isSocketConnected();
|
|
379
|
-
const wasLoggedIn = api.client.loggedIn;
|
|
380
|
-
logger.log(`Connection state before close: connected=${wasConnected}, loggedIn=${wasLoggedIn}`);
|
|
381
|
-
|
|
382
|
-
// Try to get last message info if available
|
|
383
|
-
const client = api.client as any;
|
|
384
|
-
if (client?.lastRx || client?.lastTx) {
|
|
385
|
-
logger.debug(`Last message info: lastRx=${JSON.stringify(client.lastRx)}, lastTx=${JSON.stringify(client.lastTx)}`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch (e) {
|
|
389
|
-
logger.debug(`Could not get connection state: ${e?.message || String(e)}`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const now = Date.now();
|
|
393
|
-
const timeSinceLastDisconnect = now - this.lastDisconnectTime;
|
|
394
|
-
this.lastDisconnectTime = now;
|
|
395
|
-
|
|
396
|
-
logger.log(`Socket closed, resetting client state for reconnection (last disconnect ${timeSinceLastDisconnect}ms ago)`);
|
|
397
|
-
|
|
398
|
-
// Mark as disconnected immediately to prevent reuse
|
|
399
|
-
// This prevents race conditions where ensureBaichuanClient might check
|
|
400
|
-
// isSocketConnected() before cleanup completes
|
|
401
|
-
const currentApi = this.baichuanApi;
|
|
402
|
-
if (currentApi === api) {
|
|
403
|
-
// Only cleanup if this is still the current API instance
|
|
404
|
-
// This prevents cleanup of a new connection that was created
|
|
405
|
-
// while the old one was closing
|
|
406
|
-
await this.cleanupBaichuanApi();
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Call custom close handler if provided
|
|
410
|
-
if (callbacks.onClose) {
|
|
411
|
-
try {
|
|
412
|
-
await callbacks.onClose();
|
|
413
|
-
} catch {
|
|
414
|
-
// ignore
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
304
|
|
|
419
305
|
// Attach listeners
|
|
420
|
-
|
|
421
|
-
api.client.on("close", this.closeListener);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Centralized cleanup method for Baichuan API
|
|
426
|
-
* Removes all listeners, closes connection, and resets state
|
|
427
|
-
*/
|
|
428
|
-
async cleanupBaichuanApi(): Promise<void> {
|
|
429
|
-
if (!this.baichuanApi) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const api = this.baichuanApi;
|
|
434
|
-
|
|
435
|
-
// Unsubscribe from events first
|
|
436
|
-
await this.unsubscribeFromEvents();
|
|
437
|
-
|
|
438
|
-
// Call before cleanup hook
|
|
439
|
-
await this.onBeforeCleanup();
|
|
306
|
+
this.attachBaichuanListeners(api);
|
|
440
307
|
|
|
441
|
-
|
|
442
|
-
|
|
308
|
+
this.baichuanApi = api;
|
|
309
|
+
this.connectionTime = Date.now();
|
|
443
310
|
|
|
444
|
-
//
|
|
445
|
-
if (this.
|
|
446
|
-
|
|
447
|
-
api.client.off("close", this.closeListener);
|
|
448
|
-
} catch {
|
|
449
|
-
// ignore
|
|
450
|
-
}
|
|
451
|
-
this.closeListener = undefined;
|
|
311
|
+
// Start ping and auto-renewal for TCP connections
|
|
312
|
+
if (this.transport === "tcp") {
|
|
313
|
+
// this.startConnectionMaintenance(api);
|
|
452
314
|
}
|
|
453
315
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
api.client.off("error", this.errorListener);
|
|
457
|
-
} catch {
|
|
458
|
-
// ignore
|
|
459
|
-
}
|
|
460
|
-
this.errorListener = undefined;
|
|
461
|
-
}
|
|
316
|
+
// Start event check for all connections
|
|
317
|
+
this.startEventCheck(api);
|
|
462
318
|
|
|
463
|
-
|
|
319
|
+
return api;
|
|
320
|
+
} catch (e) {
|
|
321
|
+
// Apply backoff for connection failures too, otherwise multiple callers can hammer connect().
|
|
322
|
+
this.lastDisconnectTime = Date.now();
|
|
323
|
+
// Ensure state is reset so next attempt is clean.
|
|
324
|
+
await this.cleanupBaichuanApi();
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
return await this.ensureClientPromise;
|
|
331
|
+
} finally {
|
|
332
|
+
// Allow future reconnects and avoid pinning rejected promises
|
|
333
|
+
this.ensureClientPromise = undefined;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Attach error and close listeners to Baichuan API
|
|
339
|
+
*/
|
|
340
|
+
private attachBaichuanListeners(api: ReolinkBaichuanApi): void {
|
|
341
|
+
const logger = this.getBaichuanLogger();
|
|
342
|
+
const callbacks = this.getConnectionCallbacks();
|
|
343
|
+
|
|
344
|
+
// Error listener
|
|
345
|
+
this.errorListener = (err: unknown) => {
|
|
346
|
+
const msg =
|
|
347
|
+
(err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
348
|
+
|
|
349
|
+
// Only log if it's not a recoverable error to avoid spam
|
|
350
|
+
if (
|
|
351
|
+
typeof msg === "string" &&
|
|
352
|
+
(msg.includes("Baichuan socket closed") ||
|
|
353
|
+
msg.includes("Baichuan UDP stream closed") ||
|
|
354
|
+
msg.includes("Not running"))
|
|
355
|
+
) {
|
|
356
|
+
logger.debug(`error (recoverable): ${msg}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
logger.error(`error: ${msg}`);
|
|
360
|
+
|
|
361
|
+
// Call custom error handler if provided
|
|
362
|
+
if (callbacks.onError) {
|
|
464
363
|
try {
|
|
465
|
-
|
|
466
|
-
await api.close();
|
|
467
|
-
}
|
|
364
|
+
callbacks.onError(err);
|
|
468
365
|
} catch {
|
|
469
|
-
|
|
366
|
+
// ignore
|
|
470
367
|
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Close listener
|
|
372
|
+
this.closeListener = async () => {
|
|
373
|
+
// Prevent multiple concurrent cleanup operations
|
|
374
|
+
if (!this.baichuanApi || this.baichuanApi !== api) {
|
|
375
|
+
// This close event is for a different/old client, ignore it
|
|
376
|
+
logger.debug("Close event for stale client, ignoring");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const wasConnected = api.client.isSocketConnected();
|
|
382
|
+
const wasLoggedIn = api.client.loggedIn;
|
|
383
|
+
logger.log(
|
|
384
|
+
`Connection state before close: connected=${wasConnected}, loggedIn=${wasLoggedIn}`,
|
|
385
|
+
);
|
|
471
386
|
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Reset state
|
|
479
|
-
this.baichuanApi = undefined;
|
|
480
|
-
this.ensureClientPromise = undefined;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Get all active Baichuan connections (main + stream clients)
|
|
485
|
-
*/
|
|
486
|
-
private getAllActiveConnections(): ReolinkBaichuanApi[] {
|
|
487
|
-
const connections: ReolinkBaichuanApi[] = [];
|
|
488
|
-
|
|
489
|
-
// Add main connection if exists and is valid
|
|
490
|
-
if (this.baichuanApi) {
|
|
491
|
-
const isConnected = this.baichuanApi.client.isSocketConnected();
|
|
492
|
-
const isLoggedIn = this.baichuanApi.client.loggedIn;
|
|
493
|
-
if (isConnected && isLoggedIn) {
|
|
494
|
-
connections.push(this.baichuanApi);
|
|
495
|
-
}
|
|
387
|
+
// Try to get last message info if available
|
|
388
|
+
const client = api.client as any;
|
|
389
|
+
if (client?.lastRx || client?.lastTx) {
|
|
390
|
+
logger.debug(
|
|
391
|
+
`Last message info: lastRx=${JSON.stringify(client.lastRx)}, lastTx=${JSON.stringify(client.lastTx)}`,
|
|
392
|
+
);
|
|
496
393
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
394
|
+
} catch (e) {
|
|
395
|
+
logger.debug(
|
|
396
|
+
`Could not get connection state: ${e?.message || String(e)}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const timeSinceLastDisconnect = now - this.lastDisconnectTime;
|
|
402
|
+
this.lastDisconnectTime = now;
|
|
403
|
+
|
|
404
|
+
logger.log(
|
|
405
|
+
`Socket closed, resetting client state for reconnection (last disconnect ${timeSinceLastDisconnect}ms ago)`,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Mark as disconnected immediately to prevent reuse
|
|
409
|
+
// This prevents race conditions where ensureBaichuanClient might check
|
|
410
|
+
// isSocketConnected() before cleanup completes
|
|
411
|
+
const currentApi = this.baichuanApi;
|
|
412
|
+
if (currentApi === api) {
|
|
413
|
+
// Only cleanup if this is still the current API instance
|
|
414
|
+
// This prevents cleanup of a new connection that was created
|
|
415
|
+
// while the old one was closing
|
|
416
|
+
await this.cleanupBaichuanApi();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Call custom close handler if provided
|
|
420
|
+
if (callbacks.onClose) {
|
|
421
|
+
try {
|
|
422
|
+
await callbacks.onClose();
|
|
423
|
+
} catch {
|
|
424
|
+
// ignore
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Attach listeners
|
|
430
|
+
api.client.on("error", this.errorListener);
|
|
431
|
+
api.client.on("close", this.closeListener);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Centralized cleanup method for Baichuan API
|
|
436
|
+
* Removes all listeners, closes connection, and resets state
|
|
437
|
+
*/
|
|
438
|
+
async cleanupBaichuanApi(): Promise<void> {
|
|
439
|
+
if (!this.baichuanApi) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const api = this.baichuanApi;
|
|
444
|
+
|
|
445
|
+
// Unsubscribe from events first
|
|
446
|
+
await this.unsubscribeFromEvents();
|
|
447
|
+
|
|
448
|
+
// Call before cleanup hook
|
|
449
|
+
await this.onBeforeCleanup();
|
|
450
|
+
|
|
451
|
+
// Remove all listeners
|
|
452
|
+
if (this.closeListener) {
|
|
453
|
+
try {
|
|
454
|
+
api.client.off("close", this.closeListener);
|
|
455
|
+
} catch {
|
|
456
|
+
// ignore
|
|
457
|
+
}
|
|
458
|
+
this.closeListener = undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (this.errorListener) {
|
|
462
|
+
try {
|
|
463
|
+
api.client.off("error", this.errorListener);
|
|
464
|
+
} catch {
|
|
465
|
+
// ignore
|
|
466
|
+
}
|
|
467
|
+
this.errorListener = undefined;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Close connection if still connected
|
|
471
|
+
try {
|
|
472
|
+
if (api.client.isSocketConnected()) {
|
|
473
|
+
await api.close();
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
// ignore
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Stop ping and auto-renewal intervals
|
|
480
|
+
this.stopConnectionMaintenance();
|
|
481
|
+
|
|
482
|
+
// Stop event check interval
|
|
483
|
+
this.stopEventCheck();
|
|
484
|
+
|
|
485
|
+
// Reset state
|
|
486
|
+
this.baichuanApi = undefined;
|
|
487
|
+
this.ensureClientPromise = undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get all active Baichuan connections (main client only now)
|
|
492
|
+
*/
|
|
493
|
+
private getAllActiveConnections(): ReolinkBaichuanApi[] {
|
|
494
|
+
const connections: ReolinkBaichuanApi[] = [];
|
|
495
|
+
|
|
496
|
+
// Add main connection if exists and is valid
|
|
497
|
+
if (this.baichuanApi) {
|
|
498
|
+
const isConnected = this.baichuanApi.client.isSocketConnected();
|
|
499
|
+
const isLoggedIn = this.baichuanApi.client.loggedIn;
|
|
500
|
+
if (isConnected && isLoggedIn) {
|
|
501
|
+
connections.push(this.baichuanApi);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return connections;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Start ping and auto-renewal maintenance for TCP connections
|
|
510
|
+
*/
|
|
511
|
+
private startConnectionMaintenance(api: ReolinkBaichuanApi): void {
|
|
512
|
+
const logger = this.getBaichuanLogger();
|
|
513
|
+
|
|
514
|
+
// Stop any existing intervals
|
|
515
|
+
this.stopConnectionMaintenance();
|
|
516
|
+
|
|
517
|
+
// Ping every 30 seconds to keep all connections alive
|
|
518
|
+
this.pingInterval = setInterval(async () => {
|
|
519
|
+
if (!this.baichuanApi || this.baichuanApi !== api) {
|
|
520
|
+
return; // Connection changed, stop this interval
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
// Get all active connections (main + stream clients)
|
|
525
|
+
const allConnections = this.getAllActiveConnections();
|
|
526
|
+
logger.debug(`Pinging ${allConnections.length} connections`);
|
|
527
|
+
|
|
528
|
+
if (allConnections.length === 0) {
|
|
529
|
+
this.consecutivePingFailures++;
|
|
530
|
+
logger.debug(
|
|
531
|
+
`No active connections found, failures=${this.consecutivePingFailures}`,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
if (this.consecutivePingFailures >= 3) {
|
|
535
|
+
logger.log("No active connections detected, renewing connection");
|
|
536
|
+
await this.cleanupBaichuanApi();
|
|
537
|
+
this.consecutivePingFailures = 0;
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
505
540
|
}
|
|
506
541
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Start ping and auto-renewal maintenance for TCP connections
|
|
512
|
-
*/
|
|
513
|
-
private startConnectionMaintenance(api: ReolinkBaichuanApi): void {
|
|
514
|
-
const logger = this.getBaichuanLogger();
|
|
515
|
-
|
|
516
|
-
// Stop any existing intervals
|
|
517
|
-
this.stopConnectionMaintenance();
|
|
518
|
-
|
|
519
|
-
// Ping every 30 seconds to keep all connections alive
|
|
520
|
-
this.pingInterval = setInterval(async () => {
|
|
521
|
-
if (!this.baichuanApi || this.baichuanApi !== api) {
|
|
522
|
-
return; // Connection changed, stop this interval
|
|
523
|
-
}
|
|
524
|
-
|
|
542
|
+
// Ping all connections using the specific ping method
|
|
543
|
+
const pingResults = await Promise.allSettled(
|
|
544
|
+
allConnections.map(async (conn) => {
|
|
525
545
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
logger.debug(`Pinging ${allConnections.length} connections`);
|
|
529
|
-
|
|
530
|
-
if (allConnections.length === 0) {
|
|
531
|
-
this.consecutivePingFailures++;
|
|
532
|
-
logger.debug(`No active connections found, failures=${this.consecutivePingFailures}`);
|
|
533
|
-
|
|
534
|
-
if (this.consecutivePingFailures >= 3) {
|
|
535
|
-
logger.log('No active connections detected, renewing connection');
|
|
536
|
-
await this.cleanupBaichuanApi();
|
|
537
|
-
this.consecutivePingFailures = 0;
|
|
538
|
-
}
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Ping all connections using the specific ping method
|
|
543
|
-
const pingResults = await Promise.allSettled(
|
|
544
|
-
allConnections.map(async (conn) => {
|
|
545
|
-
try {
|
|
546
|
-
await conn.ping();
|
|
547
|
-
return { success: true, conn };
|
|
548
|
-
} catch (e) {
|
|
549
|
-
return { success: false, conn, error: e };
|
|
550
|
-
}
|
|
551
|
-
})
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
// Check results
|
|
555
|
-
const failedPings = pingResults.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.success));
|
|
556
|
-
|
|
557
|
-
if (failedPings.length > 0) {
|
|
558
|
-
this.consecutivePingFailures++;
|
|
559
|
-
logger.debug(`Ping failed for ${failedPings.length}/${allConnections.length} connections, failures=${this.consecutivePingFailures}`);
|
|
560
|
-
|
|
561
|
-
if (this.consecutivePingFailures >= 3) {
|
|
562
|
-
logger.log(`Multiple ping failures detected (${failedPings.length} connections), renewing connection`);
|
|
563
|
-
await this.cleanupBaichuanApi();
|
|
564
|
-
this.consecutivePingFailures = 0;
|
|
565
|
-
}
|
|
566
|
-
} else {
|
|
567
|
-
// All pings successful, reset failure counter
|
|
568
|
-
this.consecutivePingFailures = 0;
|
|
569
|
-
if (allConnections.length > 1) {
|
|
570
|
-
logger.debug(`Ping successful for all ${allConnections.length} connections`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
546
|
+
await conn.ping();
|
|
547
|
+
return { success: true, conn };
|
|
573
548
|
} catch (e) {
|
|
574
|
-
|
|
549
|
+
return { success: false, conn, error: e };
|
|
575
550
|
}
|
|
576
|
-
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
577
553
|
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
554
|
+
// Check results
|
|
555
|
+
const failedPings = pingResults.filter(
|
|
556
|
+
(r) =>
|
|
557
|
+
r.status === "rejected" ||
|
|
558
|
+
(r.status === "fulfilled" && !r.value.success),
|
|
559
|
+
);
|
|
583
560
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
logger.log('No active streams detected, renewing connection (auto-renewal)');
|
|
590
|
-
await this.cleanupBaichuanApi();
|
|
591
|
-
} else {
|
|
592
|
-
logger.debug('Active streams detected, skipping auto-renewal');
|
|
593
|
-
}
|
|
594
|
-
} catch (e) {
|
|
595
|
-
logger.debug(`Error in auto-renewal check: ${e?.message || String(e)}`);
|
|
596
|
-
}
|
|
597
|
-
}, 5 * 60_000); // Every 5 minutes
|
|
598
|
-
}
|
|
561
|
+
if (failedPings.length > 0) {
|
|
562
|
+
this.consecutivePingFailures++;
|
|
563
|
+
logger.debug(
|
|
564
|
+
`Ping failed for ${failedPings.length}/${allConnections.length} connections, failures=${this.consecutivePingFailures}`,
|
|
565
|
+
);
|
|
599
566
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
567
|
+
if (this.consecutivePingFailures >= 3) {
|
|
568
|
+
logger.log(
|
|
569
|
+
`Multiple ping failures detected (${failedPings.length} connections), renewing connection`,
|
|
570
|
+
);
|
|
571
|
+
await this.cleanupBaichuanApi();
|
|
572
|
+
this.consecutivePingFailures = 0;
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
// All pings successful, reset failure counter
|
|
576
|
+
this.consecutivePingFailures = 0;
|
|
577
|
+
if (allConnections.length > 1) {
|
|
578
|
+
logger.debug(
|
|
579
|
+
`Ping successful for all ${allConnections.length} connections`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
607
582
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
583
|
+
} catch (e) {
|
|
584
|
+
logger.debug(`Error in ping check: ${e?.message || String(e)}`);
|
|
585
|
+
}
|
|
586
|
+
}, 30_000); // Every 30 seconds
|
|
587
|
+
|
|
588
|
+
// Auto-renewal every 5 minutes if no active streams
|
|
589
|
+
this.autoRenewInterval = setInterval(async () => {
|
|
590
|
+
if (!this.baichuanApi || this.baichuanApi !== api) {
|
|
591
|
+
return; // Connection changed, stop this interval
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Check if there are active streams
|
|
596
|
+
const hasActiveStreams =
|
|
597
|
+
this.getStreamManager?.()?.hasActiveStreams() ?? false;
|
|
598
|
+
|
|
599
|
+
if (!hasActiveStreams) {
|
|
600
|
+
logger.log(
|
|
601
|
+
"No active streams detected, renewing connection (auto-renewal)",
|
|
602
|
+
);
|
|
603
|
+
await this.cleanupBaichuanApi();
|
|
604
|
+
} else {
|
|
605
|
+
logger.debug("Active streams detected, skipping auto-renewal");
|
|
611
606
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
607
|
+
} catch (e) {
|
|
608
|
+
logger.debug(`Error in auto-renewal check: ${e?.message || String(e)}`);
|
|
609
|
+
}
|
|
610
|
+
}, 5 * 60_000); // Every 5 minutes
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Stop ping and auto-renewal maintenance
|
|
615
|
+
*/
|
|
616
|
+
private stopConnectionMaintenance(): void {
|
|
617
|
+
if (this.pingInterval) {
|
|
618
|
+
clearInterval(this.pingInterval);
|
|
619
|
+
this.pingInterval = undefined;
|
|
620
|
+
}
|
|
621
|
+
if (this.autoRenewInterval) {
|
|
622
|
+
clearInterval(this.autoRenewInterval);
|
|
623
|
+
this.autoRenewInterval = undefined;
|
|
624
|
+
}
|
|
625
|
+
this.consecutivePingFailures = 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Start event check to monitor if events are being received
|
|
630
|
+
*/
|
|
631
|
+
private startEventCheck(api: ReolinkBaichuanApi): void {
|
|
632
|
+
const logger = this.getBaichuanLogger();
|
|
633
|
+
|
|
634
|
+
// Stop any existing interval
|
|
635
|
+
this.stopEventCheck();
|
|
636
|
+
|
|
637
|
+
// Check every minute if events are being received
|
|
638
|
+
this.eventCheckInterval = setInterval(async () => {
|
|
639
|
+
if (!this.baichuanApi || this.baichuanApi !== api) {
|
|
640
|
+
return; // Connection changed, stop this interval
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Only check if event subscription is active
|
|
644
|
+
if (!this.eventSubscriptionActive) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const now = Date.now();
|
|
650
|
+
const timeSinceLastEvent = now - this.lastEventTime;
|
|
651
|
+
const fiveMinutesMs = 5 * 60 * 1000;
|
|
652
|
+
|
|
653
|
+
if (this.lastEventTime > 0 && timeSinceLastEvent > fiveMinutesMs) {
|
|
654
|
+
logger.log(
|
|
655
|
+
`No events received in the last ${Math.round(timeSinceLastEvent / 60_000)} minutes, restarting event listener`,
|
|
656
|
+
);
|
|
657
|
+
// Restart event subscription
|
|
658
|
+
await this.unsubscribeFromEvents(true);
|
|
659
|
+
await this.subscribeToEvents(true);
|
|
660
|
+
} else if (this.lastEventTime === 0) {
|
|
661
|
+
// If lastEventTime is 0, it means we just subscribed but haven't received any events yet
|
|
662
|
+
// Wait a bit longer before considering it a problem
|
|
663
|
+
const timeSinceSubscription = now - (this.connectionTime || now);
|
|
664
|
+
if (timeSinceSubscription > fiveMinutesMs) {
|
|
665
|
+
logger.log(
|
|
666
|
+
`No events received since subscription (${Math.round(timeSinceSubscription / 60_000)} minutes ago), restarting event listener`,
|
|
667
|
+
);
|
|
668
|
+
await this.unsubscribeFromEvents(true);
|
|
669
|
+
await this.subscribeToEvents(true);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (e) {
|
|
673
|
+
logger.debug(`Error in event check: ${e?.message || String(e)}`);
|
|
674
|
+
}
|
|
675
|
+
}, 5_000); // Check every minute
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Stop event check interval
|
|
680
|
+
*/
|
|
681
|
+
private stopEventCheck(): void {
|
|
682
|
+
if (this.eventCheckInterval) {
|
|
683
|
+
clearInterval(this.eventCheckInterval);
|
|
684
|
+
this.eventCheckInterval = undefined;
|
|
685
|
+
}
|
|
686
|
+
this.lastEventTime = 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Subscribe to Baichuan simple events
|
|
691
|
+
*/
|
|
692
|
+
async subscribeToEvents(silent: boolean = false): Promise<void> {
|
|
693
|
+
const logger = this.getBaichuanLogger();
|
|
694
|
+
const callbacks = this.getConnectionCallbacks();
|
|
695
|
+
|
|
696
|
+
if (!callbacks.onSimpleEvent) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// If already subscribed and connection is valid, return
|
|
701
|
+
if (this.eventSubscriptionActive && this.baichuanApi) {
|
|
702
|
+
if (
|
|
703
|
+
this.baichuanApi.client.isSocketConnected() &&
|
|
704
|
+
this.baichuanApi.client.loggedIn
|
|
705
|
+
) {
|
|
706
|
+
logger.debug("Event subscription already active");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Connection is invalid, reset subscription state
|
|
710
|
+
this.eventSubscriptionActive = false;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Unsubscribe first if handler exists (idempotent)
|
|
714
|
+
await this.unsubscribeFromEvents(silent);
|
|
715
|
+
|
|
716
|
+
// Get Baichuan client connection
|
|
717
|
+
const api = await this.ensureBaichuanClient();
|
|
718
|
+
|
|
719
|
+
// Verify connection is ready
|
|
720
|
+
if (!api.client.isSocketConnected() || !api.client.loggedIn) {
|
|
721
|
+
logger.warn("Cannot subscribe to events: connection not ready");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Check if event subscription is enabled
|
|
726
|
+
if (
|
|
727
|
+
callbacks.getEventSubscriptionEnabled &&
|
|
728
|
+
!callbacks.getEventSubscriptionEnabled()
|
|
729
|
+
) {
|
|
730
|
+
logger.debug("Event subscription disabled");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Subscribe to events with wrapper to track last event time
|
|
735
|
+
try {
|
|
736
|
+
const originalHandler = callbacks.onSimpleEvent;
|
|
737
|
+
const wrappedHandler = (ev: ReolinkSimpleEvent) => {
|
|
738
|
+
// Update last event time
|
|
739
|
+
this.lastEventTime = Date.now();
|
|
740
|
+
// Call original handler
|
|
741
|
+
originalHandler(ev);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
await api.onSimpleEvent(wrappedHandler);
|
|
745
|
+
this.eventSubscriptionActive = true;
|
|
746
|
+
this.lastEventTime = Date.now(); // Initialize on subscription
|
|
747
|
+
logger.debug("Subscribed to Baichuan events");
|
|
748
|
+
} catch (e) {
|
|
749
|
+
logger.warn("Failed to subscribe to events", e?.message || String(e));
|
|
750
|
+
this.eventSubscriptionActive = false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Unsubscribe from Baichuan simple events
|
|
756
|
+
* @param silent If true, don't log unsubscription messages
|
|
757
|
+
*/
|
|
758
|
+
async unsubscribeFromEvents(silent: boolean = false): Promise<void> {
|
|
759
|
+
const logger = this.getBaichuanLogger();
|
|
760
|
+
const callbacks = this.getConnectionCallbacks();
|
|
761
|
+
|
|
762
|
+
// Only unsubscribe if we have an active subscription
|
|
763
|
+
if (
|
|
764
|
+
this.eventSubscriptionActive &&
|
|
765
|
+
this.baichuanApi &&
|
|
766
|
+
callbacks.onSimpleEvent
|
|
767
|
+
) {
|
|
768
|
+
try {
|
|
769
|
+
this.baichuanApi.offSimpleEvent(callbacks.onSimpleEvent);
|
|
770
|
+
logger.debug("Unsubscribed from Baichuan events");
|
|
771
|
+
} catch (e) {
|
|
772
|
+
logger.warn("Error unsubscribing from events", e?.message || String(e));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.eventSubscriptionActive = false;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Create or get a dedicated Baichuan API session for streaming (used by StreamManager).
|
|
781
|
+
* Always returns the main client - the library internally manages dedicated sockets.
|
|
782
|
+
*/
|
|
783
|
+
async createStreamClient(streamKey: string): Promise<ReolinkBaichuanApi> {
|
|
784
|
+
// Always return the main client - the library handles dedicated sockets internally
|
|
785
|
+
return await this.ensureBaichuanClient();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Refresh the list of active user sessions from the device.
|
|
790
|
+
* This method uses storageSettings which must be defined in subclasses.
|
|
791
|
+
* Uses ensureClient() if available (for camera with nvrDevice), otherwise ensureBaichuanClient().
|
|
792
|
+
*/
|
|
793
|
+
protected async refreshUserSessionsList(): Promise<void> {
|
|
794
|
+
const logger = this.getBaichuanLogger();
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
// Use ensureClient() if available (e.g., camera with nvrDevice), otherwise use ensureBaichuanClient()
|
|
798
|
+
const api = (this as any).ensureClient
|
|
799
|
+
? await (this as any).ensureClient()
|
|
800
|
+
: await this.ensureBaichuanClient();
|
|
801
|
+
|
|
802
|
+
logger.log("[Sessions] Fetching active user sessions from device...");
|
|
803
|
+
|
|
804
|
+
const sessions = await api.getOnlineUserList();
|
|
805
|
+
|
|
806
|
+
// Get current socket session ID if available
|
|
807
|
+
let socketSessionId: string | undefined;
|
|
808
|
+
try {
|
|
809
|
+
socketSessionId = api.client.getSocketSessionId();
|
|
810
|
+
} catch {
|
|
811
|
+
// getSocketSessionId might not be available on all clients
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Format sessions as array of readable strings
|
|
815
|
+
const sessionStrings: string[] = [];
|
|
816
|
+
|
|
817
|
+
// Add header with timestamp and socket session ID
|
|
818
|
+
const timestamp = new Date().toLocaleString();
|
|
819
|
+
sessionStrings.push(`Last updated: ${timestamp}`);
|
|
820
|
+
if (socketSessionId) {
|
|
821
|
+
sessionStrings.push(`Current socket session ID: ${socketSessionId}`);
|
|
822
|
+
}
|
|
823
|
+
sessionStrings.push(""); // Empty line separator
|
|
824
|
+
|
|
825
|
+
// Parse sessions data (handle different response formats)
|
|
826
|
+
let sessionCount = 0;
|
|
827
|
+
const parseSessions = (data: any, prefix: string = ""): void => {
|
|
828
|
+
if (Array.isArray(data)) {
|
|
829
|
+
data.forEach((session: any, index: number) => {
|
|
830
|
+
sessionCount++;
|
|
831
|
+
const sessionInfo = formatSessionInfo(session, index + 1);
|
|
832
|
+
sessionStrings.push(sessionInfo);
|
|
833
|
+
});
|
|
834
|
+
} else if (data && typeof data === "object") {
|
|
835
|
+
// Handle nested objects
|
|
836
|
+
for (const [key, value] of Object.entries(data)) {
|
|
837
|
+
if (Array.isArray(value)) {
|
|
838
|
+
value.forEach((session: any, index: number) => {
|
|
839
|
+
sessionCount++;
|
|
840
|
+
const sessionInfo = formatSessionInfo(session, index + 1, key);
|
|
841
|
+
sessionStrings.push(sessionInfo);
|
|
842
|
+
});
|
|
843
|
+
} else if (value && typeof value === "object") {
|
|
844
|
+
parseSessions(value, prefix ? `${prefix}.${key}` : key);
|
|
845
|
+
} else {
|
|
846
|
+
// Single session object
|
|
847
|
+
sessionCount++;
|
|
848
|
+
const sessionInfo = formatSessionInfo(data, 1);
|
|
849
|
+
sessionStrings.push(sessionInfo);
|
|
850
|
+
return; // Only process once
|
|
657
851
|
}
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Stop event check interval
|
|
663
|
-
*/
|
|
664
|
-
private stopEventCheck(): void {
|
|
665
|
-
if (this.eventCheckInterval) {
|
|
666
|
-
clearInterval(this.eventCheckInterval);
|
|
667
|
-
this.eventCheckInterval = undefined;
|
|
852
|
+
}
|
|
668
853
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
if (
|
|
680
|
-
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// Helper function to format session info as readable string
|
|
857
|
+
const formatSessionInfo = (
|
|
858
|
+
session: any,
|
|
859
|
+
index: number,
|
|
860
|
+
group?: string,
|
|
861
|
+
): string => {
|
|
862
|
+
const parts: string[] = [];
|
|
863
|
+
|
|
864
|
+
if (group) {
|
|
865
|
+
parts.push(`[${group}]`);
|
|
681
866
|
}
|
|
867
|
+
parts.push(`Session ${index}:`);
|
|
682
868
|
|
|
683
|
-
//
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
logger.debug('Event subscription already active');
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
// Connection is invalid, reset subscription state
|
|
690
|
-
this.eventSubscriptionActive = false;
|
|
869
|
+
// Extract common fields
|
|
870
|
+
if (session.userName !== undefined) {
|
|
871
|
+
parts.push(`User: ${session.userName}`);
|
|
691
872
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
await this.unsubscribeFromEvents(silent);
|
|
695
|
-
|
|
696
|
-
// Get Baichuan client connection
|
|
697
|
-
const api = await this.ensureBaichuanClient();
|
|
698
|
-
|
|
699
|
-
// Verify connection is ready
|
|
700
|
-
if (!api.client.isSocketConnected() || !api.client.loggedIn) {
|
|
701
|
-
logger.warn('Cannot subscribe to events: connection not ready');
|
|
702
|
-
return;
|
|
873
|
+
if (session.user !== undefined) {
|
|
874
|
+
parts.push(`User: ${session.user}`);
|
|
703
875
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (callbacks.getEventSubscriptionEnabled && !callbacks.getEventSubscriptionEnabled()) {
|
|
707
|
-
logger.debug('Event subscription disabled');
|
|
708
|
-
return;
|
|
876
|
+
if (session.ip !== undefined) {
|
|
877
|
+
parts.push(`IP: ${session.ip}`);
|
|
709
878
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
try {
|
|
713
|
-
const originalHandler = callbacks.onSimpleEvent;
|
|
714
|
-
const wrappedHandler = (ev: ReolinkSimpleEvent) => {
|
|
715
|
-
// Update last event time
|
|
716
|
-
this.lastEventTime = Date.now();
|
|
717
|
-
// Call original handler
|
|
718
|
-
originalHandler(ev);
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
await api.onSimpleEvent(wrappedHandler);
|
|
722
|
-
this.eventSubscriptionActive = true;
|
|
723
|
-
this.lastEventTime = Date.now(); // Initialize on subscription
|
|
724
|
-
logger.debug('Subscribed to Baichuan events');
|
|
879
|
+
if (session.ipAddress !== undefined) {
|
|
880
|
+
parts.push(`IP: ${session.ipAddress}`);
|
|
725
881
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
this.eventSubscriptionActive = false;
|
|
882
|
+
if (session.port !== undefined) {
|
|
883
|
+
parts.push(`Port: ${session.port}`);
|
|
729
884
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Unsubscribe from Baichuan simple events
|
|
734
|
-
* @param silent If true, don't log unsubscription messages
|
|
735
|
-
*/
|
|
736
|
-
async unsubscribeFromEvents(silent: boolean = false): Promise<void> {
|
|
737
|
-
const logger = this.getBaichuanLogger();
|
|
738
|
-
const callbacks = this.getConnectionCallbacks();
|
|
739
|
-
|
|
740
|
-
// Only unsubscribe if we have an active subscription
|
|
741
|
-
if (this.eventSubscriptionActive && this.baichuanApi && callbacks.onSimpleEvent) {
|
|
742
|
-
try {
|
|
743
|
-
this.baichuanApi.offSimpleEvent(callbacks.onSimpleEvent);
|
|
744
|
-
logger.debug('Unsubscribed from Baichuan events');
|
|
745
|
-
}
|
|
746
|
-
catch (e) {
|
|
747
|
-
logger.warn('Error unsubscribing from events', e?.message || String(e));
|
|
748
|
-
}
|
|
885
|
+
if (session.sessionId !== undefined) {
|
|
886
|
+
parts.push(`Session ID: ${session.sessionId}`);
|
|
749
887
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
username: config.username,
|
|
762
|
-
password: config.password,
|
|
763
|
-
uid: config.uid,
|
|
764
|
-
logger,
|
|
765
|
-
debugOptions: config.debugOptions,
|
|
766
|
-
udpDiscoveryMethod: config.udpDiscoveryMethod,
|
|
767
|
-
},
|
|
768
|
-
transport: config.transport,
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
await api.login();
|
|
772
|
-
|
|
773
|
-
return api;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Create or get a dedicated Baichuan API session for streaming (used by StreamManager).
|
|
778
|
-
* Returns an existing client if one exists for the same streamKey, otherwise creates a new one.
|
|
779
|
-
* @param streamKey The unique stream key (e.g., "composite_default_main", "channel_0_main", etc.)
|
|
780
|
-
*/
|
|
781
|
-
async createStreamClient(streamKey: string): Promise<ReolinkBaichuanApi> {
|
|
782
|
-
const logger = this.getBaichuanLogger();
|
|
783
|
-
|
|
784
|
-
// Check if a client already exists for this streamKey
|
|
785
|
-
const existingClient = this.streamClients.get(streamKey);
|
|
786
|
-
if (existingClient) {
|
|
787
|
-
// Verify the client is still valid (connected and logged in)
|
|
788
|
-
const isConnected = existingClient.client.isSocketConnected();
|
|
789
|
-
const isLoggedIn = existingClient.client.loggedIn;
|
|
790
|
-
|
|
791
|
-
if (isConnected && isLoggedIn) {
|
|
792
|
-
// Return existing valid client
|
|
793
|
-
logger.log(`Reusing existing stream client for streamKey=${streamKey}`);
|
|
794
|
-
return existingClient;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
logger.log(`Stale stream client detected for streamKey=${streamKey}, recreating`);
|
|
798
|
-
try {
|
|
799
|
-
if (existingClient.client.isSocketConnected()) {
|
|
800
|
-
await existingClient.close();
|
|
801
|
-
}
|
|
802
|
-
} catch {
|
|
803
|
-
// ignore cleanup errors
|
|
804
|
-
}
|
|
805
|
-
this.streamClients.delete(streamKey);
|
|
888
|
+
if (session.id !== undefined) {
|
|
889
|
+
parts.push(`ID: ${session.id}`);
|
|
890
|
+
}
|
|
891
|
+
if (session.loginTime !== undefined) {
|
|
892
|
+
parts.push(`Login Time: ${session.loginTime}`);
|
|
893
|
+
}
|
|
894
|
+
if (session.time !== undefined) {
|
|
895
|
+
parts.push(`Time: ${session.time}`);
|
|
896
|
+
}
|
|
897
|
+
if (session.status !== undefined) {
|
|
898
|
+
parts.push(`Status: ${session.status}`);
|
|
806
899
|
}
|
|
807
900
|
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
901
|
+
// If no common fields found, show all fields
|
|
902
|
+
if (parts.length === (group ? 2 : 1)) {
|
|
903
|
+
const allFields = Object.entries(session)
|
|
904
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
905
|
+
.join(", ");
|
|
906
|
+
parts.push(allFields);
|
|
907
|
+
}
|
|
814
908
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const currentClient = this.streamClients.get(streamKey);
|
|
818
|
-
if (currentClient === api) {
|
|
819
|
-
this.streamClients.delete(streamKey);
|
|
820
|
-
}
|
|
821
|
-
});
|
|
909
|
+
return parts.join(" | ");
|
|
910
|
+
};
|
|
822
911
|
|
|
823
|
-
|
|
824
|
-
}
|
|
912
|
+
parseSessions(sessions);
|
|
825
913
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
* and need to ensure the underlying Baichuan session is torn down at the end of the operation.
|
|
831
|
-
*/
|
|
832
|
-
async teardownStreamClient(streamKey: string, options?: { timeoutMs?: number }): Promise<void> {
|
|
833
|
-
const api = this.streamClients.get(streamKey);
|
|
834
|
-
// Remove first to avoid re-use while closing (and to ensure teardown even if close hangs).
|
|
835
|
-
this.streamClients.delete(streamKey);
|
|
836
|
-
if (!api) return;
|
|
837
|
-
|
|
838
|
-
const timeoutMs = options?.timeoutMs ?? 2000;
|
|
839
|
-
const sleepMs = async (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
914
|
+
// If no sessions found, add a message
|
|
915
|
+
if (sessionCount === 0) {
|
|
916
|
+
sessionStrings.push("No active sessions found");
|
|
917
|
+
}
|
|
840
918
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
sleepMs(timeoutMs),
|
|
845
|
-
]);
|
|
846
|
-
} catch {
|
|
847
|
-
// ignore
|
|
848
|
-
}
|
|
849
|
-
}
|
|
919
|
+
// Update the setting with the formatted session strings
|
|
920
|
+
// Note: storageSettings must be defined in subclasses
|
|
921
|
+
(this as any).storageSettings.values.userSessions = sessionStrings;
|
|
850
922
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const clients = Array.from(this.streamClients.values());
|
|
856
|
-
this.streamClients.clear();
|
|
857
|
-
|
|
858
|
-
await Promise.allSettled(
|
|
859
|
-
clients.map(async (api) => {
|
|
860
|
-
try {
|
|
861
|
-
if (api.client.isSocketConnected()) {
|
|
862
|
-
await api.close();
|
|
863
|
-
}
|
|
864
|
-
} catch {
|
|
865
|
-
// ignore cleanup errors
|
|
866
|
-
}
|
|
867
|
-
})
|
|
923
|
+
logger.log(`[Sessions] Retrieved ${sessionCount} active session(s)`);
|
|
924
|
+
if (socketSessionId) {
|
|
925
|
+
logger.debug(
|
|
926
|
+
`[Sessions] Current socket session ID: ${socketSessionId}`,
|
|
868
927
|
);
|
|
869
|
-
|
|
928
|
+
}
|
|
929
|
+
} catch (e) {
|
|
930
|
+
const errorMsg = e?.message || String(e);
|
|
931
|
+
logger.error(`[Sessions] Failed to fetch user sessions: ${errorMsg}`);
|
|
932
|
+
|
|
933
|
+
// Update setting with error message
|
|
934
|
+
// Note: storageSettings must be defined in subclasses
|
|
935
|
+
(this as any).storageSettings.values.userSessions = [
|
|
936
|
+
`Error fetching sessions: ${errorMsg}`,
|
|
937
|
+
`Timestamp: ${new Date().toLocaleString()}`,
|
|
938
|
+
];
|
|
939
|
+
throw e;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
870
942
|
}
|
|
871
|
-
|