@apocaliss92/nodelink-js 0.6.3 → 0.6.5

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/index.d.ts CHANGED
@@ -233,6 +233,59 @@ export declare function alawToPcm16(bytes: Uint8Array | Buffer): Int16Array;
233
233
  */
234
234
  export declare const ALL_UDP_DISCOVERY_METHODS: readonly UdpDiscoveryMethod[];
235
235
 
236
+ export declare const ALWAYS_ON_DEFAULTS: {
237
+ triggers: AlwaysOnTrigger[];
238
+ windowMs: number;
239
+ idleFps: number;
240
+ primeOnStart: boolean;
241
+ placeholder: Required<PlaceholderOptions>;
242
+ };
243
+
244
+ export declare class AlwaysOnController {
245
+ private readonly o;
246
+ private readonly triggers;
247
+ private readonly windowMs;
248
+ private readonly primeOnStart;
249
+ private readonly logger;
250
+ private windowTimer;
251
+ private live;
252
+ private started;
253
+ private readonly handler;
254
+ constructor(o: AlwaysOnControllerOptions);
255
+ private get windowSeconds();
256
+ start(): Promise<void>;
257
+ stop(): Promise<void>;
258
+ private onEvent;
259
+ private openWindow;
260
+ private closeWindow;
261
+ }
262
+
263
+ export declare interface AlwaysOnControllerOptions {
264
+ api: ReolinkBaichuanApi;
265
+ channel: number;
266
+ options: AlwaysOnOptions;
267
+ goLive: () => Promise<void>;
268
+ goIdle: () => Promise<void>;
269
+ logger?: Logger_3;
270
+ }
271
+
272
+ export declare interface AlwaysOnOptions {
273
+ enabled: boolean;
274
+ /** Event types that open a live window. Default ["motion", "doorbell"]. */
275
+ triggers?: AlwaysOnTrigger[];
276
+ /** Live window duration after a trigger (ms), extended by new events. Default 15000. */
277
+ windowMs?: number;
278
+ /** Placeholder repeat rate while idle (fps). Default 1. */
279
+ idleFps?: number;
280
+ /** Wake once on start to capture an initial keyframe. Default true. */
281
+ primeOnStart?: boolean;
282
+ /** Placeholder appearance. */
283
+ placeholder?: PlaceholderOptions;
284
+ }
285
+
286
+ /** Event types (from ReolinkSimpleEventType) that may open a live window. */
287
+ export declare type AlwaysOnTrigger = "motion" | "doorbell" | "people" | "vehicle" | "animal" | "face" | "package";
288
+
236
289
  export declare type AnyBuffer = Buffer<ArrayBufferLike>;
237
290
 
238
291
  /**
@@ -1688,6 +1741,9 @@ export declare class BaichuanRtspServer extends EventEmitter<{
1688
1741
  private deviceId;
1689
1742
  private dedicatedSessionRelease;
1690
1743
  private externalListener;
1744
+ private readonly alwaysOnOptions;
1745
+ private continuousStream;
1746
+ private alwaysOnController;
1691
1747
  private authCredentials;
1692
1748
  private requireAuth;
1693
1749
  private authNonces;
@@ -1696,6 +1752,7 @@ export declare class BaichuanRtspServer extends EventEmitter<{
1696
1752
  private readonly lazyMetadata;
1697
1753
  private connectedClients;
1698
1754
  private nativeStreamActive;
1755
+ private tearingDown;
1699
1756
  private clientConnectionServer;
1700
1757
  private streamMetadata;
1701
1758
  private clientResources;
@@ -1804,6 +1861,24 @@ export declare class BaichuanRtspServer extends EventEmitter<{
1804
1861
  * Start ffmpeg for a specific client.
1805
1862
  */
1806
1863
  private startClientFfmpeg;
1864
+ /**
1865
+ * Always-on source: bridge a {@link ContinuousVideoStream} into the existing
1866
+ * fanout. Yields the same frame shape that `createNativeStream` produces, so
1867
+ * the rest of the pipeline (prebuffer, param-set extraction, per-client
1868
+ * subscribe, ffmpeg/direct-RTP) is unchanged.
1869
+ *
1870
+ * The CVS itself is long-lived (created once, reused across native-stream
1871
+ * restarts) and is driven by the {@link AlwaysOnController}, which opens/closes
1872
+ * live windows from camera events. Each fanout source generator only forwards
1873
+ * CVS events to the fanout pump for as long as `signal` is not aborted.
1874
+ */
1875
+ private createContinuousSource;
1876
+ /**
1877
+ * Lazily build the long-lived {@link ContinuousVideoStream} +
1878
+ * {@link AlwaysOnController} for always-on mode. Both are created once and
1879
+ * reused for the lifetime of the server (across native-stream restarts).
1880
+ */
1881
+ private ensureContinuousStream;
1807
1882
  /**
1808
1883
  * Start native stream (mark as active).
1809
1884
  * Each client will create its own generator, so we just track that the stream is active.
@@ -1967,6 +2042,15 @@ export declare interface BaichuanRtspServerOptions {
1967
2042
  * Default: false (keep existing behaviour).
1968
2043
  */
1969
2044
  lazyMetadata?: boolean;
2045
+ /**
2046
+ * Always-on continuous stream (battery cameras). When `enabled`, the server
2047
+ * sources video from a {@link ContinuousVideoStream} (real frames during
2048
+ * event-driven live windows, a low-fps placeholder while the camera sleeps)
2049
+ * driven by an {@link AlwaysOnController}. The controller owns the sleep/wake
2050
+ * decision, so the server's own battery idle-stop timers are suppressed.
2051
+ * When omitted/disabled the server behaves exactly as before.
2052
+ */
2053
+ alwaysOn?: AlwaysOnOptions;
1970
2054
  }
1971
2055
 
1972
2056
  export declare type BaichuanSerialPush = {
@@ -3240,6 +3324,11 @@ export declare function buildStartZoomFocusXml(channelId: number, movePos: numbe
3240
3324
  */
3241
3325
  export declare function buildWhiteLedStateXml(channelId: number, state: number): string;
3242
3326
 
3327
+ declare interface CachedKeyframe {
3328
+ data: Buffer;
3329
+ videoType: "H264" | "H265";
3330
+ }
3331
+
3243
3332
  declare type CameraResolver = (recipient: string) => string | undefined;
3244
3333
 
3245
3334
  /**
@@ -4391,6 +4480,46 @@ export declare function computeExpectedStreamCompatibility(params: {
4391
4480
  reason: string;
4392
4481
  }>;
4393
4482
 
4483
+ export declare class ContinuousVideoStream extends EventEmitter<{
4484
+ videoAccessUnit: [VideoAccessUnit];
4485
+ additionalHeader: [unknown];
4486
+ audioFrame: [Buffer];
4487
+ error: [Error];
4488
+ close: [];
4489
+ }> {
4490
+ private readonly opts;
4491
+ private live;
4492
+ private lastKeyframe;
4493
+ private lastMicroseconds;
4494
+ private readonly idleFps;
4495
+ private readonly renderer;
4496
+ private readonly logger;
4497
+ private stopped;
4498
+ private starting;
4499
+ private idleTimer;
4500
+ private idlePlaceholder;
4501
+ constructor(opts: ContinuousVideoStreamOptions);
4502
+ hasCachedKeyframe(): boolean;
4503
+ goLive(): Promise<void>;
4504
+ goIdle(): Promise<void>;
4505
+ stop(): Promise<void>;
4506
+ private startIdleLoop;
4507
+ private stopIdleLoop;
4508
+ private onLiveAccessUnit;
4509
+ private onAdditionalHeader;
4510
+ private onAudioFrame;
4511
+ private onLiveError;
4512
+ }
4513
+
4514
+ export declare interface ContinuousVideoStreamOptions {
4515
+ /** Returns an un-started live BaichuanVideoStream; ContinuousVideoStream calls start() itself. */
4516
+ createLiveStream: () => Promise<BaichuanVideoStream>;
4517
+ idleFps?: number;
4518
+ placeholder?: PlaceholderOptions;
4519
+ renderer?: PlaceholderRenderer;
4520
+ logger?: Logger_3;
4521
+ }
4522
+
4394
4523
  /**
4395
4524
  * Converts H.265 data from length-prefixed (HVCC) to Annex-B (start codes).
4396
4525
  *
@@ -6358,6 +6487,13 @@ declare interface Logger_2 {
6358
6487
  child?(tag: string): Logger_2;
6359
6488
  }
6360
6489
 
6490
+ declare interface Logger_3 {
6491
+ info?: (...a: unknown[]) => void;
6492
+ warn?: (...a: unknown[]) => void;
6493
+ error?: (...a: unknown[]) => void;
6494
+ debug?: (...a: unknown[]) => void;
6495
+ }
6496
+
6361
6497
  /** Logger callback type for MjpegTransformer */
6362
6498
  export declare type LoggerCallback = (level: "debug" | "info" | "warn" | "error", message: string) => void;
6363
6499
 
@@ -6723,7 +6859,28 @@ export declare function normalizeDayNightMode(input: string): string;
6723
6859
  export declare function normalizeOpenClose(input: string): string;
6724
6860
 
6725
6861
  /**
6726
- * Normalize UID string (trim and return undefined if empty).
6862
+ * Normalize UID string (trim, force uppercase, return undefined if empty).
6863
+ *
6864
+ * Uppercase is non-negotiable for two reasons:
6865
+ *
6866
+ * 1. Reolink's cloud API (`apis.reolink.com/v2/devices/{uid}/server-binding`)
6867
+ * is case-sensitive: a lowercase UID returns HTTP 400
6868
+ * `invalid_parameters` and we lose the per-UID zone hint, falling back
6869
+ * to the full 24-hostname sweep. Verified empirically against a known
6870
+ * good UID — uppercase returns 200 + zone allocation, lowercase
6871
+ * returns 400 + `invalid_parameters`.
6872
+ *
6873
+ * 2. The BCUDP discovery protocol embeds `<uid>...</uid>` in the C2D_C
6874
+ * packet. Cameras compare it against their own self-UID (which they
6875
+ * store uppercase). A lowercase mismatch is silently dropped — the
6876
+ * cam appears "asleep" from `sent=N replies=0` even though it's
6877
+ * awake and on the LAN. BaichuanClient already uppercases for the
6878
+ * XOR/AES nonce derivation; normalizing here makes the whole
6879
+ * pipeline consistent.
6880
+ *
6881
+ * Users in Scrypted / config files sometimes paste UIDs in lowercase
6882
+ * (it works visually with Reolink's own UI, which normalizes
6883
+ * internally) — we shouldn't punish them for that.
6727
6884
  */
6728
6885
  export declare function normalizeUid(uid?: string): string | undefined;
6729
6886
 
@@ -6975,6 +7132,32 @@ export declare interface PirState {
6975
7132
  };
6976
7133
  }
6977
7134
 
7135
+ export declare interface PlaceholderOptions {
7136
+ /** Decorate the still (dim + text). Falls back to raw keyframe if ffmpeg is unavailable. Default true. */
7137
+ enabled?: boolean;
7138
+ /** Overlay text. Default "Sleeping". */
7139
+ text?: string;
7140
+ /** Dim factor 0..1 (1 = original brightness). Default 0.5. */
7141
+ opacity?: number;
7142
+ }
7143
+
7144
+ export declare class PlaceholderRenderer {
7145
+ private readonly opts;
7146
+ private readonly logger;
7147
+ constructor(args: {
7148
+ placeholder?: PlaceholderOptions;
7149
+ logger?: Logger_3;
7150
+ });
7151
+ /** Returns the access unit bytes to emit as placeholder, or null if none available. */
7152
+ render(keyframe: CachedKeyframe | null): Promise<Buffer | null>;
7153
+ /** Decodes the cached keyframe access unit into a single JPEG still via ffmpeg. */
7154
+ private decodeToJpeg;
7155
+ /** Dims the still and prints the overlay text using jimp, returning a JPEG buffer. */
7156
+ private decorate;
7157
+ /** Encodes the decorated JPEG into a single IDR access unit in the target codec. */
7158
+ private encodeIdr;
7159
+ }
7160
+
6978
7161
  export declare type PlaybackSnapshotStreamInfo = {
6979
7162
  width?: number;
6980
7163
  height?: number;
@@ -11738,7 +11921,7 @@ export declare interface Rfc4571TcpServer {
11738
11921
  username: string;
11739
11922
  password: string;
11740
11923
  server: net_2.Server;
11741
- videoStream: BaichuanVideoStream | CompositeStream;
11924
+ videoStream: BaichuanVideoStream | CompositeStream | ContinuousVideoStream;
11742
11925
  close: (reason?: unknown) => Promise<void>;
11743
11926
  }
11744
11927
 
@@ -11817,6 +12000,8 @@ export declare interface Rfc4571TcpServerOptions {
11817
12000
  * The dedicated socket is automatically closed when the stream ends.
11818
12001
  */
11819
12002
  deviceId?: string;
12003
+ /** Battery always-on continuous stream (placeholder while asleep, real frames during motion). */
12004
+ alwaysOn?: AlwaysOnOptions;
11820
12005
  }
11821
12006
 
11822
12007
  export declare interface RtpPacketizationOptions {
@@ -12483,6 +12668,14 @@ export declare function upsamplePcm16(src: Int16Array, factor: number): Int16Arr
12483
12668
  */
12484
12669
  export declare function upsertXmlTag(xml: string, tag: string, value: string | number | boolean | undefined): string;
12485
12670
 
12671
+ declare type VideoAccessUnit = {
12672
+ data: Buffer;
12673
+ isKeyframe: boolean;
12674
+ videoType: "H264" | "H265";
12675
+ microseconds: number;
12676
+ time?: number;
12677
+ };
12678
+
12486
12679
  /**
12487
12680
  * Client information extracted from HTTP request headers.
12488
12681
  * Used to determine optimal video delivery format.
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  ALL_UDP_DISCOVERY_METHODS,
3
+ ALWAYS_ON_DEFAULTS,
4
+ AlwaysOnController,
3
5
  BaichuanClient,
4
6
  BaichuanEventEmitter,
5
7
  BaichuanFrameParser,
6
8
  BaichuanRtspServer,
7
9
  BcUdpStream,
10
+ ContinuousVideoStream,
8
11
  DEFAULT_SHELTER_CANVAS,
9
12
  DUAL_LENS_DUAL_MOTION_MODELS,
10
13
  DUAL_LENS_MODELS,
@@ -13,6 +16,7 @@ import {
13
16
  MpegTsMuxer,
14
17
  NVR_HUB_EXACT_TYPES,
15
18
  NVR_HUB_MODEL_PATTERNS,
19
+ PlaceholderRenderer,
16
20
  ReolinkBaichuanApi,
17
21
  _clearP2pLookupDedupForTests,
18
22
  _resetEmailPushBusForTests,
@@ -72,7 +76,7 @@ import {
72
76
  setGlobalLogger,
73
77
  tcpReachabilityProbe,
74
78
  xmlIndicatesFloodlight
75
- } from "./chunk-Q4AXRV2G.js";
79
+ } from "./chunk-WQ2TQCYP.js";
76
80
  import {
77
81
  ReolinkCgiApi,
78
82
  ReolinkHttpClient,
@@ -3286,7 +3290,8 @@ async function createRfc4571TcpServerInternal(options) {
3286
3290
  apisToClose.add(resolvedCompositeApis.widerApi);
3287
3291
  if (resolvedCompositeApis?.teleApi)
3288
3292
  apisToClose.add(resolvedCompositeApis.teleApi);
3289
- const uptimeRestartMs = uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
3293
+ const alwaysOnEnabled = Boolean(options.alwaysOn?.enabled) && !isComposite;
3294
+ const uptimeRestartMs = alwaysOnEnabled ? 0 : uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
3290
3295
  const variantSuffix = variant && variant !== "default" ? ` variant=${variant}` : "";
3291
3296
  const logPrefix = isComposite ? `[native-rfc4571 composite profile=${profile}${variantSuffix}${requestedId ? ` id=${requestedId}` : ""}]` : `[native-rfc4571 ch=${channel} profile=${profile}${variantSuffix}]`;
3292
3297
  const log = (message) => {
@@ -3310,6 +3315,7 @@ async function createRfc4571TcpServerInternal(options) {
3310
3315
  );
3311
3316
  let videoStream;
3312
3317
  let isCompositeStream = false;
3318
+ let alwaysOnController;
3313
3319
  if (isComposite) {
3314
3320
  const widerChannel = compositeOptions?.widerChannel ?? 0;
3315
3321
  const teleChannel = compositeOptions?.teleChannel ?? 1;
@@ -3452,7 +3458,7 @@ async function createRfc4571TcpServerInternal(options) {
3452
3458
  } else {
3453
3459
  streamClient = baseApi.client;
3454
3460
  }
3455
- videoStream = new BaichuanVideoStream({
3461
+ const createLiveStream = async () => new BaichuanVideoStream({
3456
3462
  client: streamClient,
3457
3463
  api: baseApi,
3458
3464
  channel: ch,
@@ -3460,10 +3466,39 @@ async function createRfc4571TcpServerInternal(options) {
3460
3466
  variant,
3461
3467
  logger
3462
3468
  });
3463
- await videoStream.start();
3464
- log(
3465
- `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
3466
- );
3469
+ if (options.alwaysOn?.enabled) {
3470
+ const cvsOpts = {
3471
+ // ContinuousVideoStream owns the lifecycle: it calls createLiveStream
3472
+ // (which returns a started stream) and re-starts it internally on goLive.
3473
+ createLiveStream,
3474
+ logger
3475
+ };
3476
+ if (options.alwaysOn.idleFps !== void 0)
3477
+ cvsOpts.idleFps = options.alwaysOn.idleFps;
3478
+ if (options.alwaysOn.placeholder !== void 0)
3479
+ cvsOpts.placeholder = options.alwaysOn.placeholder;
3480
+ const cvs = new ContinuousVideoStream(cvsOpts);
3481
+ alwaysOnController = new AlwaysOnController({
3482
+ api: baseApi,
3483
+ channel: ch,
3484
+ options: options.alwaysOn,
3485
+ goLive: () => cvs.goLive(),
3486
+ goIdle: () => cvs.goIdle(),
3487
+ logger
3488
+ });
3489
+ await alwaysOnController.start();
3490
+ videoStream = cvs;
3491
+ log(
3492
+ `always-on stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
3493
+ );
3494
+ } else {
3495
+ const live = await createLiveStream();
3496
+ await live.start();
3497
+ videoStream = live;
3498
+ log(
3499
+ `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
3500
+ );
3501
+ }
3467
3502
  }
3468
3503
  const waitForKeyframe = async () => {
3469
3504
  if (isCompositeStream) {
@@ -3591,6 +3626,12 @@ async function createRfc4571TcpServerInternal(options) {
3591
3626
  try {
3592
3627
  keyframe = await waitForKeyframe();
3593
3628
  } catch (e) {
3629
+ if (alwaysOnController) {
3630
+ try {
3631
+ await alwaysOnController.stop();
3632
+ } catch {
3633
+ }
3634
+ }
3594
3635
  try {
3595
3636
  await videoStream.stop();
3596
3637
  } catch {
@@ -3839,12 +3880,13 @@ async function createRfc4571TcpServerInternal(options) {
3839
3880
  } catch {
3840
3881
  }
3841
3882
  muxer = makeMuxer();
3883
+ const restartable = videoStream;
3842
3884
  try {
3843
- await videoStream.stop();
3885
+ await restartable.stop();
3844
3886
  } catch {
3845
3887
  }
3846
3888
  try {
3847
- await videoStream.start();
3889
+ await restartable.start();
3848
3890
  } catch (e) {
3849
3891
  restarting = false;
3850
3892
  close(e).catch(() => {
@@ -3865,6 +3907,12 @@ async function createRfc4571TcpServerInternal(options) {
3865
3907
  cancelIdleTeardown();
3866
3908
  const reasonStr = reason?.message || reason?.toString?.() || reason || "requested";
3867
3909
  muxer.close();
3910
+ if (alwaysOnController) {
3911
+ try {
3912
+ await alwaysOnController.stop();
3913
+ } catch {
3914
+ }
3915
+ }
3868
3916
  try {
3869
3917
  await videoStream.stop();
3870
3918
  } catch {
@@ -9854,7 +9902,9 @@ function buildInitialStatus(config) {
9854
9902
  }
9855
9903
  export {
9856
9904
  ALL_UDP_DISCOVERY_METHODS,
9905
+ ALWAYS_ON_DEFAULTS,
9857
9906
  AesStreamDecryptor,
9907
+ AlwaysOnController,
9858
9908
  AutodiscoveryClient,
9859
9909
  BC_AES_IV,
9860
9910
  BC_CLASS_FILE_DOWNLOAD,
@@ -10011,6 +10061,7 @@ export {
10011
10061
  BcUdpStream,
10012
10062
  CompositeRtspServer,
10013
10063
  CompositeStream,
10064
+ ContinuousVideoStream,
10014
10065
  DEFAULT_SHELTER_CANVAS,
10015
10066
  DUAL_LENS_DUAL_MOTION_MODELS,
10016
10067
  DUAL_LENS_MODELS,
@@ -10024,6 +10075,7 @@ export {
10024
10075
  MpegTsMuxer,
10025
10076
  NVR_HUB_EXACT_TYPES,
10026
10077
  NVR_HUB_MODEL_PATTERNS,
10078
+ PlaceholderRenderer,
10027
10079
  ReolinkBaichuanApi,
10028
10080
  ReolinkCgiApi,
10029
10081
  ReolinkHttpClient,