@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/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/camera.ts +15 -8
- package/src/stream-utils.ts +24 -96
- package/src/utils.ts +6 -6
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
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 =
|
|
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;
|
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,89 +93,13 @@ export function parseStreamProfileFromId(id: string | undefined): StreamProfile
|
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
type ReolinkRfc4571Metadata = {
|
|
97
|
+
profile: StreamProfile;
|
|
98
98
|
channel?: number;
|
|
99
99
|
variant?: NativeVideoStreamVariant;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
*
|
|
315
|
-
*
|
|
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,
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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 ?? '');
|