@apocaliss92/scrypted-reolink-native 0.1.31 → 0.1.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
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",
@@ -324,7 +324,4 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
324
324
  return fn(client);
325
325
  }
326
326
 
327
- async createStreamClient(): Promise<ReolinkBaichuanApi> {
328
- return await this.ensureClient();
329
- }
330
327
  }
package/src/camera.ts CHANGED
@@ -74,28 +74,6 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
74
74
  }
75
75
 
76
76
 
77
- async createStreamClient(): Promise<ReolinkBaichuanApi> {
78
- const { ipAddress, username, password } = this.storageSettings.values;
79
- const logger = this.getBaichuanLogger();
80
-
81
- const debugOptions = this.getBaichuanDebugOptions();
82
- const api = await createBaichuanApi(
83
- {
84
- inputs: {
85
- host: ipAddress,
86
- username: username,
87
- password: password,
88
- logger,
89
- debugOptions
90
- },
91
- transport: 'tcp',
92
- },
93
- );
94
- await api.login();
95
-
96
- return api;
97
- }
98
-
99
77
  private passiveRefreshTimer: ReturnType<typeof setTimeout> | undefined;
100
78
 
101
79
  async release() {
package/src/common.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
2
+ import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips, VideoClipThumbnailOptions, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import path from 'path';
5
5
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
6
6
  import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
7
- import { normalizeUid, type BaichuanTransport } from "./connect";
7
+ import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
8
8
  import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
9
9
  import { ReolinkBaichuanIntercom } from "./intercom";
10
10
  import ReolinkNativePlugin from "./main";
@@ -189,7 +189,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
189
189
  }
190
190
  }
191
191
 
192
- export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot {
192
+ export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot, VideoClips {
193
193
  storageSettings = new StorageSettings(this, {
194
194
  // Basic connection settings
195
195
  ipAddress: {
@@ -250,28 +250,40 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
250
250
  description: 'Relative size of the PIP overlay (0.1 = 10%, 0.3 = 30%, etc.)',
251
251
  type: 'number',
252
252
  defaultValue: 0.25,
253
- hide: true, // Only show for multifocal devices via getAdditionalSettings
253
+ hide: true,
254
+ onPut: async () => {
255
+ this.scheduleStreamManagerRestart('pipSize changed');
256
+ },
254
257
  },
255
258
  pipMargin: {
256
259
  title: 'PIP Margin',
257
260
  description: 'Margin from edge in pixels',
258
261
  type: 'number',
259
262
  defaultValue: 10,
260
- hide: true, // Only show for multifocal devices via getAdditionalSettings
263
+ hide: true,
264
+ onPut: async () => {
265
+ this.scheduleStreamManagerRestart('pipMargin changed');
266
+ },
261
267
  },
262
268
  widerChannel: {
263
269
  title: 'Wider Channel',
264
270
  description: 'Channel number for wider lens (typically 0)',
265
271
  type: 'number',
266
272
  defaultValue: 0,
267
- hide: true, // Only show for multifocal devices via getAdditionalSettings
273
+ hide: true,
274
+ onPut: async () => {
275
+ this.scheduleStreamManagerRestart('widerChannel changed');
276
+ },
268
277
  },
269
278
  teleChannel: {
270
279
  title: 'Tele Channel',
271
280
  description: 'Channel number for tele lens (typically 1)',
272
281
  type: 'number',
273
282
  defaultValue: 1,
274
- hide: true, // Only show for multifocal devices via getAdditionalSettings
283
+ hide: true,
284
+ onPut: async () => {
285
+ this.scheduleStreamManagerRestart('teleChannel changed');
286
+ },
275
287
  },
276
288
  // Battery camera specific
277
289
  uid: {
@@ -582,7 +594,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
582
594
 
583
595
  protected nvrDevice?: ReolinkNativeNvrDevice;
584
596
  protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
585
- thisDevice: Settings
597
+ thisDevice: Settings;
598
+ isBattery: boolean;
599
+ isMultiFocal: boolean;
600
+ private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
586
601
 
587
602
  constructor(
588
603
  nativeId: string,
@@ -596,14 +611,34 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
596
611
  this.multiFocalDevice = options.multiFocalDevice;
597
612
  this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
598
613
 
599
- const isBattery = options.type === 'battery' || options.type === 'multi-focal-battery';
600
- this.protocol = isBattery ? 'udp' : 'tcp';
614
+ this.isBattery = options.type === 'battery' || options.type === 'multi-focal-battery';
615
+ this.isMultiFocal = options.type === 'multi-focal' || options.type === 'multi-focal-battery';
616
+ this.protocol = this.isBattery ? 'udp' : 'tcp';
601
617
 
602
618
  setTimeout(async () => {
603
619
  await this.parentInit();
604
620
  }, 2000);
605
621
  }
606
622
 
623
+ /**
624
+ * TODO: Implement video clip fetching using Baichuan/NVR recordings API.
625
+ */
626
+ async getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
627
+ throw new Error("getVideoClips is not implemented yet.");
628
+ }
629
+
630
+ getVideoClip(videoId: string): Promise<MediaObject> {
631
+ throw new Error("getVideoClip is not implemented yet.");
632
+ }
633
+
634
+ getVideoClipThumbnail(thumbnailId: string, options?: VideoClipThumbnailOptions): Promise<MediaObject> {
635
+ throw new Error("getVideoClipThumbnail is not implemented yet.");
636
+ }
637
+
638
+ removeVideoClips(...videoClipIds: string[]): Promise<void> {
639
+ throw new Error("removeVideoClips is not implemented yet.");
640
+ }
641
+
607
642
  async reboot(): Promise<void> {
608
643
  const api = await this.ensureBaichuanClient();
609
644
  await api.reboot();
@@ -613,9 +648,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
613
648
  protected getConnectionConfig(): BaichuanConnectionConfig {
614
649
  const { ipAddress, username, password, uid } = this.storageSettings.values;
615
650
  const debugOptions = this.getBaichuanDebugOptions();
616
- const normalizedUid = this.protocol === 'udp' ? normalizeUid(uid) : undefined;
651
+ const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
617
652
 
618
- if (this.protocol === 'udp' && !normalizedUid) {
653
+ if (this.isBattery && !normalizedUid) {
619
654
  throw new Error('UID is required for battery cameras (BCUDP)');
620
655
  }
621
656
 
@@ -639,8 +674,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
639
674
  // For battery cameras, don't auto-resubscribe after idle disconnects
640
675
  // (idle disconnects are normal for battery cameras to save power)
641
676
  // Events will be resubscribed when ensureClient() is called for actual operations
642
- const isBattery = this.options.type === 'battery';
643
- if (!isBattery) {
677
+ if (!this.isBattery) {
644
678
  // For non-battery cameras, resubscribe to events after reconnection
645
679
  setTimeout(async () => {
646
680
  try {
@@ -666,7 +700,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
666
700
  }
667
701
 
668
702
  async withBaichuanRetry<T>(fn: () => Promise<T>): Promise<T> {
669
- if (this.protocol === 'udp') {
703
+ if (this.isBattery) {
670
704
  return await fn();
671
705
  } else {
672
706
  try {
@@ -737,8 +771,38 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
737
771
  }
738
772
  }
739
773
 
740
- createStreamClient(): Promise<ReolinkBaichuanApi> {
741
- throw new Error("Method not implemented.");
774
+ /**
775
+ * Create a dedicated Baichuan API session for streaming (used by StreamManager).
776
+ *
777
+ * - For TCP devices (regular + multifocal), this creates a new TCP session with its own client.
778
+ * - For UDP/battery devices, this reuses the existing client via ensureClient().
779
+ */
780
+ async createStreamClient(): Promise<ReolinkBaichuanApi> {
781
+ // Battery / BCUDP path: reuse the main client to avoid extra wake-ups and sockets.
782
+ if (this.isBattery) {
783
+ return await this.ensureClient();
784
+ }
785
+
786
+ // TCP path: create a separate session for streaming (RFC4571/composite/NVR-friendly).
787
+ const { ipAddress, username, password } = this.storageSettings.values;
788
+ const logger = this.getBaichuanLogger();
789
+
790
+ const debugOptions = this.getBaichuanDebugOptions();
791
+ const api = await createBaichuanApi(
792
+ {
793
+ inputs: {
794
+ host: ipAddress,
795
+ username,
796
+ password,
797
+ logger,
798
+ debugOptions,
799
+ },
800
+ transport: 'tcp',
801
+ },
802
+ );
803
+
804
+ await api.login();
805
+ return api;
742
806
  }
743
807
 
744
808
  public getAbilities(): DeviceCapabilities {
@@ -750,8 +814,78 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
750
814
  }
751
815
 
752
816
  getBaichuanDebugOptions(): any | undefined {
753
- const debugLogs = this.storageSettings.values.debugLogs || [];
754
- return convertDebugLogsToApiOptions(debugLogs);
817
+ const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
818
+ return convertDebugLogsToApiOptions(socketDebugLogs);
819
+ }
820
+
821
+ /**
822
+ * Initialize or recreate the StreamManager, taking into account multifocal composite options.
823
+ */
824
+ protected initStreamManager(logger: Console, forceRecreate: boolean = false): void {
825
+ const { username, password } = this.storageSettings.values;
826
+
827
+ const baseOptions: any = {
828
+ createStreamClient: () => this.createStreamClient(),
829
+ getLogger: () => logger,
830
+ credentials: {
831
+ username,
832
+ password,
833
+ },
834
+ sharedConnection: this.isBattery,
835
+ };
836
+
837
+ if (this.isMultiFocal) {
838
+ const values: any = this.storageSettings.values;
839
+ const pipPosition = values.pipPosition || 'bottom-right';
840
+ const pipSize = values.pipSize ?? 0.25;
841
+ const pipMargin = values.pipMargin ?? 10;
842
+ const widerChannel = values.widerChannel ?? 0;
843
+ const teleChannel = values.teleChannel ?? 1;
844
+
845
+ baseOptions.compositeOptions = {
846
+ widerChannel,
847
+ teleChannel,
848
+ pipPosition,
849
+ pipSize,
850
+ pipMargin,
851
+ };
852
+ }
853
+
854
+ if (!this.streamManager || forceRecreate) {
855
+ this.streamManager = new StreamManager(baseOptions);
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Debounced restart of StreamManager when PIP/composite settings change.
861
+ * Also notifies listeners so that active streams (prebuffer, etc.) restart cleanly.
862
+ */
863
+ protected scheduleStreamManagerRestart(reason: string): void {
864
+ const logger = this.getBaichuanLogger();
865
+ logger.log(`Scheduling StreamManager restart (${reason})`);
866
+
867
+ if (this.streamManagerRestartTimeout) {
868
+ clearTimeout(this.streamManagerRestartTimeout);
869
+ this.streamManagerRestartTimeout = undefined;
870
+ }
871
+
872
+ this.streamManagerRestartTimeout = setTimeout(async () => {
873
+ this.streamManagerRestartTimeout = undefined;
874
+ const restartLogger = this.getBaichuanLogger();
875
+ try {
876
+ restartLogger.log('Restarting StreamManager due to PIP/composite settings change');
877
+ this.initStreamManager(restartLogger, true);
878
+
879
+ // Notify consumers (e.g. prebuffer) that stream configuration changed.
880
+ try {
881
+ this.onDeviceEvent(ScryptedInterface.VideoCamera, undefined);
882
+ } catch {
883
+ // best-effort
884
+ }
885
+ } catch (e) {
886
+ restartLogger.warn('Failed to restart StreamManager after settings change', e);
887
+ }
888
+ }, 500);
755
889
  }
756
890
 
757
891
  isRecoverableBaichuanError(e: any): boolean {
@@ -1167,7 +1301,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1167
1301
  }
1168
1302
 
1169
1303
  async takePicture(options?: RequestPictureOptions) {
1170
- if (this.protocol === 'tcp') {
1304
+ if (!this.isBattery) {
1171
1305
  try {
1172
1306
  return this.withBaichuanRetry(async () => {
1173
1307
  const client = await this.ensureClient();
@@ -1666,22 +1800,20 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1666
1800
  }
1667
1801
 
1668
1802
  const { username, password } = this.storageSettings.values;
1669
- const isBattery = ['multi-focal-battery', 'battery'].includes(this.options.type);
1670
- const isMultiFocal = ['multi-focal', 'multi-focal'].includes(this.options.type);
1671
1803
 
1672
- this.storageSettings.settings.uid.hide = !isBattery;
1673
- this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
1674
- this.storageSettings.settings.lowThresholdBatteryRecording.hide = !isBattery;
1675
- this.storageSettings.settings.highThresholdBatteryRecording.hide = !isBattery;
1804
+ this.storageSettings.settings.uid.hide = !this.isBattery;
1805
+ this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !this.isBattery;
1806
+ this.storageSettings.settings.lowThresholdBatteryRecording.hide = !this.isBattery;
1807
+ this.storageSettings.settings.highThresholdBatteryRecording.hide = !this.isBattery;
1676
1808
 
1677
1809
  // Show PIP settings only for multifocal devices
1678
- this.storageSettings.settings.pipPosition.hide = !isMultiFocal;
1679
- this.storageSettings.settings.pipSize.hide = !isMultiFocal;
1680
- this.storageSettings.settings.pipMargin.hide = !isMultiFocal;
1681
- this.storageSettings.settings.widerChannel.hide = !isMultiFocal;
1682
- this.storageSettings.settings.teleChannel.hide = !isMultiFocal;
1810
+ this.storageSettings.settings.pipPosition.hide = !this.isMultiFocal;
1811
+ this.storageSettings.settings.pipSize.hide = !this.isMultiFocal;
1812
+ this.storageSettings.settings.pipMargin.hide = !this.isMultiFocal;
1813
+ this.storageSettings.settings.widerChannel.hide = !this.isMultiFocal;
1814
+ this.storageSettings.settings.teleChannel.hide = !this.isMultiFocal;
1683
1815
 
1684
- if (isBattery && !this.storageSettings.values.mixinsSetup) {
1816
+ if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
1685
1817
  try {
1686
1818
  const device = sdk.systemManager.getDeviceById<Settings>(this.id);
1687
1819
  if (device) {
@@ -1703,15 +1835,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1703
1835
  logger.warn('Failed to subscribe to Baichuan events', e);
1704
1836
  }
1705
1837
 
1706
- this.streamManager = new StreamManager({
1707
- createStreamClient: () => this.createStreamClient(),
1708
- getLogger: () => logger,
1709
- credentials: {
1710
- username,
1711
- password
1712
- },
1713
- sharedConnection: isBattery,
1714
- });
1838
+ // Initialize StreamManager (with composite options for multifocal devices)
1839
+ this.initStreamManager(logger);
1715
1840
 
1716
1841
  const { hasIntercom, hasPtz } = this.getAbilities();
1717
1842
 
package/src/main.ts CHANGED
@@ -96,7 +96,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
96
96
  device.storageSettings.values.ipAddress = ipAddress;
97
97
  device.storageSettings.values.username = username;
98
98
  device.storageSettings.values.password = password;
99
- device.storageSettings.values.uid = detection.uid || '';
99
+ device.storageSettings.values.uid = uid;
100
100
  device.storageSettings.values.capabilities = capabilities;
101
101
 
102
102
  return nativeId;
@@ -177,7 +177,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
177
177
  device.storageSettings.values.rtspChannel = rtspChannel;
178
178
  device.storageSettings.values.ipAddress = ipAddress;
179
179
  device.storageSettings.values.capabilities = capabilities;
180
- device.storageSettings.values.uid = detection.uid;
180
+ device.storageSettings.values.uid = uid;
181
181
 
182
182
  return nativeId;
183
183
  }
package/src/multiFocal.ts CHANGED
@@ -1,12 +1,9 @@
1
- import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent, ReolinkSupportedStream } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import sdk, { Device, DeviceProvider, MediaObject, Reboot, RequestMediaStreamOptions, ScryptedDeviceType, Setting, Settings, SettingValue } from "@scrypted/sdk";
3
- import { type BaichuanConnectionCallbacks } from "./baichuan-base";
1
+ import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
+ import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Setting, Settings, SettingValue } from "@scrypted/sdk";
4
3
  import { ReolinkNativeCamera } from "./camera";
5
4
  import { ReolinkNativeBatteryCamera } from "./camera-battery";
6
5
  import { CameraType, CommonCameraMixin } from "./common";
7
6
  import ReolinkNativePlugin from "./main";
8
- import { StreamManager } from "./stream-utils";
9
- import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
10
7
  import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, updateDeviceInfo } from "./utils";
11
8
 
12
9
  export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements Settings, DeviceProvider, Reboot {
@@ -36,28 +33,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
36
33
  }
37
34
  }
38
35
 
39
- protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
40
- return {
41
- onError: undefined, // Use default error handling
42
- onClose: async () => {
43
- // Reinit after cleanup
44
- await this.reinit();
45
- if (!this.isBattery) {
46
- setTimeout(async () => {
47
- try {
48
- await this.subscribeToEvents();
49
- } catch (e) {
50
- const logger = this.getBaichuanLogger();
51
- logger.warn('Failed to resubscribe to events after reconnection', e);
52
- }
53
- }, 1000);
54
- }
55
- },
56
- onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
57
- getEventSubscriptionEnabled: () => true,
58
- };
59
- }
60
-
61
36
  protected async onBeforeCleanup(): Promise<void> {
62
37
  await this.unsubscribeFromAllEvents();
63
38
  }
@@ -220,53 +195,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
220
195
  }
221
196
 
222
197
  await super.reportDevices();
223
-
224
- // Initialize StreamManager with composite options for multifocal device
225
- // Use saved settings or defaults
226
- const values = this.storageSettings.values as any;
227
- const pipPosition = (values.pipPosition || 'bottom-right') as any;
228
- const pipSize = values.pipSize ?? 0.25;
229
- const pipMargin = values.pipMargin ?? 10;
230
- const widerChannel = values.widerChannel ?? 0;
231
- const teleChannel = values.teleChannel ?? 1;
232
-
233
- if (!this.streamManager) {
234
- this.streamManager = new StreamManager({
235
- createStreamClient: () => this.createStreamClient(),
236
- getLogger: () => logger,
237
- credentials: {
238
- username,
239
- password
240
- },
241
- sharedConnection: this.isBattery,
242
- compositeOptions: {
243
- widerChannel,
244
- teleChannel,
245
- pipPosition: pipPosition as any,
246
- pipSize,
247
- pipMargin,
248
- },
249
- });
250
- } else {
251
- // Recreate StreamManager with new settings if they changed
252
- // StreamManager doesn't expose opts, so we need to recreate it
253
- this.streamManager = new StreamManager({
254
- createStreamClient: () => this.createStreamClient(),
255
- getLogger: () => logger,
256
- credentials: {
257
- username,
258
- password
259
- },
260
- sharedConnection: this.isBattery,
261
- compositeOptions: {
262
- widerChannel,
263
- teleChannel,
264
- pipPosition: pipPosition as any,
265
- pipSize,
266
- pipMargin,
267
- },
268
- });
269
- }
270
198
  }
271
199
 
272
200
  async getDevice(nativeId: string) {
@@ -113,11 +113,15 @@ export async function createRfc4571MediaObjectFromStreamManager(params: {
113
113
  mso.audio.sampleRate = audio.sampleRate;
114
114
  mso.audio.channels = audio.channels;
115
115
  }
116
- const url = new URL(host);
116
+
117
+ const url = new URL(`tcp://${host}`);
117
118
  url.port = port.toString();
118
- url.protocol = 'tcp';
119
- url.username = username;
120
- url.password = password;
119
+ if (username) {
120
+ url.username = username;
121
+ }
122
+ if (password) {
123
+ url.password = password;
124
+ }
121
125
 
122
126
  const rfc = {
123
127
  url,
@@ -200,11 +204,14 @@ export class StreamManager {
200
204
  return this.opts.getLogger() ;
201
205
  }
202
206
 
203
- private async ensureNativeRfcServer(
207
+ private async ensureRfcServer(
204
208
  streamKey: string,
205
- channel: number,
206
209
  profile: StreamProfile,
207
- expectedVideoType?: 'H264' | 'H265',
210
+ expectedVideoType: 'H264' | 'H265' | undefined,
211
+ options: {
212
+ channel?: number;
213
+ compositeOptions?: CompositeStreamPipOptions;
214
+ },
208
215
  ): Promise<RfcServerInfo> {
209
216
  const existingCreate = this.nativeRfcServerCreatePromises.get(streamKey);
210
217
  if (existingCreate) {
@@ -215,8 +222,9 @@ export class StreamManager {
215
222
  const cached = this.nativeRfcServers.get(streamKey);
216
223
  if (cached?.server?.listening) {
217
224
  if (expectedVideoType && cached.videoType !== expectedVideoType) {
225
+ const kind = options.channel === undefined ? 'composite' : 'native';
218
226
  this.getLogger().warn(
219
- `Native RFC cache codec mismatch for ${streamKey}: cached=${cached.videoType} expected=${expectedVideoType}; recreating server.`,
227
+ `Native RFC ${kind} cache codec mismatch for ${streamKey}: cached=${cached.videoType} expected=${expectedVideoType}; recreating server.`,
220
228
  );
221
229
  }
222
230
  else {
@@ -252,13 +260,14 @@ export class StreamManager {
252
260
 
253
261
  const created = await createRfc4571TcpServer({
254
262
  api,
255
- channel,
263
+ channel: options.channel,
256
264
  profile,
257
265
  logger: this.getLogger(),
258
266
  expectedVideoType: expectedVideoType as VideoType | undefined,
259
267
  closeApiOnTeardown,
260
268
  username,
261
269
  password,
270
+ ...(options.compositeOptions ? { compositeOptions: options.compositeOptions } : {}),
262
271
  });
263
272
 
264
273
  this.nativeRfcServers.set(streamKey, created);
@@ -292,7 +301,9 @@ export class StreamManager {
292
301
  streamKey: string,
293
302
  expectedVideoType?: 'H264' | 'H265',
294
303
  ): Promise<RfcServerInfo> {
295
- return await this.ensureNativeRfcServer(streamKey, channel, profile, expectedVideoType);
304
+ return await this.ensureRfcServer(streamKey, profile, expectedVideoType, {
305
+ channel,
306
+ });
296
307
  }
297
308
 
298
309
  async getRfcCompositeStream(
@@ -300,85 +311,10 @@ export class StreamManager {
300
311
  streamKey: string,
301
312
  expectedVideoType?: 'H264' | 'H265',
302
313
  ): Promise<RfcServerInfo> {
303
- const existingCreate = this.nativeRfcServerCreatePromises.get(streamKey);
304
- if (existingCreate) {
305
- return await existingCreate;
306
- }
307
-
308
- const createPromise = (async () => {
309
- const cached = this.nativeRfcServers.get(streamKey);
310
- if (cached?.server?.listening) {
311
- if (expectedVideoType && cached.videoType !== expectedVideoType) {
312
- this.getLogger().warn(
313
- `Native RFC composite cache codec mismatch for ${streamKey}: cached=${cached.videoType} expected=${expectedVideoType}; recreating server.`,
314
- );
315
- }
316
- else {
317
- return {
318
- host: cached.host,
319
- port: cached.port,
320
- sdp: cached.sdp,
321
- audio: cached.audio,
322
- username: (cached as any).username || this.opts.credentials.username,
323
- password: (cached as any).password || this.opts.credentials.password,
324
- };
325
- }
326
- }
327
-
328
- if (cached) {
329
- try {
330
- await cached.close('recreate');
331
- }
332
- catch {
333
- // ignore
334
- }
335
- this.nativeRfcServers.delete(streamKey);
336
- }
337
-
338
- const api = await this.opts.createStreamClient();
339
- const { createRfc4571TcpServer } = await import('@apocaliss92/reolink-baichuan-js');
340
-
341
- // Use the same credentials as the main connection
342
- const { username, password } = this.opts.credentials;
343
-
344
- // If connection is shared, don't close it when stream teardown happens
345
- const closeApiOnTeardown = !(this.opts.sharedConnection ?? false);
346
-
347
- const created = await createRfc4571TcpServer({
348
- api,
349
- channel: undefined, // Undefined channel indicates composite stream
350
- profile,
351
- logger: this.getLogger(),
352
- expectedVideoType: expectedVideoType as VideoType | undefined,
353
- closeApiOnTeardown,
354
- username,
355
- password,
356
- compositeOptions: this.opts.compositeOptions,
357
- });
358
-
359
- this.nativeRfcServers.set(streamKey, created);
360
- created.server.once('close', () => {
361
- const current = this.nativeRfcServers.get(streamKey);
362
- if (current?.server === created.server) this.nativeRfcServers.delete(streamKey);
363
- });
364
-
365
- return {
366
- host: created.host,
367
- port: created.port,
368
- sdp: created.sdp,
369
- audio: created.audio,
370
- username: (created as any).username || this.opts.credentials.username,
371
- password: (created as any).password || this.opts.credentials.password,
372
- };
373
- })();
374
-
375
- this.nativeRfcServerCreatePromises.set(streamKey, createPromise);
376
- try {
377
- return await createPromise;
378
- }
379
- finally {
380
- this.nativeRfcServerCreatePromises.delete(streamKey);
381
- }
314
+ return await this.ensureRfcServer(streamKey, profile, expectedVideoType, {
315
+ channel: undefined, // Undefined channel indicates composite stream
316
+ compositeOptions: this.opts.compositeOptions,
317
+ });
382
318
  }
383
319
 
384
320
  /**
package/src/utils.ts CHANGED
@@ -44,6 +44,7 @@ export const getDeviceInterfaces = (props: {
44
44
  ScryptedInterface.AudioSensor,
45
45
  ScryptedInterface.MotionSensor,
46
46
  ScryptedInterface.VideoTextOverlays,
47
+ ScryptedInterface.VideoClips,
47
48
  ];
48
49
 
49
50
  try {