@apocaliss92/scrypted-reolink-native 0.5.33 → 0.5.35

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.5.33",
3
+ "version": "0.5.35",
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",
@@ -44,7 +44,7 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@apocaliss92/nodelink-js": "^0.4.33",
47
+ "@apocaliss92/nodelink-js": "^0.4.34",
48
48
  "@scrypted/common": "file:../../scrypted/common",
49
49
  "@scrypted/rtsp": "file:../../scrypted/plugins/rtsp",
50
50
  "@scrypted/sdk": "^0.3.118"
@@ -270,6 +270,175 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
270
270
  // Override in subclasses if needed
271
271
  }
272
272
 
273
+ /**
274
+ * Create + login a Baichuan api. For UDP transport, the chain is:
275
+ * 1. `local-direct` (LAN unicast + broadcast — fastest, no internet)
276
+ * with a tight timeout so it doesn't block the chain if blocked.
277
+ * 2. The configured/saved `udpDiscoveryMethod`.
278
+ * 3. Parallel race of all remaining BCUDP methods.
279
+ * The persisted `udpDiscoveryMethod` is left untouched — the autodetect
280
+ * choice is preserved across reconnects, the fallback only changes
281
+ * behavior for this single attempt.
282
+ *
283
+ * Why: after the initial autodetect picks (say) `remote` because the
284
+ * camera was reachable via Reolink's P2P servers, a later network
285
+ * change can break that path forever — e.g. the camera's VLAN gets
286
+ * blocked from internet, or inter-VLAN broadcast stops working.
287
+ * Without a fallback the camera stays unreachable until manual
288
+ * settings change. And privileging `local-direct` up front handles
289
+ * the LAN-restored case without paying the full saved-method timeout.
290
+ */
291
+ private async createApiWithUdpFallback(
292
+ config: BaichuanConnectionConfig,
293
+ logger: BaichuanLogger,
294
+ ): Promise<ReolinkBaichuanApi> {
295
+ const buildInputs = (
296
+ methodOverride?: BaichuanClientOptions["udpDiscoveryMethod"],
297
+ ) => ({
298
+ host: config.host,
299
+ username: config.username,
300
+ password: config.password,
301
+ uid: config.uid,
302
+ logger,
303
+ debugOptions: config.debugOptions,
304
+ udpDiscoveryMethod:
305
+ methodOverride !== undefined
306
+ ? methodOverride
307
+ : config.udpDiscoveryMethod,
308
+ ...(config.emailPushCameraId
309
+ ? { emailPushCameraId: config.emailPushCameraId }
310
+ : {}),
311
+ });
312
+
313
+ // The lib's internal default when `udpDiscoveryMethod` is unset is
314
+ // "local-direct" (see BcUdpStream).
315
+ const configured: NonNullable<
316
+ BaichuanClientOptions["udpDiscoveryMethod"]
317
+ > = config.udpDiscoveryMethod ?? "local-direct";
318
+
319
+ // Step 1 (UDP only, skip when configured is already local-direct):
320
+ // preliminary local-direct attempt with a tight 8s timeout. If LAN
321
+ // works, this short-circuits the whole chain in 1-2s. If not, we
322
+ // pay 8s and move on. The lib's internal discovery timeout is 30s,
323
+ // so wrapping with a shorter Promise.race avoids dragging out the
324
+ // common "LAN blocked, use saved method" path.
325
+ if (config.transport === "udp" && configured !== "local-direct") {
326
+ const PRELIM_TIMEOUT_MS = 8000;
327
+ let prelimApi: ReolinkBaichuanApi | undefined;
328
+ try {
329
+ prelimApi = await createBaichuanApi({
330
+ inputs: buildInputs("local-direct"),
331
+ transport: "udp",
332
+ });
333
+ const loginPromise = prelimApi.login();
334
+ const timeoutPromise = new Promise<never>((_, reject) => {
335
+ setTimeout(
336
+ () =>
337
+ reject(
338
+ new Error(
339
+ `local-direct prelim exceeded ${PRELIM_TIMEOUT_MS}ms`,
340
+ ),
341
+ ),
342
+ PRELIM_TIMEOUT_MS,
343
+ );
344
+ });
345
+ await Promise.race([loginPromise, timeoutPromise]);
346
+ logger.log(
347
+ `UDP local-direct (preliminary, before saved=${configured}) succeeded`,
348
+ );
349
+ return prelimApi;
350
+ } catch (prelimErr) {
351
+ const prelimMsg =
352
+ prelimErr instanceof Error ? prelimErr.message : String(prelimErr);
353
+ logger.debug(
354
+ `UDP local-direct preliminary failed (${prelimMsg}); falling through to saved=${configured}`,
355
+ );
356
+ if (prelimApi) {
357
+ prelimApi.close({ reason: "udp_prelim_failed" }).catch(() => {
358
+ // ignore
359
+ });
360
+ }
361
+ }
362
+ }
363
+
364
+ // Step 2: configured method (or non-UDP transport).
365
+ try {
366
+ const api = await createBaichuanApi({
367
+ inputs: buildInputs(),
368
+ transport: config.transport,
369
+ });
370
+ await api.login();
371
+ return api;
372
+ } catch (primaryErr) {
373
+ if (config.transport !== "udp") throw primaryErr;
374
+
375
+ const primaryMsg =
376
+ primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
377
+ logger.warn(
378
+ `UDP connect with discoveryMethod=${configured} failed: ${primaryMsg}; racing remaining methods (saved setting preserved)`,
379
+ );
380
+
381
+ // Step 3: race the methods we haven't tried yet. local-direct is
382
+ // always excluded (it was either the configured method or the
383
+ // preliminary attempt above), so the race covers at most 4 methods.
384
+ const allMethods: NonNullable<
385
+ BaichuanClientOptions["udpDiscoveryMethod"]
386
+ >[] = ["local-direct", "local-broadcast", "remote", "map", "relay"];
387
+ const remaining = allMethods.filter(
388
+ (m) => m !== configured && m !== "local-direct",
389
+ );
390
+ const created: ReolinkBaichuanApi[] = [];
391
+
392
+ try {
393
+ const winner = await Promise.any(
394
+ remaining.map(async (m) => {
395
+ const api = await createBaichuanApi({
396
+ inputs: buildInputs(m),
397
+ transport: "udp",
398
+ });
399
+ created.push(api);
400
+ try {
401
+ await api.login();
402
+ return { method: m, api };
403
+ } catch (e) {
404
+ try {
405
+ await api.close({ reason: `udp_fallback_failed:${m}` });
406
+ } catch {
407
+ // ignore
408
+ }
409
+ throw e;
410
+ }
411
+ }),
412
+ );
413
+
414
+ logger.log(
415
+ `UDP fallback succeeded with discoveryMethod=${winner.method} (saved method=${configured} preserved)`,
416
+ );
417
+
418
+ // Close the race losers. Some may still be mid-login; api.close()
419
+ // is idempotent and safe to call concurrently with login().
420
+ for (const a of created) {
421
+ if (a !== winner.api) {
422
+ a.close({ reason: "udp_fallback_loser" }).catch(() => {
423
+ // ignore
424
+ });
425
+ }
426
+ }
427
+
428
+ return winner.api;
429
+ } catch {
430
+ // All fallback methods also failed. Surface the original error —
431
+ // it's almost always more informative than the AggregateError
432
+ // from Promise.any and matches the pre-fallback behavior callers
433
+ // expect.
434
+ logger.error(
435
+ `UDP fallback exhausted (${remaining.length} methods tried)`,
436
+ );
437
+ throw primaryErr;
438
+ }
439
+ }
440
+ }
441
+
273
442
  /**
274
443
  * Ensure Baichuan client is connected and ready
275
444
  */
@@ -343,27 +512,13 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
343
512
  await this.cleanupBaichuanApi();
344
513
  }
345
514
 
346
- // Create new Baichuan client
347
- // BaichuanLogger implements Console, so it can be used directly
515
+ // Create new Baichuan client. For UDP transports a fallback chain
516
+ // (preliminary local-direct saved method race) is applied
517
+ // inside the helper so the cam recovers from post-autodetect
518
+ // network changes without losing the persisted setting.
348
519
  const logger = this.getBaichuanLogger();
349
520
  try {
350
- const api = await createBaichuanApi({
351
- inputs: {
352
- host: config.host,
353
- username: config.username,
354
- password: config.password,
355
- uid: config.uid,
356
- logger,
357
- debugOptions: config.debugOptions,
358
- udpDiscoveryMethod: config.udpDiscoveryMethod,
359
- ...(config.emailPushCameraId
360
- ? { emailPushCameraId: config.emailPushCameraId }
361
- : {}),
362
- },
363
- transport: config.transport,
364
- });
365
-
366
- await api.login();
521
+ const api = await this.createApiWithUdpFallback(config, logger);
367
522
 
368
523
  // Set NVR flag BEFORE any streaming to ensure correct socket pooling
369
524
  // NVR devices need separate sockets per channel
package/src/main.ts CHANGED
@@ -260,25 +260,22 @@ class ReolinkNativePlugin
260
260
 
261
261
  const deviceTypeSetting = settings.deviceType?.toString() || "Auto";
262
262
  const forceType =
263
- deviceTypeSetting === "Auto"
264
- ? undefined
265
- : deviceTypeSetting.toLowerCase();
263
+ deviceTypeSetting === "Auto" ? undefined : deviceTypeSetting;
266
264
 
267
265
  this.console.log(
268
266
  `[AutoDetect] Starting device type detection for ${ipAddress}...${forceType ? ` (forcing type: ${forceType})` : ""}`,
269
267
  );
270
268
  const { autoDetectDeviceType } = await import("@apocaliss92/nodelink-js");
271
- // 'Auto', 'NVR', 'Battery Camera', 'Regular Camera
269
+ // 'Auto', 'NVR', 'Battery Camera', 'Regular Camera'
270
+ const normalizedForceType = forceType?.trim().toLowerCase();
272
271
  const mode: AutoDetectMode =
273
- forceType === "Auto"
274
- ? "auto"
275
- : forceType === "Battery Camera"
276
- ? "udp"
277
- : forceType === "Regular Camera"
272
+ normalizedForceType === "battery camera"
273
+ ? "udp"
274
+ : normalizedForceType === "regular camera"
275
+ ? "tcp"
276
+ : normalizedForceType === "nvr"
278
277
  ? "tcp"
279
- : forceType === "NVR"
280
- ? "tcp"
281
- : "auto";
278
+ : "auto";
282
279
 
283
280
  const maxRetries = mode === "auto" ? 2 : 10;
284
281