@apocaliss92/scrypted-reolink-native 0.3.12 → 0.3.13

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.3.12",
3
+ "version": "0.3.13",
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",
package/src/camera.ts CHANGED
@@ -341,19 +341,26 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
341
341
  defaultValue: [],
342
342
  },
343
343
  intercomBlocksPerPayload: {
344
- subgroup: 'Advanced',
345
- title: 'Intercom Blocks Per Payload',
344
+ subgroup: 'Intercom',
345
+ title: 'Blocks Per Payload',
346
346
  description: 'Lower reduces latency (more packets). Typical: 1-4. Requires restarting talk session to take effect.',
347
347
  type: 'number',
348
348
  defaultValue: 1,
349
349
  },
350
350
  intercomMaxBacklogMs: {
351
- subgroup: 'Advanced',
352
- title: 'Intercom Max Backlog (ms)',
351
+ subgroup: 'Intercom',
352
+ title: 'Max Backlog (ms)',
353
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
354
  type: 'number',
355
355
  defaultValue: 120,
356
356
  },
357
+ intercomGain: {
358
+ subgroup: 'Intercom',
359
+ title: 'Gain',
360
+ description: 'Output gain multiplier applied before encoding. 1.0 = normal, 2.0 ≈ +6dB, 0.5 ≈ -6dB. Requires restarting talk session to take effect.',
361
+ type: 'number',
362
+ defaultValue: 1.0,
363
+ },
357
364
  // PTZ Presets
358
365
  presets: {
359
366
  subgroup: 'PTZ',
package/src/intercom.ts CHANGED
@@ -33,6 +33,13 @@ export class ReolinkBaichuanIntercom {
33
33
  return Math.max(1, Math.min(8, this.camera.storageSettings.values.intercomBlocksPerPayload ?? 1));
34
34
  }
35
35
 
36
+ private get outputGain(): number {
37
+ const configured = Number(this.camera.storageSettings.values.intercomGain);
38
+ // Keep safe bounds: too high can clip and distort.
39
+ if (Number.isFinite(configured)) return Math.max(0.1, Math.min(10, configured));
40
+ return 1.0;
41
+ }
42
+
36
43
  async start(media: MediaObject): Promise<void> {
37
44
  const logger = this.camera.getBaichuanLogger();
38
45
 
@@ -44,13 +51,14 @@ export class ReolinkBaichuanIntercom {
44
51
  await this.stop();
45
52
  const channel = this.camera.storageSettings.values.rtspChannel;
46
53
 
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
+ try {
55
+ // IMPORTANT: intercom must run on its own independent Baichuan session (separate socket)
56
+ // to avoid interference with any other sessions (streams/events/etc).
57
+ const intercomStreamKey = `intercom_${Date.now()}_${Math.random().toString(16).slice(2)}`;
58
+ const intercomApi = await this.camera.withBaichuanRetry(async () => {
59
+ return await this.camera.createStreamClient(intercomStreamKey);
60
+ });
61
+ this.intercomApi = intercomApi;
54
62
 
55
63
  // Best-effort: log codec requirements exposed by the camera.
56
64
  // This mirrors neolink's source of truth: TalkAbility (cmd_id=10).
@@ -97,6 +105,9 @@ export class ReolinkBaichuanIntercom {
97
105
 
98
106
  return await api.createTalkSession(channel, {
99
107
  blocksPerPayload: this.blocksPerPayload,
108
+ // IMPORTANT: for dedicated intercom sessions, teardown should be owned by the socket/session.
109
+ // This mirrors stream behavior (closeApiOnTeardown) but for talk: session.stop() will close.
110
+ closeSocketOnStop: true,
100
111
  });
101
112
  });
102
113
 
@@ -155,9 +166,12 @@ export class ReolinkBaichuanIntercom {
155
166
 
156
167
  // IMPORTANT: incoming audio from Scrypted/WebRTC is typically Opus.
157
168
  // We must decode to PCM before IMA ADPCM encoding, otherwise it will be noise.
169
+ const gain = this.outputGain;
158
170
  const ffmpegArgs = this.buildFfmpegPcmArgs(ffmpegInput, {
159
171
  sampleRate,
160
172
  channels: 1,
173
+ gain,
174
+ logger,
161
175
  });
162
176
 
163
177
  logger.log("Intercom ffmpeg decode args", ffmpegArgs);
@@ -193,6 +207,12 @@ export class ReolinkBaichuanIntercom {
193
207
  });
194
208
 
195
209
  logger.log("Intercom started (ffmpeg decode -> PCM -> IMA ADPCM)");
210
+ }
211
+ catch (e) {
212
+ // Ensure the dedicated session gets torn down even if start fails half-way.
213
+ await this.stop();
214
+ throw e;
215
+ }
196
216
  }
197
217
 
198
218
  stop(): Promise<void> {
@@ -250,8 +270,10 @@ export class ReolinkBaichuanIntercom {
250
270
  }
251
271
  }
252
272
 
253
- // Close the dedicated intercom API session to keep it independent and short-lived.
254
- if (intercomApi) {
273
+ // Socket teardown is handled by session.stop() (closeSocketOnStop).
274
+ // Fallback cleanup: if we never created a session but we did create a dedicated client,
275
+ // ensure it doesn't leak.
276
+ if (!session && intercomApi) {
255
277
  try {
256
278
  await Promise.race([intercomApi.close(), sleepMs(2000)]);
257
279
  }
@@ -344,6 +366,8 @@ export class ReolinkBaichuanIntercom {
344
366
  options: {
345
367
  sampleRate: number;
346
368
  channels: number;
369
+ gain?: number;
370
+ logger?: any;
347
371
  },
348
372
  ): string[] {
349
373
  const inputArgs = ffmpegInput.inputArguments ?? [];
@@ -375,6 +399,16 @@ export class ReolinkBaichuanIntercom {
375
399
  throw new Error("FFmpegInput missing url/input");
376
400
  }
377
401
 
402
+ const gain = options.gain ?? 1.0;
403
+ const hasExistingAudioFilter = sanitizedArgs.includes("-af") || sanitizedArgs.includes("-filter:a") || sanitizedArgs.includes("-filter_complex");
404
+ const gainArgs = (gain !== 1.0)
405
+ ? (
406
+ hasExistingAudioFilter
407
+ ? (options.logger?.warn?.("Intercom gain skipped: FFmpegInput already contains audio filters") ?? undefined, [])
408
+ : ["-filter:a", `volume=${gain}`]
409
+ )
410
+ : [];
411
+
378
412
  return [
379
413
  ...sanitizedArgs,
380
414
  "-i", url,
@@ -387,6 +421,7 @@ export class ReolinkBaichuanIntercom {
387
421
  "-flush_packets", "1",
388
422
 
389
423
  "-vn", "-sn", "-dn",
424
+ ...gainArgs,
390
425
  "-acodec", "pcm_s16le",
391
426
  "-ar", options.sampleRate.toString(),
392
427
  "-ac", options.channels.toString(),