@apocaliss92/scrypted-reolink-native 0.2.7 → 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.7",
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
@@ -1478,10 +1478,12 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1478
1478
  }
1479
1479
 
1480
1480
  // Case 4: Standalone camera -> create its own socket using base class method
1481
- // For battery cameras, reuse the main client
1482
- // if (this.isBattery) {
1483
- // return await this.ensureClient();
1484
- // }
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
+ }
1485
1487
 
1486
1488
  // For TCP standalone cameras, use base class createStreamClient which manages stream clients per streamKey
1487
1489
  return await super.createStreamClient(streamKey);
@@ -1546,9 +1548,37 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1546
1548
  const wider = this.isOnNvr ? rtspChannel : undefined;
1547
1549
  const tele = this.isOnNvr ? rtspChannel : undefined;
1548
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
+
1549
1579
  baseOptions.compositeOptions = {
1550
- widerChannel: wider,
1551
- teleChannel: tele,
1580
+ widerChannel: derivedWider,
1581
+ teleChannel: derivedTele,
1552
1582
  pipPosition,
1553
1583
  pipSize,
1554
1584
  pipMargin,
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,