@apocaliss92/scrypted-reolink-native 0.1.3 → 0.1.5

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/src/presets.ts CHANGED
@@ -90,7 +90,7 @@ export class ReolinkPtzPresets {
90
90
 
91
91
  async refreshPtzPresets(): Promise<PtzPreset[]> {
92
92
  const client = await this.camera.ensureClient();
93
- const channel = this.camera.getRtspChannel();
93
+ const channel = this.camera.storageSettings.values.rtspChannel;
94
94
  const presets = await client.getPtzPresets(channel);
95
95
  this.setCachedPtzPresets(presets);
96
96
  this.syncEnabledPresetsSettingAndCaps(presets);
@@ -99,14 +99,14 @@ export class ReolinkPtzPresets {
99
99
 
100
100
  async moveToPreset(presetId: number): Promise<void> {
101
101
  const client = await this.camera.ensureClient();
102
- const channel = this.camera.getRtspChannel();
102
+ const channel = this.camera.storageSettings.values.rtspChannel;
103
103
  await client.moveToPtzPreset(channel, presetId);
104
104
  }
105
105
 
106
106
  /** Create a new PTZ preset at current position. */
107
107
  async createPtzPreset(name: string, presetId?: number): Promise<PtzPreset> {
108
108
  const client = await this.camera.ensureClient();
109
- const channel = this.camera.getRtspChannel();
109
+ const channel = this.camera.storageSettings.values.rtspChannel;
110
110
  const trimmed = String(name ?? '').trim();
111
111
  if (!trimmed) throw new Error('Preset name is required');
112
112
  const existing = await client.getPtzPresets(channel);
@@ -150,7 +150,7 @@ export class ReolinkPtzPresets {
150
150
  /** Overwrite an existing preset with the current PTZ position (and keep its current name). */
151
151
  async updatePtzPresetToCurrentPosition(presetId: number): Promise<void> {
152
152
  const client = await this.camera.ensureClient();
153
- const channel = this.camera.getRtspChannel();
153
+ const channel = this.camera.storageSettings.values.rtspChannel;
154
154
 
155
155
  const current = this.getCachedPtzPresets();
156
156
  const found = current.find((p) => p.id === presetId);
@@ -163,7 +163,7 @@ export class ReolinkPtzPresets {
163
163
  /** Best-effort delete/disable a preset (firmware dependent). */
164
164
  async deletePtzPreset(presetId: number): Promise<void> {
165
165
  const client = await this.camera.ensureClient();
166
- const channel = this.camera.getRtspChannel();
166
+ const channel = this.camera.storageSettings.values.rtspChannel;
167
167
  await client.deletePtzPreset(channel, presetId);
168
168
 
169
169
  // Keep enabled preset list clean (remove deleted id), but do not rewrite names for others.
@@ -181,7 +181,7 @@ export class ReolinkPtzPresets {
181
181
  /** Rename a preset while trying to preserve its position (will move camera to that preset first). */
182
182
  async renamePtzPreset(presetId: number, newName: string): Promise<void> {
183
183
  const client = await this.camera.ensureClient();
184
- const channel = this.camera.getRtspChannel();
184
+ const channel = this.camera.storageSettings.values.rtspChannel;
185
185
  const trimmed = String(newName ?? '').trim();
186
186
  if (!trimmed) throw new Error('Preset name is required');
187
187
 
@@ -97,14 +97,18 @@ export async function fetchVideoStreamOptionsFromApi(
97
97
  }
98
98
 
99
99
  export async function buildVideoStreamOptionsFromRtspRtmp(
100
- client: ReolinkBaichuanApi,
101
- channel: number,
102
- ipAddress: string,
103
- username: string,
104
- password: string,
105
- cachedNetPort?: { rtsp?: { port?: number; enable?: number }; rtmp?: { port?: number; enable?: number } },
100
+ props: {
101
+ client: ReolinkBaichuanApi,
102
+ ipAddress: string,
103
+ cachedNetPort: { rtsp?: { port?: number; enable?: number }; rtmp?: { port?: number; enable?: number } },
104
+ isFromNvr: boolean,
105
+ rtspChannel: number,
106
+ logger: Console
107
+ },
106
108
  ): Promise<UrlMediaStreamOptions[]> {
107
- const streams: UrlMediaStreamOptions[] = [];
109
+ const { client, ipAddress, cachedNetPort, rtspChannel, logger } = props;
110
+ const rtspStreams: UrlMediaStreamOptions[] = [];
111
+ const rtmpStreams: UrlMediaStreamOptions[] = [];
108
112
 
109
113
  // Use cached net port if provided, otherwise fetch it
110
114
  const netPort = cachedNetPort || await client.getNetPort();
@@ -113,13 +117,8 @@ export async function buildVideoStreamOptionsFromRtspRtmp(
113
117
  const rtspPort = netPort.rtsp?.port ?? 554;
114
118
  const rtmpPort = netPort.rtmp?.port ?? 1935;
115
119
 
116
- if (!rtspEnabled && !rtmpEnabled) {
117
- // If neither RTSP nor RTMP are enabled, return empty array
118
- return streams;
119
- }
120
-
121
120
  // Get stream metadata to build options
122
- const streamMetadata = await client.getStreamMetadata(channel);
121
+ const streamMetadata = await client.getStreamMetadata(rtspChannel);
123
122
  const list = streamMetadata?.streams || [];
124
123
 
125
124
  for (const stream of list) {
@@ -134,12 +133,12 @@ export async function buildVideoStreamOptionsFromRtspRtmp(
134
133
  if (rtspEnabled && profile !== 'ext') {
135
134
  // RTSP format: rtsp://ip:port/h264Preview_XX_profile
136
135
  // XX is 1-based channel with 2-digit padding
137
- const channelStr = String(channel + 1).padStart(2, '0');
136
+ const channelStr = String(rtspChannel + 1).padStart(2, '0');
138
137
  const profileStr = profile === 'main' ? 'main' : 'sub';
139
138
  const rtspPath = `/h264Preview_${channelStr}_${profileStr}`;
140
139
  const rtspId = `h264Preview_${channelStr}_${profileStr}`;
141
140
 
142
- streams.push({
141
+ rtspStreams.push({
143
142
  name: `RTSP ${rtspId}`,
144
143
  id: rtspId,
145
144
  container: 'rtsp',
@@ -160,14 +159,14 @@ export async function buildVideoStreamOptionsFromRtspRtmp(
160
159
  const rtmpId = `${streamName}.bcs`; // ID for Scrypted (main.bcs, sub.bcs, ext.bcs)
161
160
 
162
161
  // Use channel directly (0-based) in path, matching reolink_aio behavior
163
- const rtmpPath = `/bcs/channel${channel}_${streamName}.bcs`;
162
+ const rtmpPath = `/bcs/channel${rtspChannel}_${streamName}.bcs`;
164
163
  const rtmpUrl = new URL(`rtmp://${ipAddress}:${rtmpPort}${rtmpPath}`);
165
164
  const params = rtmpUrl.searchParams;
166
- params.set('channel', channel.toString());
165
+ params.set('channel', rtspChannel.toString());
167
166
  params.set('stream', streamType.toString());
168
167
  // Credentials will be added by addRtspCredentials as user/password query params
169
168
 
170
- streams.push({
169
+ rtmpStreams.push({
171
170
  name: `RTMP ${rtmpId}`,
172
171
  id: rtmpId,
173
172
  container: 'rtmp',
@@ -177,12 +176,14 @@ export async function buildVideoStreamOptionsFromRtspRtmp(
177
176
  }
178
177
  }
179
178
 
180
- // Sort streams: RTMP first, then RTSP
181
- streams.sort((a, b) => {
182
- if (a.container === 'rtmp' && b.container !== 'rtmp') return -1;
183
- if (a.container !== 'rtmp' && b.container === 'rtmp') return 1;
184
- return 0;
185
- });
179
+
180
+ const nativeStreams = await fetchVideoStreamOptionsFromApi(client, rtspChannel, logger);
181
+
182
+ const streams: UrlMediaStreamOptions[] = [
183
+ ...rtspStreams,
184
+ ...rtmpStreams,
185
+ ...nativeStreams,
186
+ ];
186
187
 
187
188
  return streams;
188
189
  }
@@ -378,7 +379,7 @@ export class StreamManager {
378
379
  async closeAllStreams(reason?: string): Promise<void> {
379
380
  const servers = Array.from(this.nativeRfcServers.values());
380
381
  this.nativeRfcServers.clear();
381
-
382
+
382
383
  await Promise.allSettled(
383
384
  servers.map(async (server) => {
384
385
  try {
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { DeviceCapabilities } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import { ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
1
+ import type { DeviceCapabilities, ReolinkDeviceInfo } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
+ import { DeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
3
3
 
4
4
  export const getDeviceInterfaces = (props: {
5
5
  capabilities: DeviceCapabilities,
@@ -49,4 +49,33 @@ export const getDeviceInterfaces = (props: {
49
49
  }
50
50
 
51
51
  return { interfaces, type: capabilities.isDoorbell ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera };
52
+ }
53
+
54
+ export const updateDeviceInfo = async (props: {
55
+ device: DeviceBase,
56
+ ipAddress: string,
57
+ deviceData: ReolinkDeviceInfo
58
+ }) => {
59
+ const { device, ipAddress, deviceData } = props;
60
+ try {
61
+ const info = device.info || {};
62
+
63
+ info.ip = ipAddress;
64
+ info.serialNumber = deviceData?.serialNumber || deviceData?.itemNo;
65
+ info.firmware = deviceData?.firmwareVersion;
66
+ info.version = deviceData?.hardwareVersion;
67
+ info.model = deviceData?.type;
68
+ info.manufacturer = 'Reolink';
69
+ info.managementUrl = `http://${ipAddress}`;
70
+ device.info = info;
71
+ } catch (e) {
72
+ // If API call fails, at least set basic info
73
+ const info = device.info || {};
74
+ info.ip = ipAddress;
75
+ info.manufacturer = 'Reolink native';
76
+ info.managementUrl = `http://${ipAddress}`;
77
+ device.info = info;
78
+
79
+ throw e;
80
+ }
52
81
  }