@apocaliss92/scrypted-reolink-native 0.2.6 → 0.2.8

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.2.6",
3
+ "version": "0.2.8",
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
@@ -207,6 +207,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
207
207
  // Basic connection settings
208
208
  ipAddress: {
209
209
  title: 'IP Address',
210
+ hide: true,
210
211
  type: 'string',
211
212
  onPut: async () => {
212
213
  await this.credentialsChanged();
@@ -214,6 +215,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
214
215
  },
215
216
  username: {
216
217
  type: 'string',
218
+ hide: true,
217
219
  title: 'Username',
218
220
  onPut: async () => {
219
221
  await this.credentialsChanged();
@@ -221,6 +223,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
221
223
  },
222
224
  password: {
223
225
  type: 'password',
226
+ hide: true,
224
227
  title: 'Password',
225
228
  onPut: async () => {
226
229
  await this.credentialsChanged();
@@ -1475,10 +1478,12 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1475
1478
  }
1476
1479
 
1477
1480
  // Case 4: Standalone camera -> create its own socket using base class method
1478
- // For battery cameras, reuse the main client
1479
- // if (this.isBattery) {
1480
- // return await this.ensureClient();
1481
- // }
1481
+ // For battery (BCUDP) cameras, streaming must be keyed by streamKey.
1482
+ // Do NOT reuse ensureClient(): composite needs two concurrent streams, and single-lens streams
1483
+ // should reuse the same API that composite already created for that same streamKey.
1484
+ if (this.isBattery) {
1485
+ return await super.createStreamClient(streamKey);
1486
+ }
1482
1487
 
1483
1488
  // For TCP standalone cameras, use base class createStreamClient which manages stream clients per streamKey
1484
1489
  return await super.createStreamClient(streamKey);
@@ -1543,9 +1548,37 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1543
1548
  const wider = this.isOnNvr ? rtspChannel : undefined;
1544
1549
  const tele = this.isOnNvr ? rtspChannel : undefined;
1545
1550
 
1551
+ // On standalone TrackMix/Duo, lens channels are often separate, but they are not always 0/1.
1552
+ // Prefer using the discovered multifocalInfo mapping when available.
1553
+ let derivedWider: number | undefined = wider;
1554
+ let derivedTele: number | undefined = tele;
1555
+ if (!this.isOnNvr) {
1556
+ try {
1557
+ const info: any = this.storageSettings.values.multifocalInfo;
1558
+ const channels: any[] = Array.isArray(info?.channels) ? info.channels : [];
1559
+
1560
+ const wideCh = channels.find((c) => c?.lensType === 'wide')?.channel
1561
+ ?? channels.find((c) => c?.variantType === 'default')?.channel;
1562
+ const teleCh = channels.find((c) => c?.lensType === 'telephoto')?.channel
1563
+ ?? channels.find((c) => c?.variantType === 'telephoto')?.channel;
1564
+
1565
+ if (Number.isFinite(wideCh)) derivedWider = wideCh;
1566
+ if (Number.isFinite(teleCh)) derivedTele = teleCh;
1567
+
1568
+ // Avoid setting nonsense; leave undefined to fall back to library defaults.
1569
+ if (derivedWider === derivedTele) {
1570
+ // Keep undefined behavior (defaults inside the library) unless we are on NVR.
1571
+ derivedWider = undefined;
1572
+ derivedTele = undefined;
1573
+ }
1574
+ } catch {
1575
+ // ignore and fall back to defaults
1576
+ }
1577
+ }
1578
+
1546
1579
  baseOptions.compositeOptions = {
1547
- widerChannel: wider,
1548
- teleChannel: tele,
1580
+ widerChannel: derivedWider,
1581
+ teleChannel: derivedTele,
1549
1582
  pipPosition,
1550
1583
  pipSize,
1551
1584
  pipMargin,
@@ -2538,11 +2571,20 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2538
2571
  // Extract variant from stream ID or URL if present (e.g., "autotrack" from "native_autotrack_main" or "?variant=autotrack")
2539
2572
  let variant = extractVariantFromStreamId(selected.id, selected.url);
2540
2573
 
2541
- // Fallback: if no variant found in stream ID/URL, use variantType from device settings
2542
- // This is important for multi-focal devices where the device has a variantType setting
2574
+ // Fallback: if no variant found in stream ID/URL, use variantType from device settings.
2575
+ // IMPORTANT:
2576
+ // - On NVR/Hub multifocal setups, the tele lens is often selected via a variant (autotrack/telephoto) on the same channel.
2577
+ // - On standalone TrackMix (no NVR), the tele lens is selected via channel=1 (no variant).
2578
+ // Forcing a variant on standalone can result in a started stream with no frames.
2543
2579
  if (!variant && this.storageSettings.values.variantType && this.storageSettings.values.variantType !== 'default') {
2544
- variant = this.storageSettings.values.variantType as 'autotrack' | 'telephoto';
2545
- logger.log(`Using variant from device settings: '${variant}' (not found in stream ID/URL)`);
2580
+ if (this.isOnNvr) {
2581
+ variant = this.storageSettings.values.variantType as 'autotrack' | 'telephoto';
2582
+ logger.log(`Using variant from device settings: '${variant}' (not found in stream ID/URL)`);
2583
+ } else {
2584
+ logger.log(
2585
+ `Ignoring device variantType '${this.storageSettings.values.variantType}' for standalone stream (channel-based lens selection)`
2586
+ );
2587
+ }
2546
2588
  }
2547
2589
 
2548
2590
  logger.log(`Stream selection: id='${selected.id}', profile='${profile}', channel=${channel}, variant='${variant || 'default'}'`);
@@ -2725,8 +2767,9 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2725
2767
  this.storageSettings.settings.pipSize.hide = !this.isMultiFocal;
2726
2768
  this.storageSettings.settings.pipMargin.hide = !this.isMultiFocal;
2727
2769
 
2728
- this.storageSettings.settings.uid.hide = !this.isBattery || this.isOnNvr;
2729
- this.storageSettings.settings.discoveryMethod.hide = !this.isBattery && !this.nvrDevice;
2770
+ const hideUid = !this.isBattery || this.isOnNvr || !!this.multiFocalDevice
2771
+ this.storageSettings.settings.uid.hide = hideUid;
2772
+ this.storageSettings.settings.discoveryMethod.hide = hideUid;
2730
2773
 
2731
2774
  if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
2732
2775
  try {
package/src/multiFocal.ts CHANGED
@@ -224,6 +224,14 @@ export class ReolinkNativeMultiFocalDevice extends ReolinkCamera implements Sett
224
224
  return await this.nvrDevice.createStreamClient(streamKey);
225
225
  }
226
226
 
227
+ // For multifocal battery cams (BCUDP), reuse the main client to avoid D2C_DISC storms.
228
+ if (this.isBattery) {
229
+ // For battery (BCUDP) cameras, streaming must be keyed by streamKey.
230
+ // Do NOT reuse ensureClient(): composite needs two concurrent streams, and single-lens streams
231
+ // should reuse the same API that composite already created for that same streamKey.
232
+ return await super.createStreamClient(streamKey);
233
+ }
234
+
227
235
  // Otherwise, use base class createStreamClient which manages stream clients per streamKey
228
236
  return await super.createStreamClient(streamKey);
229
237
  }
@@ -337,25 +337,60 @@ export class StreamManager {
337
337
 
338
338
  const isComposite = options.channel === undefined;
339
339
 
340
- // For composite streams, MUST use two distinct Baichuan sessions (widerApi and teleApi).
341
- // Otherwise cmd_id=3 frames can mix when streamType overlaps (wide/tele alternation/corruption).
342
- // Each stream needs its own dedicated socket to avoid frame mixing.
343
- // Create separate streamKeys for wider and tele to ensure distinct sockets:
344
- // Format: composite_${variantType}_${profile}_wider and composite_${variantType}_${profile}_tele
345
- const compositeApis = isComposite
346
- ? {
347
- widerApi: await this.opts.createStreamClient(`${streamKey}_wider`),
348
- teleApi: await this.opts.createStreamClient(`${streamKey}_tele`),
349
- }
340
+ // For composite streams, we may want two distinct Baichuan sessions (wider + tele)
341
+ // to avoid frame mixing on some firmwares. On BCUDP/battery devices, extra sessions
342
+ // can be harmful; in that case, createStreamClient may return the same underlying client.
343
+ //
344
+ // IMPORTANT: Use the same per-lens streamKey format as regular streams so that later
345
+ // requests for a single lens can reuse these same cached APIs.
346
+ const compositeWiderChannel = options.compositeOptions?.widerChannel ?? 0;
347
+ const compositeTeleChannel = options.compositeOptions?.teleChannel ?? 1;
348
+ const compositeTeleIsVariantOnSameChannel =
349
+ Boolean(options.compositeOptions?.onNvr) || compositeTeleChannel === compositeWiderChannel;
350
+
351
+ const compositeWiderStreamKey = `${compositeWiderChannel}_${profile}`;
352
+ const compositeTeleVariant = compositeTeleIsVariantOnSameChannel
353
+ ? (options.variant && options.variant !== 'default' ? options.variant : 'telephoto')
350
354
  : undefined;
355
+ const compositeTeleStreamKey = compositeTeleVariant
356
+ ? `${compositeTeleChannel}_${compositeTeleVariant}_${profile}`
357
+ : `${compositeTeleChannel}_${profile}`;
358
+
359
+ // For composite streams, using two distinct Baichuan sessions can avoid frame mixing on some firmwares.
360
+ // However, for UDP/battery devices extra BCUDP sessions can trigger storms; if we detect the same
361
+ // underlying client, fall back to single-session composite.
362
+ let compositeApis:
363
+ | {
364
+ widerApi: ReolinkBaichuanApi;
365
+ teleApi: ReolinkBaichuanApi;
366
+ }
367
+ | undefined;
368
+ if (isComposite) {
369
+ try {
370
+ const widerApi = await this.opts.createStreamClient(compositeWiderStreamKey);
371
+ const teleApi = await this.opts.createStreamClient(compositeTeleStreamKey);
372
+
373
+ const sameApiObject = widerApi === teleApi;
374
+ const sameUnderlyingClient = (widerApi as any)?.client && (teleApi as any)?.client
375
+ ? (widerApi as any).client === (teleApi as any).client
376
+ : false;
377
+
378
+ if (!sameApiObject && !sameUnderlyingClient) {
379
+ compositeApis = { widerApi, teleApi };
380
+ } else {
381
+ // Likely a shared/battery connection: avoid forcing multi-session behavior.
382
+ compositeApis = undefined;
383
+ }
384
+ } catch {
385
+ // Best-effort: if creating dedicated sessions fails, fall back to single-session composite.
386
+ compositeApis = undefined;
387
+ }
388
+ }
351
389
 
352
- // For non-composite streams, create a single API client
353
- // For composite streams, api is still required as baseApi but widerApi and teleApi are used instead
354
- // Pass streamKey to createStreamClient - it contains all necessary information (profile, variantType, channel)
355
- // For composite streams, streamKey format: composite_${variantType}_${profile}
356
- // For regular streams, streamKey format: channel_${channel}_${profile}_${variantType} or similar
390
+ // For non-composite streams, create a single API client.
391
+ // For composite streams, base api must be a real lens streamKey (not the composite RFC key).
357
392
  const api = isComposite
358
- ? compositeApis.widerApi // For composite, use widerApi as baseApi (it will be overridden by compositeApis)
393
+ ? (compositeApis?.widerApi ?? await this.opts.createStreamClient(compositeWiderStreamKey))
359
394
  : await this.opts.createStreamClient(streamKey);
360
395
 
361
396
  const { createRfc4571TcpServer } = await import('@apocaliss92/reolink-baichuan-js');
@@ -364,17 +399,14 @@ export class StreamManager {
364
399
 
365
400
  // If connection is shared, don't close it when stream teardown happens
366
401
  // For composite, we create dedicated APIs even if the device uses a shared main connection.
367
- // Ensure they are closed on teardown.
368
- const closeApiOnTeardown = isComposite ? true : !(this.opts.sharedConnection ?? false);
402
+ // On battery/BCUDP (sharedConnection=true), prefer keeping them alive to avoid reconnect storms.
403
+ const closeApiOnTeardown = isComposite
404
+ ? (Boolean(compositeApis) && !(this.opts.sharedConnection ?? false))
405
+ : !(this.opts.sharedConnection ?? false);
369
406
 
370
407
  let created: any;
371
408
  try {
372
- const compositeOptions = isComposite
373
- ? {
374
- ...(options.compositeOptions ?? {}),
375
- forceH264: true,
376
- }
377
- : undefined;
409
+ const compositeOptions = isComposite ? options.compositeOptions : undefined;
378
410
 
379
411
  created = await createRfc4571TcpServer({
380
412
  api,