@apocaliss92/scrypted-reolink-native 0.3.10 → 0.3.12
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 +1 -1
- package/src/camera.ts +7 -0
- package/src/intercom.ts +87 -26
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/camera.ts
CHANGED
|
@@ -347,6 +347,13 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
347
347
|
type: 'number',
|
|
348
348
|
defaultValue: 1,
|
|
349
349
|
},
|
|
350
|
+
intercomMaxBacklogMs: {
|
|
351
|
+
subgroup: 'Advanced',
|
|
352
|
+
title: 'Intercom Max Backlog (ms)',
|
|
353
|
+
description: 'Maximum PCM backlog before dropping old audio to cap latency. Higher improves stability on slow systems but increases latency. Typical: 80-250. Requires restarting talk session to take effect.',
|
|
354
|
+
type: 'number',
|
|
355
|
+
defaultValue: 120,
|
|
356
|
+
},
|
|
350
357
|
// PTZ Presets
|
|
351
358
|
presets: {
|
|
352
359
|
subgroup: 'PTZ',
|
package/src/intercom.ts
CHANGED
|
@@ -7,20 +7,25 @@ import { ReolinkCamera } from "./camera";
|
|
|
7
7
|
// A small backlog avoids multi-second latency when the pipeline stalls.
|
|
8
8
|
// Aim for ~1 block of latency (a block is ~64ms at 16kHz for Reolink talk).
|
|
9
9
|
// This clamps the internal buffer to (approximately) one block.
|
|
10
|
-
const DEFAULT_MAX_BACKLOG_MS =
|
|
10
|
+
const DEFAULT_MAX_BACKLOG_MS = 120;
|
|
11
11
|
|
|
12
12
|
export class ReolinkBaichuanIntercom {
|
|
13
|
+
private intercomApi: ReolinkBaichuanApi | undefined;
|
|
13
14
|
private session: Awaited<ReturnType<ReolinkBaichuanApi["createTalkSession"]>> | undefined;
|
|
14
15
|
private ffmpeg: ChildProcessWithoutNullStreams | undefined;
|
|
15
16
|
private stopping: Promise<void> | undefined;
|
|
16
17
|
private loggedCodecInfo = false;
|
|
17
18
|
|
|
18
|
-
private
|
|
19
|
+
private maxBacklogMs = DEFAULT_MAX_BACKLOG_MS;
|
|
19
20
|
private maxBacklogBytes: number | undefined;
|
|
20
21
|
|
|
21
|
-
private sendChain: Promise<void> = Promise.resolve();
|
|
22
22
|
private pcmBuffer: Buffer = Buffer.alloc(0);
|
|
23
23
|
|
|
24
|
+
private pumping = false;
|
|
25
|
+
private pumpPromise: Promise<void> | undefined;
|
|
26
|
+
|
|
27
|
+
private lastBacklogClampLogAtMs = 0;
|
|
28
|
+
|
|
24
29
|
constructor(private camera: ReolinkCamera) {
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -39,13 +44,20 @@ export class ReolinkBaichuanIntercom {
|
|
|
39
44
|
await this.stop();
|
|
40
45
|
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
41
46
|
|
|
47
|
+
// IMPORTANT: intercom must run on its own independent Baichuan session (separate socket)
|
|
48
|
+
// to avoid interference with any other sessions (streams/events/etc).
|
|
49
|
+
const intercomStreamKey = `intercom_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
50
|
+
const intercomApi = await this.camera.withBaichuanRetry(async () => {
|
|
51
|
+
return await this.camera.createStreamClient(intercomStreamKey);
|
|
52
|
+
});
|
|
53
|
+
this.intercomApi = intercomApi;
|
|
54
|
+
|
|
42
55
|
// Best-effort: log codec requirements exposed by the camera.
|
|
43
56
|
// This mirrors neolink's source of truth: TalkAbility (cmd_id=10).
|
|
44
57
|
if (!this.loggedCodecInfo) {
|
|
45
58
|
this.loggedCodecInfo = true;
|
|
46
59
|
try {
|
|
47
|
-
const
|
|
48
|
-
const ability = await api.getTalkAbility(channel);
|
|
60
|
+
const ability = await intercomApi.getTalkAbility(channel);
|
|
49
61
|
const audioConfigs = ability.audioConfigList?.map((c) => ({
|
|
50
62
|
audioType: c.audioType,
|
|
51
63
|
sampleRate: c.sampleRate,
|
|
@@ -66,7 +78,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
const session = await this.camera.withBaichuanRetry(async () => {
|
|
69
|
-
const api =
|
|
81
|
+
const api = intercomApi;
|
|
70
82
|
|
|
71
83
|
// For UDP/battery cameras, wake up the camera if it's sleeping before creating talk session
|
|
72
84
|
if (this.camera.options?.type === 'battery') {
|
|
@@ -90,11 +102,23 @@ export class ReolinkBaichuanIntercom {
|
|
|
90
102
|
|
|
91
103
|
this.session = session;
|
|
92
104
|
this.pcmBuffer = Buffer.alloc(0);
|
|
93
|
-
this.
|
|
105
|
+
this.pumping = false;
|
|
106
|
+
this.pumpPromise = undefined;
|
|
94
107
|
|
|
95
108
|
const { audioConfig, blockSize, fullBlockSize } = session.info;
|
|
96
109
|
const sampleRate = audioConfig.sampleRate;
|
|
97
110
|
|
|
111
|
+
// Configurable backlog to trade latency vs stability.
|
|
112
|
+
// If the pipeline (ffmpeg decode + encode + send) can't keep up,
|
|
113
|
+
// dropping old audio avoids accumulating multi-second latency.
|
|
114
|
+
const configuredBacklog = Number(this.camera.storageSettings.values.intercomMaxBacklogMs);
|
|
115
|
+
if (Number.isFinite(configuredBacklog)) {
|
|
116
|
+
this.maxBacklogMs = Math.max(20, Math.min(5000, configuredBacklog));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.maxBacklogMs = DEFAULT_MAX_BACKLOG_MS;
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
// Mirror native-api.ts: receive PCM s16le from the forwarder and encode IMA ADPCM in JS.
|
|
99
123
|
const samplesPerBlock = blockSize * 2 + 1;
|
|
100
124
|
const bytesNeeded = samplesPerBlock * 2; // Int16 PCM
|
|
@@ -183,6 +207,9 @@ export class ReolinkBaichuanIntercom {
|
|
|
183
207
|
const session = this.session;
|
|
184
208
|
this.session = undefined;
|
|
185
209
|
|
|
210
|
+
const intercomApi = this.intercomApi;
|
|
211
|
+
this.intercomApi = undefined;
|
|
212
|
+
|
|
186
213
|
this.pcmBuffer = Buffer.alloc(0);
|
|
187
214
|
|
|
188
215
|
const sleepMs = async (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
@@ -207,12 +234,12 @@ export class ReolinkBaichuanIntercom {
|
|
|
207
234
|
}
|
|
208
235
|
|
|
209
236
|
try {
|
|
210
|
-
await Promise.race([this.
|
|
237
|
+
await Promise.race([this.pumpPromise ?? Promise.resolve(), sleepMs(250)]);
|
|
211
238
|
}
|
|
212
239
|
catch {
|
|
213
240
|
// ignore
|
|
214
241
|
}
|
|
215
|
-
this.
|
|
242
|
+
this.pumpPromise = undefined;
|
|
216
243
|
|
|
217
244
|
if (session) {
|
|
218
245
|
try {
|
|
@@ -222,6 +249,16 @@ export class ReolinkBaichuanIntercom {
|
|
|
222
249
|
logger.warn("Intercom session stop error", e?.message || String(e));
|
|
223
250
|
}
|
|
224
251
|
}
|
|
252
|
+
|
|
253
|
+
// Close the dedicated intercom API session to keep it independent and short-lived.
|
|
254
|
+
if (intercomApi) {
|
|
255
|
+
try {
|
|
256
|
+
await Promise.race([intercomApi.close(), sleepMs(2000)]);
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
logger.warn("Intercom client close error", e?.message || String(e));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
225
262
|
})().finally(() => {
|
|
226
263
|
this.stopping = undefined;
|
|
227
264
|
});
|
|
@@ -243,23 +280,43 @@ export class ReolinkBaichuanIntercom {
|
|
|
243
280
|
): void {
|
|
244
281
|
const logger = this.camera.getBaichuanLogger();
|
|
245
282
|
|
|
246
|
-
this.
|
|
247
|
-
|
|
248
|
-
|
|
283
|
+
if (this.session !== session) return;
|
|
284
|
+
|
|
285
|
+
this.pcmBuffer = this.pcmBuffer.length
|
|
286
|
+
? Buffer.concat([this.pcmBuffer, pcmChunk])
|
|
287
|
+
: pcmChunk;
|
|
288
|
+
|
|
289
|
+
// Cap backlog to keep latency bounded (drop oldest samples).
|
|
290
|
+
// IMPORTANT: do this on the shared buffer (not in a promise chain),
|
|
291
|
+
// otherwise old PCM chunks can pile up in queued closures and bypass
|
|
292
|
+
// this clamp, causing multi-second latency and degraded audio.
|
|
293
|
+
const maxBytes = this.maxBacklogBytes ?? bytesNeeded;
|
|
294
|
+
if (this.pcmBuffer.length > maxBytes) {
|
|
295
|
+
// Align to 16-bit samples.
|
|
296
|
+
const keep = maxBytes - (maxBytes % 2);
|
|
297
|
+
const dropped = this.pcmBuffer.length - keep;
|
|
298
|
+
this.pcmBuffer = this.pcmBuffer.subarray(this.pcmBuffer.length - keep);
|
|
299
|
+
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
if (now - this.lastBacklogClampLogAtMs > 2000) {
|
|
302
|
+
this.lastBacklogClampLogAtMs = now;
|
|
303
|
+
logger.warn("Intercom backlog clamped (dropping PCM)", {
|
|
304
|
+
droppedBytes: dropped,
|
|
305
|
+
keptBytes: keep,
|
|
306
|
+
maxBytes,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
249
310
|
|
|
250
|
-
|
|
251
|
-
? Buffer.concat([this.pcmBuffer, pcmChunk])
|
|
252
|
-
: pcmChunk;
|
|
311
|
+
if (this.pumping) return;
|
|
253
312
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
313
|
+
this.pumping = true;
|
|
314
|
+
this.pumpPromise = (async () => {
|
|
315
|
+
try {
|
|
316
|
+
while (true) {
|
|
317
|
+
if (this.session !== session) return;
|
|
318
|
+
if (this.pcmBuffer.length < bytesNeeded) return;
|
|
261
319
|
|
|
262
|
-
while (this.pcmBuffer.length >= bytesNeeded) {
|
|
263
320
|
const chunk = this.pcmBuffer.subarray(0, bytesNeeded);
|
|
264
321
|
this.pcmBuffer = this.pcmBuffer.subarray(bytesNeeded);
|
|
265
322
|
|
|
@@ -272,10 +329,14 @@ export class ReolinkBaichuanIntercom {
|
|
|
272
329
|
const adpcmChunk = this.encodeImaAdpcm(pcmSamples, blockSize);
|
|
273
330
|
await session.sendAudio(adpcmChunk);
|
|
274
331
|
}
|
|
275
|
-
}
|
|
276
|
-
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
277
334
|
logger.warn("Intercom PCM->ADPCM pipeline error", e?.message || String(e));
|
|
278
|
-
}
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
this.pumping = false;
|
|
338
|
+
}
|
|
339
|
+
})();
|
|
279
340
|
}
|
|
280
341
|
|
|
281
342
|
private buildFfmpegPcmArgs(
|