@apocaliss92/scrypted-reolink-native 0.5.33 → 0.5.36
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/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -2
- package/src/baichuan-base.ts +174 -19
- package/src/main.ts +9 -12
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.
|
|
3
|
+
"version": "0.5.36",
|
|
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.
|
|
47
|
+
"@apocaliss92/nodelink-js": "^0.4.35",
|
|
48
48
|
"@scrypted/common": "file:../../scrypted/common",
|
|
49
49
|
"@scrypted/rtsp": "file:../../scrypted/plugins/rtsp",
|
|
50
50
|
"@scrypted/sdk": "^0.3.118"
|
package/src/baichuan-base.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
274
|
-
? "
|
|
275
|
-
:
|
|
276
|
-
? "
|
|
277
|
-
:
|
|
272
|
+
normalizedForceType === "battery camera"
|
|
273
|
+
? "udp"
|
|
274
|
+
: normalizedForceType === "regular camera"
|
|
275
|
+
? "tcp"
|
|
276
|
+
: normalizedForceType === "nvr"
|
|
278
277
|
? "tcp"
|
|
279
|
-
:
|
|
280
|
-
? "tcp"
|
|
281
|
-
: "auto";
|
|
278
|
+
: "auto";
|
|
282
279
|
|
|
283
280
|
const maxRetries = mode === "auto" ? 2 : 10;
|
|
284
281
|
|