@apocaliss92/nodelink-js 0.4.35 → 0.5.0

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.cjs CHANGED
@@ -8310,6 +8310,7 @@ __export(index_exports, {
8310
8310
  BaichuanHlsServer: () => BaichuanHlsServer,
8311
8311
  BaichuanHttpStreamServer: () => BaichuanHttpStreamServer,
8312
8312
  BaichuanMjpegServer: () => BaichuanMjpegServer,
8313
+ BaichuanRtspBackchannelServer: () => BaichuanRtspBackchannelServer,
8313
8314
  BaichuanRtspServer: () => BaichuanRtspServer,
8314
8315
  BaichuanVideoStream: () => BaichuanVideoStream,
8315
8316
  BaichuanWebRTCServer: () => BaichuanWebRTCServer,
@@ -8335,10 +8336,12 @@ __export(index_exports, {
8335
8336
  ReolinkCgiApi: () => ReolinkCgiApi,
8336
8337
  ReolinkHttpClient: () => ReolinkHttpClient,
8337
8338
  Rfc4571Muxer: () => Rfc4571Muxer,
8339
+ RtspBackchannel: () => RtspBackchannel,
8338
8340
  _resetEmailPushBusForTests: () => _resetEmailPushBusForTests,
8339
8341
  abilitiesHasAny: () => abilitiesHasAny,
8340
8342
  aesDecrypt: () => aesDecrypt,
8341
8343
  aesEncrypt: () => aesEncrypt,
8344
+ alawToPcm16: () => alawToPcm16,
8342
8345
  applyStreamPatch: () => applyStreamPatch,
8343
8346
  applyXmlTagPatch: () => applyXmlTagPatch,
8344
8347
  asLogger: () => asLogger,
@@ -8411,6 +8414,7 @@ __export(index_exports, {
8411
8414
  discoverViaUdpDirect: () => discoverViaUdpDirect,
8412
8415
  emitEmailPushEvent: () => emitEmailPushEvent,
8413
8416
  encodeHeader: () => encodeHeader,
8417
+ encodeImaAdpcm: () => encodeImaAdpcm,
8414
8418
  encodeMotionScopeBitmap: () => encodeMotionScopeBitmap,
8415
8419
  encodeMotionSensitivityListXml: () => encodeMotionSensitivityListXml,
8416
8420
  encodeShelterCoord: () => encodeShelterCoord,
@@ -8453,6 +8457,7 @@ __export(index_exports, {
8453
8457
  maskUid: () => maskUid,
8454
8458
  md5HexUpper: () => md5HexUpper,
8455
8459
  md5StrModern: () => md5StrModern,
8460
+ mulawToPcm16: () => mulawToPcm16,
8456
8461
  normalizeDayNightMode: () => normalizeDayNightMode,
8457
8462
  normalizeOpenClose: () => normalizeOpenClose,
8458
8463
  normalizeUid: () => normalizeUid,
@@ -8464,6 +8469,7 @@ __export(index_exports, {
8464
8469
  parseAdtsHeader: () => parseAdtsHeader,
8465
8470
  parseBcMedia: () => parseBcMedia,
8466
8471
  parseRecordingFileName: () => parseRecordingFileName,
8472
+ parseRtpPacket: () => parseRtpPacket,
8467
8473
  parseSupportXml: () => parseSupportXml,
8468
8474
  patchAiDetectCfgXml: () => patchAiDetectCfgXml,
8469
8475
  patchMotionSensitivityListXml: () => patchMotionSensitivityListXml,
@@ -8481,6 +8487,7 @@ __export(index_exports, {
8481
8487
  splitAnnexBToNals: () => splitAnnexBToNals,
8482
8488
  splitH265AnnexBToNalPayloads: () => splitAnnexBToNalPayloads2,
8483
8489
  testChannelStreams: () => testChannelStreams,
8490
+ upsamplePcm16: () => upsamplePcm16,
8484
8491
  upsertXmlTag: () => upsertXmlTag,
8485
8492
  xmlEscape: () => xmlEscape,
8486
8493
  xmlIndicatesFloodlight: () => xmlIndicatesFloodlight,
@@ -27576,8 +27583,18 @@ ${xml}`
27576
27583
  * This is more reliable than autoPt in SupportInfo which can be a false positive
27577
27584
  * (e.g., NVR channels report autoPt=1 but don't actually support autotracking).
27578
27585
  *
27586
+ * Doorbell exception (mirrors the {@link computeDeviceCapabilities} floodlight
27587
+ * rule): video-doorbell firmwares (doorbellVersion > 0) ship AiCfg with
27588
+ * `smartTrackMode=1` (the current mode) yet `smartTrackModeAbility=0` (the
27589
+ * firmware's own "no, this device cannot autotrack" flag). Without a PT
27590
+ * motor a doorbell physically cannot autotrack, so when the caller flags the
27591
+ * device as a doorbell AND the firmware admits `smartTrackModeAbility=0`,
27592
+ * trust the firmware and return false. Verified against UID
27593
+ * 9527000ICL1T1MDS: smartTrackMode=1, smartTrackModeAbility=0,
27594
+ * ptzType=0, ptzMode="none", doorbellVersion=31.
27595
+ *
27579
27596
  * @param channel - Channel number (0-based)
27580
- * @param options - Optional timeout
27597
+ * @param options - Optional timeout and doorbell context hint
27581
27598
  * @returns true if autotracking is supported, false otherwise
27582
27599
  */
27583
27600
  async probeAutotrackingSupport(channel, options) {
@@ -27587,6 +27604,14 @@ ${xml}`
27587
27604
  const xml = await this.sendXml({ cmdId: 299, channel: ch, timeoutMs });
27588
27605
  const smartTrackModeRaw = getXmlText(xml, "smartTrackMode");
27589
27606
  const smartTrackMode = Number(smartTrackModeRaw ?? 0);
27607
+ const smartTrackModeAbilityRaw = getXmlText(
27608
+ xml,
27609
+ "smartTrackModeAbility"
27610
+ );
27611
+ const smartTrackModeAbility = smartTrackModeAbilityRaw === void 0 ? void 0 : Number(smartTrackModeAbilityRaw);
27612
+ if (options?.isDoorbell && Number.isFinite(smartTrackModeAbility) && smartTrackModeAbility === 0) {
27613
+ return false;
27614
+ }
27590
27615
  return smartTrackMode > 0;
27591
27616
  } catch {
27592
27617
  return false;
@@ -27679,7 +27704,8 @@ ${xml}`
27679
27704
  const features = this.parseFeaturesFromSupport(support);
27680
27705
  const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
27681
27706
  const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
27682
- timeoutMs: 1500
27707
+ timeoutMs: 1500,
27708
+ isDoorbell: capabilities.isDoorbell === true
27683
27709
  });
27684
27710
  capabilities.hasAutotracking = autotrackingProbed;
27685
27711
  let presets;
@@ -31013,10 +31039,10 @@ ${scheduleItems}
31013
31039
  const os2 = await import("os");
31014
31040
  const path7 = await import("path");
31015
31041
  const fs7 = await import("fs/promises");
31016
- const crypto3 = await import("crypto");
31042
+ const crypto4 = await import("crypto");
31017
31043
  const tempDir = path7.join(
31018
31044
  os2.tmpdir(),
31019
- `reolink-hls-${crypto3.randomBytes(8).toString("hex")}`
31045
+ `reolink-hls-${crypto4.randomBytes(8).toString("hex")}`
31020
31046
  );
31021
31047
  await fs7.mkdir(tempDir, { recursive: true });
31022
31048
  const playlistPath = path7.join(tempDir, "playlist.m3u8");
@@ -41147,6 +41173,947 @@ function base64DecodeToBytes(b64) {
41147
41173
  return out.subarray(0, outIdx);
41148
41174
  }
41149
41175
 
41176
+ // src/reolink/baichuan/utils/imaAdpcm.ts
41177
+ var IMA_INDEX_TABLE = [
41178
+ -1,
41179
+ -1,
41180
+ -1,
41181
+ -1,
41182
+ 2,
41183
+ 4,
41184
+ 6,
41185
+ 8,
41186
+ -1,
41187
+ -1,
41188
+ -1,
41189
+ -1,
41190
+ 2,
41191
+ 4,
41192
+ 6,
41193
+ 8
41194
+ ];
41195
+ var IMA_STEP_TABLE = [
41196
+ 7,
41197
+ 8,
41198
+ 9,
41199
+ 10,
41200
+ 11,
41201
+ 12,
41202
+ 13,
41203
+ 14,
41204
+ 16,
41205
+ 17,
41206
+ 19,
41207
+ 21,
41208
+ 23,
41209
+ 25,
41210
+ 28,
41211
+ 31,
41212
+ 34,
41213
+ 37,
41214
+ 41,
41215
+ 45,
41216
+ 50,
41217
+ 55,
41218
+ 60,
41219
+ 66,
41220
+ 73,
41221
+ 80,
41222
+ 88,
41223
+ 97,
41224
+ 107,
41225
+ 118,
41226
+ 130,
41227
+ 143,
41228
+ 157,
41229
+ 173,
41230
+ 190,
41231
+ 209,
41232
+ 230,
41233
+ 253,
41234
+ 279,
41235
+ 307,
41236
+ 337,
41237
+ 371,
41238
+ 408,
41239
+ 449,
41240
+ 494,
41241
+ 544,
41242
+ 598,
41243
+ 658,
41244
+ 724,
41245
+ 796,
41246
+ 876,
41247
+ 963,
41248
+ 1060,
41249
+ 1166,
41250
+ 1282,
41251
+ 1411,
41252
+ 1552,
41253
+ 1707,
41254
+ 1878,
41255
+ 2066,
41256
+ 2272,
41257
+ 2499,
41258
+ 2749,
41259
+ 3024,
41260
+ 3327,
41261
+ 3660,
41262
+ 4026,
41263
+ 4428,
41264
+ 4871,
41265
+ 5358,
41266
+ 5894,
41267
+ 6484,
41268
+ 7132,
41269
+ 7845,
41270
+ 8630,
41271
+ 9493,
41272
+ 10442,
41273
+ 11487,
41274
+ 12635,
41275
+ 13899,
41276
+ 15289,
41277
+ 16818,
41278
+ 18500,
41279
+ 20350,
41280
+ 22385,
41281
+ 24623,
41282
+ 27086,
41283
+ 29794,
41284
+ 32767
41285
+ ];
41286
+ function clamp16(x) {
41287
+ if (x > 32767) return 32767;
41288
+ if (x < -32768) return -32768;
41289
+ return x | 0;
41290
+ }
41291
+ function encodeImaAdpcm(pcm, blockSizeBytes) {
41292
+ if (pcm.length === 0) return Buffer.alloc(0);
41293
+ if (!Number.isInteger(blockSizeBytes) || blockSizeBytes <= 0) {
41294
+ throw new RangeError(
41295
+ `encodeImaAdpcm: blockSizeBytes must be a positive integer, got ${blockSizeBytes}`
41296
+ );
41297
+ }
41298
+ const samplesPerBlock = blockSizeBytes * 2 + 1;
41299
+ const totalBlocks = Math.ceil(pcm.length / samplesPerBlock);
41300
+ const out = Buffer.alloc(totalBlocks * (4 + blockSizeBytes));
41301
+ let sampleIndex = 0;
41302
+ let outOffset = 0;
41303
+ for (let b = 0; b < totalBlocks; b++) {
41304
+ const blockStart = outOffset;
41305
+ const first = pcm[sampleIndex] ?? 0;
41306
+ let predictor = first;
41307
+ let index = 0;
41308
+ out.writeInt16LE(predictor, blockStart);
41309
+ out.writeUInt8(index, blockStart + 2);
41310
+ out.writeUInt8(0, blockStart + 3);
41311
+ sampleIndex++;
41312
+ const codes = new Uint8Array(blockSizeBytes * 2);
41313
+ for (let i = 0; i < codes.length; i++) {
41314
+ const sample = pcm[sampleIndex] ?? predictor;
41315
+ sampleIndex++;
41316
+ let diff = sample - predictor;
41317
+ let sign = 0;
41318
+ if (diff < 0) {
41319
+ sign = 8;
41320
+ diff = -diff;
41321
+ }
41322
+ let step = IMA_STEP_TABLE[index] ?? 7;
41323
+ let delta = 0;
41324
+ let vpdiff = step >> 3;
41325
+ if (diff >= step) {
41326
+ delta |= 4;
41327
+ diff -= step;
41328
+ vpdiff += step;
41329
+ }
41330
+ step >>= 1;
41331
+ if (diff >= step) {
41332
+ delta |= 2;
41333
+ diff -= step;
41334
+ vpdiff += step;
41335
+ }
41336
+ step >>= 1;
41337
+ if (diff >= step) {
41338
+ delta |= 1;
41339
+ vpdiff += step;
41340
+ }
41341
+ predictor = clamp16(sign ? predictor - vpdiff : predictor + vpdiff);
41342
+ index += IMA_INDEX_TABLE[delta] ?? 0;
41343
+ if (index < 0) index = 0;
41344
+ if (index > 88) index = 88;
41345
+ codes[i] = (delta | sign) & 15;
41346
+ }
41347
+ for (let i = 0; i < blockSizeBytes; i++) {
41348
+ const lo = codes[i * 2] ?? 0;
41349
+ const hi = codes[i * 2 + 1] ?? 0;
41350
+ out[blockStart + 4 + i] = lo & 15 | (hi & 15) << 4;
41351
+ }
41352
+ outOffset += 4 + blockSizeBytes;
41353
+ }
41354
+ return out;
41355
+ }
41356
+
41357
+ // src/reolink/baichuan/utils/audioMulaw.ts
41358
+ var import_alawmulaw = __toESM(require("alawmulaw"), 1);
41359
+ var { mulaw, alaw } = import_alawmulaw.default;
41360
+ function mulawToPcm16(bytes) {
41361
+ if (bytes.length === 0) return new Int16Array(0);
41362
+ return mulaw.decode(bytes);
41363
+ }
41364
+ function alawToPcm16(bytes) {
41365
+ if (bytes.length === 0) return new Int16Array(0);
41366
+ return alaw.decode(bytes);
41367
+ }
41368
+
41369
+ // src/reolink/baichuan/utils/audioResample.ts
41370
+ function clamp162(x) {
41371
+ if (x > 32767) return 32767;
41372
+ if (x < -32768) return -32768;
41373
+ return x | 0;
41374
+ }
41375
+ function upsamplePcm16(src, factor) {
41376
+ if (!Number.isInteger(factor) || factor < 1) {
41377
+ throw new RangeError(
41378
+ `upsamplePcm16: factor must be a positive integer, got ${factor}`
41379
+ );
41380
+ }
41381
+ if (src.length === 0) return new Int16Array(0);
41382
+ if (factor === 1) return Int16Array.from(src);
41383
+ const out = new Int16Array(src.length * factor);
41384
+ const last = src.length - 1;
41385
+ for (let i = 0; i < last; i++) {
41386
+ const a = src[i];
41387
+ const b = src[i + 1];
41388
+ const base = i * factor;
41389
+ for (let k = 0; k < factor; k++) {
41390
+ const v = a + Math.round((b - a) * k / factor);
41391
+ out[base + k] = clamp162(v);
41392
+ }
41393
+ }
41394
+ const tail = src[last] | 0;
41395
+ for (let k = 0; k < factor; k++) {
41396
+ out[last * factor + k] = tail;
41397
+ }
41398
+ return out;
41399
+ }
41400
+
41401
+ // src/baichuan/stream/RtspBackchannel.ts
41402
+ var RTP_PT_PCMA = 8;
41403
+ var RTP_FIXED_HEADER_BYTES = 12;
41404
+ var G711_SOURCE_RATE_HZ = 8e3;
41405
+ var TALK_TARGET_RATE_HZ = 16e3;
41406
+ function parseRtpPacket(packet) {
41407
+ if (packet.length < RTP_FIXED_HEADER_BYTES) return null;
41408
+ const b0 = packet.readUInt8(0);
41409
+ const version = b0 >> 6;
41410
+ if (version !== 2) return null;
41411
+ const padding = (b0 & 32) !== 0;
41412
+ const extension = (b0 & 16) !== 0;
41413
+ const csrcCount = b0 & 15;
41414
+ const b1 = packet.readUInt8(1);
41415
+ const payloadType = b1 & 127;
41416
+ let offset = RTP_FIXED_HEADER_BYTES + csrcCount * 4;
41417
+ if (offset > packet.length) return null;
41418
+ if (extension) {
41419
+ if (offset + 4 > packet.length) return null;
41420
+ const extensionLengthWords = packet.readUInt16BE(offset + 2);
41421
+ offset += 4 + extensionLengthWords * 4;
41422
+ if (offset > packet.length) return null;
41423
+ }
41424
+ let end = packet.length;
41425
+ if (padding) {
41426
+ if (end - offset < 1) return null;
41427
+ const padLength = packet.readUInt8(end - 1);
41428
+ if (padLength < 1 || padLength > end - offset) return null;
41429
+ end -= padLength;
41430
+ }
41431
+ if (end <= offset) return null;
41432
+ return {
41433
+ payloadType,
41434
+ payload: packet.subarray(offset, end)
41435
+ };
41436
+ }
41437
+ var RtspBackchannel = class _RtspBackchannel {
41438
+ constructor(opts) {
41439
+ this.opts = opts;
41440
+ }
41441
+ session = void 0;
41442
+ pcmTail = new Int16Array(0);
41443
+ // residual samples below one full ADPCM block
41444
+ pcmBacklogBytes = 0;
41445
+ pumping = false;
41446
+ active = false;
41447
+ starting = void 0;
41448
+ stopping = void 0;
41449
+ lastBacklogClampLogMs = 0;
41450
+ rtpPacketsReceived = 0;
41451
+ rtpPacketsDropped = 0;
41452
+ adpcmBlocksSent = 0;
41453
+ /** Lazily-set on the first decoded RTP packet so we log the negotiated codec exactly once. */
41454
+ observedCodec = void 0;
41455
+ /** Sampled every `STATS_LOG_INTERVAL_MS` while audio flows. */
41456
+ lastStatsLogMs = 0;
41457
+ lastStatsAdpcmBlocks = 0;
41458
+ lastStatsRtpPackets = 0;
41459
+ startedAtMs = 0;
41460
+ static STATS_LOG_INTERVAL_MS = 5e3;
41461
+ get isActive() {
41462
+ return this.active;
41463
+ }
41464
+ get stats() {
41465
+ return {
41466
+ rtpPacketsReceived: this.rtpPacketsReceived,
41467
+ rtpPacketsDropped: this.rtpPacketsDropped,
41468
+ adpcmBlocksSent: this.adpcmBlocksSent
41469
+ };
41470
+ }
41471
+ /** Open the underlying TalkSession. Safe to call concurrently; resolves to the same session. */
41472
+ async start() {
41473
+ if (this.session) return this.session;
41474
+ if (!this.starting) {
41475
+ const openStart = Date.now();
41476
+ this.opts.logger?.log?.(
41477
+ `[RtspBackchannel] opening TalkSession on camera\u2026`
41478
+ );
41479
+ this.starting = (async () => {
41480
+ const s = await this.opts.openTalkSession();
41481
+ this.session = s;
41482
+ this.active = true;
41483
+ this.startedAtMs = Date.now();
41484
+ this.lastStatsLogMs = this.startedAtMs;
41485
+ this.opts.logger?.log?.(
41486
+ `[RtspBackchannel] TalkSession opened channel=${s.info.channel} block=${s.info.blockSize} fullBlock=${s.info.fullBlockSize} audioType=${s.info.audioConfig.audioType} sampleRate=${s.info.audioConfig.sampleRate} samplePrecision=${s.info.audioConfig.samplePrecision} soundTrack=${s.info.audioConfig.soundTrack} openMs=${Date.now() - openStart}`
41487
+ );
41488
+ return s;
41489
+ })().catch((e) => {
41490
+ this.starting = void 0;
41491
+ this.opts.logger?.log?.(
41492
+ `[RtspBackchannel] TalkSession open FAILED openMs=${Date.now() - openStart} error="${e.message}"`
41493
+ );
41494
+ throw e;
41495
+ });
41496
+ }
41497
+ return this.starting;
41498
+ }
41499
+ /**
41500
+ * Feed one inbound RTP packet (as carried in TCP-interleaved framing or UDP
41501
+ * datagram). Discards malformed packets and packets received before `start()`.
41502
+ * The actual audio dispatch to the TalkSession is awaited internally; callers
41503
+ * may fire-and-forget.
41504
+ */
41505
+ feedRtp(packet) {
41506
+ this.rtpPacketsReceived++;
41507
+ if (!this.active || !this.session) {
41508
+ this.rtpPacketsDropped++;
41509
+ if (this.rtpPacketsDropped === 1) {
41510
+ this.opts.logger?.log?.(
41511
+ `[RtspBackchannel] dropping RTP packets \u2014 session not active yet (received ${packet.length}B before start())`
41512
+ );
41513
+ }
41514
+ return;
41515
+ }
41516
+ const parsed = parseRtpPacket(packet);
41517
+ if (!parsed) {
41518
+ this.rtpPacketsDropped++;
41519
+ this.opts.logger?.log?.(
41520
+ `[RtspBackchannel] malformed RTP packet \u2014 dropping (len=${packet.length} firstByte=0x${packet.length ? (packet[0] ?? 0).toString(16) : "??"})`
41521
+ );
41522
+ return;
41523
+ }
41524
+ const codec = this.opts.forceCodec ?? (parsed.payloadType === RTP_PT_PCMA ? "pcma" : "pcmu");
41525
+ if (this.observedCodec === void 0) {
41526
+ this.observedCodec = codec;
41527
+ const firstPacketMs = Date.now() - this.startedAtMs;
41528
+ this.opts.logger?.log?.(
41529
+ `[RtspBackchannel] first RTP packet \u2014 codec=${codec} payloadType=${parsed.payloadType} payload=${parsed.payload.length}B firstByteMs=${firstPacketMs}` + (this.opts.forceCodec ? " (forced by config)" : "")
41530
+ );
41531
+ } else if (codec !== this.observedCodec) {
41532
+ this.opts.logger?.log?.(
41533
+ `[RtspBackchannel] codec switched mid-stream ${this.observedCodec} \u2192 ${codec} (payloadType=${parsed.payloadType})`
41534
+ );
41535
+ this.observedCodec = codec;
41536
+ }
41537
+ const decoded = codec === "pcma" ? alawToPcm16(parsed.payload) : mulawToPcm16(parsed.payload);
41538
+ if (decoded.length === 0) return;
41539
+ const upsampled = upsamplePcm16(decoded, TALK_TARGET_RATE_HZ / G711_SOURCE_RATE_HZ);
41540
+ this.enqueuePcm(upsampled);
41541
+ this.maybeLogStats();
41542
+ }
41543
+ /**
41544
+ * Throttled progress log (~every 5s while audio is flowing) so operators
41545
+ * can confirm the pipeline is making progress without per-packet noise.
41546
+ */
41547
+ maybeLogStats() {
41548
+ const now = Date.now();
41549
+ if (now - this.lastStatsLogMs < _RtspBackchannel.STATS_LOG_INTERVAL_MS) return;
41550
+ const elapsedSec = (now - this.lastStatsLogMs) / 1e3;
41551
+ const rtpDelta = this.rtpPacketsReceived - this.lastStatsRtpPackets;
41552
+ const adpcmDelta = this.adpcmBlocksSent - this.lastStatsAdpcmBlocks;
41553
+ this.lastStatsLogMs = now;
41554
+ this.lastStatsRtpPackets = this.rtpPacketsReceived;
41555
+ this.lastStatsAdpcmBlocks = this.adpcmBlocksSent;
41556
+ this.opts.logger?.log?.(
41557
+ `[RtspBackchannel] stats codec=${this.observedCodec ?? "?"} rtpRx=${this.rtpPacketsReceived} (+${rtpDelta} in ${elapsedSec.toFixed(1)}s) dropped=${this.rtpPacketsDropped} adpcmBlocks=${this.adpcmBlocksSent} (+${adpcmDelta}) pcmBacklog=${this.pcmBacklogBytes}B`
41558
+ );
41559
+ }
41560
+ /** Flush remaining audio and stop the talk session. Idempotent. */
41561
+ async stop() {
41562
+ if (this.stopping) return this.stopping;
41563
+ this.active = false;
41564
+ this.stopping = (async () => {
41565
+ const s = this.session;
41566
+ this.session = void 0;
41567
+ this.pcmTail = new Int16Array(0);
41568
+ this.pcmBacklogBytes = 0;
41569
+ const durationMs = this.startedAtMs ? Date.now() - this.startedAtMs : 0;
41570
+ this.opts.logger?.log?.(
41571
+ `[RtspBackchannel] closing TalkSession durationMs=${durationMs} rtpRx=${this.rtpPacketsReceived} rtpDropped=${this.rtpPacketsDropped} adpcmBlocks=${this.adpcmBlocksSent} pcmBacklogResidual=${this.pcmBacklogBytes}B codec=${this.observedCodec ?? "(none)"}`
41572
+ );
41573
+ if (s) {
41574
+ try {
41575
+ await s.stop();
41576
+ } catch (e) {
41577
+ this.opts.logger?.log?.(
41578
+ `[RtspBackchannel] TalkSession stop error: ${e.message}`
41579
+ );
41580
+ }
41581
+ }
41582
+ })();
41583
+ return this.stopping;
41584
+ }
41585
+ enqueuePcm(chunk) {
41586
+ if (chunk.length === 0) return;
41587
+ const tailLen = this.pcmTail.length;
41588
+ const merged = new Int16Array(tailLen + chunk.length);
41589
+ merged.set(this.pcmTail, 0);
41590
+ merged.set(chunk, tailLen);
41591
+ this.pcmBacklogBytes = merged.length * 2;
41592
+ const maxBytes = Math.max(2, this.opts.maxPcmBacklogBytes ?? 96e3);
41593
+ if (this.pcmBacklogBytes > maxBytes) {
41594
+ const keepSamples = Math.floor(maxBytes / 2);
41595
+ const dropSamples = merged.length - keepSamples;
41596
+ this.pcmTail = merged.subarray(merged.length - keepSamples);
41597
+ this.pcmBacklogBytes = this.pcmTail.length * 2;
41598
+ const now = Date.now();
41599
+ if (now - this.lastBacklogClampLogMs > 2e3) {
41600
+ this.lastBacklogClampLogMs = now;
41601
+ this.opts.logger?.log?.(
41602
+ `[RtspBackchannel] PCM backlog clamped \u2014 dropped ${dropSamples} samples, kept ${keepSamples}`
41603
+ );
41604
+ }
41605
+ } else {
41606
+ this.pcmTail = merged;
41607
+ }
41608
+ void this.pumpAdpcm();
41609
+ }
41610
+ async pumpAdpcm() {
41611
+ if (this.pumping) return;
41612
+ const session = this.session;
41613
+ if (!session) return;
41614
+ this.pumping = true;
41615
+ try {
41616
+ const samplesPerBlock = session.info.blockSize * 2 + 1;
41617
+ const blockSizeBytes = session.info.blockSize;
41618
+ while (this.active && this.pcmTail.length >= samplesPerBlock) {
41619
+ const consumeSamples = Math.floor(this.pcmTail.length / samplesPerBlock) * samplesPerBlock;
41620
+ const head = this.pcmTail.subarray(0, consumeSamples);
41621
+ const adpcm = encodeImaAdpcm(head, blockSizeBytes);
41622
+ const blocksInBatch = consumeSamples / samplesPerBlock;
41623
+ this.pcmTail = this.pcmTail.slice(consumeSamples);
41624
+ this.pcmBacklogBytes = this.pcmTail.length * 2;
41625
+ try {
41626
+ await session.sendAudio(adpcm);
41627
+ this.adpcmBlocksSent += blocksInBatch;
41628
+ } catch (e) {
41629
+ this.opts.logger?.log?.(
41630
+ `[RtspBackchannel] sendAudio FAILED \u2014 disabling pipeline blocksInBatch=${blocksInBatch} adpcmBytes=${adpcm.length} pcmBacklogBefore=${this.pcmBacklogBytes + adpcm.length}B error="${e.message}"`
41631
+ );
41632
+ this.pcmTail = new Int16Array(0);
41633
+ this.pcmBacklogBytes = 0;
41634
+ this.active = false;
41635
+ break;
41636
+ }
41637
+ }
41638
+ } finally {
41639
+ this.pumping = false;
41640
+ }
41641
+ }
41642
+ };
41643
+
41644
+ // src/baichuan/stream/BaichuanRtspBackchannelServer.ts
41645
+ var import_node_events15 = require("events");
41646
+ var net6 = __toESM(require("net"), 1);
41647
+ var crypto3 = __toESM(require("crypto"), 1);
41648
+ var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
41649
+ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
41650
+ api;
41651
+ channel;
41652
+ listenHost;
41653
+ listenPort;
41654
+ path;
41655
+ logger;
41656
+ authCredentials;
41657
+ requireAuth;
41658
+ authRealm;
41659
+ deviceId;
41660
+ server = void 0;
41661
+ /** Active backchannel sessions keyed by their per-client unique id. */
41662
+ sessionByClient = /* @__PURE__ */ new Map();
41663
+ nonces = /* @__PURE__ */ new Map();
41664
+ static NONCE_TTL_MS = 5 * 60 * 1e3;
41665
+ constructor(options) {
41666
+ super();
41667
+ this.api = options.api;
41668
+ this.channel = options.channel;
41669
+ this.listenHost = options.listenHost ?? "127.0.0.1";
41670
+ this.listenPort = options.listenPort ?? 8555;
41671
+ this.path = options.path ?? "/talk";
41672
+ this.logger = options.logger ?? console;
41673
+ this.authCredentials = (options.credentials ?? []).map((c) => ({
41674
+ username: c.username,
41675
+ ...c.password !== void 0 ? { password: c.password } : {},
41676
+ ...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
41677
+ }));
41678
+ this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
41679
+ this.authRealm = options.authRealm ?? "BaichuanRtspBackchannelServer";
41680
+ this.deviceId = options.deviceId;
41681
+ }
41682
+ get listening() {
41683
+ return this.server !== void 0 && this.server.listening;
41684
+ }
41685
+ async start() {
41686
+ if (this.server) return;
41687
+ await new Promise((resolve, reject) => {
41688
+ const server = net6.createServer((socket) => this.handleConnection(socket));
41689
+ const onError = (err) => {
41690
+ server.removeListener("error", onError);
41691
+ reject(err);
41692
+ };
41693
+ server.once("error", onError);
41694
+ server.listen(this.listenPort, this.listenHost, () => {
41695
+ server.removeListener("error", onError);
41696
+ this.server = server;
41697
+ this.logger.info?.(
41698
+ `[BaichuanRtspBackchannelServer] listening on ${this.listenHost}:${this.listenPort} path=${this.path}`
41699
+ );
41700
+ resolve();
41701
+ });
41702
+ });
41703
+ }
41704
+ async stop() {
41705
+ const server = this.server;
41706
+ this.server = void 0;
41707
+ for (const session of this.sessionByClient.values()) {
41708
+ this.sessionByClient.delete(session.clientId);
41709
+ if (session.handler) {
41710
+ void session.handler.stop();
41711
+ }
41712
+ try {
41713
+ session.socket.destroy();
41714
+ } catch {
41715
+ }
41716
+ }
41717
+ if (!server) return;
41718
+ await new Promise((resolve) => {
41719
+ server.close(() => resolve());
41720
+ });
41721
+ }
41722
+ handleConnection(socket) {
41723
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
41724
+ const connectedAt = Date.now();
41725
+ let buffer = Buffer.alloc(0);
41726
+ this.logger.info?.(
41727
+ `[BaichuanRtspBackchannelServer] client connected client=${clientId} path=${this.path}`
41728
+ );
41729
+ this.emit("client", clientId);
41730
+ const cleanup = () => {
41731
+ const session = this.sessionByClient.get(clientId);
41732
+ const durationMs = Date.now() - connectedAt;
41733
+ if (session) {
41734
+ this.sessionByClient.delete(clientId);
41735
+ if (session.handler) {
41736
+ const stats = session.handler.stats;
41737
+ this.logger.info?.(
41738
+ `[BaichuanRtspBackchannelServer] client disconnected client=${clientId} session=${session.sessionId} duration=${durationMs}ms rtpRx=${stats.rtpPacketsReceived} rtpDropped=${stats.rtpPacketsDropped} adpcmBlocks=${stats.adpcmBlocksSent}`
41739
+ );
41740
+ void session.handler.stop();
41741
+ } else {
41742
+ this.logger.info?.(
41743
+ `[BaichuanRtspBackchannelServer] client disconnected client=${clientId} session=${session.sessionId} duration=${durationMs}ms (no RECORD)`
41744
+ );
41745
+ }
41746
+ } else {
41747
+ this.logger.info?.(
41748
+ `[BaichuanRtspBackchannelServer] client disconnected client=${clientId} duration=${durationMs}ms (no SETUP)`
41749
+ );
41750
+ }
41751
+ this.nonces.delete(clientId);
41752
+ this.emit("clientDisconnected", clientId);
41753
+ };
41754
+ socket.on("close", cleanup);
41755
+ socket.on("error", (err) => {
41756
+ this.logger.warn?.(
41757
+ `[BaichuanRtspBackchannelServer] socket error client=${clientId}: ${err.message}`
41758
+ );
41759
+ cleanup();
41760
+ });
41761
+ socket.on("data", (data) => {
41762
+ buffer = Buffer.concat([buffer, data]);
41763
+ this.drainBuffer(socket, clientId, (b) => {
41764
+ buffer = b;
41765
+ }, () => buffer);
41766
+ });
41767
+ }
41768
+ /**
41769
+ * Drain pending bytes from the per-client buffer. Each iteration first
41770
+ * peels off any TCP-interleaved `$` frames at the head (routing them to
41771
+ * the session's RtspBackchannel handler when their channel matches) and
41772
+ * then attempts to parse a complete RTSP request.
41773
+ *
41774
+ * Implemented as a thin loop so the parser can yield back to the event
41775
+ * loop when the buffer is partial — no async work between frames keeps
41776
+ * the data path tight.
41777
+ */
41778
+ drainBuffer(socket, clientId, setBuffer, getBuffer) {
41779
+ const tryDrainInterleaved = () => {
41780
+ let consumed = false;
41781
+ while (true) {
41782
+ const buf = getBuffer();
41783
+ if (buf.length === 0 || buf[0] !== 36) return consumed;
41784
+ if (buf.length < 4) return consumed;
41785
+ const channel = buf[1] ?? 0;
41786
+ const len = buf.readUInt16BE(2);
41787
+ if (buf.length < 4 + len) return consumed;
41788
+ const rtpPacket = buf.subarray(4, 4 + len);
41789
+ setBuffer(buf.subarray(4 + len));
41790
+ consumed = true;
41791
+ const session = this.sessionByClient.get(clientId);
41792
+ if (session && session.rtpChannel === channel && session.handler) {
41793
+ session.handler.feedRtp(Buffer.from(rtpPacket));
41794
+ }
41795
+ }
41796
+ };
41797
+ void (async () => {
41798
+ while (true) {
41799
+ tryDrainInterleaved();
41800
+ const buf = getBuffer();
41801
+ if (buf.length === 0) return;
41802
+ if (buf[0] === 36) return;
41803
+ const headerEnd = buf.indexOf("\r\n\r\n");
41804
+ if (headerEnd < 0) return;
41805
+ const requestText = buf.subarray(0, headerEnd).toString("utf8");
41806
+ setBuffer(buf.subarray(headerEnd + 4));
41807
+ try {
41808
+ await this.handleRequest(socket, clientId, requestText);
41809
+ } catch (e) {
41810
+ this.logger.warn?.(
41811
+ `[BaichuanRtspBackchannelServer] handleRequest failed for ${clientId}: ${e.message}`
41812
+ );
41813
+ try {
41814
+ socket.destroy();
41815
+ } catch {
41816
+ }
41817
+ return;
41818
+ }
41819
+ }
41820
+ })();
41821
+ }
41822
+ async handleRequest(socket, clientId, requestText) {
41823
+ const lines = requestText.split("\r\n");
41824
+ const head = lines[0]?.split(" ") ?? [];
41825
+ if (head.length < 3) {
41826
+ this.logger.warn?.(
41827
+ `[BaichuanRtspBackchannelServer] malformed request from ${clientId}: "${(lines[0] ?? "").slice(0, 120)}"`
41828
+ );
41829
+ return;
41830
+ }
41831
+ const method = head[0] ?? "";
41832
+ const url = head[1] ?? "";
41833
+ const protocol = head[2] ?? "RTSP/1.0";
41834
+ const cseqMatch = requestText.match(/CSeq:\s*(\d+)/i);
41835
+ const cseq = cseqMatch ? parseInt(cseqMatch[1] ?? "0", 10) : 0;
41836
+ const sessionMatch = requestText.match(/Session:\s*([^;\r\n]+)/i);
41837
+ let sessionId = sessionMatch?.[1]?.trim();
41838
+ const userAgent = requestText.match(/User-Agent:\s*([^\r\n]+)/i)?.[1]?.trim();
41839
+ this.logger.info?.(
41840
+ `[BaichuanRtspBackchannelServer] >> ${method} ${url} client=${clientId} cseq=${cseq}` + (sessionId ? ` session=${sessionId}` : "") + (userAgent ? ` ua="${userAgent}"` : "")
41841
+ );
41842
+ const send = (status, reason, headers = {}, body) => {
41843
+ let r = `${protocol} ${status} ${reason}\r
41844
+ CSeq: ${cseq}\r
41845
+ `;
41846
+ for (const [k, v] of Object.entries(headers)) {
41847
+ r += `${k}: ${v}\r
41848
+ `;
41849
+ }
41850
+ if (body) {
41851
+ const buf = Buffer.from(body, "utf8");
41852
+ r += `Content-Length: ${buf.length}\r
41853
+ \r
41854
+ `;
41855
+ socket.write(r);
41856
+ socket.write(buf);
41857
+ } else {
41858
+ r += "\r\n";
41859
+ socket.write(r);
41860
+ }
41861
+ const headerSummary = Object.entries(headers).map(([k, v]) => `${k}=${v}`).join(", ");
41862
+ this.logger.info?.(
41863
+ `[BaichuanRtspBackchannelServer] << ${status} ${reason} client=${clientId} cseq=${cseq}` + (headerSummary ? ` [${headerSummary}]` : "") + (body ? ` body=${body.length}B` : "")
41864
+ );
41865
+ };
41866
+ if (this.requireAuth && method !== "OPTIONS") {
41867
+ const authHeader = requestText.match(/Authorization:\s*([^\r\n]+)/i)?.[1] ?? "";
41868
+ if (!authHeader) {
41869
+ this.logger.info?.(
41870
+ `[BaichuanRtspBackchannelServer] auth challenge issued client=${clientId} method=${method}`
41871
+ );
41872
+ send(401, "Unauthorized", {
41873
+ "WWW-Authenticate": this.wwwAuthenticate(clientId)
41874
+ });
41875
+ return;
41876
+ }
41877
+ if (!this.validateDigest(authHeader, method, url, clientId)) {
41878
+ this.logger.warn?.(
41879
+ `[BaichuanRtspBackchannelServer] auth rejected client=${clientId} method=${method}`
41880
+ );
41881
+ this.nonces.delete(clientId);
41882
+ send(401, "Unauthorized", {
41883
+ "WWW-Authenticate": this.wwwAuthenticate(clientId)
41884
+ });
41885
+ return;
41886
+ }
41887
+ }
41888
+ switch (method) {
41889
+ case "OPTIONS":
41890
+ send(200, "OK", {
41891
+ Public: "OPTIONS, DESCRIBE, SETUP, RECORD, TEARDOWN"
41892
+ });
41893
+ return;
41894
+ case "DESCRIBE": {
41895
+ const sdp = this.buildSdp();
41896
+ this.logger.debug?.(
41897
+ `[BaichuanRtspBackchannelServer] DESCRIBE sdp for ${clientId}:
41898
+ ${sdp.trimEnd()}`
41899
+ );
41900
+ send(
41901
+ 200,
41902
+ "OK",
41903
+ {
41904
+ "Content-Type": "application/sdp",
41905
+ "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
41906
+ },
41907
+ sdp
41908
+ );
41909
+ return;
41910
+ }
41911
+ case "SETUP": {
41912
+ if (!url.includes("audiobackchannel")) {
41913
+ this.logger.warn?.(
41914
+ `[BaichuanRtspBackchannelServer] SETUP rejected (unknown track) client=${clientId} url=${url}`
41915
+ );
41916
+ send(404, "Not Found");
41917
+ return;
41918
+ }
41919
+ const transportLine = requestText.match(/Transport:\s*([^\r\n]+)/i)?.[1] ?? "";
41920
+ this.logger.info?.(
41921
+ `[BaichuanRtspBackchannelServer] SETUP transport="${transportLine}" client=${clientId}`
41922
+ );
41923
+ if (!transportLine.toUpperCase().includes("RTP/AVP/TCP") && !transportLine.toLowerCase().includes("interleaved")) {
41924
+ this.logger.warn?.(
41925
+ `[BaichuanRtspBackchannelServer] SETUP rejected (non-TCP transport) client=${clientId}`
41926
+ );
41927
+ send(461, "Unsupported Transport");
41928
+ return;
41929
+ }
41930
+ const interleaved = parseInterleavedChannels(transportLine) ?? {
41931
+ rtp: 0,
41932
+ rtcp: 1
41933
+ };
41934
+ if (!sessionId) {
41935
+ sessionId = newSessionId();
41936
+ }
41937
+ this.sessionByClient.set(clientId, {
41938
+ sessionId,
41939
+ clientId,
41940
+ socket,
41941
+ rtpChannel: interleaved.rtp,
41942
+ rtcpChannel: interleaved.rtcp
41943
+ });
41944
+ this.logger.info?.(
41945
+ `[BaichuanRtspBackchannelServer] SETUP ok client=${clientId} session=${sessionId} interleaved=${interleaved.rtp}-${interleaved.rtcp}`
41946
+ );
41947
+ send(200, "OK", {
41948
+ Transport: `RTP/AVP/TCP;unicast;interleaved=${interleaved.rtp}-${interleaved.rtcp};mode=record`,
41949
+ Session: sessionId
41950
+ });
41951
+ return;
41952
+ }
41953
+ case "RECORD": {
41954
+ const session = sessionId ? Array.from(this.sessionByClient.values()).find(
41955
+ (s) => s.sessionId === sessionId
41956
+ ) : this.sessionByClient.get(clientId);
41957
+ if (!session) {
41958
+ this.logger.warn?.(
41959
+ `[BaichuanRtspBackchannelServer] RECORD without active session client=${clientId} requestedSession=${sessionId ?? "(none)"}`
41960
+ );
41961
+ send(454, "Session Not Found");
41962
+ return;
41963
+ }
41964
+ if (session.handler) {
41965
+ this.logger.info?.(
41966
+ `[BaichuanRtspBackchannelServer] RECORD idempotent (already recording) client=${clientId} session=${session.sessionId}`
41967
+ );
41968
+ send(200, "OK", { Session: session.sessionId });
41969
+ return;
41970
+ }
41971
+ const apiRef = this.api;
41972
+ const channelForCamera = this.channel;
41973
+ const loggerRef = this.logger;
41974
+ const deviceIdRef = this.deviceId ?? `rtsp-backchannel-${clientId}`;
41975
+ const handler = new RtspBackchannel({
41976
+ openTalkSession: () => apiRef.createDedicatedTalkSession(channelForCamera, {
41977
+ deviceId: deviceIdRef,
41978
+ logger: loggerRef
41979
+ }),
41980
+ logger: loggerRef
41981
+ });
41982
+ const recordStart = Date.now();
41983
+ this.logger.info?.(
41984
+ `[BaichuanRtspBackchannelServer] RECORD opening TalkSession client=${clientId} session=${session.sessionId} channel=${channelForCamera} deviceId="${deviceIdRef}"`
41985
+ );
41986
+ try {
41987
+ const talk = await handler.start();
41988
+ this.logger.info?.(
41989
+ `[BaichuanRtspBackchannelServer] RECORD talk session ready client=${clientId} session=${session.sessionId} blockSize=${talk.info.blockSize} fullBlockSize=${talk.info.fullBlockSize} sampleRate=${talk.info.audioConfig.sampleRate} audioType=${talk.info.audioConfig.audioType} duplex setupMs=${Date.now() - recordStart}`
41990
+ );
41991
+ } catch (e) {
41992
+ this.logger.warn?.(
41993
+ `[BaichuanRtspBackchannelServer] RECORD failed to open TalkSession client=${clientId} session=${session.sessionId} setupMs=${Date.now() - recordStart} error="${e.message}"`
41994
+ );
41995
+ send(503, "Service Unavailable");
41996
+ return;
41997
+ }
41998
+ session.handler = handler;
41999
+ send(200, "OK", { Session: session.sessionId });
42000
+ return;
42001
+ }
42002
+ case "TEARDOWN": {
42003
+ const session = this.sessionByClient.get(clientId);
42004
+ if (session) {
42005
+ this.sessionByClient.delete(clientId);
42006
+ if (session.handler) {
42007
+ const stats = session.handler.stats;
42008
+ this.logger.info?.(
42009
+ `[BaichuanRtspBackchannelServer] TEARDOWN client=${clientId} session=${session.sessionId} rtpRx=${stats.rtpPacketsReceived} rtpDropped=${stats.rtpPacketsDropped} adpcmBlocks=${stats.adpcmBlocksSent}`
42010
+ );
42011
+ void session.handler.stop();
42012
+ } else {
42013
+ this.logger.info?.(
42014
+ `[BaichuanRtspBackchannelServer] TEARDOWN client=${clientId} session=${session.sessionId} (no RECORD)`
42015
+ );
42016
+ }
42017
+ } else {
42018
+ this.logger.info?.(
42019
+ `[BaichuanRtspBackchannelServer] TEARDOWN client=${clientId} (no active session)`
42020
+ );
42021
+ }
42022
+ send(200, "OK", { Session: sessionId ?? "" });
42023
+ try {
42024
+ socket.end();
42025
+ } catch {
42026
+ }
42027
+ return;
42028
+ }
42029
+ default:
42030
+ this.logger.warn?.(
42031
+ `[BaichuanRtspBackchannelServer] unknown method ${method} from ${clientId} \u2014 replying 501`
42032
+ );
42033
+ send(501, "Not Implemented");
42034
+ }
42035
+ }
42036
+ buildSdp() {
42037
+ return `v=0\r
42038
+ o=- ${Date.now()} ${Date.now()} IN IP4 ${this.listenHost}\r
42039
+ s=Baichuan Backchannel\r
42040
+ c=IN IP4 ${this.listenHost}\r
42041
+ t=0 0\r
42042
+ a=control:*\r
42043
+ m=audio 0 RTP/AVP 0\r
42044
+ a=rtpmap:0 PCMU/8000\r
42045
+ a=sendonly\r
42046
+ a=control:audiobackchannel\r
42047
+ `;
42048
+ }
42049
+ // ─────────────────────────────────────────────────────────────────────
42050
+ // Digest auth helpers (mirror BaichuanRtspServer's behaviour so
42051
+ // operators can re-use the same credentials store across both servers).
42052
+ // ─────────────────────────────────────────────────────────────────────
42053
+ wwwAuthenticate(clientId) {
42054
+ const nonce = this.nonceFor(clientId);
42055
+ return `Digest realm="${this.authRealm}", nonce="${nonce}"`;
42056
+ }
42057
+ nonceFor(clientId) {
42058
+ const now = Date.now();
42059
+ const existing = this.nonces.get(clientId);
42060
+ if (existing && now - existing.ts < _BaichuanRtspBackchannelServer.NONCE_TTL_MS) {
42061
+ return existing.nonce;
42062
+ }
42063
+ const nonce = crypto3.randomBytes(16).toString("hex");
42064
+ this.nonces.set(clientId, { nonce, ts: now });
42065
+ return nonce;
42066
+ }
42067
+ validateDigest(header, method, uri, clientId) {
42068
+ const params = parseDigestHeader(header);
42069
+ if (!params) return false;
42070
+ if (params.realm && params.realm !== this.authRealm) return false;
42071
+ const nonce = this.nonces.get(clientId);
42072
+ if (!nonce || nonce.nonce !== params.nonce) return false;
42073
+ if (Date.now() - nonce.ts > _BaichuanRtspBackchannelServer.NONCE_TTL_MS)
42074
+ return false;
42075
+ const cred = this.authCredentials.find((c) => c.username === params.username);
42076
+ if (!cred) return false;
42077
+ const ha1 = cred.ha1 ?? (cred.password !== void 0 ? md5Hex(`${cred.username}:${this.authRealm}:${cred.password}`) : void 0);
42078
+ if (!ha1) return false;
42079
+ const ha2 = md5Hex(`${method}:${params.uri || uri}`);
42080
+ const expected = md5Hex(`${ha1}:${params.nonce}:${ha2}`);
42081
+ return expected === params.response;
42082
+ }
42083
+ };
42084
+ function newSessionId() {
42085
+ return `talk_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
42086
+ }
42087
+ function parseInterleavedChannels(transport) {
42088
+ const m = transport.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
42089
+ if (!m) return void 0;
42090
+ const rtp = parseInt(m[1] ?? "", 10);
42091
+ const rtcp = parseInt(m[2] ?? "", 10);
42092
+ if (!Number.isFinite(rtp) || !Number.isFinite(rtcp)) return void 0;
42093
+ return { rtp, rtcp };
42094
+ }
42095
+ function parseDigestHeader(header) {
42096
+ if (!/^digest\s+/i.test(header)) return null;
42097
+ const params = {};
42098
+ const re = /(\w+)\s*=\s*("([^"]*)"|([^,\s]+))/g;
42099
+ let match;
42100
+ while ((match = re.exec(header)) !== null) {
42101
+ const key = (match[1] ?? "").toLowerCase();
42102
+ const value = match[3] ?? match[4] ?? "";
42103
+ params[key] = value;
42104
+ }
42105
+ if (!params.username || !params.nonce || !params.uri || !params.response) {
42106
+ return null;
42107
+ }
42108
+ return {
42109
+ username: params.username,
42110
+ realm: params.realm ?? "",
42111
+ nonce: params.nonce,
42112
+ uri: params.uri,
42113
+ response: params.response
42114
+ };
42115
+ }
42116
+
41150
42117
  // src/emailPush/server.ts
41151
42118
  var import_node_util2 = require("util");
41152
42119
  var import_smtp_server = require("smtp-server");
@@ -41610,6 +42577,7 @@ function buildInitialStatus(config) {
41610
42577
  BaichuanHlsServer,
41611
42578
  BaichuanHttpStreamServer,
41612
42579
  BaichuanMjpegServer,
42580
+ BaichuanRtspBackchannelServer,
41613
42581
  BaichuanRtspServer,
41614
42582
  BaichuanVideoStream,
41615
42583
  BaichuanWebRTCServer,
@@ -41635,10 +42603,12 @@ function buildInitialStatus(config) {
41635
42603
  ReolinkCgiApi,
41636
42604
  ReolinkHttpClient,
41637
42605
  Rfc4571Muxer,
42606
+ RtspBackchannel,
41638
42607
  _resetEmailPushBusForTests,
41639
42608
  abilitiesHasAny,
41640
42609
  aesDecrypt,
41641
42610
  aesEncrypt,
42611
+ alawToPcm16,
41642
42612
  applyStreamPatch,
41643
42613
  applyXmlTagPatch,
41644
42614
  asLogger,
@@ -41711,6 +42681,7 @@ function buildInitialStatus(config) {
41711
42681
  discoverViaUdpDirect,
41712
42682
  emitEmailPushEvent,
41713
42683
  encodeHeader,
42684
+ encodeImaAdpcm,
41714
42685
  encodeMotionScopeBitmap,
41715
42686
  encodeMotionSensitivityListXml,
41716
42687
  encodeShelterCoord,
@@ -41753,6 +42724,7 @@ function buildInitialStatus(config) {
41753
42724
  maskUid,
41754
42725
  md5HexUpper,
41755
42726
  md5StrModern,
42727
+ mulawToPcm16,
41756
42728
  normalizeDayNightMode,
41757
42729
  normalizeOpenClose,
41758
42730
  normalizeUid,
@@ -41764,6 +42736,7 @@ function buildInitialStatus(config) {
41764
42736
  parseAdtsHeader,
41765
42737
  parseBcMedia,
41766
42738
  parseRecordingFileName,
42739
+ parseRtpPacket,
41767
42740
  parseSupportXml,
41768
42741
  patchAiDetectCfgXml,
41769
42742
  patchMotionSensitivityListXml,
@@ -41781,6 +42754,7 @@ function buildInitialStatus(config) {
41781
42754
  splitAnnexBToNals,
41782
42755
  splitH265AnnexBToNalPayloads,
41783
42756
  testChannelStreams,
42757
+ upsamplePcm16,
41784
42758
  upsertXmlTag,
41785
42759
  xmlEscape,
41786
42760
  xmlIndicatesFloodlight,