@apocaliss92/scrypted-reolink-native 0.1.31 → 0.1.33

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/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
@@ -33,7 +33,8 @@
33
33
  "ScryptedDeviceCreator",
34
34
  "DeviceProvider",
35
35
  "DeviceCreator",
36
- "Settings"
36
+ "Settings",
37
+ "HttpRequestHandler"
37
38
  ]
38
39
  },
39
40
  "dependencies": {
@@ -208,14 +208,27 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
208
208
  * Ensure Baichuan client is connected and ready
209
209
  */
210
210
  async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
211
+ // Prevent concurrent login storms - check promise first
212
+ if (this.ensureClientPromise) return await this.ensureClientPromise;
213
+
211
214
  // Reuse existing client if socket is still connected and logged in
212
- if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
213
- return this.baichuanApi;
215
+ // Check this AFTER checking the promise to avoid race conditions
216
+ if (this.baichuanApi) {
217
+ const isConnected = this.baichuanApi.client.isSocketConnected();
218
+ const isLoggedIn = this.baichuanApi.client.loggedIn;
219
+
220
+ // Only reuse if both conditions are true
221
+ if (isConnected && isLoggedIn) {
222
+ return this.baichuanApi;
223
+ }
224
+
225
+ // If socket is not connected or not logged in, cleanup the stale client
226
+ // This prevents leaking connections when the socket appears connected but isn't
227
+ const logger = this.getBaichuanLogger();
228
+ logger.log(`Stale client detected: connected=${isConnected}, loggedIn=${isLoggedIn}, cleaning up`);
229
+ await this.cleanupBaichuanApi();
214
230
  }
215
231
 
216
- // Prevent concurrent login storms
217
- if (this.ensureClientPromise) return await this.ensureClientPromise;
218
-
219
232
  // Apply backoff to avoid aggressive reconnection after disconnection
220
233
  if (this.lastDisconnectTime > 0) {
221
234
  const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
@@ -309,6 +322,13 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
309
322
 
310
323
  // Close listener
311
324
  this.closeListener = async () => {
325
+ // Prevent multiple concurrent cleanup operations
326
+ if (!this.baichuanApi || this.baichuanApi !== api) {
327
+ // This close event is for a different/old client, ignore it
328
+ logger.debug('Close event for stale client, ignoring');
329
+ return;
330
+ }
331
+
312
332
  try {
313
333
  const wasConnected = api.client.isSocketConnected();
314
334
  const wasLoggedIn = api.client.loggedIn;
@@ -330,8 +350,16 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
330
350
 
331
351
  logger.log(`Socket closed, resetting client state for reconnection (last disconnect ${timeSinceLastDisconnect}ms ago)`);
332
352
 
333
- // Cleanup
334
- await this.cleanupBaichuanApi();
353
+ // Mark as disconnected immediately to prevent reuse
354
+ // This prevents race conditions where ensureBaichuanClient might check
355
+ // isSocketConnected() before cleanup completes
356
+ const currentApi = this.baichuanApi;
357
+ if (currentApi === api) {
358
+ // Only cleanup if this is still the current API instance
359
+ // This prevents cleanup of a new connection that was created
360
+ // while the old one was closing
361
+ await this.cleanupBaichuanApi();
362
+ }
335
363
 
336
364
  // Call custom close handler if provided
337
365
  if (callbacks.onClose) {
@@ -318,13 +318,4 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
318
318
  this.getBaichuanLogger().warn(`Baichuan client reset requested: ${message}`);
319
319
  }
320
320
  }
321
-
322
- protected async withBaichuanClient<T>(fn: (api: ReolinkBaichuanApi) => Promise<T>): Promise<T> {
323
- const client = await this.ensureClient();
324
- return fn(client);
325
- }
326
-
327
- async createStreamClient(): Promise<ReolinkBaichuanApi> {
328
- return await this.ensureClient();
329
- }
330
321
  }
package/src/camera.ts CHANGED
@@ -74,28 +74,6 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
74
74
  }
75
75
 
76
76
 
77
- async createStreamClient(): Promise<ReolinkBaichuanApi> {
78
- const { ipAddress, username, password } = this.storageSettings.values;
79
- const logger = this.getBaichuanLogger();
80
-
81
- const debugOptions = this.getBaichuanDebugOptions();
82
- const api = await createBaichuanApi(
83
- {
84
- inputs: {
85
- host: ipAddress,
86
- username: username,
87
- password: password,
88
- logger,
89
- debugOptions
90
- },
91
- transport: 'tcp',
92
- },
93
- );
94
- await api.login();
95
-
96
- return api;
97
- }
98
-
99
77
  private passiveRefreshTimer: ReturnType<typeof setTimeout> | undefined;
100
78
 
101
79
  async release() {
@@ -125,24 +103,15 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
125
103
  await this.alignAuxDevicesState();
126
104
  }
127
105
 
128
- async getDetectionInput(detectionId: string, eventId?: any): Promise<MediaObject> {
129
- return null;
130
- }
131
-
132
106
  async processEvents(events: { motion?: boolean; objects?: string[] }) {
133
107
  const logger = this.getBaichuanLogger();
134
108
 
135
109
  if (!this.isEventDispatchEnabled()) return;
136
110
 
137
- if (this.storageSettings.values.dispatchEvents.includes('eventLogs')) {
111
+ if (this.isDebugEnabled()) {
138
112
  logger.debug(`Events received: ${JSON.stringify(events)}`);
139
113
  }
140
114
 
141
- // const debugEvents = this.storageSettings.values.debugEvents;
142
- // if (debugEvents) {
143
- // logger.debug(`Events received: ${JSON.stringify(events)}`);
144
- // }
145
-
146
115
  if (this.shouldDispatchMotion() && events.motion !== this.motionDetected) {
147
116
  if (events.motion) {
148
117
  this.motionDetected = true;
@@ -169,30 +138,4 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
169
138
  sdk.deviceManager.onDeviceEvent(this.nativeId, ScryptedInterface.ObjectDetector, od);
170
139
  }
171
140
  }
172
-
173
- protected async withBaichuanClient<T>(fn: (api: ReolinkBaichuanApi) => Promise<T>): Promise<T> {
174
- const client = await this.ensureClient();
175
- return fn(client);
176
- }
177
-
178
- async getPictureOptions(): Promise<ResponsePictureOptions[]> {
179
- return [];
180
- }
181
-
182
- async getOtherSettings(): Promise<Setting[]> {
183
- return await this.getSettings();
184
- }
185
-
186
- showRtspUrlOverride() {
187
- return false;
188
- }
189
-
190
-
191
- async startIntercom(media: MediaObject): Promise<void> {
192
- await this.intercom.start(media);
193
- }
194
-
195
- stopIntercom(): Promise<void> {
196
- return this.intercom.stop();
197
- }
198
141
  }