@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.
@@ -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
- const signal = options?.signal;
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
- const timer = setTimeout(() => {
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
- const onAbort = () => {
8230
- clearTimeout(timer);
8231
- if (frameResolve === resolve) frameResolve = null;
8232
- resolve();
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.extractParameterSets(frame.data);
10281
- const { hasParamSets } = this.flow.getFmtp();
10282
- if (hasParamSets) {
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 {@link isReady}.
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
- * {@link reconnectGeneralSocket}.
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 {@link onSimpleEvent} but is fed by the streaming side-channel:
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 {@link onSimpleEvent} end-to-end: on the first listener for a given
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 {@link getStreamInfoList}.
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: params.authUsername ?? recipient,
29252
+ userName: wireUser,
29078
29253
  password: params.authPassword ?? "",
29079
29254
  address1: recipient,
29080
29255
  address2: "",