@apocaliss92/scrypted-reolink-native 0.2.7 → 0.2.9

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.9",
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
@@ -618,15 +618,38 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
618
618
  },
619
619
  pipMargin: {
620
620
  title: 'PIP Margin',
621
- description: 'Margin from edge in pixels',
621
+ description: 'Margin from edge as a fraction of the output size (e.g. 0.01 = 1%). Values > 1 are treated as pixels (legacy).',
622
622
  type: 'number',
623
- defaultValue: 10,
623
+ defaultValue: 0.01,
624
624
  group: 'Composite stream',
625
625
  hide: true,
626
626
  onPut: async () => {
627
627
  this.scheduleStreamManagerRestart('pipMargin changed');
628
628
  },
629
629
  },
630
+
631
+ compositeAssumeH264: {
632
+ title: 'Composite: Assume H.264 Inputs',
633
+ description: 'Assume both wider+tele inputs are H.264 (skips codec detection). Recommended when using sub+sub on TrackMix. If inputs are actually H.265, the composite may fail to start.',
634
+ type: 'boolean',
635
+ defaultValue: true,
636
+ group: 'Composite stream',
637
+ hide: true,
638
+ onPut: async () => {
639
+ this.scheduleStreamManagerRestart('compositeAssumeH264 changed');
640
+ },
641
+ },
642
+ compositeDisableTranscode: {
643
+ title: 'Composite: Disable Codec Transcode (Best-effort)',
644
+ description: 'Best-effort knob. Overlay requires re-encode in ffmpeg; this option only avoids HEVC->H264 codec assumptions when possible. Leave off unless you know what you are doing.',
645
+ type: 'boolean',
646
+ defaultValue: false,
647
+ group: 'Composite stream',
648
+ hide: true,
649
+ onPut: async () => {
650
+ this.scheduleStreamManagerRestart('compositeDisableTranscode changed');
651
+ },
652
+ },
630
653
  });
631
654
 
632
655
  ptzPresets = new ReolinkPtzPresets(this);
@@ -1478,10 +1501,12 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1478
1501
  }
1479
1502
 
1480
1503
  // 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
- // }
1504
+ // For battery (BCUDP) cameras, streaming must be keyed by streamKey.
1505
+ // Do NOT reuse ensureClient(): composite needs two concurrent streams, and single-lens streams
1506
+ // should reuse the same API that composite already created for that same streamKey.
1507
+ if (this.isBattery) {
1508
+ return await super.createStreamClient(streamKey);
1509
+ }
1485
1510
 
1486
1511
  // For TCP standalone cameras, use base class createStreamClient which manages stream clients per streamKey
1487
1512
  return await super.createStreamClient(streamKey);
@@ -1539,20 +1564,52 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1539
1564
  };
1540
1565
 
1541
1566
  if (this.isMultiFocal) {
1542
- const { pipPosition, pipSize, pipMargin, rtspChannel } = this.storageSettings.values;
1567
+ const { pipPosition, pipSize, pipMargin, rtspChannel, compositeAssumeH264, compositeDisableTranscode } = this.storageSettings.values;
1543
1568
 
1544
1569
  // On NVR/Hub, TrackMix lenses are selected via stream variant, not via a separate channel.
1545
1570
  // Use rtspChannel for BOTH wide and tele so the library can request tele via streamType/variant.
1546
1571
  const wider = this.isOnNvr ? rtspChannel : undefined;
1547
1572
  const tele = this.isOnNvr ? rtspChannel : undefined;
1548
1573
 
1574
+ // On standalone TrackMix/Duo, lens channels are often separate, but they are not always 0/1.
1575
+ // Prefer using the discovered multifocalInfo mapping when available.
1576
+ let derivedWider: number | undefined = wider;
1577
+ let derivedTele: number | undefined = tele;
1578
+ if (!this.isOnNvr) {
1579
+ try {
1580
+ const info: any = this.storageSettings.values.multifocalInfo;
1581
+ const channels: any[] = Array.isArray(info?.channels) ? info.channels : [];
1582
+
1583
+ const wideCh = channels.find((c) => c?.lensType === 'wide')?.channel
1584
+ ?? channels.find((c) => c?.variantType === 'default')?.channel;
1585
+ const teleCh = channels.find((c) => c?.lensType === 'telephoto')?.channel
1586
+ ?? channels.find((c) => c?.variantType === 'telephoto')?.channel;
1587
+
1588
+ if (Number.isFinite(wideCh)) derivedWider = wideCh;
1589
+ if (Number.isFinite(teleCh)) derivedTele = teleCh;
1590
+
1591
+ // Avoid setting nonsense; leave undefined to fall back to library defaults.
1592
+ if (derivedWider === derivedTele) {
1593
+ // Keep undefined behavior (defaults inside the library) unless we are on NVR.
1594
+ derivedWider = undefined;
1595
+ derivedTele = undefined;
1596
+ }
1597
+ } catch {
1598
+ // ignore and fall back to defaults
1599
+ }
1600
+ }
1601
+
1549
1602
  baseOptions.compositeOptions = {
1550
- widerChannel: wider,
1551
- teleChannel: tele,
1603
+ widerChannel: derivedWider,
1604
+ teleChannel: derivedTele,
1552
1605
  pipPosition,
1553
1606
  pipSize,
1554
1607
  pipMargin,
1555
1608
  onNvr: this.isOnNvr,
1609
+ // Prefer H.264 for composite (sub+sub by default) to reduce GOP latency.
1610
+ forceH264: true,
1611
+ assumeH264Inputs: compositeAssumeH264 ?? true,
1612
+ disableTranscode: compositeDisableTranscode ?? false,
1556
1613
  };
1557
1614
  }
1558
1615
 
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,