@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.
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +3 -3
- package/src/camera.ts +83 -184
- package/src/debug-options.ts +4 -12
- package/src/intercom.ts +1 -1
- package/src/main.ts +2 -2
- package/src/multiFocal.ts +19 -3
- package/src/stream-utils.ts +59 -100
- package/src/utils.ts +2 -2
package/src/debug-options.ts
CHANGED
|
@@ -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
|
-
/**
|
|
14
|
-
|
|
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.
|
|
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.
|
|
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
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
|
|
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(`
|
|
180
|
-
logger.
|
|
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;
|
package/src/stream-utils.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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,
|
|
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.
|
|
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
|
-
//
|
|
345
|
-
|
|
346
|
-
const
|
|
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(
|
|
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
|
-
|
|
468
|
-
|
|
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
|
-
//
|
|
479
|
-
|
|
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,
|
|
495
|
-
variant:
|
|
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
|
}
|