@apocaliss92/nodelink-js 0.6.4 → 0.6.6

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
 
@@ -6996,6 +7132,32 @@ export declare interface PirState {
6996
7132
  };
6997
7133
  }
6998
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
+
6999
7161
  export declare type PlaybackSnapshotStreamInfo = {
7000
7162
  width?: number;
7001
7163
  height?: number;
@@ -8201,10 +8363,12 @@ export declare class ReolinkBaichuanApi {
8201
8363
  * @param options.source - Data source for the channel list (default: `"cgi"`):
8202
8364
  * - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
8203
8365
  * no dependency on async push messages. Recommended for first-call discovery.
8204
- * - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
8205
- * info after login + event subscription. This push is *asynchronous*: if it has not
8206
- * arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
8207
- * with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
8366
+ * - `"baichuan"`: HTTP-free discovery. Prefers the cmd_id 145 push cache when
8367
+ * populated; otherwise actively probes the channel slots advertised by Support
8368
+ * (`items[].chnID`) via `getInfo`. Use this for hubs with HTTP disabled.
8369
+ *
8370
+ * When the api was constructed with `nativeOnly`, the source is forced to
8371
+ * `"baichuan"` regardless of this option (no HTTP/CGI is ever attempted).
8208
8372
  */
8209
8373
  getNvrChannelsSummary(options?: {
8210
8374
  channels?: number[];
@@ -11759,7 +11923,7 @@ export declare interface Rfc4571TcpServer {
11759
11923
  username: string;
11760
11924
  password: string;
11761
11925
  server: net_2.Server;
11762
- videoStream: BaichuanVideoStream | CompositeStream;
11926
+ videoStream: BaichuanVideoStream | CompositeStream | ContinuousVideoStream;
11763
11927
  close: (reason?: unknown) => Promise<void>;
11764
11928
  }
11765
11929
 
@@ -11838,6 +12002,8 @@ export declare interface Rfc4571TcpServerOptions {
11838
12002
  * The dedicated socket is automatically closed when the stream ends.
11839
12003
  */
11840
12004
  deviceId?: string;
12005
+ /** Battery always-on continuous stream (placeholder while asleep, real frames during motion). */
12006
+ alwaysOn?: AlwaysOnOptions;
11841
12007
  }
11842
12008
 
11843
12009
  export declare interface RtpPacketizationOptions {
@@ -12504,6 +12670,14 @@ export declare function upsamplePcm16(src: Int16Array, factor: number): Int16Arr
12504
12670
  */
12505
12671
  export declare function upsertXmlTag(xml: string, tag: string, value: string | number | boolean | undefined): string;
12506
12672
 
12673
+ declare type VideoAccessUnit = {
12674
+ data: Buffer;
12675
+ isKeyframe: boolean;
12676
+ videoType: "H264" | "H265";
12677
+ microseconds: number;
12678
+ time?: number;
12679
+ };
12680
+
12507
12681
  /**
12508
12682
  * Client information extracted from HTTP request headers.
12509
12683
  * 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-UL34MR4L.js";
79
+ } from "./chunk-JQ5NSEVD.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,