@apocaliss92/scrypted-reolink-native 0.5.32 → 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/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/camera.ts +15 -9
- 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.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.
|
|
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"
|
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/camera.ts
CHANGED
|
@@ -3944,20 +3944,26 @@ export class ReolinkCamera
|
|
|
3944
3944
|
}
|
|
3945
3945
|
}
|
|
3946
3946
|
} else if (sleepStatus.state === "awake") {
|
|
3947
|
-
// Camera is awake
|
|
3947
|
+
// Camera is awake — but DON'T do any proactive networking
|
|
3948
|
+
// here. Sleep inference can flicker (idle_disconnect FIN
|
|
3949
|
+
// packets, brief housekeeping packets from the cam, etc.),
|
|
3950
|
+
// and reacting to every flicker with `ensureClient + wakeUp
|
|
3951
|
+
// + takePicture + battery + aux state` ends up waking the cam
|
|
3952
|
+
// far more than the original flicker — the reconnect alone
|
|
3953
|
+
// re-logs in, re-subscribes events, and triggers a fresh
|
|
3954
|
+
// sleep/awake cycle. Real motion-triggered wake-ups arrive
|
|
3955
|
+
// via SMTP / native push and have their own snapshot refresh
|
|
3956
|
+
// path (`refreshSnapshotOnMotion`). Periodic battery polling
|
|
3957
|
+
// is owned by `batteryUpdateTimer` (hourly), not by sleep
|
|
3958
|
+
// inference. So here we only flip the local state flag.
|
|
3948
3959
|
const wasSleeping = this.sleeping;
|
|
3949
3960
|
if (wasSleeping) {
|
|
3950
3961
|
this.getBaichuanLogger().log(`Camera is awake`);
|
|
3951
3962
|
this.sleeping = false;
|
|
3952
|
-
const client = await this.ensureClient();
|
|
3953
|
-
await client.wakeUp();
|
|
3954
|
-
await this.takePictureInternal(client);
|
|
3955
|
-
}
|
|
3956
|
-
|
|
3957
|
-
if (wasSleeping) {
|
|
3958
|
-
this.updateBatteryInfo().catch(() => {});
|
|
3959
|
-
this.alignAuxDevicesState().catch(() => {});
|
|
3960
3963
|
if (this.forceNewSnapshot) {
|
|
3964
|
+
// Honor an explicit refresh requested by some other path
|
|
3965
|
+
// (e.g. user-triggered or capability discovery) — they
|
|
3966
|
+
// set `forceNewSnapshot=true` and we serve it here.
|
|
3961
3967
|
this.takePicture().catch(() => {});
|
|
3962
3968
|
}
|
|
3963
3969
|
}
|
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
|
|