@apocaliss92/scrypted-reolink-native 0.1.32 → 0.1.35
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/README.md +6 -4
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +3 -2
- package/src/baichuan-base.ts +32 -4
- package/src/camera-battery.ts +0 -6
- package/src/camera.ts +1 -36
- package/src/common.ts +646 -17
- package/src/debug-options.ts +4 -0
- package/src/main.ts +158 -4
- package/src/multiFocal.ts +1 -29
- package/src/nvr.ts +37 -32
- package/src/stream-utils.ts +24 -7
- package/src/utils.ts +870 -2
- package/logs/composite-stream.txt +0 -16390
- package/logs/lense.txt +0 -44
- package/logs/multifocal.txt +0 -136
- package/logs/multifocal2.txt +0 -3585
package/src/debug-options.ts
CHANGED
|
@@ -8,6 +8,8 @@ export enum DebugLogOption {
|
|
|
8
8
|
General = 'general',
|
|
9
9
|
/** RTSP proxy/server debug logs */
|
|
10
10
|
DebugRtsp = 'debugRtsp',
|
|
11
|
+
/** Low-level tracing for recording-related commands */
|
|
12
|
+
TraceRecordings = 'traceRecordings',
|
|
11
13
|
/** Stream command tracing */
|
|
12
14
|
TraceStream = 'traceStream',
|
|
13
15
|
/** Talkback tracing */
|
|
@@ -27,6 +29,7 @@ export function mapDebugLogToApiOption(option: DebugLogOption): keyof DebugOptio
|
|
|
27
29
|
const mapping: Record<DebugLogOption, keyof DebugOptions | null> = {
|
|
28
30
|
[DebugLogOption.General]: 'general',
|
|
29
31
|
[DebugLogOption.DebugRtsp]: 'debugRtsp',
|
|
32
|
+
[DebugLogOption.TraceRecordings]: 'traceRecordings',
|
|
30
33
|
[DebugLogOption.TraceStream]: 'traceStream',
|
|
31
34
|
[DebugLogOption.TraceTalk]: 'traceTalk',
|
|
32
35
|
[DebugLogOption.TraceEvents]: 'traceEvents',
|
|
@@ -77,6 +80,7 @@ export function getApiRelevantDebugLogs(debugLogs: string[]): string[] {
|
|
|
77
80
|
export const DebugLogDisplayNames: Record<DebugLogOption, string> = {
|
|
78
81
|
[DebugLogOption.General]: 'General',
|
|
79
82
|
[DebugLogOption.DebugRtsp]: 'RTSP',
|
|
83
|
+
[DebugLogOption.TraceRecordings]: 'Trace recordings',
|
|
80
84
|
[DebugLogOption.TraceStream]: 'Trace stream',
|
|
81
85
|
[DebugLogOption.TraceTalk]: 'Trace talk',
|
|
82
86
|
[DebugLogOption.TraceEvents]: 'Trace events XML',
|
package/src/main.ts
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
|
-
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
1
|
+
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, HttpRequest, HttpResponse, MediaObject, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoClips } from "@scrypted/sdk";
|
|
2
2
|
import { BaseBaichuanClass } from "./baichuan-base";
|
|
3
3
|
import { ReolinkNativeCamera } from "./camera";
|
|
4
4
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
5
5
|
import { CommonCameraMixin } from "./common";
|
|
6
|
-
import { createBaichuanApi } from "./connect";
|
|
7
|
-
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
8
|
-
import { batteryCameraSuffix, batteryMultifocalSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, nvrSuffix } from "./utils";
|
|
9
6
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
7
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
8
|
+
import { batteryCameraSuffix, batteryMultifocalSuffix, cameraSuffix, extractThumbnailFromVideo, getDeviceInterfaces, handleVideoClipRequest, multifocalSuffix, nvrSuffix } from "./utils";
|
|
9
|
+
|
|
10
|
+
interface ThumbnailRequest {
|
|
11
|
+
deviceId: string;
|
|
12
|
+
fileId: string;
|
|
13
|
+
rtmpUrl?: string;
|
|
14
|
+
filePath?: string;
|
|
15
|
+
logger: Console;
|
|
16
|
+
resolve: (mo: MediaObject) => void;
|
|
17
|
+
reject: (error: Error) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ThumbnailRequestInput {
|
|
21
|
+
deviceId: string;
|
|
22
|
+
fileId: string;
|
|
23
|
+
rtmpUrl?: string;
|
|
24
|
+
filePath?: string;
|
|
25
|
+
logger: Console;
|
|
26
|
+
}
|
|
10
27
|
|
|
11
28
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
12
29
|
devices = new Map<string, BaseBaichuanClass>();
|
|
30
|
+
mixinsMap = new Map<string, CommonCameraMixin>();
|
|
13
31
|
nvrDeviceId: string;
|
|
32
|
+
private thumbnailQueue: ThumbnailRequest[] = [];
|
|
33
|
+
private thumbnailProcessing = false;
|
|
14
34
|
|
|
15
35
|
constructor(nativeId: string) {
|
|
16
36
|
super(nativeId);
|
|
@@ -236,6 +256,140 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
236
256
|
return new ReolinkNativeCamera(nativeId, this);
|
|
237
257
|
}
|
|
238
258
|
}
|
|
259
|
+
|
|
260
|
+
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
|
261
|
+
const logger = this.console;
|
|
262
|
+
const url = new URL(`http://localhost${request.url}`);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Parse webhook path: /.../webhook/{type}/{deviceId}/{fileId}
|
|
266
|
+
// The path may include prefix like /endpoint/@apocaliss92/scrypted-reolink-native/public/webhook/...
|
|
267
|
+
const pathParts = url.pathname.split('/').filter(p => p);
|
|
268
|
+
|
|
269
|
+
// Find the index of 'webhook' in the path
|
|
270
|
+
const webhookIndex = pathParts.indexOf('webhook');
|
|
271
|
+
if (webhookIndex === -1 || pathParts.length < webhookIndex + 4) {
|
|
272
|
+
response.send('Invalid webhook path', { code: 404 });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Extract type, deviceId, and fileId after 'webhook'
|
|
277
|
+
const type = pathParts[webhookIndex + 1];
|
|
278
|
+
const encodedDeviceId = pathParts[webhookIndex + 2];
|
|
279
|
+
// fileId may contain slashes, so join all remaining parts
|
|
280
|
+
const encodedFileId = pathParts.slice(webhookIndex + 3).join('/');
|
|
281
|
+
const deviceId = decodeURIComponent(encodedDeviceId);
|
|
282
|
+
let fileId = decodeURIComponent(encodedFileId);
|
|
283
|
+
|
|
284
|
+
// Restore leading slash if the original fileId had it (we removed it during encoding)
|
|
285
|
+
// The API expects fileId with leading slash for absolute paths
|
|
286
|
+
if (!fileId.startsWith('/') && !fileId.startsWith('http')) {
|
|
287
|
+
// If it looks like an absolute path (starts with common path prefixes), add slash
|
|
288
|
+
if (fileId.startsWith('mnt/') || fileId.startsWith('var/') || fileId.startsWith('tmp/')) {
|
|
289
|
+
fileId = `/${fileId}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// logger.log(`Webhook request: type=${type}, deviceId=${deviceId}, fileId=${fileId}`);
|
|
294
|
+
|
|
295
|
+
// Get the device
|
|
296
|
+
const device = this.mixinsMap.get(deviceId);
|
|
297
|
+
if (!device) {
|
|
298
|
+
response.send('Device not found', { code: 404 });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (type === 'video') {
|
|
303
|
+
await handleVideoClipRequest({
|
|
304
|
+
device,
|
|
305
|
+
deviceId,
|
|
306
|
+
fileId,
|
|
307
|
+
request,
|
|
308
|
+
response,
|
|
309
|
+
logger,
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
} else if (type === 'thumbnail') {
|
|
313
|
+
// Get thumbnail MediaObject
|
|
314
|
+
const mo = await device.getVideoClipThumbnail(fileId);
|
|
315
|
+
|
|
316
|
+
// Convert to buffer
|
|
317
|
+
const buffer = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
318
|
+
|
|
319
|
+
// Send image
|
|
320
|
+
response.send(buffer, {
|
|
321
|
+
code: 200,
|
|
322
|
+
headers: {
|
|
323
|
+
'Content-Type': 'image/jpeg',
|
|
324
|
+
'Cache-Control': 'max-age=31536000',
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
} else {
|
|
329
|
+
response.send('Invalid webhook type', { code: 404 });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
} catch (e: any) {
|
|
333
|
+
logger.error('Error in onRequest', e);
|
|
334
|
+
response.send(`Error: ${e.message}`, {
|
|
335
|
+
code: 500,
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
onPush(request: HttpRequest): Promise<void> {
|
|
342
|
+
return this.onRequest(request, undefined);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Add a thumbnail generation request to the queue
|
|
347
|
+
*/
|
|
348
|
+
async generateThumbnail(request: ThumbnailRequestInput): Promise<MediaObject> {
|
|
349
|
+
const queueLength = this.thumbnailQueue.length;
|
|
350
|
+
request.logger.log(`[Thumbnail] Download start: fileId=${request.fileId}, queuePosition=${queueLength + 1}`);
|
|
351
|
+
|
|
352
|
+
return new Promise((resolve, reject) => {
|
|
353
|
+
this.thumbnailQueue.push({
|
|
354
|
+
...request,
|
|
355
|
+
resolve,
|
|
356
|
+
reject,
|
|
357
|
+
});
|
|
358
|
+
this.processThumbnailQueue();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Process the thumbnail queue sequentially
|
|
364
|
+
*/
|
|
365
|
+
private async processThumbnailQueue(): Promise<void> {
|
|
366
|
+
if (this.thumbnailProcessing || this.thumbnailQueue.length === 0) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.thumbnailProcessing = true;
|
|
371
|
+
|
|
372
|
+
while (this.thumbnailQueue.length > 0) {
|
|
373
|
+
const request = this.thumbnailQueue.shift()!;
|
|
374
|
+
const logger = request.logger;
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const thumbnail = await extractThumbnailFromVideo(request);
|
|
378
|
+
logger.log(`[Thumbnail] OK: fileId=${request.fileId}`);
|
|
379
|
+
request.resolve(thumbnail);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
logger.error(`[Thumbnail] Error: fileId=${request.fileId}`, error);
|
|
382
|
+
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add 2 second delay between thumbnails (except after the last one)
|
|
386
|
+
if (this.thumbnailQueue.length > 0) {
|
|
387
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.thumbnailProcessing = false;
|
|
392
|
+
}
|
|
239
393
|
}
|
|
240
394
|
|
|
241
395
|
export default ReolinkNativePlugin;
|
package/src/multiFocal.ts
CHANGED
|
@@ -75,7 +75,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
75
75
|
this.storageSettings.settings.uid.hide = !this.isBattery;
|
|
76
76
|
|
|
77
77
|
await this.ensureBaichuanClient();
|
|
78
|
-
await this.updateDeviceInfo();
|
|
79
78
|
await this.reportDevices();
|
|
80
79
|
await this.subscribeToEvents();
|
|
81
80
|
} catch (e) {
|
|
@@ -89,27 +88,9 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
async updateDeviceInfo(): Promise<void> {
|
|
93
|
-
const logger = this.getBaichuanLogger();
|
|
94
|
-
try {
|
|
95
|
-
const api = await this.ensureBaichuanClient();
|
|
96
|
-
const deviceData = await api.getInfo();
|
|
97
|
-
|
|
98
|
-
await updateDeviceInfo({
|
|
99
|
-
device: this,
|
|
100
|
-
deviceData,
|
|
101
|
-
ipAddress: this.storageSettings.values.ipAddress,
|
|
102
|
-
logger,
|
|
103
|
-
});
|
|
104
|
-
} catch (e) {
|
|
105
|
-
logger.warn('Failed to fetch device info', e);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
91
|
getInterfaces(channel: number) {
|
|
110
92
|
const logger = this.getBaichuanLogger();
|
|
111
|
-
const
|
|
112
|
-
const { capabilities: caps, multifocalInfo } = values;
|
|
93
|
+
const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
|
|
113
94
|
const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.channel === channel);
|
|
114
95
|
|
|
115
96
|
const capabilities: DeviceCapabilities = {
|
|
@@ -213,15 +194,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
213
194
|
}
|
|
214
195
|
}
|
|
215
196
|
|
|
216
|
-
async getSettings(): Promise<Setting[]> {
|
|
217
|
-
const settings = await this.storageSettings.getSettings();
|
|
218
|
-
return settings;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
222
|
-
return this.storageSettings.putSetting(key, value);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
197
|
async releaseDevice(id: string, nativeId: string) {
|
|
226
198
|
this.cameraNativeMap.delete(nativeId);
|
|
227
199
|
super.releaseDevice(id, nativeId);
|
package/src/nvr.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { DeviceInfoResponse,
|
|
2
|
-
import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot,
|
|
1
|
+
import type { DeviceInfoResponse, EnrichedRecordingFile, EventsResponse, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
3
3
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
|
-
import { BaseBaichuanClass, type
|
|
4
|
+
import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
|
|
5
5
|
import { ReolinkNativeCamera } from "./camera";
|
|
6
6
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
7
7
|
import { normalizeUid } from "./connect";
|
|
@@ -80,8 +80,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
async reboot(): Promise<void> {
|
|
83
|
-
const api = await this.
|
|
84
|
-
await api.
|
|
83
|
+
const api = await this.ensureBaichuanClient();
|
|
84
|
+
await api.reboot();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// BaseBaichuanClass abstract methods implementation
|
|
@@ -175,16 +175,41 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
178
|
+
const logger = this.getBaichuanLogger();
|
|
178
179
|
this.nvrApi = new ReolinkCgiApi({
|
|
179
180
|
host: ipAddress,
|
|
180
181
|
username,
|
|
181
182
|
password,
|
|
183
|
+
logger,
|
|
182
184
|
});
|
|
183
185
|
|
|
184
186
|
await this.nvrApi.login();
|
|
185
187
|
return this.nvrApi;
|
|
186
188
|
}
|
|
187
189
|
|
|
190
|
+
/**
|
|
191
|
+
* List enriched VOD files (with proper parsing and detection info)
|
|
192
|
+
* This uses the library's enrichVodFile which handles all parsing correctly
|
|
193
|
+
*/
|
|
194
|
+
async listEnrichedVodFiles(params: {
|
|
195
|
+
channel: number;
|
|
196
|
+
start: Date;
|
|
197
|
+
end: Date;
|
|
198
|
+
streamType?: "main" | "sub";
|
|
199
|
+
autoSearchByDay?: boolean;
|
|
200
|
+
bypassCache?: boolean;
|
|
201
|
+
}): Promise<Array<EnrichedRecordingFile>> {
|
|
202
|
+
const api = await this.ensureClient();
|
|
203
|
+
return await api.listEnrichedVodFiles({
|
|
204
|
+
channel: params.channel,
|
|
205
|
+
start: params.start,
|
|
206
|
+
end: params.end,
|
|
207
|
+
streamType: params.streamType,
|
|
208
|
+
autoSearchByDay: params.autoSearchByDay,
|
|
209
|
+
bypassCache: params.bypassCache,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
188
213
|
private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
189
214
|
const logger = this.getBaichuanLogger();
|
|
190
215
|
|
|
@@ -222,9 +247,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
222
247
|
case 'doorbell':
|
|
223
248
|
// Handle doorbell if camera supports it
|
|
224
249
|
try {
|
|
225
|
-
|
|
226
|
-
(targetCamera as any).handleDoorbellEvent();
|
|
227
|
-
}
|
|
250
|
+
targetCamera.handleDoorbellEvent();
|
|
228
251
|
}
|
|
229
252
|
catch (e) {
|
|
230
253
|
logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
|
|
@@ -469,33 +492,15 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
469
492
|
async syncEntitiesFromRemote() {
|
|
470
493
|
const logger = this.getBaichuanLogger();
|
|
471
494
|
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
const baichuanApi = await this.ensureBaichuanClient();
|
|
475
|
-
|
|
476
|
-
// Wait for Baichuan connection to be fully established
|
|
477
|
-
if (baichuanApi?.client) {
|
|
478
|
-
// Check if already connected
|
|
479
|
-
if (!baichuanApi.client.isSocketConnected()) {
|
|
480
|
-
logger.debug('Waiting for Baichuan connection to be established...');
|
|
481
|
-
// Wait up to 5 seconds for connection
|
|
482
|
-
let attempts = 0;
|
|
483
|
-
while (!baichuanApi.client.isSocketConnected() && attempts < 50) {
|
|
484
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
485
|
-
attempts++;
|
|
486
|
-
}
|
|
487
|
-
if (!baichuanApi.client.isSocketConnected()) {
|
|
488
|
-
logger.warn('Baichuan connection not established after waiting, proceeding anyway');
|
|
489
|
-
} else {
|
|
490
|
-
logger.debug('Baichuan connection established');
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
495
|
+
const cgiApi = await this.ensureClient();
|
|
496
|
+
const { devicesData, channels } = await cgiApi.getDevicesInfo();
|
|
494
497
|
|
|
495
|
-
const
|
|
498
|
+
// const api = await this.ensureBaichuanClient();
|
|
499
|
+
// const devicesMap = api.getDevicesInfo();
|
|
500
|
+
// const deviceEntries = Object.entries(devicesMap);
|
|
496
501
|
|
|
497
502
|
if (!channels.length) {
|
|
498
|
-
logger.debug(`No channels found, ${JSON.stringify({
|
|
503
|
+
logger.debug(`No channels found, ${JSON.stringify({ channels, devicesData })}`);
|
|
499
504
|
return;
|
|
500
505
|
}
|
|
501
506
|
|
package/src/stream-utils.ts
CHANGED
|
@@ -17,8 +17,9 @@ 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 profile The stream profile (main, sub, ext) - used to determine if a new client is needed.
|
|
20
21
|
*/
|
|
21
|
-
createStreamClient: () => Promise<ReolinkBaichuanApi>;
|
|
22
|
+
createStreamClient: (profile?: StreamProfile) => Promise<ReolinkBaichuanApi>;
|
|
22
23
|
getLogger: () => Console;
|
|
23
24
|
/**
|
|
24
25
|
* Credentials to include in the TCP stream (username, password).
|
|
@@ -201,7 +202,7 @@ export class StreamManager {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
private getLogger() {
|
|
204
|
-
return this.opts.getLogger()
|
|
205
|
+
return this.opts.getLogger();
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
private async ensureRfcServer(
|
|
@@ -213,11 +214,27 @@ export class StreamManager {
|
|
|
213
214
|
compositeOptions?: CompositeStreamPipOptions;
|
|
214
215
|
},
|
|
215
216
|
): Promise<RfcServerInfo> {
|
|
217
|
+
// Check for existing promise first to prevent duplicate server creation
|
|
216
218
|
const existingCreate = this.nativeRfcServerCreatePromises.get(streamKey);
|
|
217
219
|
if (existingCreate) {
|
|
218
220
|
return await existingCreate;
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
// Double-check: if server already exists and is listening, return it immediately
|
|
224
|
+
const existingServer = this.nativeRfcServers.get(streamKey);
|
|
225
|
+
if (existingServer?.server?.listening) {
|
|
226
|
+
if (!expectedVideoType || existingServer.videoType === expectedVideoType) {
|
|
227
|
+
return {
|
|
228
|
+
host: existingServer.host,
|
|
229
|
+
port: existingServer.port,
|
|
230
|
+
sdp: existingServer.sdp,
|
|
231
|
+
audio: existingServer.audio,
|
|
232
|
+
username: existingServer.username || this.opts.credentials.username,
|
|
233
|
+
password: existingServer.password || this.opts.credentials.password,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
221
238
|
const createPromise = (async () => {
|
|
222
239
|
const cached = this.nativeRfcServers.get(streamKey);
|
|
223
240
|
if (cached?.server?.listening) {
|
|
@@ -233,8 +250,8 @@ export class StreamManager {
|
|
|
233
250
|
port: cached.port,
|
|
234
251
|
sdp: cached.sdp,
|
|
235
252
|
audio: cached.audio,
|
|
236
|
-
username:
|
|
237
|
-
password:
|
|
253
|
+
username: cached.username || this.opts.credentials.username,
|
|
254
|
+
password: cached.password || this.opts.credentials.password,
|
|
238
255
|
};
|
|
239
256
|
}
|
|
240
257
|
}
|
|
@@ -249,7 +266,7 @@ export class StreamManager {
|
|
|
249
266
|
this.nativeRfcServers.delete(streamKey);
|
|
250
267
|
}
|
|
251
268
|
|
|
252
|
-
const api = await this.opts.createStreamClient();
|
|
269
|
+
const api = await this.opts.createStreamClient(profile);
|
|
253
270
|
const { createRfc4571TcpServer } = await import('@apocaliss92/reolink-baichuan-js');
|
|
254
271
|
|
|
255
272
|
// Use the same credentials as the main connection
|
|
@@ -281,8 +298,8 @@ export class StreamManager {
|
|
|
281
298
|
port: created.port,
|
|
282
299
|
sdp: created.sdp,
|
|
283
300
|
audio: created.audio,
|
|
284
|
-
username:
|
|
285
|
-
password:
|
|
301
|
+
username: created.username || this.opts.credentials.username,
|
|
302
|
+
password: created.password || this.opts.credentials.password,
|
|
286
303
|
};
|
|
287
304
|
})();
|
|
288
305
|
|