@apocaliss92/nodelink-js 0.4.24 → 0.4.28
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 +4 -2
- package/dist/{chunk-JYHK2ZSH.js → chunk-NQ7D5TLR.js} +241 -27
- package/dist/chunk-NQ7D5TLR.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +201 -26
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +569 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +263 -6
- package/dist/index.d.ts +269 -5
- package/dist/index.js +290 -5
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunk-JYHK2ZSH.js.map +0 -1
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -8114,6 +8114,21 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
8114
8114
|
let audioSampleRate = null;
|
|
8115
8115
|
let streamStarted = false;
|
|
8116
8116
|
let closed = false;
|
|
8117
|
+
const signal = options?.signal;
|
|
8118
|
+
let sleepResolve = null;
|
|
8119
|
+
let sleepTimer = null;
|
|
8120
|
+
const clearSleepTimer = () => {
|
|
8121
|
+
if (sleepTimer) {
|
|
8122
|
+
clearTimeout(sleepTimer);
|
|
8123
|
+
sleepTimer = null;
|
|
8124
|
+
}
|
|
8125
|
+
};
|
|
8126
|
+
const handleAbort = () => {
|
|
8127
|
+
clearSleepTimer();
|
|
8128
|
+
const r = sleepResolve;
|
|
8129
|
+
sleepResolve = null;
|
|
8130
|
+
r?.();
|
|
8131
|
+
};
|
|
8117
8132
|
const onError = (_error) => {
|
|
8118
8133
|
closed = true;
|
|
8119
8134
|
api.logger?.warn?.(
|
|
@@ -8211,7 +8226,13 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
8211
8226
|
}
|
|
8212
8227
|
});
|
|
8213
8228
|
streamStarted = true;
|
|
8214
|
-
|
|
8229
|
+
if (signal) {
|
|
8230
|
+
if (signal.aborted) {
|
|
8231
|
+
closed = true;
|
|
8232
|
+
} else {
|
|
8233
|
+
signal.addEventListener("abort", handleAbort);
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8215
8236
|
while (!closed && !signal?.aborted) {
|
|
8216
8237
|
if (frameQueue.length > 0) {
|
|
8217
8238
|
const frame = frameQueue.shift();
|
|
@@ -8219,31 +8240,30 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
8219
8240
|
} else {
|
|
8220
8241
|
await new Promise((resolve) => {
|
|
8221
8242
|
frameResolve = resolve;
|
|
8222
|
-
|
|
8243
|
+
sleepResolve = resolve;
|
|
8244
|
+
sleepTimer = setTimeout(() => {
|
|
8245
|
+
sleepTimer = null;
|
|
8246
|
+
if (sleepResolve === resolve) sleepResolve = null;
|
|
8223
8247
|
if (frameResolve === resolve) {
|
|
8224
8248
|
frameResolve = null;
|
|
8225
8249
|
resolve();
|
|
8226
8250
|
}
|
|
8227
8251
|
}, 1e3);
|
|
8228
|
-
if (signal) {
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
};
|
|
8234
|
-
if (signal.aborted) {
|
|
8235
|
-
clearTimeout(timer);
|
|
8236
|
-
frameResolve = null;
|
|
8237
|
-
resolve();
|
|
8238
|
-
} else {
|
|
8239
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
8240
|
-
}
|
|
8252
|
+
if (signal?.aborted) {
|
|
8253
|
+
clearSleepTimer();
|
|
8254
|
+
sleepResolve = null;
|
|
8255
|
+
frameResolve = null;
|
|
8256
|
+
resolve();
|
|
8241
8257
|
}
|
|
8242
8258
|
});
|
|
8259
|
+
sleepResolve = null;
|
|
8260
|
+
clearSleepTimer();
|
|
8243
8261
|
}
|
|
8244
8262
|
}
|
|
8245
8263
|
} finally {
|
|
8246
8264
|
closed = true;
|
|
8265
|
+
if (signal) signal.removeEventListener("abort", handleAbort);
|
|
8266
|
+
clearSleepTimer();
|
|
8247
8267
|
try {
|
|
8248
8268
|
await videoStream.stop();
|
|
8249
8269
|
} catch {
|
|
@@ -10277,12 +10297,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10277
10297
|
if (frame.videoType === "H264" || frame.videoType === "H265") {
|
|
10278
10298
|
this.setFlowVideoType(frame.videoType, "native stream");
|
|
10279
10299
|
}
|
|
10280
|
-
this.flow.
|
|
10281
|
-
|
|
10282
|
-
|
|
10300
|
+
if (!this.flow.getFmtp().hasParamSets) {
|
|
10301
|
+
this.flow.extractParameterSets(frame.data);
|
|
10302
|
+
}
|
|
10303
|
+
if (this.flow.getFmtp().hasParamSets) {
|
|
10283
10304
|
this.markFirstFrameReceived();
|
|
10284
10305
|
}
|
|
10285
|
-
const isKeyframe = this.isRawFrameKeyframe(frame);
|
|
10306
|
+
const isKeyframe = typeof frame.isKeyframe === "boolean" ? frame.isKeyframe : this.isRawFrameKeyframe(frame);
|
|
10286
10307
|
this.prebuffer.push({
|
|
10287
10308
|
frame: { ...frame, data: Buffer.from(frame.data) },
|
|
10288
10309
|
time: Date.now(),
|
|
@@ -19000,6 +19021,62 @@ var applyFloodlightSettingsToXml = (xml, settings) => {
|
|
|
19000
19021
|
return modifiedXml;
|
|
19001
19022
|
};
|
|
19002
19023
|
|
|
19024
|
+
// src/reolink/baichuan/utils/whiteLedStatusPush.ts
|
|
19025
|
+
var parseFloodlightStatusListPushXml = (xml) => {
|
|
19026
|
+
const out = [];
|
|
19027
|
+
const re = /<channel>\s*(\d+)\s*<\/channel>[\s\S]*?<status>\s*(\d+)\s*<\/status>/gi;
|
|
19028
|
+
let m;
|
|
19029
|
+
while ((m = re.exec(xml)) !== null) {
|
|
19030
|
+
const channel = Number.parseInt(m[1] ?? "", 10);
|
|
19031
|
+
const status = Number.parseInt(m[2] ?? "", 10);
|
|
19032
|
+
if (!Number.isFinite(channel) || !Number.isFinite(status)) continue;
|
|
19033
|
+
out.push({ channel, status });
|
|
19034
|
+
}
|
|
19035
|
+
return out;
|
|
19036
|
+
};
|
|
19037
|
+
|
|
19038
|
+
// src/reolink/baichuan/utils/sirenStatusPush.ts
|
|
19039
|
+
var parseSirenStatusListPushXml = (xml) => {
|
|
19040
|
+
const out = [];
|
|
19041
|
+
const re = /<(?:channelId|channel)>\s*(\d+)\s*<\/(?:channelId|channel)>[\s\S]*?<status>\s*(\d+)\s*<\/status>(?:[\s\S]*?<playing>\s*(\d+)\s*<\/playing>)?/gi;
|
|
19042
|
+
let m;
|
|
19043
|
+
while ((m = re.exec(xml)) !== null) {
|
|
19044
|
+
const channel = Number.parseInt(m[1] ?? "", 10);
|
|
19045
|
+
const status = Number.parseInt(m[2] ?? "", 10);
|
|
19046
|
+
if (!Number.isFinite(channel) || !Number.isFinite(status)) continue;
|
|
19047
|
+
const entry = { channel, status };
|
|
19048
|
+
if (m[3] !== void 0) {
|
|
19049
|
+
const playing = Number.parseInt(m[3], 10);
|
|
19050
|
+
if (Number.isFinite(playing)) entry.playing = playing;
|
|
19051
|
+
}
|
|
19052
|
+
out.push(entry);
|
|
19053
|
+
}
|
|
19054
|
+
return out;
|
|
19055
|
+
};
|
|
19056
|
+
|
|
19057
|
+
// src/emailPush/bus.ts
|
|
19058
|
+
var import_node_events5 = require("events");
|
|
19059
|
+
var emitter = new import_node_events5.EventEmitter();
|
|
19060
|
+
function onEmailPushEvent(handler) {
|
|
19061
|
+
emitter.on("event", handler);
|
|
19062
|
+
return () => emitter.off("event", handler);
|
|
19063
|
+
}
|
|
19064
|
+
function mapEmailPushInferredType(inferred) {
|
|
19065
|
+
switch (inferred) {
|
|
19066
|
+
case "motion":
|
|
19067
|
+
case "doorbell":
|
|
19068
|
+
case "people":
|
|
19069
|
+
case "vehicle":
|
|
19070
|
+
case "animal":
|
|
19071
|
+
case "face":
|
|
19072
|
+
case "package":
|
|
19073
|
+
return inferred;
|
|
19074
|
+
case "other":
|
|
19075
|
+
default:
|
|
19076
|
+
return "motion";
|
|
19077
|
+
}
|
|
19078
|
+
}
|
|
19079
|
+
|
|
19003
19080
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
19004
19081
|
var DUAL_LENS_DUAL_MOTION_MODELS = /* @__PURE__ */ new Set([
|
|
19005
19082
|
"Reolink Duo PoE",
|
|
@@ -19145,7 +19222,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19145
19222
|
* general socket is created, logged in, and all event/push/guard listeners
|
|
19146
19223
|
* are re-attached automatically.
|
|
19147
19224
|
*
|
|
19148
|
-
* This is a **no-op** when the API is already
|
|
19225
|
+
* This is a **no-op** when the API is already ready (see `isReadyState()`).
|
|
19149
19226
|
*
|
|
19150
19227
|
* @throws If `close()` was called — the API is permanently closed and a new
|
|
19151
19228
|
* instance must be created.
|
|
@@ -19226,7 +19303,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19226
19303
|
/**
|
|
19227
19304
|
* Attach event, push, channelInfo, and guard listeners to the current
|
|
19228
19305
|
* "general" client. Called from the constructor and from
|
|
19229
|
-
*
|
|
19306
|
+
* `reconnectGeneralSocket()`.
|
|
19230
19307
|
*/
|
|
19231
19308
|
setupGeneralClientListeners() {
|
|
19232
19309
|
const client = this.client;
|
|
@@ -19278,7 +19355,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19278
19355
|
});
|
|
19279
19356
|
client.on("push", (frame) => {
|
|
19280
19357
|
const cmdId = frame.header.cmdId;
|
|
19281
|
-
if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
|
|
19358
|
+
if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST && cmdId !== BC_CMD_ID_FLOODLIGHT_STATUS_LIST && cmdId !== BC_CMD_ID_GET_AUDIO_ALARM) {
|
|
19282
19359
|
return;
|
|
19283
19360
|
}
|
|
19284
19361
|
try {
|
|
@@ -20815,6 +20892,40 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20815
20892
|
this.econnresetStormRebootInFlight = void 0;
|
|
20816
20893
|
});
|
|
20817
20894
|
}
|
|
20895
|
+
/**
|
|
20896
|
+
* Bind this API instance to the global email-push bus so that incoming
|
|
20897
|
+
* SMTP-delivered motion / AI events for the matching camera surface on
|
|
20898
|
+
* this instance's standard `onSimpleEvent` channel. The consumer keeps
|
|
20899
|
+
* a single subscription (`onSimpleEvent`) and gets both the native
|
|
20900
|
+
* Baichuan push and the email-push transport on the same stream.
|
|
20901
|
+
*
|
|
20902
|
+
* - `cameraId` shorthand: match events with `event.cameraId === cameraId`.
|
|
20903
|
+
* - `match`: arbitrary predicate (e.g. when the consumer uses a
|
|
20904
|
+
* nickname-based mapping or wants to handle multiple recipients).
|
|
20905
|
+
*
|
|
20906
|
+
* Returns an `off()` handle. Safe to call repeatedly — each call
|
|
20907
|
+
* registers its own listener.
|
|
20908
|
+
*/
|
|
20909
|
+
subscribeEmailPushEvents(params) {
|
|
20910
|
+
const channel = params.channel ?? 0;
|
|
20911
|
+
const matches = "match" in params ? params.match : (event) => event.cameraId === params.cameraId;
|
|
20912
|
+
const off = onEmailPushEvent((event) => {
|
|
20913
|
+
if (!matches(event)) return;
|
|
20914
|
+
this.dispatchSimpleEvent({
|
|
20915
|
+
type: mapEmailPushInferredType(event.inferredType),
|
|
20916
|
+
channel,
|
|
20917
|
+
timestamp: event.receivedAtMs
|
|
20918
|
+
});
|
|
20919
|
+
if (event.inferredType !== "motion" && event.inferredType !== "doorbell" && event.inferredType !== "other") {
|
|
20920
|
+
this.dispatchSimpleEvent({
|
|
20921
|
+
type: "motion",
|
|
20922
|
+
channel,
|
|
20923
|
+
timestamp: event.receivedAtMs
|
|
20924
|
+
});
|
|
20925
|
+
}
|
|
20926
|
+
});
|
|
20927
|
+
return off;
|
|
20928
|
+
}
|
|
20818
20929
|
/**
|
|
20819
20930
|
* Subscribe to minimal high-level events.
|
|
20820
20931
|
* The API manages Baichuan subscribe/unsubscribe automatically.
|
|
@@ -20873,7 +20984,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20873
20984
|
* Subscribe to per-frame detection events sourced from the BcMedia
|
|
20874
20985
|
* `additionalHeader` block on active video streams.
|
|
20875
20986
|
*
|
|
20876
|
-
* Mirrors
|
|
20987
|
+
* Mirrors `onSimpleEvent()` but is fed by the streaming side-channel:
|
|
20877
20988
|
* one event fires for every I-frame / P-frame that carries an overlay block.
|
|
20878
20989
|
* Coordinates are reported in normalized [0, 1] fractions of the source
|
|
20879
20990
|
* frame, so the same box renders correctly on mainStream, subStream, and
|
|
@@ -20900,7 +21011,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20900
21011
|
* Subscribe to AI object detections (people / vehicle / animal / face boxes
|
|
20901
21012
|
* with class label and confidence) without managing a video stream yourself.
|
|
20902
21013
|
*
|
|
20903
|
-
* Mirrors
|
|
21014
|
+
* Mirrors `onSimpleEvent()` end-to-end: on the first listener for a given
|
|
20904
21015
|
* `(channel, profile)` tuple the API ensures the corresponding video stream
|
|
20905
21016
|
* is running (the pool socket may already be shared with a regular consumer),
|
|
20906
21017
|
* forwards every box-bearing `additionalHeader` to your callback, and tears
|
|
@@ -27744,7 +27855,7 @@ ${xml}`
|
|
|
27744
27855
|
* Field meaning per stream:
|
|
27745
27856
|
* - `audio` — 0/1 toggle
|
|
27746
27857
|
* - `width`/`height` — resolution in pixels. Must be one of the
|
|
27747
|
-
* resolutions returned by
|
|
27858
|
+
* resolutions returned by `getStreamInfoList()`.
|
|
27748
27859
|
* - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
|
|
27749
27860
|
* - `frameRate` — fps. Must match the table from `getStreamInfoList`.
|
|
27750
27861
|
* - `videoEncType` — `"h264"` or `"h265"`
|
|
@@ -28319,6 +28430,33 @@ ${xml}`
|
|
|
28319
28430
|
};
|
|
28320
28431
|
return;
|
|
28321
28432
|
}
|
|
28433
|
+
if (cmdId === BC_CMD_ID_FLOODLIGHT_STATUS_LIST) {
|
|
28434
|
+
const entries = parseFloodlightStatusListPushXml(xml);
|
|
28435
|
+
if (entries.length === 0) return;
|
|
28436
|
+
for (const entry of entries) {
|
|
28437
|
+
const channel = normalizePushChannel(entry.channel) ?? channelFromHeader;
|
|
28438
|
+
getEntry(channel).floodlightStatus = {
|
|
28439
|
+
updatedAtMs: now,
|
|
28440
|
+
value: { status: entry.status === 1 }
|
|
28441
|
+
};
|
|
28442
|
+
}
|
|
28443
|
+
return;
|
|
28444
|
+
}
|
|
28445
|
+
if (cmdId === BC_CMD_ID_GET_AUDIO_ALARM) {
|
|
28446
|
+
const entries = parseSirenStatusListPushXml(xml);
|
|
28447
|
+
if (entries.length === 0) return;
|
|
28448
|
+
for (const entry of entries) {
|
|
28449
|
+
const channel = normalizePushChannel(entry.channel) ?? channelFromHeader;
|
|
28450
|
+
getEntry(channel).sirenStatus = {
|
|
28451
|
+
updatedAtMs: now,
|
|
28452
|
+
value: {
|
|
28453
|
+
status: entry.status === 1,
|
|
28454
|
+
...entry.playing !== void 0 ? { playing: entry.playing === 1 } : {}
|
|
28455
|
+
}
|
|
28456
|
+
};
|
|
28457
|
+
}
|
|
28458
|
+
return;
|
|
28459
|
+
}
|
|
28322
28460
|
}
|
|
28323
28461
|
/** Read-only snapshot of cached settings pushes (cmd_id 78/79/464/484/623/723). */
|
|
28324
28462
|
getSettingsPushCacheSnapshot() {
|
|
@@ -28351,6 +28489,18 @@ ${xml}`
|
|
|
28351
28489
|
...entry.coordinatePointList,
|
|
28352
28490
|
value: { ...entry.coordinatePointList.value }
|
|
28353
28491
|
}
|
|
28492
|
+
} : {},
|
|
28493
|
+
...entry.floodlightStatus ? {
|
|
28494
|
+
floodlightStatus: {
|
|
28495
|
+
...entry.floodlightStatus,
|
|
28496
|
+
value: { ...entry.floodlightStatus.value }
|
|
28497
|
+
}
|
|
28498
|
+
} : {},
|
|
28499
|
+
...entry.sirenStatus ? {
|
|
28500
|
+
sirenStatus: {
|
|
28501
|
+
...entry.sirenStatus,
|
|
28502
|
+
value: { ...entry.sirenStatus.value }
|
|
28503
|
+
}
|
|
28354
28504
|
} : {}
|
|
28355
28505
|
});
|
|
28356
28506
|
}
|
|
@@ -28374,6 +28524,29 @@ ${xml}`
|
|
|
28374
28524
|
getCoordinatePointListFromPushCache(channel = 0) {
|
|
28375
28525
|
return this.settingsPushCache.get(channel)?.coordinatePointList;
|
|
28376
28526
|
}
|
|
28527
|
+
/**
|
|
28528
|
+
* Last cmd_id 291 (FloodlightStatusList) push observed for the channel.
|
|
28529
|
+
* The camera emits this whenever the floodlight transitions on/off,
|
|
28530
|
+
* including the auto-off after the FloodlightManual duration. This is
|
|
28531
|
+
* the only reliable source for the current manual state because cmd 289
|
|
28532
|
+
* only returns the FloodlightTask config.
|
|
28533
|
+
*
|
|
28534
|
+
* Returns undefined when no push has been received yet.
|
|
28535
|
+
*/
|
|
28536
|
+
getCachedFloodlightStatus(channel = 0) {
|
|
28537
|
+
return this.settingsPushCache.get(channel)?.floodlightStatus;
|
|
28538
|
+
}
|
|
28539
|
+
/**
|
|
28540
|
+
* Last cmd_id 547 (SirenStatusList) push observed for the channel.
|
|
28541
|
+
* Captures the actual on/off transitions including the firmware's
|
|
28542
|
+
* built-in auto-off after the siren playback duration expires —
|
|
28543
|
+
* polling cmd 547 alone can race that auto-off.
|
|
28544
|
+
*
|
|
28545
|
+
* Returns undefined when no push has been received yet.
|
|
28546
|
+
*/
|
|
28547
|
+
getCachedSirenStatus(channel = 0) {
|
|
28548
|
+
return this.settingsPushCache.get(channel)?.sirenStatus;
|
|
28549
|
+
}
|
|
28377
28550
|
// --------------------
|
|
28378
28551
|
// PCAP-derived settings getters (typed wrappers)
|
|
28379
28552
|
// --------------------
|
|
@@ -29071,10 +29244,12 @@ ${xml}`
|
|
|
29071
29244
|
const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
|
|
29072
29245
|
const attachmentType = params.attachmentType ?? "picture";
|
|
29073
29246
|
const interval = params.interval ?? 30;
|
|
29247
|
+
const rawUser = params.authUsername ?? recipient;
|
|
29248
|
+
const wireUser = rawUser.includes("@") ? rawUser : `${rawUser}@${domain}`;
|
|
29074
29249
|
const emailPatch = {
|
|
29075
29250
|
smtpServer: params.managerHost,
|
|
29076
29251
|
smtpPort: port,
|
|
29077
|
-
userName:
|
|
29252
|
+
userName: wireUser,
|
|
29078
29253
|
password: params.authPassword ?? "",
|
|
29079
29254
|
address1: recipient,
|
|
29080
29255
|
address2: "",
|