@apocaliss92/nodelink-js 0.2.3 → 0.2.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.
@@ -3,10 +3,10 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-RWYEGEWG.js";
6
+ } from "../chunk-EG5IY3CM.js";
7
7
  import {
8
8
  __require
9
- } from "../chunk-NLTB7GTA.js";
9
+ } from "../chunk-APEEZ4UN.js";
10
10
 
11
11
  // src/cli/rtsp-server.ts
12
12
  function parseArgs() {
package/dist/index.cjs CHANGED
@@ -3000,14 +3000,6 @@ var init_BaichuanVideoStream = __esm({
3000
3000
  this.profile,
3001
3001
  { variant: this.variant, client: this.client }
3002
3002
  );
3003
- if (this.client.getTransport?.() === "udp") {
3004
- await startPromise;
3005
- } else {
3006
- await Promise.race([
3007
- startPromise,
3008
- new Promise((resolve) => setTimeout(resolve, 400))
3009
- ]);
3010
- }
3011
3003
  const updateActiveMsgNum = () => {
3012
3004
  try {
3013
3005
  const getMsgNum = this.api.getActiveVideoMsgNumWithVariant;
@@ -3017,6 +3009,15 @@ var init_BaichuanVideoStream = __esm({
3017
3009
  }
3018
3010
  };
3019
3011
  updateActiveMsgNum();
3012
+ if (this.client.getTransport?.() === "udp") {
3013
+ await startPromise;
3014
+ } else {
3015
+ await Promise.race([
3016
+ startPromise,
3017
+ new Promise((resolve) => setTimeout(resolve, 400))
3018
+ ]);
3019
+ }
3020
+ updateActiveMsgNum();
3020
3021
  void startPromise.then(() => updateActiveMsgNum()).catch((e) => {
3021
3022
  const err = e instanceof Error ? e : new Error(String(e));
3022
3023
  this.emitSafeError(err);
@@ -12542,8 +12543,24 @@ var BaichuanEventEmitter = class {
12542
12543
  }
12543
12544
  };
12544
12545
  async function* createNativeStream(api, channel, profile, options) {
12546
+ let client = options?.client;
12547
+ let dedicatedRelease;
12548
+ if (!client) {
12549
+ const variantSuffix = options?.variant && options.variant !== "default" ? `:${options.variant}` : "";
12550
+ const sessionKey = `live:native${variantSuffix}:ch${channel}:${profile}`;
12551
+ try {
12552
+ api.logger?.info?.(`[createNativeStream] acquiring dedicated session key=${sessionKey}`);
12553
+ const session = await api.createDedicatedSession(sessionKey);
12554
+ client = session.client;
12555
+ dedicatedRelease = session.release;
12556
+ api.logger?.info?.(`[createNativeStream] dedicated session acquired key=${sessionKey}`);
12557
+ } catch (e) {
12558
+ api.logger?.warn?.(`[createNativeStream] dedicated session failed, using shared client: ${e instanceof Error ? e.message : e}`);
12559
+ client = api.client;
12560
+ }
12561
+ }
12545
12562
  const videoStream = new BaichuanVideoStream({
12546
- client: api.client,
12563
+ client,
12547
12564
  api,
12548
12565
  channel,
12549
12566
  profile,
@@ -12557,9 +12574,15 @@ async function* createNativeStream(api, channel, profile, options) {
12557
12574
  let closed = false;
12558
12575
  const onError = (_error) => {
12559
12576
  closed = true;
12577
+ api.logger?.warn?.(
12578
+ `[createNativeStream] stream error \u2192 closed channel=${channel} profile=${profile} error=${_error?.message ?? _error}`
12579
+ );
12560
12580
  };
12561
12581
  const onClose = () => {
12562
12582
  closed = true;
12583
+ api.logger?.warn?.(
12584
+ `[createNativeStream] stream close \u2192 closed channel=${channel} profile=${profile}`
12585
+ );
12563
12586
  };
12564
12587
  try {
12565
12588
  videoStream.on("error", onError);
@@ -12670,6 +12693,10 @@ async function* createNativeStream(api, channel, profile, options) {
12670
12693
  }
12671
12694
  videoStream.removeListener("error", onError);
12672
12695
  videoStream.removeListener("close", onClose);
12696
+ if (dedicatedRelease) {
12697
+ dedicatedRelease().catch(() => {
12698
+ });
12699
+ }
12673
12700
  }
12674
12701
  }
12675
12702
 
@@ -12909,6 +12936,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
12909
12936
  tcpRtpFraming;
12910
12937
  active = false;
12911
12938
  flow;
12939
+ deviceId;
12940
+ dedicatedSessionRelease;
12912
12941
  // Authentication
12913
12942
  authCredentials = [];
12914
12943
  requireAuth;
@@ -13075,6 +13104,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13075
13104
  this.path = options.path ?? `/stream/${this.profile}`;
13076
13105
  this.logger = options.logger ?? console;
13077
13106
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
13107
+ this.deviceId = options.deviceId;
13078
13108
  this.authCredentials = options.credentials ?? [];
13079
13109
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
13080
13110
  const transport = this.api.client.getTransport();
@@ -14460,14 +14490,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14460
14490
  this.firstAudioPromise = new Promise((resolve) => {
14461
14491
  this.firstAudioResolve = resolve;
14462
14492
  });
14493
+ let dedicatedClient;
14494
+ const variantSuffix = this.variant && this.variant !== "default" ? `:${this.variant}` : "";
14495
+ const deviceIdPart = this.deviceId ?? "rtsp-server";
14496
+ const sessionKey = `live:${deviceIdPart}:ch${this.channel}:${this.profile}${variantSuffix}`;
14497
+ try {
14498
+ const session = await this.api.createDedicatedSession(sessionKey, this.logger);
14499
+ dedicatedClient = session.client;
14500
+ this.dedicatedSessionRelease = session.release;
14501
+ this.logger.info(
14502
+ `[rebroadcast] dedicated session acquired sessionKey=${sessionKey}`
14503
+ );
14504
+ } catch (e) {
14505
+ this.logger.warn(
14506
+ `[rebroadcast] failed to acquire dedicated session, falling back to shared socket: ${e}`
14507
+ );
14508
+ }
14463
14509
  this.logger.info(
14464
- `[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
14510
+ `[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size} dedicated=${!!dedicatedClient}`
14465
14511
  );
14466
14512
  await this.flow.startKeepAlive(this.api);
14467
14513
  this.nativeFanout = new NativeStreamFanout({
14468
14514
  maxQueueItems: 200,
14469
14515
  createSource: () => createNativeStream(this.api, this.channel, this.profile, {
14470
- variant: this.variant
14516
+ variant: this.variant,
14517
+ ...dedicatedClient ? { client: dedicatedClient } : {}
14471
14518
  }),
14472
14519
  onFrame: (frame) => {
14473
14520
  if (frame.audio) {
@@ -14525,6 +14572,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14525
14572
  this.firstFrameResolve = null;
14526
14573
  this.nativeFanout = null;
14527
14574
  this.prebuffer = [];
14575
+ if (this.dedicatedSessionRelease) {
14576
+ const release = this.dedicatedSessionRelease;
14577
+ this.dedicatedSessionRelease = void 0;
14578
+ release().catch(() => {
14579
+ });
14580
+ }
14528
14581
  this.logger.info(
14529
14582
  `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
14530
14583
  );
@@ -14594,6 +14647,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14594
14647
  await fanout.stop();
14595
14648
  }
14596
14649
  this.prebuffer = [];
14650
+ if (this.dedicatedSessionRelease) {
14651
+ const release = this.dedicatedSessionRelease;
14652
+ this.dedicatedSessionRelease = void 0;
14653
+ try {
14654
+ await release();
14655
+ } catch {
14656
+ }
14657
+ }
14597
14658
  if (this.tempStreamGenerator) {
14598
14659
  try {
14599
14660
  await this.tempStreamGenerator.return(void 0);
@@ -14603,14 +14664,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14603
14664
  }
14604
14665
  }
14605
14666
  /**
14606
- * Remove a client and stop native stream if no clients remain.
14667
+ * Remove a client and schedule native stream stop if no clients remain.
14668
+ * Uses a grace period so rapid reconnects (e.g. Frigate polling) reuse the running stream
14669
+ * and benefit from the prebuffer instead of waiting for a fresh keyframe.
14607
14670
  */
14608
14671
  removeClient(clientId) {
14609
14672
  if (this.connectedClients.has(clientId)) {
14610
14673
  this.connectedClients.delete(clientId);
14611
14674
  this.emit("clientDisconnected", clientId);
14612
14675
  if (this.connectedClients.size === 0) {
14613
- void this.stopNativeStream();
14676
+ this.clearNoClientAutoStopTimer();
14677
+ this.noClientAutoStopTimer = setTimeout(() => {
14678
+ if (this.connectedClients.size === 0) {
14679
+ void this.stopNativeStream();
14680
+ }
14681
+ }, 3e4);
14682
+ this.noClientAutoStopTimer?.unref?.();
14614
14683
  }
14615
14684
  }
14616
14685
  }
@@ -34182,6 +34251,7 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
34182
34251
  segmentDuration;
34183
34252
  playlistSize;
34184
34253
  ffmpegPath;
34254
+ externalVideoStream;
34185
34255
  log;
34186
34256
  outputDir = null;
34187
34257
  createdTempDir = false;
@@ -34208,6 +34278,7 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
34208
34278
  this.outputDir = options.outputDir;
34209
34279
  this.createdTempDir = false;
34210
34280
  }
34281
+ this.externalVideoStream = options.externalVideoStream;
34211
34282
  this.log = options.logger ?? (() => {
34212
34283
  });
34213
34284
  }
@@ -34232,12 +34303,16 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
34232
34303
  this.playlistPath = import_node_path3.default.join(this.outputDir, "playlist.m3u8");
34233
34304
  this.segmentPattern = import_node_path3.default.join(this.outputDir, "segment_%05d.ts");
34234
34305
  this.log("info", `Starting HLS stream to ${this.outputDir}`);
34235
- this.nativeStream = createNativeStream(
34236
- this.api,
34237
- this.channel,
34238
- this.profile,
34239
- this.variant ? { variant: this.variant } : void 0
34240
- );
34306
+ if (this.externalVideoStream) {
34307
+ this.nativeStream = this.wrapVideoStreamAsGenerator(this.externalVideoStream);
34308
+ } else {
34309
+ this.nativeStream = createNativeStream(
34310
+ this.api,
34311
+ this.channel,
34312
+ this.profile,
34313
+ this.variant ? { variant: this.variant } : void 0
34314
+ );
34315
+ }
34241
34316
  this.pumpPromise = this.pumpNativeToFfmpeg();
34242
34317
  this.startedAt = /* @__PURE__ */ new Date();
34243
34318
  this.state = "running";
@@ -34356,6 +34431,56 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
34356
34431
  // ============================================================================
34357
34432
  // Private Methods
34358
34433
  // ============================================================================
34434
+ /**
34435
+ * Wrap a BaichuanVideoStream's videoAccessUnit events into an async generator
34436
+ * compatible with createNativeStream's output format.
34437
+ */
34438
+ async *wrapVideoStreamAsGenerator(videoStream) {
34439
+ const queue = [];
34440
+ let resolve = null;
34441
+ let done = false;
34442
+ const onFrame = (au) => {
34443
+ queue.push(au);
34444
+ resolve?.();
34445
+ resolve = null;
34446
+ };
34447
+ const onClose = () => {
34448
+ done = true;
34449
+ resolve?.();
34450
+ resolve = null;
34451
+ };
34452
+ const onError = () => {
34453
+ done = true;
34454
+ resolve?.();
34455
+ resolve = null;
34456
+ };
34457
+ videoStream.on("videoAccessUnit", onFrame);
34458
+ videoStream.on("close", onClose);
34459
+ videoStream.on("error", onError);
34460
+ try {
34461
+ while (!done) {
34462
+ if (queue.length === 0) {
34463
+ await new Promise((r) => {
34464
+ resolve = r;
34465
+ });
34466
+ }
34467
+ while (queue.length > 0) {
34468
+ const frame = queue.shift();
34469
+ yield {
34470
+ audio: false,
34471
+ data: frame.data,
34472
+ videoType: frame.videoType,
34473
+ isKeyframe: frame.isKeyframe,
34474
+ microseconds: frame.microseconds
34475
+ };
34476
+ }
34477
+ }
34478
+ } finally {
34479
+ videoStream.removeListener("videoAccessUnit", onFrame);
34480
+ videoStream.removeListener("close", onClose);
34481
+ videoStream.removeListener("error", onError);
34482
+ }
34483
+ }
34359
34484
  async pumpNativeToFfmpeg() {
34360
34485
  if (!this.nativeStream || !this.playlistPath || !this.segmentPattern) {
34361
34486
  return;