@apocaliss92/nodelink-js 0.4.26 โ 0.4.29
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-F3XCYKYT.js โ chunk-NQ7D5TLR.js} +145 -26
- package/dist/chunk-NQ7D5TLR.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +105 -25
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +473 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +217 -6
- package/dist/index.d.ts +221 -5
- package/dist/index.js +290 -5
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunk-F3XCYKYT.js.map +0 -1
package/README.md
CHANGED
|
@@ -39,13 +39,15 @@ await api.onSimpleEvent((event) => {
|
|
|
39
39
|
|
|
40
40
|
## Email Push for Battery Cameras
|
|
41
41
|
|
|
42
|
+
> ๐งช **Experimental** โ the feature is officially enabled but still under active testing. The default flow uses the manager's built-in SMTP server; expect rough edges and please report any issue you hit.
|
|
43
|
+
|
|
42
44
|
Battery cameras (Argus, Go, โฆ) can't reliably keep a TCP/ONVIF push subscription alive while sleeping. The manager app embeds an SMTP server so the camera can deliver motion alerts via email โ the most resilient path for sleep-heavy devices.
|
|
43
45
|
|
|
44
46
|
**Flow**:
|
|
45
47
|
|
|
46
|
-
1. Enable the manager's built-in SMTP server (**Settings โ Email Push**, default port `2525`).
|
|
48
|
+
1. Enable the manager's built-in SMTP server (**Settings โ Email Push**, default port `2525`). All settings (host, port, domain, auth) remain manually editable.
|
|
47
49
|
2. Each camera gets a unique recipient `cam-<id>@<domain>` (`emailPush.getCameraAddress`).
|
|
48
|
-
3. From the camera's **Email Push** tab in the manager UI, click **Auto-configure** โ the manager pushes the right SMTP server, recipients and 24/7 schedule to the camera via Baichuan (`baichuan.setupEmailPushToManager`).
|
|
50
|
+
3. From the camera's **Email Push** tab in the manager UI (Camera Settings modal), click **Auto-configure** โ the manager pushes the right SMTP server, recipients and 24/7 schedule to the camera via Baichuan (`baichuan.setupEmailPushToManager`). You can also fill the fields by hand in the Reolink app: server = your manager `domain`, sender = `authUsername`, password = `authPassword`, port = `2525`, TLS off, receiver = `cam-<id>@<domain>`.
|
|
49
51
|
4. On motion, the camera sends an email. The manager parses it, classifies the trigger (people/vehicle/motion), saves the snapshot under `${DATA_PATH}/email-push/<cameraId>/`, and emits a synthetic motion event into the same bus used by native Baichuan push โ so MQTT, Home Assistant, Frigate, etc. see it transparently.
|
|
50
52
|
|
|
51
53
|
See [documentation/baichuan-api/email.md](./documentation/baichuan-api/email.md) for the full API and [documentation/baichuan-api/time.md](./documentation/baichuan-api/time.md) for the related NTP / DST / system clock setters.
|
|
@@ -5317,6 +5317,21 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
5317
5317
|
let audioSampleRate = null;
|
|
5318
5318
|
let streamStarted = false;
|
|
5319
5319
|
let closed = false;
|
|
5320
|
+
const signal = options?.signal;
|
|
5321
|
+
let sleepResolve = null;
|
|
5322
|
+
let sleepTimer = null;
|
|
5323
|
+
const clearSleepTimer = () => {
|
|
5324
|
+
if (sleepTimer) {
|
|
5325
|
+
clearTimeout(sleepTimer);
|
|
5326
|
+
sleepTimer = null;
|
|
5327
|
+
}
|
|
5328
|
+
};
|
|
5329
|
+
const handleAbort = () => {
|
|
5330
|
+
clearSleepTimer();
|
|
5331
|
+
const r = sleepResolve;
|
|
5332
|
+
sleepResolve = null;
|
|
5333
|
+
r?.();
|
|
5334
|
+
};
|
|
5320
5335
|
const onError = (_error) => {
|
|
5321
5336
|
closed = true;
|
|
5322
5337
|
api.logger?.warn?.(
|
|
@@ -5414,7 +5429,13 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
5414
5429
|
}
|
|
5415
5430
|
});
|
|
5416
5431
|
streamStarted = true;
|
|
5417
|
-
|
|
5432
|
+
if (signal) {
|
|
5433
|
+
if (signal.aborted) {
|
|
5434
|
+
closed = true;
|
|
5435
|
+
} else {
|
|
5436
|
+
signal.addEventListener("abort", handleAbort);
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5418
5439
|
while (!closed && !signal?.aborted) {
|
|
5419
5440
|
if (frameQueue.length > 0) {
|
|
5420
5441
|
const frame = frameQueue.shift();
|
|
@@ -5422,31 +5443,30 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
5422
5443
|
} else {
|
|
5423
5444
|
await new Promise((resolve) => {
|
|
5424
5445
|
frameResolve = resolve;
|
|
5425
|
-
|
|
5446
|
+
sleepResolve = resolve;
|
|
5447
|
+
sleepTimer = setTimeout(() => {
|
|
5448
|
+
sleepTimer = null;
|
|
5449
|
+
if (sleepResolve === resolve) sleepResolve = null;
|
|
5426
5450
|
if (frameResolve === resolve) {
|
|
5427
5451
|
frameResolve = null;
|
|
5428
5452
|
resolve();
|
|
5429
5453
|
}
|
|
5430
5454
|
}, 1e3);
|
|
5431
|
-
if (signal) {
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
};
|
|
5437
|
-
if (signal.aborted) {
|
|
5438
|
-
clearTimeout(timer);
|
|
5439
|
-
frameResolve = null;
|
|
5440
|
-
resolve();
|
|
5441
|
-
} else {
|
|
5442
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
5443
|
-
}
|
|
5455
|
+
if (signal?.aborted) {
|
|
5456
|
+
clearSleepTimer();
|
|
5457
|
+
sleepResolve = null;
|
|
5458
|
+
frameResolve = null;
|
|
5459
|
+
resolve();
|
|
5444
5460
|
}
|
|
5445
5461
|
});
|
|
5462
|
+
sleepResolve = null;
|
|
5463
|
+
clearSleepTimer();
|
|
5446
5464
|
}
|
|
5447
5465
|
}
|
|
5448
5466
|
} finally {
|
|
5449
5467
|
closed = true;
|
|
5468
|
+
if (signal) signal.removeEventListener("abort", handleAbort);
|
|
5469
|
+
clearSleepTimer();
|
|
5450
5470
|
try {
|
|
5451
5471
|
await videoStream.stop();
|
|
5452
5472
|
} catch {
|
|
@@ -7483,12 +7503,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7483
7503
|
if (frame.videoType === "H264" || frame.videoType === "H265") {
|
|
7484
7504
|
this.setFlowVideoType(frame.videoType, "native stream");
|
|
7485
7505
|
}
|
|
7486
|
-
this.flow.
|
|
7487
|
-
|
|
7488
|
-
|
|
7506
|
+
if (!this.flow.getFmtp().hasParamSets) {
|
|
7507
|
+
this.flow.extractParameterSets(frame.data);
|
|
7508
|
+
}
|
|
7509
|
+
if (this.flow.getFmtp().hasParamSets) {
|
|
7489
7510
|
this.markFirstFrameReceived();
|
|
7490
7511
|
}
|
|
7491
|
-
const isKeyframe = this.isRawFrameKeyframe(frame);
|
|
7512
|
+
const isKeyframe = typeof frame.isKeyframe === "boolean" ? frame.isKeyframe : this.isRawFrameKeyframe(frame);
|
|
7492
7513
|
this.prebuffer.push({
|
|
7493
7514
|
frame: { ...frame, data: Buffer.from(frame.data) },
|
|
7494
7515
|
time: Date.now(),
|
|
@@ -8668,6 +8689,60 @@ function patchMotionSensitivityListXml(currentXml, bands) {
|
|
|
8668
8689
|
);
|
|
8669
8690
|
}
|
|
8670
8691
|
|
|
8692
|
+
// src/emailPush/bus.ts
|
|
8693
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
8694
|
+
var emitter = new EventEmitter4();
|
|
8695
|
+
var cameraResolver = () => void 0;
|
|
8696
|
+
var lastEventByCamera = /* @__PURE__ */ new Map();
|
|
8697
|
+
var MAX_GLOBAL_EVENTS = 300;
|
|
8698
|
+
var globalRecentEvents = [];
|
|
8699
|
+
function setEmailPushCameraResolver(resolver) {
|
|
8700
|
+
cameraResolver = resolver;
|
|
8701
|
+
}
|
|
8702
|
+
function getEmailPushCameraResolver() {
|
|
8703
|
+
return cameraResolver;
|
|
8704
|
+
}
|
|
8705
|
+
function onEmailPushEvent(handler) {
|
|
8706
|
+
emitter.on("event", handler);
|
|
8707
|
+
return () => emitter.off("event", handler);
|
|
8708
|
+
}
|
|
8709
|
+
function emitEmailPushEvent(event) {
|
|
8710
|
+
lastEventByCamera.set(event.cameraId, event);
|
|
8711
|
+
globalRecentEvents.unshift(event);
|
|
8712
|
+
if (globalRecentEvents.length > MAX_GLOBAL_EVENTS) {
|
|
8713
|
+
globalRecentEvents.length = MAX_GLOBAL_EVENTS;
|
|
8714
|
+
}
|
|
8715
|
+
emitter.emit("event", event);
|
|
8716
|
+
}
|
|
8717
|
+
function getLastEmailPushEvent(cameraId) {
|
|
8718
|
+
return lastEventByCamera.get(cameraId);
|
|
8719
|
+
}
|
|
8720
|
+
function getRecentEmailPushEvents(limit = MAX_GLOBAL_EVENTS) {
|
|
8721
|
+
const clamped = Math.max(0, Math.min(limit, MAX_GLOBAL_EVENTS));
|
|
8722
|
+
return globalRecentEvents.slice(0, clamped);
|
|
8723
|
+
}
|
|
8724
|
+
function mapEmailPushInferredType(inferred) {
|
|
8725
|
+
switch (inferred) {
|
|
8726
|
+
case "motion":
|
|
8727
|
+
case "doorbell":
|
|
8728
|
+
case "people":
|
|
8729
|
+
case "vehicle":
|
|
8730
|
+
case "animal":
|
|
8731
|
+
case "face":
|
|
8732
|
+
case "package":
|
|
8733
|
+
return inferred;
|
|
8734
|
+
case "other":
|
|
8735
|
+
default:
|
|
8736
|
+
return "motion";
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
function _resetEmailPushBusForTests() {
|
|
8740
|
+
emitter.removeAllListeners();
|
|
8741
|
+
cameraResolver = () => void 0;
|
|
8742
|
+
lastEventByCamera.clear();
|
|
8743
|
+
globalRecentEvents.length = 0;
|
|
8744
|
+
}
|
|
8745
|
+
|
|
8671
8746
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
8672
8747
|
import { spawn as spawn2 } from "child_process";
|
|
8673
8748
|
import { mkdir } from "fs/promises";
|
|
@@ -11443,7 +11518,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11443
11518
|
* general socket is created, logged in, and all event/push/guard listeners
|
|
11444
11519
|
* are re-attached automatically.
|
|
11445
11520
|
*
|
|
11446
|
-
* This is a **no-op** when the API is already
|
|
11521
|
+
* This is a **no-op** when the API is already ready (see `isReadyState()`).
|
|
11447
11522
|
*
|
|
11448
11523
|
* @throws If `close()` was called โ the API is permanently closed and a new
|
|
11449
11524
|
* instance must be created.
|
|
@@ -11524,7 +11599,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11524
11599
|
/**
|
|
11525
11600
|
* Attach event, push, channelInfo, and guard listeners to the current
|
|
11526
11601
|
* "general" client. Called from the constructor and from
|
|
11527
|
-
*
|
|
11602
|
+
* `reconnectGeneralSocket()`.
|
|
11528
11603
|
*/
|
|
11529
11604
|
setupGeneralClientListeners() {
|
|
11530
11605
|
const client = this.client;
|
|
@@ -13113,6 +13188,40 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
13113
13188
|
this.econnresetStormRebootInFlight = void 0;
|
|
13114
13189
|
});
|
|
13115
13190
|
}
|
|
13191
|
+
/**
|
|
13192
|
+
* Bind this API instance to the global email-push bus so that incoming
|
|
13193
|
+
* SMTP-delivered motion / AI events for the matching camera surface on
|
|
13194
|
+
* this instance's standard `onSimpleEvent` channel. The consumer keeps
|
|
13195
|
+
* a single subscription (`onSimpleEvent`) and gets both the native
|
|
13196
|
+
* Baichuan push and the email-push transport on the same stream.
|
|
13197
|
+
*
|
|
13198
|
+
* - `cameraId` shorthand: match events with `event.cameraId === cameraId`.
|
|
13199
|
+
* - `match`: arbitrary predicate (e.g. when the consumer uses a
|
|
13200
|
+
* nickname-based mapping or wants to handle multiple recipients).
|
|
13201
|
+
*
|
|
13202
|
+
* Returns an `off()` handle. Safe to call repeatedly โ each call
|
|
13203
|
+
* registers its own listener.
|
|
13204
|
+
*/
|
|
13205
|
+
subscribeEmailPushEvents(params) {
|
|
13206
|
+
const channel = params.channel ?? 0;
|
|
13207
|
+
const matches = "match" in params ? params.match : (event) => event.cameraId === params.cameraId;
|
|
13208
|
+
const off = onEmailPushEvent((event) => {
|
|
13209
|
+
if (!matches(event)) return;
|
|
13210
|
+
this.dispatchSimpleEvent({
|
|
13211
|
+
type: mapEmailPushInferredType(event.inferredType),
|
|
13212
|
+
channel,
|
|
13213
|
+
timestamp: event.receivedAtMs
|
|
13214
|
+
});
|
|
13215
|
+
if (event.inferredType !== "motion" && event.inferredType !== "doorbell" && event.inferredType !== "other") {
|
|
13216
|
+
this.dispatchSimpleEvent({
|
|
13217
|
+
type: "motion",
|
|
13218
|
+
channel,
|
|
13219
|
+
timestamp: event.receivedAtMs
|
|
13220
|
+
});
|
|
13221
|
+
}
|
|
13222
|
+
});
|
|
13223
|
+
return off;
|
|
13224
|
+
}
|
|
13116
13225
|
/**
|
|
13117
13226
|
* Subscribe to minimal high-level events.
|
|
13118
13227
|
* The API manages Baichuan subscribe/unsubscribe automatically.
|
|
@@ -13171,7 +13280,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
13171
13280
|
* Subscribe to per-frame detection events sourced from the BcMedia
|
|
13172
13281
|
* `additionalHeader` block on active video streams.
|
|
13173
13282
|
*
|
|
13174
|
-
* Mirrors
|
|
13283
|
+
* Mirrors `onSimpleEvent()` but is fed by the streaming side-channel:
|
|
13175
13284
|
* one event fires for every I-frame / P-frame that carries an overlay block.
|
|
13176
13285
|
* Coordinates are reported in normalized [0, 1] fractions of the source
|
|
13177
13286
|
* frame, so the same box renders correctly on mainStream, subStream, and
|
|
@@ -13198,7 +13307,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
13198
13307
|
* Subscribe to AI object detections (people / vehicle / animal / face boxes
|
|
13199
13308
|
* with class label and confidence) without managing a video stream yourself.
|
|
13200
13309
|
*
|
|
13201
|
-
* Mirrors
|
|
13310
|
+
* Mirrors `onSimpleEvent()` end-to-end: on the first listener for a given
|
|
13202
13311
|
* `(channel, profile)` tuple the API ensures the corresponding video stream
|
|
13203
13312
|
* is running (the pool socket may already be shared with a regular consumer),
|
|
13204
13313
|
* forwards every box-bearing `additionalHeader` to your callback, and tears
|
|
@@ -20042,7 +20151,7 @@ ${xml}`
|
|
|
20042
20151
|
* Field meaning per stream:
|
|
20043
20152
|
* - `audio` โ 0/1 toggle
|
|
20044
20153
|
* - `width`/`height` โ resolution in pixels. Must be one of the
|
|
20045
|
-
* resolutions returned by
|
|
20154
|
+
* resolutions returned by `getStreamInfoList()`.
|
|
20046
20155
|
* - `bitRate` โ kbps. Must match the table from `getStreamInfoList`.
|
|
20047
20156
|
* - `frameRate` โ fps. Must match the table from `getStreamInfoList`.
|
|
20048
20157
|
* - `videoEncType` โ `"h264"` or `"h265"`
|
|
@@ -21431,10 +21540,12 @@ ${xml}`
|
|
|
21431
21540
|
const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
|
|
21432
21541
|
const attachmentType = params.attachmentType ?? "picture";
|
|
21433
21542
|
const interval = params.interval ?? 30;
|
|
21543
|
+
const rawUser = params.authUsername ?? recipient;
|
|
21544
|
+
const wireUser = rawUser.includes("@") ? rawUser : `${rawUser}@${domain}`;
|
|
21434
21545
|
const emailPatch = {
|
|
21435
21546
|
smtpServer: params.managerHost,
|
|
21436
21547
|
smtpPort: port,
|
|
21437
|
-
userName:
|
|
21548
|
+
userName: wireUser,
|
|
21438
21549
|
password: params.authPassword ?? "",
|
|
21439
21550
|
address1: recipient,
|
|
21440
21551
|
address2: "",
|
|
@@ -24583,6 +24694,14 @@ export {
|
|
|
24583
24694
|
patchAiDetectCfgXml,
|
|
24584
24695
|
encodeMotionSensitivityListXml,
|
|
24585
24696
|
patchMotionSensitivityListXml,
|
|
24697
|
+
setEmailPushCameraResolver,
|
|
24698
|
+
getEmailPushCameraResolver,
|
|
24699
|
+
onEmailPushEvent,
|
|
24700
|
+
emitEmailPushEvent,
|
|
24701
|
+
getLastEmailPushEvent,
|
|
24702
|
+
getRecentEmailPushEvents,
|
|
24703
|
+
mapEmailPushInferredType,
|
|
24704
|
+
_resetEmailPushBusForTests,
|
|
24586
24705
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
24587
24706
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
24588
24707
|
DUAL_LENS_MODELS,
|
|
@@ -24604,4 +24723,4 @@ export {
|
|
|
24604
24723
|
isTcpFailureThatShouldFallbackToUdp,
|
|
24605
24724
|
autoDetectDeviceType
|
|
24606
24725
|
};
|
|
24607
|
-
//# sourceMappingURL=chunk-
|
|
24726
|
+
//# sourceMappingURL=chunk-NQ7D5TLR.js.map
|