@apocaliss92/scrypted-reolink-native 0.2.10 → 0.2.12

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.10",
3
+ "version": "0.2.12",
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
@@ -1162,17 +1162,17 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1162
1162
  const useNvr = clipsSource === "NVR" && this.nvrDevice && fileId.includes('/');
1163
1163
 
1164
1164
  if (useNvr) {
1165
- logger.debug(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
1165
+ // logger.debug(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
1166
1166
  const api = await this.ensureClient();
1167
1167
  const channel = this.storageSettings.values.rtspChannel ?? 0;
1168
1168
 
1169
1169
  try {
1170
- logger.debug(`[getVideoClipRtmpUrl] Trying getVodUrl with Download requestType...`);
1170
+ // logger.debug(`[getVideoClipRtmpUrl] Trying getVodUrl with Download requestType...`);
1171
1171
  const url = await api.getVodUrl(fileId, channel, {
1172
1172
  requestType: "Download",
1173
1173
  streamType: "main",
1174
1174
  });
1175
- logger.debug(`[getVideoClipRtmpUrl] NVR getVodUrl Download URL received: url="${url || 'none'}"`);
1175
+ // logger.debug(`[getVideoClipRtmpUrl] NVR getVodUrl Download URL received: url="${url || 'none'}"`);
1176
1176
  if (url) return url;
1177
1177
  } catch (e: any) {
1178
1178
  logger.error(`[getVideoClipRtmpUrl] getVodUrl Download failed: ${e?.message || String(e)}`);
@@ -1181,7 +1181,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1181
1181
  throw new Error(`No streaming URL found from NVR for file ${fileId} after trying Playback and Download methods`);
1182
1182
  } else {
1183
1183
  // Camera standalone: DEVE usare RTMP da Baichuan API
1184
- logger.debug(`[getVideoClipRtmpUrl] Getting RTMP URL from Baichuan API for fileId="${fileId}" (camera standalone)`);
1184
+ // logger.debug(`[getVideoClipRtmpUrl] Getting RTMP URL from Baichuan API for fileId="${fileId}" (camera standalone)`);
1185
1185
  const api = await this.ensureClient();
1186
1186
  const result = await api.getRecordingPlaybackUrls({
1187
1187
  fileName: fileId,
@@ -2438,11 +2438,11 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2438
2438
  // logger.log({ supportedStreams, variantType, lensParam, rtspChannel, onNvr: this.isOnNvr, nativeStreams: nativeStreams.map(s => ({ id: s.id, nativeVariant: s.nativeVariant, lens: s.lens })), rtspStreams: rtspStreams.map(s => ({ id: s.id, lens: s.lens })), rtmpStreams: rtmpStreams.map(s => ({ id: s.id, lens: s.lens })) });
2439
2439
 
2440
2440
  for (const supportedStream of supportedStreams) {
2441
- const { id, metadata, url, name, container, lens } = supportedStream;
2441
+ const { id, metadata, url, name, container, lens, channel, profile, nativeVariant } = supportedStream;
2442
2442
 
2443
2443
  // Composite streams are re-encoded to H.264 by the library (ffmpeg/libx264).
2444
2444
  // Do not infer codec from underlying camera metadata.
2445
- const isComposite = id.startsWith('composite_') || lens === 'composite';
2445
+ const isComposite = lens === 'composite' || channel === undefined;
2446
2446
  const codec = (() => {
2447
2447
  if (isComposite) return 'h264';
2448
2448
 
@@ -2472,7 +2472,14 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2472
2472
  container,
2473
2473
  video: { codec, width: metadata.width, height: metadata.height },
2474
2474
  // audio: { codec: metadata.audioCodec }
2475
- })
2475
+
2476
+ // Provide explicit RFC4571 metadata so stream-utils can avoid parsing the streamKey.
2477
+ reolinkRfc4571: {
2478
+ channel,
2479
+ profile,
2480
+ variant: nativeVariant,
2481
+ },
2482
+ } as any)
2476
2483
  }
2477
2484
  } catch (e) {
2478
2485
  if (!this.isRecoverableBaichuanError?.(e)) {
@@ -2725,7 +2732,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2725
2732
  this.intercom = new ReolinkBaichuanIntercom(this);
2726
2733
  }
2727
2734
 
2728
- if (hasPtz) {
2735
+ if (hasPtz && !this.multiFocalDevice) {
2729
2736
  const choices = (this.presets || []).map((preset: any) => preset.id + '=' + preset.name);
2730
2737
 
2731
2738
  this.storageSettings.settings.presets.choices = choices;
@@ -17,8 +17,8 @@ export interface StreamManagerOptions {
17
17
  /**
18
18
  * Creates a dedicated Baichuan session for streaming.
19
19
  * Required to support concurrent main+ext streams on firmwares where streamType overlaps.
20
- * @param streamKey The unique stream key (e.g., "composite_default_main", "channel_0_main", etc.)
21
- * Contains all necessary information (profile, variantType, channel) for stream identification.
20
+ * @param streamKey The unique stream key (e.g., "composite-rtsp-default-sub-sub", "channel_0_main", etc.)
21
+ * Forwarded to the library as `requestedId`.
22
22
  */
23
23
  createStreamClient: (streamKey: string) => Promise<ReolinkBaichuanApi>;
24
24
  logger: Console;
@@ -93,89 +93,13 @@ export function parseStreamProfileFromId(id: string | undefined): StreamProfile
93
93
  return;
94
94
  }
95
95
 
96
- function parseRfcStreamKey(streamKey: string): {
97
- isComposite: boolean;
96
+ type ReolinkRfc4571Metadata = {
97
+ profile: StreamProfile;
98
98
  channel?: number;
99
99
  variant?: NativeVideoStreamVariant;
100
- profile?: StreamProfile;
101
- } {
102
- const key = String(streamKey ?? '');
103
- if (!key) {
104
- throw new Error('parseRfcStreamKey: missing streamKey');
105
- }
106
-
107
- // Composite forms supported by the library/server:
108
- // - composite-main-main (wider-tele)
109
- // - composite_<profile>
110
- // - composite_<variant>_<profile>
111
- // - composite_<variant>_<wider>_<tele>
112
- if (key.startsWith('composite-')) {
113
- const parts = key.split('-').filter(Boolean);
114
- const tele = parts.length >= 3 ? parts[2] : undefined;
115
- const teleProfile = tele === 'main' || tele === 'sub' || tele === 'ext' ? (tele as StreamProfile) : undefined;
116
- return { isComposite: true, profile: teleProfile };
117
- }
118
-
119
- if (key.startsWith('composite_')) {
120
- const parts = key.split('_').filter(Boolean);
121
- // parts[0] === 'composite'
122
- const maybeVariant = parts.length >= 2 ? parts[1] : undefined;
123
- const variant =
124
- maybeVariant === 'default' || maybeVariant === 'autotrack' || maybeVariant === 'telephoto'
125
- ? (maybeVariant as NativeVideoStreamVariant)
126
- : undefined;
127
-
128
- // Heuristic: pick last token that looks like a profile as the tele profile.
129
- const last = parts[parts.length - 1];
130
- const profile = last === 'main' || last === 'sub' || last === 'ext' ? (last as StreamProfile) : undefined;
131
- return {
132
- isComposite: true,
133
- ...(variant && variant !== 'default' ? { variant } : {}),
134
- ...(profile ? { profile } : {}),
135
- };
136
- }
137
-
138
- // Non-composite forms supported by the plugin:
139
- // - channel_<ch>_<profile>
140
- // - channel_<ch>_<variant>_<profile>
141
- // - <ch>_<profile>
142
- // - <ch>_<variant>_<profile>
143
- const parts = key.split('_').filter(Boolean);
144
- if (!parts.length) {
145
- throw new Error(`parseRfcStreamKey: invalid streamKey='${key}'`);
146
- }
147
-
148
- let idx = 0;
149
- if (parts[0] === 'channel') {
150
- idx = 1;
151
- }
152
-
153
- const channelStr = parts[idx];
154
- const channel = channelStr !== undefined ? Number(channelStr) : NaN;
155
- if (!Number.isFinite(channel)) {
156
- throw new Error(`parseRfcStreamKey: could not parse channel from streamKey='${key}'`);
157
- }
158
-
159
- const tail = parts.slice(idx + 1);
160
- const last = tail[tail.length - 1];
161
- const profile = last === 'main' || last === 'sub' || last === 'ext' ? (last as StreamProfile) : undefined;
162
-
163
- // tail can be:
164
- // - [profile]
165
- // - [variant, profile]
166
- const maybeVariant = tail.length >= 2 ? tail[0] : undefined;
167
- const variant =
168
- maybeVariant === 'default' || maybeVariant === 'autotrack' || maybeVariant === 'telephoto'
169
- ? (maybeVariant as NativeVideoStreamVariant)
170
- : undefined;
171
-
172
- return {
173
- isComposite: false,
174
- channel,
175
- ...(variant && variant !== 'default' ? { variant } : {}),
176
- ...(profile ? { profile } : {}),
177
- };
178
- }
100
+ /** Explicitly mark composite (channel-less) streams. If omitted, `channel===undefined` implies composite. */
101
+ isComposite?: boolean;
102
+ };
179
103
 
180
104
  /**
181
105
  * Extract and normalize variant type from stream ID or URL (e.g., "autotrack" from "native_autotrack_main" or "?variant=autotrack")
@@ -253,7 +177,12 @@ export async function createRfc4571MediaObjectFromStreamManager(params: {
253
177
  }): Promise<MediaObject> {
254
178
  const { streamManager, streamKey, selected, sourceId } = params;
255
179
 
256
- const { host, port, sdp, audio, username, password } = await streamManager.getRfcServer(streamKey);
180
+ const meta = (selected as any)?.reolinkRfc4571 as ReolinkRfc4571Metadata | undefined;
181
+ if (!meta?.profile) {
182
+ throw new Error(`Missing RFC4571 metadata/profile for streamKey='${streamKey}'`);
183
+ }
184
+
185
+ const { host, port, sdp, audio, username, password } = await streamManager.getRfcServer(streamKey, meta);
257
186
 
258
187
  const { url: _ignoredUrl, ...mso }: any = selected;
259
188
  mso.container = 'rtp';
@@ -311,21 +240,20 @@ export class StreamManager {
311
240
  /**
312
241
  * Unified RFC4571 server accessor.
313
242
  *
314
- * The streamKey is the single source of truth:
315
- * - Composite: composite_* (channel-less)
316
- * - Single channel: channel_<ch>_* or <ch>_*
243
+ * `stream-utils` does not parse `streamKey`. It forwards the `streamKey` as `requestedId`
244
+ * and relies on explicit metadata from the selected stream option (profile/channel/variant).
317
245
  */
318
- async getRfcServer(streamKey: string, profile?: StreamProfile): Promise<RfcServerInfo> {
319
- const parsed = parseRfcStreamKey(streamKey);
320
- const resolvedProfile = profile ?? parsed.profile;
321
- if (!resolvedProfile) {
322
- throw new Error(`getRfcServer: could not infer profile from streamKey='${streamKey}'`);
246
+ async getRfcServer(streamKey: string, meta: ReolinkRfc4571Metadata): Promise<RfcServerInfo> {
247
+ if (!meta?.profile) {
248
+ throw new Error(`getRfcServer: missing profile for streamKey='${streamKey}'`);
323
249
  }
324
250
 
325
- return await this.ensureRfcServer(streamKey, resolvedProfile, {
326
- channel: parsed.isComposite ? undefined : parsed.channel,
327
- variant: parsed.variant,
328
- compositeOptions: parsed.isComposite ? this.opts.compositeOptions : undefined,
251
+ const isComposite = meta.isComposite ?? (meta.channel === undefined);
252
+
253
+ return await this.ensureRfcServer(streamKey, meta.profile, {
254
+ channel: isComposite ? undefined : meta.channel,
255
+ variant: meta.variant,
256
+ compositeOptions: isComposite ? this.opts.compositeOptions : undefined,
329
257
  });
330
258
  }
331
259
 
package/src/utils.ts CHANGED
@@ -198,11 +198,11 @@ export async function recordingFileToVideoClip(
198
198
  let videoHref: string | undefined = providedVideoHref;
199
199
  let thumbnailHref: string | undefined;
200
200
 
201
- logger?.debug(`[recordingFileToVideoClip] URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
201
+ // logger?.debug(`[recordingFileToVideoClip] URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
202
202
 
203
203
  // If webhook is enabled, generate webhook URLs
204
204
  if (useWebhook && plugin && deviceId) {
205
- logger?.debug(`[recordingFileToVideoClip] Generating webhook URLs for fileId=${id}`);
205
+ // logger?.debug(`[recordingFileToVideoClip] Generating webhook URLs for fileId=${id}`);
206
206
  try {
207
207
  const { videoUrl, thumbnailUrl } = await getVideoClipWebhookUrls({
208
208
  deviceId,
@@ -212,24 +212,24 @@ export async function recordingFileToVideoClip(
212
212
  });
213
213
  videoHref = videoUrl;
214
214
  thumbnailHref = thumbnailUrl;
215
- logger?.debug(`[recordingFileToVideoClip] Webhook URLs generated successfully: videoHref="${videoHref}", thumbnailHref="${thumbnailHref}"`);
215
+ // logger?.debug(`[recordingFileToVideoClip] Webhook URLs generated successfully: videoHref="${videoHref}", thumbnailHref="${thumbnailHref}"`);
216
216
  } catch (e) {
217
217
  logger?.error(`[recordingFileToVideoClip] Failed to generate webhook URLs for fileId=${id}:`, e?.message || String(e));
218
218
  }
219
219
  } else if (!videoHref && api) {
220
220
  // Fallback to direct RTMP URL if webhook is not used
221
- logger?.debug(`[recordingFileToVideoClip] Fetching RTMP playback URL for fileName=${rec.fileName}`);
221
+ // logger?.debug(`[recordingFileToVideoClip] Fetching RTMP playback URL for fileName=${rec.fileName}`);
222
222
  try {
223
223
  const { rtmpVodUrl } = await api.getRecordingPlaybackUrls({
224
224
  fileName: rec.fileName,
225
225
  });
226
226
  videoHref = rtmpVodUrl;
227
- logger?.debug(`[recordingFileToVideoClip] RTMP URL fetched successfully: videoHref="${videoHref}"`);
227
+ // logger?.debug(`[recordingFileToVideoClip] RTMP URL fetched successfully: videoHref="${videoHref}"`);
228
228
  } catch (e) {
229
229
  logger?.debug(`[recordingFileToVideoClip] Failed to build playback URL for recording fileName=${rec.fileName}:`, e?.message || String(e));
230
230
  }
231
231
  } else {
232
- logger?.debug(`[recordingFileToVideoClip] No URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
232
+ // logger?.debug(`[recordingFileToVideoClip] No URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
233
233
  }
234
234
 
235
235
  const description = ('name' in rec && typeof rec.name === 'string' && rec.name) ? rec.name : (rec.fileName ?? rec.id ?? '');