@apocaliss92/scrypted-reolink-native 0.4.28 → 0.4.30
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/.claude/settings.local.json +12 -0
- package/CLAUDE.md +75 -0
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +38 -31
- package/src/camera.ts +16 -32
- package/src/intercom.ts +41 -0
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/baichuan-base.ts
CHANGED
|
@@ -277,26 +277,39 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
277
277
|
return await this.ensureClientPromise;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
// Reuse existing
|
|
281
|
-
// Check this AFTER checking the promise to avoid race conditions
|
|
280
|
+
// Reuse existing API if possible
|
|
282
281
|
if (this.baichuanApi) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Only reuse if both conditions are true
|
|
287
|
-
if (isConnected && isLoggedIn) {
|
|
282
|
+
// Already connected and ready → reuse immediately
|
|
283
|
+
if (this.baichuanApi.isReady) {
|
|
288
284
|
logger.debug(
|
|
289
285
|
`ensureBaichuanClient: reusing existing client (caller: ${caller})`,
|
|
290
286
|
);
|
|
291
287
|
return this.baichuanApi;
|
|
292
288
|
}
|
|
293
289
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
290
|
+
// API was explicitly closed → destroy and recreate from scratch
|
|
291
|
+
if (this.baichuanApi.isClosed) {
|
|
292
|
+
logger.log(
|
|
293
|
+
`API is closed, creating new instance (caller: ${caller})`,
|
|
294
|
+
);
|
|
295
|
+
await this.cleanupBaichuanApi();
|
|
296
|
+
} else {
|
|
297
|
+
// Socket disconnected but API still valid → let the library reconnect
|
|
298
|
+
// the general socket internally (preserves NVR/multifocal flags,
|
|
299
|
+
// streaming sockets, and all library-side state)
|
|
300
|
+
try {
|
|
301
|
+
logger.log(
|
|
302
|
+
`General socket lost, reconnecting via ensureConnected (caller: ${caller})`,
|
|
303
|
+
);
|
|
304
|
+
await this.baichuanApi.ensureConnected();
|
|
305
|
+
return this.baichuanApi;
|
|
306
|
+
} catch (e) {
|
|
307
|
+
logger.log(
|
|
308
|
+
`ensureConnected failed: ${e instanceof Error ? e.message : String(e)}, creating new instance`,
|
|
309
|
+
);
|
|
310
|
+
await this.cleanupBaichuanApi();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
300
313
|
}
|
|
301
314
|
|
|
302
315
|
logger.log(`ensureBaichuanClient: creating NEW client (caller: ${caller})`);
|
|
@@ -543,11 +556,14 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
543
556
|
|
|
544
557
|
// Stop event check interval
|
|
545
558
|
this.stopEventCheck();
|
|
546
|
-
|
|
547
|
-
// Reset state
|
|
559
|
+
} finally {
|
|
560
|
+
// Reset state ALWAYS — even if an earlier step threw.
|
|
561
|
+
// This prevents the client from being permanently "stuck":
|
|
562
|
+
// if baichuanApi remains set with a destroyed socket pool,
|
|
563
|
+
// ensureBaichuanClient() will repeatedly crash on the `client` getter
|
|
564
|
+
// and never recover.
|
|
548
565
|
this.baichuanApi = undefined;
|
|
549
566
|
this.ensureClientPromise = undefined;
|
|
550
|
-
} finally {
|
|
551
567
|
this.cleanupInProgress = false;
|
|
552
568
|
}
|
|
553
569
|
}
|
|
@@ -558,13 +574,9 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
558
574
|
private getAllActiveConnections(): ReolinkBaichuanApi[] {
|
|
559
575
|
const connections: ReolinkBaichuanApi[] = [];
|
|
560
576
|
|
|
561
|
-
// Add main connection if exists and is
|
|
562
|
-
if (this.baichuanApi) {
|
|
563
|
-
|
|
564
|
-
const isLoggedIn = this.baichuanApi.client.loggedIn;
|
|
565
|
-
if (isConnected && isLoggedIn) {
|
|
566
|
-
connections.push(this.baichuanApi);
|
|
567
|
-
}
|
|
577
|
+
// Add main connection if it exists and is ready (safe, never throws)
|
|
578
|
+
if (this.baichuanApi?.isReady) {
|
|
579
|
+
connections.push(this.baichuanApi);
|
|
568
580
|
}
|
|
569
581
|
|
|
570
582
|
return connections;
|
|
@@ -715,14 +727,10 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
715
727
|
return; // Connection changed, stop this interval
|
|
716
728
|
}
|
|
717
729
|
|
|
718
|
-
// If event subscription is not active
|
|
719
|
-
// event
|
|
720
|
-
//
|
|
721
|
-
// reconnection (e.g. ECONNREFUSED) but the connection was later
|
|
722
|
-
// re-established by the streaming infrastructure.
|
|
730
|
+
// If event subscription is not active, skip — the library's
|
|
731
|
+
// built-in event watchdog handles auto-recovery with exponential
|
|
732
|
+
// backoff (including after reconnection / camera reboot).
|
|
723
733
|
if (!this.eventSubscriptionActive) {
|
|
724
|
-
// The library's built-in event watchdog handles auto-recovery
|
|
725
|
-
// with exponential backoff. No need to retry here.
|
|
726
734
|
return;
|
|
727
735
|
}
|
|
728
736
|
|
|
@@ -849,7 +857,6 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
849
857
|
*/
|
|
850
858
|
async unsubscribeFromEvents(silent: boolean = false): Promise<void> {
|
|
851
859
|
const logger = this.getBaichuanLogger();
|
|
852
|
-
const callbacks = this.getConnectionCallbacks();
|
|
853
860
|
|
|
854
861
|
// Only unsubscribe if we have an active subscription
|
|
855
862
|
if (
|
package/src/camera.ts
CHANGED
|
@@ -3507,38 +3507,22 @@ export class ReolinkCamera
|
|
|
3507
3507
|
}
|
|
3508
3508
|
|
|
3509
3509
|
async resetBaichuanClient(reason?: any): Promise<void> {
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
);
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
this.unsubscribedToEvents?.();
|
|
3531
|
-
await this.baichuanApi?.close();
|
|
3532
|
-
} catch (e) {
|
|
3533
|
-
this.getBaichuanLogger().error(
|
|
3534
|
-
"Error closing Baichuan client during reset",
|
|
3535
|
-
e?.message || String(e),
|
|
3536
|
-
);
|
|
3537
|
-
} finally {
|
|
3538
|
-
this.baichuanApi = undefined;
|
|
3539
|
-
this.connectionTime = undefined;
|
|
3540
|
-
this.ensureClientPromise = undefined;
|
|
3541
|
-
}
|
|
3510
|
+
// Delegate to cleanupBaichuanApi() which properly:
|
|
3511
|
+
// - unsubscribes from events (offSimpleEvent)
|
|
3512
|
+
// - calls onBeforeCleanup (closes streams, etc.)
|
|
3513
|
+
// - removes close/error listeners
|
|
3514
|
+
// - stops connection maintenance timers (ping, auto-renewal)
|
|
3515
|
+
// - stops event check interval
|
|
3516
|
+
// - uses cleanupInProgress guard to prevent races
|
|
3517
|
+
// Previously this method called api.close() directly, which left
|
|
3518
|
+
// listeners attached, timers running, and could race with the
|
|
3519
|
+
// close handler's own cleanupBaichuanApi() call.
|
|
3520
|
+
await this.cleanupBaichuanApi();
|
|
3521
|
+
|
|
3522
|
+
// Ensure state is fully reset even if cleanup was a no-op
|
|
3523
|
+
// (e.g. baichuanApi was already undefined)
|
|
3524
|
+
this.connectionTime = undefined;
|
|
3525
|
+
this.ensureClientPromise = undefined;
|
|
3542
3526
|
|
|
3543
3527
|
if (reason) {
|
|
3544
3528
|
const message = reason?.message || reason?.toString?.() || reason;
|
package/src/intercom.ts
CHANGED
|
@@ -205,9 +205,32 @@ export class ReolinkBaichuanIntercom {
|
|
|
205
205
|
|
|
206
206
|
this.ffmpeg = ffmpeg;
|
|
207
207
|
|
|
208
|
+
// Startup timeout: if ffmpeg has not produced any PCM output within
|
|
209
|
+
// this window it is likely stuck on RTSP connection negotiation.
|
|
210
|
+
// Kill it early with a clear error rather than hanging silently.
|
|
211
|
+
const STARTUP_TIMEOUT_MS = 10_000;
|
|
212
|
+
let receivedFirstPcm = false;
|
|
213
|
+
const startupTimer = setTimeout(() => {
|
|
214
|
+
if (!receivedFirstPcm && this.ffmpeg === ffmpeg) {
|
|
215
|
+
logger.warn(
|
|
216
|
+
`Intercom ffmpeg startup timeout (${STARTUP_TIMEOUT_MS}ms): no PCM data received, killing process`,
|
|
217
|
+
);
|
|
218
|
+
try {
|
|
219
|
+
ffmpeg.kill("SIGKILL");
|
|
220
|
+
} catch {
|
|
221
|
+
// ignore
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}, STARTUP_TIMEOUT_MS);
|
|
225
|
+
|
|
208
226
|
ffmpeg.stdout.on("data", (chunk: Buffer) => {
|
|
209
227
|
if (this.session !== session) return;
|
|
210
228
|
if (!chunk?.length) return;
|
|
229
|
+
if (!receivedFirstPcm) {
|
|
230
|
+
receivedFirstPcm = true;
|
|
231
|
+
clearTimeout(startupTimer);
|
|
232
|
+
logger.log("Intercom ffmpeg: first PCM data received");
|
|
233
|
+
}
|
|
211
234
|
this.enqueuePcm(session, chunk, bytesNeeded, blockSize);
|
|
212
235
|
});
|
|
213
236
|
|
|
@@ -220,6 +243,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
220
243
|
});
|
|
221
244
|
|
|
222
245
|
ffmpeg.on("exit", (code, signal) => {
|
|
246
|
+
clearTimeout(startupTimer);
|
|
223
247
|
logger.warn(`Intercom ffmpeg exited code=${code} signal=${signal}`);
|
|
224
248
|
this.stop().catch(() => {});
|
|
225
249
|
});
|
|
@@ -380,9 +404,20 @@ export class ReolinkBaichuanIntercom {
|
|
|
380
404
|
|
|
381
405
|
// FFmpegInput may already contain one or more "-i" entries.
|
|
382
406
|
// For intercom decode, we only need a single input and only the first audio stream.
|
|
407
|
+
//
|
|
408
|
+
// IMPORTANT: We must also strip `-rtsp_transport tcp` (and similar transport
|
|
409
|
+
// overrides) inherited from the upstream FFmpegInput. The RTSP URL points at a
|
|
410
|
+
// local relay server (e.g. from the WebRTC plugin) that typically only supports
|
|
411
|
+
// RTP/UDP. Forcing TCP causes ffmpeg to hang on SETUP negotiation (the relay
|
|
412
|
+
// replies 461 Unsupported Transport) and eventually get SIGKILLed with no audio
|
|
413
|
+
// ever reaching the intercom pipeline.
|
|
383
414
|
const sanitizedArgs: string[] = [];
|
|
384
415
|
let chosenInput: string | undefined;
|
|
385
416
|
|
|
417
|
+
// Set of pre-input flags that take a value argument and should be stripped
|
|
418
|
+
// because they conflict with the local RTSP relay's transport capabilities.
|
|
419
|
+
const stripWithValue = new Set(["-rtsp_transport"]);
|
|
420
|
+
|
|
386
421
|
for (let i = 0; i < inputArgs.length; i++) {
|
|
387
422
|
const arg = inputArgs[i];
|
|
388
423
|
if (arg === "-i") {
|
|
@@ -397,6 +432,12 @@ export class ReolinkBaichuanIntercom {
|
|
|
397
432
|
}
|
|
398
433
|
}
|
|
399
434
|
|
|
435
|
+
// Strip transport-related flags that break the local RTSP relay.
|
|
436
|
+
if (stripWithValue.has(arg)) {
|
|
437
|
+
i++; // skip the value argument as well
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
400
441
|
sanitizedArgs.push(arg);
|
|
401
442
|
}
|
|
402
443
|
|