@apocaliss92/scrypted-reolink-native 0.2.9 → 0.2.11

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.
@@ -10,16 +10,12 @@ export enum DebugLogOption {
10
10
  DebugRtsp = 'debugRtsp',
11
11
  /** Low-level tracing for recording-related commands */
12
12
  TraceRecordings = 'traceRecordings',
13
- /** Stream command tracing */
14
- TraceStream = 'traceStream',
13
+ /** Native stream tracing (stream tx/rx + H264/H265 + param sets) */
14
+ TraceNativeStream = 'traceNativeStream',
15
15
  /** Talkback tracing */
16
16
  TraceTalk = 'traceTalk',
17
17
  /** Event tracing */
18
18
  TraceEvents = 'traceEvents',
19
- /** H.264 debug logs */
20
- DebugH264 = 'debugH264',
21
- /** SPS/PPS parameter sets debug logs */
22
- DebugParamSets = 'debugParamSets',
23
19
  }
24
20
 
25
21
  /**
@@ -30,11 +26,9 @@ export function mapDebugLogToApiOption(option: DebugLogOption): keyof DebugOptio
30
26
  [DebugLogOption.General]: 'general',
31
27
  [DebugLogOption.DebugRtsp]: 'debugRtsp',
32
28
  [DebugLogOption.TraceRecordings]: 'traceRecordings',
33
- [DebugLogOption.TraceStream]: 'traceStream',
29
+ [DebugLogOption.TraceNativeStream]: 'traceNativeStream',
34
30
  [DebugLogOption.TraceTalk]: 'traceTalk',
35
31
  [DebugLogOption.TraceEvents]: 'traceEvents',
36
- [DebugLogOption.DebugH264]: 'debugH264',
37
- [DebugLogOption.DebugParamSets]: 'debugParamSets',
38
32
  };
39
33
  return mapping[option];
40
34
  }
@@ -81,11 +75,9 @@ export const DebugLogDisplayNames: Record<DebugLogOption, string> = {
81
75
  [DebugLogOption.General]: 'General',
82
76
  [DebugLogOption.DebugRtsp]: 'RTSP',
83
77
  [DebugLogOption.TraceRecordings]: 'Trace recordings',
84
- [DebugLogOption.TraceStream]: 'Trace stream',
78
+ [DebugLogOption.TraceNativeStream]: 'Trace native stream',
85
79
  [DebugLogOption.TraceTalk]: 'Trace talk',
86
80
  [DebugLogOption.TraceEvents]: 'Trace events XML',
87
- [DebugLogOption.DebugH264]: 'H264',
88
- [DebugLogOption.DebugParamSets]: 'Video param sets',
89
81
  };
90
82
 
91
83
  /**
package/src/intercom.ts CHANGED
@@ -274,7 +274,7 @@ export class ReolinkBaichuanIntercom {
274
274
  }
275
275
  })
276
276
  .catch((e) => {
277
- logger.warn("Intercom PCM->ADPCM pipeline error", e);
277
+ logger.warn("Intercom PCM->ADPCM pipeline error", e?.message || String(e));
278
278
  });
279
279
  }
280
280
 
package/src/main.ts CHANGED
@@ -324,7 +324,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
324
324
  return;
325
325
  }
326
326
  } catch (e: any) {
327
- logger.error('Error in onRequest', e);
327
+ logger.error('Error in onRequest', e?.message || String(e));
328
328
  response.send(`Error: ${e.message}`, {
329
329
  code: 500,
330
330
  });
@@ -374,7 +374,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
374
374
  logger.log(`[Thumbnail] Download completed: fileId=${request.fileId}`);
375
375
  request.resolve(thumbnail);
376
376
  } catch (error) {
377
- logger.error(`[Thumbnail] Error: fileId=${request.fileId}`, error);
377
+ logger.error(`[Thumbnail] Error: fileId=${request.fileId}`, error?.message || String(error));
378
378
  request.reject(error instanceof Error ? error : new Error(String(error)));
379
379
  }
380
380
 
package/src/multiFocal.ts CHANGED
@@ -172,12 +172,28 @@ export class ReolinkNativeMultiFocalDevice extends ReolinkCamera implements Sett
172
172
  throw new Error('Missing device credentials');
173
173
  }
174
174
 
175
+ const outputPath = this.storageSettings.values.diagnosticsOutputPath || process.env.SCRYPTED_PLUGIN_VOLUME || "";
176
+ if (!outputPath) {
177
+ throw new Error('Diagnostics output path is required');
178
+ }
179
+
175
180
  const api = await this.ensureClient();
176
181
 
177
- const multifocalDiagnostics = await api.collectMultifocalDiagnostics(logger);
182
+ const channel = this.storageSettings.values.rtspChannel || 0;
183
+ const durationSeconds = 8;
184
+ const result = await api.runMultifocalDiagnosticsConsecutively({
185
+ logger,
186
+ outDir: outputPath,
187
+ channel,
188
+ durationSeconds,
189
+ rtmpApps: ["bcs"],
190
+ probeFull: true,
191
+ onNvr: !!this.nvrDevice,
192
+ });
178
193
 
179
- logger.log(`NVR diagnostics completed successfully.`);
180
- logger.debug(JSON.stringify(multifocalDiagnostics));
194
+ logger.log(`Multifocal diagnostics completed successfully. Output directory: ${result.runDir}`);
195
+ logger.log(`Results file: ${result.resultsPath}`);
196
+ logger.log(`Streams directory: ${result.streamsDir}`);
181
197
  } catch (e) {
182
198
  logger.error('Failed to run NVR diagnostics', e?.message || String(e));
183
199
  throw e;
@@ -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,6 +93,14 @@ export function parseStreamProfileFromId(id: string | undefined): StreamProfile
93
93
  return;
94
94
  }
95
95
 
96
+ type ReolinkRfc4571Metadata = {
97
+ profile: StreamProfile;
98
+ channel?: number;
99
+ variant?: NativeVideoStreamVariant;
100
+ /** Explicitly mark composite (channel-less) streams. If omitted, `channel===undefined` implies composite. */
101
+ isComposite?: boolean;
102
+ };
103
+
96
104
  /**
97
105
  * Extract and normalize variant type from stream ID or URL (e.g., "autotrack" from "native_autotrack_main" or "?variant=autotrack")
98
106
  * Returns undefined if no variant is present, or "autotrack"/"telephoto" if present
@@ -163,16 +171,18 @@ export function selectStreamOption(
163
171
 
164
172
  export async function createRfc4571MediaObjectFromStreamManager(params: {
165
173
  streamManager: StreamManager;
166
- channel: number;
167
- profile: StreamProfile;
168
174
  streamKey: string;
169
- variant?: NativeVideoStreamVariant;
170
175
  selected: UrlMediaStreamOptions;
171
176
  sourceId: string;
172
177
  }): Promise<MediaObject> {
173
- const { streamManager, channel, profile, streamKey, variant, selected, sourceId } = params;
178
+ const { streamManager, streamKey, selected, sourceId } = params;
179
+
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
+ }
174
184
 
175
- const { host, port, sdp, audio, username, password } = await streamManager.getRfcStream(channel, profile, streamKey, variant);
185
+ const { host, port, sdp, audio, username, password } = await streamManager.getRfcServer(streamKey, meta);
176
186
 
177
187
  const { url: _ignoredUrl, ...mso }: any = selected;
178
188
  mso.container = 'rtp';
@@ -203,62 +213,6 @@ export async function createRfc4571MediaObjectFromStreamManager(params: {
203
213
  });
204
214
  }
205
215
 
206
- export async function createRfc4571CompositeMediaObjectFromStreamManager(params: {
207
- streamManager: StreamManager;
208
- profile: StreamProfile;
209
- streamKey: string;
210
- selected: UrlMediaStreamOptions;
211
- sourceId: string;
212
- variantType?: NativeVideoStreamVariant;
213
- }): Promise<MediaObject> {
214
- const { streamManager, profile, streamKey, selected, sourceId, variantType } = params;
215
-
216
- // Extract variantType from streamKey if not provided (format: composite_${variantType}_${profile})
217
- let extractedVariantType = variantType;
218
- if (!extractedVariantType && streamKey.startsWith('composite_')) {
219
- const parts = streamKey.split('_');
220
- if (parts.length >= 3) {
221
- // Format: composite_${variantType}_${profile}
222
- const variantPart = parts[1];
223
- if (variantPart === 'default' || variantPart === 'autotrack' || variantPart === 'telephoto') {
224
- extractedVariantType = variantPart as NativeVideoStreamVariant;
225
- }
226
- }
227
- }
228
-
229
- const { host, port, sdp, audio, username, password } = await streamManager.getRfcCompositeStream(profile, streamKey, extractedVariantType);
230
-
231
- const { url: _ignoredUrl, ...mso }: any = selected;
232
- mso.container = 'rtp';
233
- if (audio) {
234
- mso.audio ||= {};
235
- mso.audio.codec = audio.codec;
236
- mso.audio.sampleRate = audio.sampleRate;
237
- mso.audio.channels = audio.channels;
238
- }
239
-
240
- // Build URL with credentials: tcp://username:password@host:port
241
- // Keep this consistent with non-composite path (URL object -> JSON string via toJSON()).
242
- const urlObj = new URL(`tcp://${host}`);
243
- urlObj.port = port.toString();
244
- if (username) {
245
- urlObj.username = username;
246
- }
247
- if (password) {
248
- urlObj.password = password;
249
- }
250
-
251
- const rfc = {
252
- url: urlObj,
253
- sdp,
254
- mediaStreamOptions: mso as ResponseMediaStreamOptions,
255
- };
256
-
257
- return await sdk.mediaManager.createMediaObject(Buffer.from(JSON.stringify(rfc)), 'x-scrypted/x-rfc4571', {
258
- sourceId,
259
- });
260
- }
261
-
262
216
  type RfcServerInfo = {
263
217
  host: string;
264
218
  port: number;
@@ -283,6 +237,26 @@ export class StreamManager {
283
237
  return this.opts.logger || console;
284
238
  }
285
239
 
240
+ /**
241
+ * Unified RFC4571 server accessor.
242
+ *
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).
245
+ */
246
+ async getRfcServer(streamKey: string, meta: ReolinkRfc4571Metadata): Promise<RfcServerInfo> {
247
+ if (!meta?.profile) {
248
+ throw new Error(`getRfcServer: missing profile for streamKey='${streamKey}'`);
249
+ }
250
+
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,
257
+ });
258
+ }
259
+
286
260
  private async ensureRfcServer(
287
261
  streamKey: string,
288
262
  profile: StreamProfile,
@@ -337,24 +311,16 @@ export class StreamManager {
337
311
 
338
312
  const isComposite = options.channel === undefined;
339
313
 
314
+ // IMPORTANT: do not parse composite profiles here.
315
+ // The library/server derives the composite pairing from the requested id.
316
+
340
317
  // For composite streams, we may want two distinct Baichuan sessions (wider + tele)
341
318
  // to avoid frame mixing on some firmwares. On BCUDP/battery devices, extra sessions
342
319
  // can be harmful; in that case, createStreamClient may return the same underlying client.
343
320
  //
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')
354
- : undefined;
355
- const compositeTeleStreamKey = compositeTeleVariant
356
- ? `${compositeTeleChannel}_${compositeTeleVariant}_${profile}`
357
- : `${compositeTeleChannel}_${profile}`;
321
+ // Use stable per-request keys; include variant for tele to keep sessions distinct.
322
+ const compositeWiderStreamKey = `${streamKey}_wider`;
323
+ const compositeTeleStreamKey = `${streamKey}_tele_${options.variant ?? 'default'}`;
358
324
 
359
325
  // For composite streams, using two distinct Baichuan sessions can avoid frame mixing on some firmwares.
360
326
  // However, for UDP/battery devices extra BCUDP sessions can trigger storms; if we detect the same
@@ -387,11 +353,18 @@ export class StreamManager {
387
353
  }
388
354
  }
389
355
 
390
- // For non-composite streams, create a single API client.
356
+ // For non-composite streams, create/reuse a single API client.
357
+ // For NVR/Hub (sharedConnection=true), avoid creating one TCP session per profile
358
+ // (e.g. main+sub would otherwise become two sockets). Group by (channel, variant).
391
359
  // For composite streams, base api must be a real lens streamKey (not the composite RFC key).
360
+ const shared = this.opts.sharedConnection ?? false;
361
+ const nonCompositeApiKey = (!isComposite && shared && options.channel !== undefined)
362
+ ? `channel_${options.channel}_${options.variant ?? 'default'}`
363
+ : streamKey;
364
+
392
365
  const api = isComposite
393
366
  ? (compositeApis?.widerApi ?? await this.opts.createStreamClient(compositeWiderStreamKey))
394
- : await this.opts.createStreamClient(streamKey);
367
+ : await this.opts.createStreamClient(nonCompositeApiKey);
395
368
 
396
369
  const { createRfc4571TcpServer } = await import('@apocaliss92/reolink-baichuan-js');
397
370
 
@@ -417,6 +390,7 @@ export class StreamManager {
417
390
  closeApiOnTeardown,
418
391
  username,
419
392
  password,
393
+ requestedId: streamKey,
420
394
  // Composite can take a bit longer (ffmpeg warmup + first IDR).
421
395
  ...(isComposite ? { keyframeTimeoutMs: 20_000, idleTeardownMs: 20_000 } : {}),
422
396
  ...(compositeOptions ? { compositeOptions } : {}),
@@ -464,10 +438,8 @@ export class StreamManager {
464
438
  streamKey: string,
465
439
  variant?: NativeVideoStreamVariant,
466
440
  ): Promise<RfcServerInfo> {
467
- return await this.ensureRfcServer(streamKey, profile, {
468
- channel,
469
- variant,
470
- });
441
+ // Back-compat wrapper. Prefer getRfcServer(streamKey).
442
+ return await this.ensureRfcServer(streamKey, profile, { channel, variant });
471
443
  }
472
444
 
473
445
  async getRfcCompositeStream(
@@ -475,24 +447,11 @@ export class StreamManager {
475
447
  streamKey: string,
476
448
  variantType?: NativeVideoStreamVariant,
477
449
  ): Promise<RfcServerInfo> {
478
- // Extract variantType from streamKey if not provided (format: composite_${variantType}_${profile})
479
- let extractedVariantType = variantType;
480
- if (!extractedVariantType && streamKey.startsWith('composite_')) {
481
- const parts = streamKey.split('_');
482
- if (parts.length >= 3) {
483
- // Format: composite_${variantType}_${profile}
484
- const variantPart = parts[1];
485
- if (variantPart === 'default' || variantPart === 'autotrack' || variantPart === 'telephoto') {
486
- extractedVariantType = variantPart as NativeVideoStreamVariant;
487
- }
488
- }
489
- }
490
-
491
- // Pass variantType to ensureRfcServer so it can be used when creating the stream client
492
- // This ensures each variantType gets its own socket
450
+ // Back-compat wrapper. Prefer getRfcServer(streamKey).
451
+ // Pass variantType to ensureRfcServer so it can be used when creating the stream client.
493
452
  return await this.ensureRfcServer(streamKey, profile, {
494
- channel: undefined, // Undefined channel indicates composite stream
495
- variant: extractedVariantType,
453
+ channel: undefined,
454
+ variant: variantType,
496
455
  compositeOptions: this.opts.compositeOptions,
497
456
  });
498
457
  }
package/src/utils.ts CHANGED
@@ -1043,7 +1043,7 @@ export async function vodSearchResultsToVideoClips(
1043
1043
  videoHref = videoUrl;
1044
1044
  thumbnailHref = thumbnailUrl;
1045
1045
  } catch (e) {
1046
- logger?.debug('Failed to generate webhook URLs for VOD file', fileName, e);
1046
+ logger?.debug('Failed to generate webhook URLs for VOD file', fileName, e?.message || String(e));
1047
1047
  }
1048
1048
 
1049
1049
  const clip: VideoClip = {
@@ -1060,7 +1060,7 @@ export async function vodSearchResultsToVideoClips(
1060
1060
 
1061
1061
  clips.push(clip);
1062
1062
  } catch (e) {
1063
- logger?.warn(`Failed to convert VOD file to clip: ${file.name}`, e);
1063
+ logger?.warn(`Failed to convert VOD file to clip: ${file.name}`, e?.message || String(e));
1064
1064
  }
1065
1065
  }
1066
1066
  }