@apocaliss92/nodelink-js 0.4.34 → 0.4.36
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/{chunk-OJ5RWPGY.js → chunk-6ILAHQF5.js} +35 -4
- package/dist/chunk-6ILAHQF5.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +34 -3
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +990 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +237 -2
- package/dist/index.d.ts +242 -1
- package/dist/index.js +948 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-OJ5RWPGY.js.map +0 -1
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,
|
|
@@ -16956,7 +16963,19 @@ function computeDeviceCapabilities(params) {
|
|
|
16956
16963
|
// lightType >= 2 indicates controllable white LED / floodlight (1 = IR only).
|
|
16957
16964
|
// ledCtrl > 1 is a secondary signal that rescues firmwares like the Duo 3
|
|
16958
16965
|
// WiFi which under-report lightType (=1) despite having a real spotlight.
|
|
16959
|
-
|
|
16966
|
+
//
|
|
16967
|
+
// Doorbell exception: the Reolink desktop app's native SDK exposes
|
|
16968
|
+
// three SEPARATE ability flags — `supportFloodLight`, `supportDoorbellLight`
|
|
16969
|
+
// (with KeepOff/KeepOn sub-flags) and `supportIndicatorLight`. Doorbell-class
|
|
16970
|
+
// devices route the ring / button-area LED through `supportDoorbellLight`,
|
|
16971
|
+
// which is NOT a controllable white-light floodlight. We don't have a
|
|
16972
|
+
// documented bit-level mapping for ledCtrl, so we use the narrowest
|
|
16973
|
+
// rule that matches Reolink's own taxonomy: when the firmware is
|
|
16974
|
+
// categorical with lightType=0 AND the device identifies itself as a
|
|
16975
|
+
// doorbell (doorbellVersion > 0), trust lightType=0 and ignore the
|
|
16976
|
+
// ledCtrl bitmask. Verified against UID 9527000ICL1T1MDS (lightType=0,
|
|
16977
|
+
// ledCtrl=3073, doorbellVersion=31, no spotlight hardware).
|
|
16978
|
+
hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 || hasFloodlightFromLedCtrl && !(isDoorbellFromSupport && lightType === 0) : hasFloodlightFromAbilities || hasFloodlightFromLedCtrl,
|
|
16960
16979
|
hasPir: hasPirFromAbilities || hasPirFromSupport,
|
|
16961
16980
|
isDoorbell,
|
|
16962
16981
|
hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
|
|
@@ -27564,8 +27583,18 @@ ${xml}`
|
|
|
27564
27583
|
* This is more reliable than autoPt in SupportInfo which can be a false positive
|
|
27565
27584
|
* (e.g., NVR channels report autoPt=1 but don't actually support autotracking).
|
|
27566
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
|
+
*
|
|
27567
27596
|
* @param channel - Channel number (0-based)
|
|
27568
|
-
* @param options - Optional timeout
|
|
27597
|
+
* @param options - Optional timeout and doorbell context hint
|
|
27569
27598
|
* @returns true if autotracking is supported, false otherwise
|
|
27570
27599
|
*/
|
|
27571
27600
|
async probeAutotrackingSupport(channel, options) {
|
|
@@ -27575,6 +27604,14 @@ ${xml}`
|
|
|
27575
27604
|
const xml = await this.sendXml({ cmdId: 299, channel: ch, timeoutMs });
|
|
27576
27605
|
const smartTrackModeRaw = getXmlText(xml, "smartTrackMode");
|
|
27577
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
|
+
}
|
|
27578
27615
|
return smartTrackMode > 0;
|
|
27579
27616
|
} catch {
|
|
27580
27617
|
return false;
|
|
@@ -27667,7 +27704,8 @@ ${xml}`
|
|
|
27667
27704
|
const features = this.parseFeaturesFromSupport(support);
|
|
27668
27705
|
const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
|
|
27669
27706
|
const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
|
|
27670
|
-
timeoutMs: 1500
|
|
27707
|
+
timeoutMs: 1500,
|
|
27708
|
+
isDoorbell: capabilities.isDoorbell === true
|
|
27671
27709
|
});
|
|
27672
27710
|
capabilities.hasAutotracking = autotrackingProbed;
|
|
27673
27711
|
let presets;
|
|
@@ -31001,10 +31039,10 @@ ${scheduleItems}
|
|
|
31001
31039
|
const os2 = await import("os");
|
|
31002
31040
|
const path7 = await import("path");
|
|
31003
31041
|
const fs7 = await import("fs/promises");
|
|
31004
|
-
const
|
|
31042
|
+
const crypto4 = await import("crypto");
|
|
31005
31043
|
const tempDir = path7.join(
|
|
31006
31044
|
os2.tmpdir(),
|
|
31007
|
-
`reolink-hls-${
|
|
31045
|
+
`reolink-hls-${crypto4.randomBytes(8).toString("hex")}`
|
|
31008
31046
|
);
|
|
31009
31047
|
await fs7.mkdir(tempDir, { recursive: true });
|
|
31010
31048
|
const playlistPath = path7.join(tempDir, "playlist.m3u8");
|
|
@@ -41135,6 +41173,946 @@ function base64DecodeToBytes(b64) {
|
|
|
41135
41173
|
return out.subarray(0, outIdx);
|
|
41136
41174
|
}
|
|
41137
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 = require("alawmulaw");
|
|
41359
|
+
function mulawToPcm16(bytes) {
|
|
41360
|
+
if (bytes.length === 0) return new Int16Array(0);
|
|
41361
|
+
return import_alawmulaw.mulaw.decode(bytes);
|
|
41362
|
+
}
|
|
41363
|
+
function alawToPcm16(bytes) {
|
|
41364
|
+
if (bytes.length === 0) return new Int16Array(0);
|
|
41365
|
+
return import_alawmulaw.alaw.decode(bytes);
|
|
41366
|
+
}
|
|
41367
|
+
|
|
41368
|
+
// src/reolink/baichuan/utils/audioResample.ts
|
|
41369
|
+
function clamp162(x) {
|
|
41370
|
+
if (x > 32767) return 32767;
|
|
41371
|
+
if (x < -32768) return -32768;
|
|
41372
|
+
return x | 0;
|
|
41373
|
+
}
|
|
41374
|
+
function upsamplePcm16(src, factor) {
|
|
41375
|
+
if (!Number.isInteger(factor) || factor < 1) {
|
|
41376
|
+
throw new RangeError(
|
|
41377
|
+
`upsamplePcm16: factor must be a positive integer, got ${factor}`
|
|
41378
|
+
);
|
|
41379
|
+
}
|
|
41380
|
+
if (src.length === 0) return new Int16Array(0);
|
|
41381
|
+
if (factor === 1) return Int16Array.from(src);
|
|
41382
|
+
const out = new Int16Array(src.length * factor);
|
|
41383
|
+
const last = src.length - 1;
|
|
41384
|
+
for (let i = 0; i < last; i++) {
|
|
41385
|
+
const a = src[i];
|
|
41386
|
+
const b = src[i + 1];
|
|
41387
|
+
const base = i * factor;
|
|
41388
|
+
for (let k = 0; k < factor; k++) {
|
|
41389
|
+
const v = a + Math.round((b - a) * k / factor);
|
|
41390
|
+
out[base + k] = clamp162(v);
|
|
41391
|
+
}
|
|
41392
|
+
}
|
|
41393
|
+
const tail = src[last] | 0;
|
|
41394
|
+
for (let k = 0; k < factor; k++) {
|
|
41395
|
+
out[last * factor + k] = tail;
|
|
41396
|
+
}
|
|
41397
|
+
return out;
|
|
41398
|
+
}
|
|
41399
|
+
|
|
41400
|
+
// src/baichuan/stream/RtspBackchannel.ts
|
|
41401
|
+
var RTP_PT_PCMA = 8;
|
|
41402
|
+
var RTP_FIXED_HEADER_BYTES = 12;
|
|
41403
|
+
var G711_SOURCE_RATE_HZ = 8e3;
|
|
41404
|
+
var TALK_TARGET_RATE_HZ = 16e3;
|
|
41405
|
+
function parseRtpPacket(packet) {
|
|
41406
|
+
if (packet.length < RTP_FIXED_HEADER_BYTES) return null;
|
|
41407
|
+
const b0 = packet.readUInt8(0);
|
|
41408
|
+
const version = b0 >> 6;
|
|
41409
|
+
if (version !== 2) return null;
|
|
41410
|
+
const padding = (b0 & 32) !== 0;
|
|
41411
|
+
const extension = (b0 & 16) !== 0;
|
|
41412
|
+
const csrcCount = b0 & 15;
|
|
41413
|
+
const b1 = packet.readUInt8(1);
|
|
41414
|
+
const payloadType = b1 & 127;
|
|
41415
|
+
let offset = RTP_FIXED_HEADER_BYTES + csrcCount * 4;
|
|
41416
|
+
if (offset > packet.length) return null;
|
|
41417
|
+
if (extension) {
|
|
41418
|
+
if (offset + 4 > packet.length) return null;
|
|
41419
|
+
const extensionLengthWords = packet.readUInt16BE(offset + 2);
|
|
41420
|
+
offset += 4 + extensionLengthWords * 4;
|
|
41421
|
+
if (offset > packet.length) return null;
|
|
41422
|
+
}
|
|
41423
|
+
let end = packet.length;
|
|
41424
|
+
if (padding) {
|
|
41425
|
+
if (end - offset < 1) return null;
|
|
41426
|
+
const padLength = packet.readUInt8(end - 1);
|
|
41427
|
+
if (padLength < 1 || padLength > end - offset) return null;
|
|
41428
|
+
end -= padLength;
|
|
41429
|
+
}
|
|
41430
|
+
if (end <= offset) return null;
|
|
41431
|
+
return {
|
|
41432
|
+
payloadType,
|
|
41433
|
+
payload: packet.subarray(offset, end)
|
|
41434
|
+
};
|
|
41435
|
+
}
|
|
41436
|
+
var RtspBackchannel = class _RtspBackchannel {
|
|
41437
|
+
constructor(opts) {
|
|
41438
|
+
this.opts = opts;
|
|
41439
|
+
}
|
|
41440
|
+
session = void 0;
|
|
41441
|
+
pcmTail = new Int16Array(0);
|
|
41442
|
+
// residual samples below one full ADPCM block
|
|
41443
|
+
pcmBacklogBytes = 0;
|
|
41444
|
+
pumping = false;
|
|
41445
|
+
active = false;
|
|
41446
|
+
starting = void 0;
|
|
41447
|
+
stopping = void 0;
|
|
41448
|
+
lastBacklogClampLogMs = 0;
|
|
41449
|
+
rtpPacketsReceived = 0;
|
|
41450
|
+
rtpPacketsDropped = 0;
|
|
41451
|
+
adpcmBlocksSent = 0;
|
|
41452
|
+
/** Lazily-set on the first decoded RTP packet so we log the negotiated codec exactly once. */
|
|
41453
|
+
observedCodec = void 0;
|
|
41454
|
+
/** Sampled every `STATS_LOG_INTERVAL_MS` while audio flows. */
|
|
41455
|
+
lastStatsLogMs = 0;
|
|
41456
|
+
lastStatsAdpcmBlocks = 0;
|
|
41457
|
+
lastStatsRtpPackets = 0;
|
|
41458
|
+
startedAtMs = 0;
|
|
41459
|
+
static STATS_LOG_INTERVAL_MS = 5e3;
|
|
41460
|
+
get isActive() {
|
|
41461
|
+
return this.active;
|
|
41462
|
+
}
|
|
41463
|
+
get stats() {
|
|
41464
|
+
return {
|
|
41465
|
+
rtpPacketsReceived: this.rtpPacketsReceived,
|
|
41466
|
+
rtpPacketsDropped: this.rtpPacketsDropped,
|
|
41467
|
+
adpcmBlocksSent: this.adpcmBlocksSent
|
|
41468
|
+
};
|
|
41469
|
+
}
|
|
41470
|
+
/** Open the underlying TalkSession. Safe to call concurrently; resolves to the same session. */
|
|
41471
|
+
async start() {
|
|
41472
|
+
if (this.session) return this.session;
|
|
41473
|
+
if (!this.starting) {
|
|
41474
|
+
const openStart = Date.now();
|
|
41475
|
+
this.opts.logger?.log?.(
|
|
41476
|
+
`[RtspBackchannel] opening TalkSession on camera\u2026`
|
|
41477
|
+
);
|
|
41478
|
+
this.starting = (async () => {
|
|
41479
|
+
const s = await this.opts.openTalkSession();
|
|
41480
|
+
this.session = s;
|
|
41481
|
+
this.active = true;
|
|
41482
|
+
this.startedAtMs = Date.now();
|
|
41483
|
+
this.lastStatsLogMs = this.startedAtMs;
|
|
41484
|
+
this.opts.logger?.log?.(
|
|
41485
|
+
`[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}`
|
|
41486
|
+
);
|
|
41487
|
+
return s;
|
|
41488
|
+
})().catch((e) => {
|
|
41489
|
+
this.starting = void 0;
|
|
41490
|
+
this.opts.logger?.log?.(
|
|
41491
|
+
`[RtspBackchannel] TalkSession open FAILED openMs=${Date.now() - openStart} error="${e.message}"`
|
|
41492
|
+
);
|
|
41493
|
+
throw e;
|
|
41494
|
+
});
|
|
41495
|
+
}
|
|
41496
|
+
return this.starting;
|
|
41497
|
+
}
|
|
41498
|
+
/**
|
|
41499
|
+
* Feed one inbound RTP packet (as carried in TCP-interleaved framing or UDP
|
|
41500
|
+
* datagram). Discards malformed packets and packets received before `start()`.
|
|
41501
|
+
* The actual audio dispatch to the TalkSession is awaited internally; callers
|
|
41502
|
+
* may fire-and-forget.
|
|
41503
|
+
*/
|
|
41504
|
+
feedRtp(packet) {
|
|
41505
|
+
this.rtpPacketsReceived++;
|
|
41506
|
+
if (!this.active || !this.session) {
|
|
41507
|
+
this.rtpPacketsDropped++;
|
|
41508
|
+
if (this.rtpPacketsDropped === 1) {
|
|
41509
|
+
this.opts.logger?.log?.(
|
|
41510
|
+
`[RtspBackchannel] dropping RTP packets \u2014 session not active yet (received ${packet.length}B before start())`
|
|
41511
|
+
);
|
|
41512
|
+
}
|
|
41513
|
+
return;
|
|
41514
|
+
}
|
|
41515
|
+
const parsed = parseRtpPacket(packet);
|
|
41516
|
+
if (!parsed) {
|
|
41517
|
+
this.rtpPacketsDropped++;
|
|
41518
|
+
this.opts.logger?.log?.(
|
|
41519
|
+
`[RtspBackchannel] malformed RTP packet \u2014 dropping (len=${packet.length} firstByte=0x${packet.length ? (packet[0] ?? 0).toString(16) : "??"})`
|
|
41520
|
+
);
|
|
41521
|
+
return;
|
|
41522
|
+
}
|
|
41523
|
+
const codec = this.opts.forceCodec ?? (parsed.payloadType === RTP_PT_PCMA ? "pcma" : "pcmu");
|
|
41524
|
+
if (this.observedCodec === void 0) {
|
|
41525
|
+
this.observedCodec = codec;
|
|
41526
|
+
const firstPacketMs = Date.now() - this.startedAtMs;
|
|
41527
|
+
this.opts.logger?.log?.(
|
|
41528
|
+
`[RtspBackchannel] first RTP packet \u2014 codec=${codec} payloadType=${parsed.payloadType} payload=${parsed.payload.length}B firstByteMs=${firstPacketMs}` + (this.opts.forceCodec ? " (forced by config)" : "")
|
|
41529
|
+
);
|
|
41530
|
+
} else if (codec !== this.observedCodec) {
|
|
41531
|
+
this.opts.logger?.log?.(
|
|
41532
|
+
`[RtspBackchannel] codec switched mid-stream ${this.observedCodec} \u2192 ${codec} (payloadType=${parsed.payloadType})`
|
|
41533
|
+
);
|
|
41534
|
+
this.observedCodec = codec;
|
|
41535
|
+
}
|
|
41536
|
+
const decoded = codec === "pcma" ? alawToPcm16(parsed.payload) : mulawToPcm16(parsed.payload);
|
|
41537
|
+
if (decoded.length === 0) return;
|
|
41538
|
+
const upsampled = upsamplePcm16(decoded, TALK_TARGET_RATE_HZ / G711_SOURCE_RATE_HZ);
|
|
41539
|
+
this.enqueuePcm(upsampled);
|
|
41540
|
+
this.maybeLogStats();
|
|
41541
|
+
}
|
|
41542
|
+
/**
|
|
41543
|
+
* Throttled progress log (~every 5s while audio is flowing) so operators
|
|
41544
|
+
* can confirm the pipeline is making progress without per-packet noise.
|
|
41545
|
+
*/
|
|
41546
|
+
maybeLogStats() {
|
|
41547
|
+
const now = Date.now();
|
|
41548
|
+
if (now - this.lastStatsLogMs < _RtspBackchannel.STATS_LOG_INTERVAL_MS) return;
|
|
41549
|
+
const elapsedSec = (now - this.lastStatsLogMs) / 1e3;
|
|
41550
|
+
const rtpDelta = this.rtpPacketsReceived - this.lastStatsRtpPackets;
|
|
41551
|
+
const adpcmDelta = this.adpcmBlocksSent - this.lastStatsAdpcmBlocks;
|
|
41552
|
+
this.lastStatsLogMs = now;
|
|
41553
|
+
this.lastStatsRtpPackets = this.rtpPacketsReceived;
|
|
41554
|
+
this.lastStatsAdpcmBlocks = this.adpcmBlocksSent;
|
|
41555
|
+
this.opts.logger?.log?.(
|
|
41556
|
+
`[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`
|
|
41557
|
+
);
|
|
41558
|
+
}
|
|
41559
|
+
/** Flush remaining audio and stop the talk session. Idempotent. */
|
|
41560
|
+
async stop() {
|
|
41561
|
+
if (this.stopping) return this.stopping;
|
|
41562
|
+
this.active = false;
|
|
41563
|
+
this.stopping = (async () => {
|
|
41564
|
+
const s = this.session;
|
|
41565
|
+
this.session = void 0;
|
|
41566
|
+
this.pcmTail = new Int16Array(0);
|
|
41567
|
+
this.pcmBacklogBytes = 0;
|
|
41568
|
+
const durationMs = this.startedAtMs ? Date.now() - this.startedAtMs : 0;
|
|
41569
|
+
this.opts.logger?.log?.(
|
|
41570
|
+
`[RtspBackchannel] closing TalkSession durationMs=${durationMs} rtpRx=${this.rtpPacketsReceived} rtpDropped=${this.rtpPacketsDropped} adpcmBlocks=${this.adpcmBlocksSent} pcmBacklogResidual=${this.pcmBacklogBytes}B codec=${this.observedCodec ?? "(none)"}`
|
|
41571
|
+
);
|
|
41572
|
+
if (s) {
|
|
41573
|
+
try {
|
|
41574
|
+
await s.stop();
|
|
41575
|
+
} catch (e) {
|
|
41576
|
+
this.opts.logger?.log?.(
|
|
41577
|
+
`[RtspBackchannel] TalkSession stop error: ${e.message}`
|
|
41578
|
+
);
|
|
41579
|
+
}
|
|
41580
|
+
}
|
|
41581
|
+
})();
|
|
41582
|
+
return this.stopping;
|
|
41583
|
+
}
|
|
41584
|
+
enqueuePcm(chunk) {
|
|
41585
|
+
if (chunk.length === 0) return;
|
|
41586
|
+
const tailLen = this.pcmTail.length;
|
|
41587
|
+
const merged = new Int16Array(tailLen + chunk.length);
|
|
41588
|
+
merged.set(this.pcmTail, 0);
|
|
41589
|
+
merged.set(chunk, tailLen);
|
|
41590
|
+
this.pcmBacklogBytes = merged.length * 2;
|
|
41591
|
+
const maxBytes = Math.max(2, this.opts.maxPcmBacklogBytes ?? 96e3);
|
|
41592
|
+
if (this.pcmBacklogBytes > maxBytes) {
|
|
41593
|
+
const keepSamples = Math.floor(maxBytes / 2);
|
|
41594
|
+
const dropSamples = merged.length - keepSamples;
|
|
41595
|
+
this.pcmTail = merged.subarray(merged.length - keepSamples);
|
|
41596
|
+
this.pcmBacklogBytes = this.pcmTail.length * 2;
|
|
41597
|
+
const now = Date.now();
|
|
41598
|
+
if (now - this.lastBacklogClampLogMs > 2e3) {
|
|
41599
|
+
this.lastBacklogClampLogMs = now;
|
|
41600
|
+
this.opts.logger?.log?.(
|
|
41601
|
+
`[RtspBackchannel] PCM backlog clamped \u2014 dropped ${dropSamples} samples, kept ${keepSamples}`
|
|
41602
|
+
);
|
|
41603
|
+
}
|
|
41604
|
+
} else {
|
|
41605
|
+
this.pcmTail = merged;
|
|
41606
|
+
}
|
|
41607
|
+
void this.pumpAdpcm();
|
|
41608
|
+
}
|
|
41609
|
+
async pumpAdpcm() {
|
|
41610
|
+
if (this.pumping) return;
|
|
41611
|
+
const session = this.session;
|
|
41612
|
+
if (!session) return;
|
|
41613
|
+
this.pumping = true;
|
|
41614
|
+
try {
|
|
41615
|
+
const samplesPerBlock = session.info.blockSize * 2 + 1;
|
|
41616
|
+
const blockSizeBytes = session.info.blockSize;
|
|
41617
|
+
while (this.active && this.pcmTail.length >= samplesPerBlock) {
|
|
41618
|
+
const consumeSamples = Math.floor(this.pcmTail.length / samplesPerBlock) * samplesPerBlock;
|
|
41619
|
+
const head = this.pcmTail.subarray(0, consumeSamples);
|
|
41620
|
+
const adpcm = encodeImaAdpcm(head, blockSizeBytes);
|
|
41621
|
+
const blocksInBatch = consumeSamples / samplesPerBlock;
|
|
41622
|
+
this.pcmTail = this.pcmTail.slice(consumeSamples);
|
|
41623
|
+
this.pcmBacklogBytes = this.pcmTail.length * 2;
|
|
41624
|
+
try {
|
|
41625
|
+
await session.sendAudio(adpcm);
|
|
41626
|
+
this.adpcmBlocksSent += blocksInBatch;
|
|
41627
|
+
} catch (e) {
|
|
41628
|
+
this.opts.logger?.log?.(
|
|
41629
|
+
`[RtspBackchannel] sendAudio FAILED \u2014 disabling pipeline blocksInBatch=${blocksInBatch} adpcmBytes=${adpcm.length} pcmBacklogBefore=${this.pcmBacklogBytes + adpcm.length}B error="${e.message}"`
|
|
41630
|
+
);
|
|
41631
|
+
this.pcmTail = new Int16Array(0);
|
|
41632
|
+
this.pcmBacklogBytes = 0;
|
|
41633
|
+
this.active = false;
|
|
41634
|
+
break;
|
|
41635
|
+
}
|
|
41636
|
+
}
|
|
41637
|
+
} finally {
|
|
41638
|
+
this.pumping = false;
|
|
41639
|
+
}
|
|
41640
|
+
}
|
|
41641
|
+
};
|
|
41642
|
+
|
|
41643
|
+
// src/baichuan/stream/BaichuanRtspBackchannelServer.ts
|
|
41644
|
+
var import_node_events15 = require("events");
|
|
41645
|
+
var net6 = __toESM(require("net"), 1);
|
|
41646
|
+
var crypto3 = __toESM(require("crypto"), 1);
|
|
41647
|
+
var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
|
|
41648
|
+
var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
|
|
41649
|
+
api;
|
|
41650
|
+
channel;
|
|
41651
|
+
listenHost;
|
|
41652
|
+
listenPort;
|
|
41653
|
+
path;
|
|
41654
|
+
logger;
|
|
41655
|
+
authCredentials;
|
|
41656
|
+
requireAuth;
|
|
41657
|
+
authRealm;
|
|
41658
|
+
deviceId;
|
|
41659
|
+
server = void 0;
|
|
41660
|
+
/** Active backchannel sessions keyed by their per-client unique id. */
|
|
41661
|
+
sessionByClient = /* @__PURE__ */ new Map();
|
|
41662
|
+
nonces = /* @__PURE__ */ new Map();
|
|
41663
|
+
static NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
41664
|
+
constructor(options) {
|
|
41665
|
+
super();
|
|
41666
|
+
this.api = options.api;
|
|
41667
|
+
this.channel = options.channel;
|
|
41668
|
+
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
41669
|
+
this.listenPort = options.listenPort ?? 8555;
|
|
41670
|
+
this.path = options.path ?? "/talk";
|
|
41671
|
+
this.logger = options.logger ?? console;
|
|
41672
|
+
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
41673
|
+
username: c.username,
|
|
41674
|
+
...c.password !== void 0 ? { password: c.password } : {},
|
|
41675
|
+
...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
|
|
41676
|
+
}));
|
|
41677
|
+
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
41678
|
+
this.authRealm = options.authRealm ?? "BaichuanRtspBackchannelServer";
|
|
41679
|
+
this.deviceId = options.deviceId;
|
|
41680
|
+
}
|
|
41681
|
+
get listening() {
|
|
41682
|
+
return this.server !== void 0 && this.server.listening;
|
|
41683
|
+
}
|
|
41684
|
+
async start() {
|
|
41685
|
+
if (this.server) return;
|
|
41686
|
+
await new Promise((resolve, reject) => {
|
|
41687
|
+
const server = net6.createServer((socket) => this.handleConnection(socket));
|
|
41688
|
+
const onError = (err) => {
|
|
41689
|
+
server.removeListener("error", onError);
|
|
41690
|
+
reject(err);
|
|
41691
|
+
};
|
|
41692
|
+
server.once("error", onError);
|
|
41693
|
+
server.listen(this.listenPort, this.listenHost, () => {
|
|
41694
|
+
server.removeListener("error", onError);
|
|
41695
|
+
this.server = server;
|
|
41696
|
+
this.logger.info?.(
|
|
41697
|
+
`[BaichuanRtspBackchannelServer] listening on ${this.listenHost}:${this.listenPort} path=${this.path}`
|
|
41698
|
+
);
|
|
41699
|
+
resolve();
|
|
41700
|
+
});
|
|
41701
|
+
});
|
|
41702
|
+
}
|
|
41703
|
+
async stop() {
|
|
41704
|
+
const server = this.server;
|
|
41705
|
+
this.server = void 0;
|
|
41706
|
+
for (const session of this.sessionByClient.values()) {
|
|
41707
|
+
this.sessionByClient.delete(session.clientId);
|
|
41708
|
+
if (session.handler) {
|
|
41709
|
+
void session.handler.stop();
|
|
41710
|
+
}
|
|
41711
|
+
try {
|
|
41712
|
+
session.socket.destroy();
|
|
41713
|
+
} catch {
|
|
41714
|
+
}
|
|
41715
|
+
}
|
|
41716
|
+
if (!server) return;
|
|
41717
|
+
await new Promise((resolve) => {
|
|
41718
|
+
server.close(() => resolve());
|
|
41719
|
+
});
|
|
41720
|
+
}
|
|
41721
|
+
handleConnection(socket) {
|
|
41722
|
+
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
41723
|
+
const connectedAt = Date.now();
|
|
41724
|
+
let buffer = Buffer.alloc(0);
|
|
41725
|
+
this.logger.info?.(
|
|
41726
|
+
`[BaichuanRtspBackchannelServer] client connected client=${clientId} path=${this.path}`
|
|
41727
|
+
);
|
|
41728
|
+
this.emit("client", clientId);
|
|
41729
|
+
const cleanup = () => {
|
|
41730
|
+
const session = this.sessionByClient.get(clientId);
|
|
41731
|
+
const durationMs = Date.now() - connectedAt;
|
|
41732
|
+
if (session) {
|
|
41733
|
+
this.sessionByClient.delete(clientId);
|
|
41734
|
+
if (session.handler) {
|
|
41735
|
+
const stats = session.handler.stats;
|
|
41736
|
+
this.logger.info?.(
|
|
41737
|
+
`[BaichuanRtspBackchannelServer] client disconnected client=${clientId} session=${session.sessionId} duration=${durationMs}ms rtpRx=${stats.rtpPacketsReceived} rtpDropped=${stats.rtpPacketsDropped} adpcmBlocks=${stats.adpcmBlocksSent}`
|
|
41738
|
+
);
|
|
41739
|
+
void session.handler.stop();
|
|
41740
|
+
} else {
|
|
41741
|
+
this.logger.info?.(
|
|
41742
|
+
`[BaichuanRtspBackchannelServer] client disconnected client=${clientId} session=${session.sessionId} duration=${durationMs}ms (no RECORD)`
|
|
41743
|
+
);
|
|
41744
|
+
}
|
|
41745
|
+
} else {
|
|
41746
|
+
this.logger.info?.(
|
|
41747
|
+
`[BaichuanRtspBackchannelServer] client disconnected client=${clientId} duration=${durationMs}ms (no SETUP)`
|
|
41748
|
+
);
|
|
41749
|
+
}
|
|
41750
|
+
this.nonces.delete(clientId);
|
|
41751
|
+
this.emit("clientDisconnected", clientId);
|
|
41752
|
+
};
|
|
41753
|
+
socket.on("close", cleanup);
|
|
41754
|
+
socket.on("error", (err) => {
|
|
41755
|
+
this.logger.warn?.(
|
|
41756
|
+
`[BaichuanRtspBackchannelServer] socket error client=${clientId}: ${err.message}`
|
|
41757
|
+
);
|
|
41758
|
+
cleanup();
|
|
41759
|
+
});
|
|
41760
|
+
socket.on("data", (data) => {
|
|
41761
|
+
buffer = Buffer.concat([buffer, data]);
|
|
41762
|
+
this.drainBuffer(socket, clientId, (b) => {
|
|
41763
|
+
buffer = b;
|
|
41764
|
+
}, () => buffer);
|
|
41765
|
+
});
|
|
41766
|
+
}
|
|
41767
|
+
/**
|
|
41768
|
+
* Drain pending bytes from the per-client buffer. Each iteration first
|
|
41769
|
+
* peels off any TCP-interleaved `$` frames at the head (routing them to
|
|
41770
|
+
* the session's RtspBackchannel handler when their channel matches) and
|
|
41771
|
+
* then attempts to parse a complete RTSP request.
|
|
41772
|
+
*
|
|
41773
|
+
* Implemented as a thin loop so the parser can yield back to the event
|
|
41774
|
+
* loop when the buffer is partial — no async work between frames keeps
|
|
41775
|
+
* the data path tight.
|
|
41776
|
+
*/
|
|
41777
|
+
drainBuffer(socket, clientId, setBuffer, getBuffer) {
|
|
41778
|
+
const tryDrainInterleaved = () => {
|
|
41779
|
+
let consumed = false;
|
|
41780
|
+
while (true) {
|
|
41781
|
+
const buf = getBuffer();
|
|
41782
|
+
if (buf.length === 0 || buf[0] !== 36) return consumed;
|
|
41783
|
+
if (buf.length < 4) return consumed;
|
|
41784
|
+
const channel = buf[1] ?? 0;
|
|
41785
|
+
const len = buf.readUInt16BE(2);
|
|
41786
|
+
if (buf.length < 4 + len) return consumed;
|
|
41787
|
+
const rtpPacket = buf.subarray(4, 4 + len);
|
|
41788
|
+
setBuffer(buf.subarray(4 + len));
|
|
41789
|
+
consumed = true;
|
|
41790
|
+
const session = this.sessionByClient.get(clientId);
|
|
41791
|
+
if (session && session.rtpChannel === channel && session.handler) {
|
|
41792
|
+
session.handler.feedRtp(Buffer.from(rtpPacket));
|
|
41793
|
+
}
|
|
41794
|
+
}
|
|
41795
|
+
};
|
|
41796
|
+
void (async () => {
|
|
41797
|
+
while (true) {
|
|
41798
|
+
tryDrainInterleaved();
|
|
41799
|
+
const buf = getBuffer();
|
|
41800
|
+
if (buf.length === 0) return;
|
|
41801
|
+
if (buf[0] === 36) return;
|
|
41802
|
+
const headerEnd = buf.indexOf("\r\n\r\n");
|
|
41803
|
+
if (headerEnd < 0) return;
|
|
41804
|
+
const requestText = buf.subarray(0, headerEnd).toString("utf8");
|
|
41805
|
+
setBuffer(buf.subarray(headerEnd + 4));
|
|
41806
|
+
try {
|
|
41807
|
+
await this.handleRequest(socket, clientId, requestText);
|
|
41808
|
+
} catch (e) {
|
|
41809
|
+
this.logger.warn?.(
|
|
41810
|
+
`[BaichuanRtspBackchannelServer] handleRequest failed for ${clientId}: ${e.message}`
|
|
41811
|
+
);
|
|
41812
|
+
try {
|
|
41813
|
+
socket.destroy();
|
|
41814
|
+
} catch {
|
|
41815
|
+
}
|
|
41816
|
+
return;
|
|
41817
|
+
}
|
|
41818
|
+
}
|
|
41819
|
+
})();
|
|
41820
|
+
}
|
|
41821
|
+
async handleRequest(socket, clientId, requestText) {
|
|
41822
|
+
const lines = requestText.split("\r\n");
|
|
41823
|
+
const head = lines[0]?.split(" ") ?? [];
|
|
41824
|
+
if (head.length < 3) {
|
|
41825
|
+
this.logger.warn?.(
|
|
41826
|
+
`[BaichuanRtspBackchannelServer] malformed request from ${clientId}: "${(lines[0] ?? "").slice(0, 120)}"`
|
|
41827
|
+
);
|
|
41828
|
+
return;
|
|
41829
|
+
}
|
|
41830
|
+
const method = head[0] ?? "";
|
|
41831
|
+
const url = head[1] ?? "";
|
|
41832
|
+
const protocol = head[2] ?? "RTSP/1.0";
|
|
41833
|
+
const cseqMatch = requestText.match(/CSeq:\s*(\d+)/i);
|
|
41834
|
+
const cseq = cseqMatch ? parseInt(cseqMatch[1] ?? "0", 10) : 0;
|
|
41835
|
+
const sessionMatch = requestText.match(/Session:\s*([^;\r\n]+)/i);
|
|
41836
|
+
let sessionId = sessionMatch?.[1]?.trim();
|
|
41837
|
+
const userAgent = requestText.match(/User-Agent:\s*([^\r\n]+)/i)?.[1]?.trim();
|
|
41838
|
+
this.logger.info?.(
|
|
41839
|
+
`[BaichuanRtspBackchannelServer] >> ${method} ${url} client=${clientId} cseq=${cseq}` + (sessionId ? ` session=${sessionId}` : "") + (userAgent ? ` ua="${userAgent}"` : "")
|
|
41840
|
+
);
|
|
41841
|
+
const send = (status, reason, headers = {}, body) => {
|
|
41842
|
+
let r = `${protocol} ${status} ${reason}\r
|
|
41843
|
+
CSeq: ${cseq}\r
|
|
41844
|
+
`;
|
|
41845
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
41846
|
+
r += `${k}: ${v}\r
|
|
41847
|
+
`;
|
|
41848
|
+
}
|
|
41849
|
+
if (body) {
|
|
41850
|
+
const buf = Buffer.from(body, "utf8");
|
|
41851
|
+
r += `Content-Length: ${buf.length}\r
|
|
41852
|
+
\r
|
|
41853
|
+
`;
|
|
41854
|
+
socket.write(r);
|
|
41855
|
+
socket.write(buf);
|
|
41856
|
+
} else {
|
|
41857
|
+
r += "\r\n";
|
|
41858
|
+
socket.write(r);
|
|
41859
|
+
}
|
|
41860
|
+
const headerSummary = Object.entries(headers).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
41861
|
+
this.logger.info?.(
|
|
41862
|
+
`[BaichuanRtspBackchannelServer] << ${status} ${reason} client=${clientId} cseq=${cseq}` + (headerSummary ? ` [${headerSummary}]` : "") + (body ? ` body=${body.length}B` : "")
|
|
41863
|
+
);
|
|
41864
|
+
};
|
|
41865
|
+
if (this.requireAuth && method !== "OPTIONS") {
|
|
41866
|
+
const authHeader = requestText.match(/Authorization:\s*([^\r\n]+)/i)?.[1] ?? "";
|
|
41867
|
+
if (!authHeader) {
|
|
41868
|
+
this.logger.info?.(
|
|
41869
|
+
`[BaichuanRtspBackchannelServer] auth challenge issued client=${clientId} method=${method}`
|
|
41870
|
+
);
|
|
41871
|
+
send(401, "Unauthorized", {
|
|
41872
|
+
"WWW-Authenticate": this.wwwAuthenticate(clientId)
|
|
41873
|
+
});
|
|
41874
|
+
return;
|
|
41875
|
+
}
|
|
41876
|
+
if (!this.validateDigest(authHeader, method, url, clientId)) {
|
|
41877
|
+
this.logger.warn?.(
|
|
41878
|
+
`[BaichuanRtspBackchannelServer] auth rejected client=${clientId} method=${method}`
|
|
41879
|
+
);
|
|
41880
|
+
this.nonces.delete(clientId);
|
|
41881
|
+
send(401, "Unauthorized", {
|
|
41882
|
+
"WWW-Authenticate": this.wwwAuthenticate(clientId)
|
|
41883
|
+
});
|
|
41884
|
+
return;
|
|
41885
|
+
}
|
|
41886
|
+
}
|
|
41887
|
+
switch (method) {
|
|
41888
|
+
case "OPTIONS":
|
|
41889
|
+
send(200, "OK", {
|
|
41890
|
+
Public: "OPTIONS, DESCRIBE, SETUP, RECORD, TEARDOWN"
|
|
41891
|
+
});
|
|
41892
|
+
return;
|
|
41893
|
+
case "DESCRIBE": {
|
|
41894
|
+
const sdp = this.buildSdp();
|
|
41895
|
+
this.logger.debug?.(
|
|
41896
|
+
`[BaichuanRtspBackchannelServer] DESCRIBE sdp for ${clientId}:
|
|
41897
|
+
${sdp.trimEnd()}`
|
|
41898
|
+
);
|
|
41899
|
+
send(
|
|
41900
|
+
200,
|
|
41901
|
+
"OK",
|
|
41902
|
+
{
|
|
41903
|
+
"Content-Type": "application/sdp",
|
|
41904
|
+
"Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
|
|
41905
|
+
},
|
|
41906
|
+
sdp
|
|
41907
|
+
);
|
|
41908
|
+
return;
|
|
41909
|
+
}
|
|
41910
|
+
case "SETUP": {
|
|
41911
|
+
if (!url.includes("audiobackchannel")) {
|
|
41912
|
+
this.logger.warn?.(
|
|
41913
|
+
`[BaichuanRtspBackchannelServer] SETUP rejected (unknown track) client=${clientId} url=${url}`
|
|
41914
|
+
);
|
|
41915
|
+
send(404, "Not Found");
|
|
41916
|
+
return;
|
|
41917
|
+
}
|
|
41918
|
+
const transportLine = requestText.match(/Transport:\s*([^\r\n]+)/i)?.[1] ?? "";
|
|
41919
|
+
this.logger.info?.(
|
|
41920
|
+
`[BaichuanRtspBackchannelServer] SETUP transport="${transportLine}" client=${clientId}`
|
|
41921
|
+
);
|
|
41922
|
+
if (!transportLine.toUpperCase().includes("RTP/AVP/TCP") && !transportLine.toLowerCase().includes("interleaved")) {
|
|
41923
|
+
this.logger.warn?.(
|
|
41924
|
+
`[BaichuanRtspBackchannelServer] SETUP rejected (non-TCP transport) client=${clientId}`
|
|
41925
|
+
);
|
|
41926
|
+
send(461, "Unsupported Transport");
|
|
41927
|
+
return;
|
|
41928
|
+
}
|
|
41929
|
+
const interleaved = parseInterleavedChannels(transportLine) ?? {
|
|
41930
|
+
rtp: 0,
|
|
41931
|
+
rtcp: 1
|
|
41932
|
+
};
|
|
41933
|
+
if (!sessionId) {
|
|
41934
|
+
sessionId = newSessionId();
|
|
41935
|
+
}
|
|
41936
|
+
this.sessionByClient.set(clientId, {
|
|
41937
|
+
sessionId,
|
|
41938
|
+
clientId,
|
|
41939
|
+
socket,
|
|
41940
|
+
rtpChannel: interleaved.rtp,
|
|
41941
|
+
rtcpChannel: interleaved.rtcp
|
|
41942
|
+
});
|
|
41943
|
+
this.logger.info?.(
|
|
41944
|
+
`[BaichuanRtspBackchannelServer] SETUP ok client=${clientId} session=${sessionId} interleaved=${interleaved.rtp}-${interleaved.rtcp}`
|
|
41945
|
+
);
|
|
41946
|
+
send(200, "OK", {
|
|
41947
|
+
Transport: `RTP/AVP/TCP;unicast;interleaved=${interleaved.rtp}-${interleaved.rtcp};mode=record`,
|
|
41948
|
+
Session: sessionId
|
|
41949
|
+
});
|
|
41950
|
+
return;
|
|
41951
|
+
}
|
|
41952
|
+
case "RECORD": {
|
|
41953
|
+
const session = sessionId ? Array.from(this.sessionByClient.values()).find(
|
|
41954
|
+
(s) => s.sessionId === sessionId
|
|
41955
|
+
) : this.sessionByClient.get(clientId);
|
|
41956
|
+
if (!session) {
|
|
41957
|
+
this.logger.warn?.(
|
|
41958
|
+
`[BaichuanRtspBackchannelServer] RECORD without active session client=${clientId} requestedSession=${sessionId ?? "(none)"}`
|
|
41959
|
+
);
|
|
41960
|
+
send(454, "Session Not Found");
|
|
41961
|
+
return;
|
|
41962
|
+
}
|
|
41963
|
+
if (session.handler) {
|
|
41964
|
+
this.logger.info?.(
|
|
41965
|
+
`[BaichuanRtspBackchannelServer] RECORD idempotent (already recording) client=${clientId} session=${session.sessionId}`
|
|
41966
|
+
);
|
|
41967
|
+
send(200, "OK", { Session: session.sessionId });
|
|
41968
|
+
return;
|
|
41969
|
+
}
|
|
41970
|
+
const apiRef = this.api;
|
|
41971
|
+
const channelForCamera = this.channel;
|
|
41972
|
+
const loggerRef = this.logger;
|
|
41973
|
+
const deviceIdRef = this.deviceId ?? `rtsp-backchannel-${clientId}`;
|
|
41974
|
+
const handler = new RtspBackchannel({
|
|
41975
|
+
openTalkSession: () => apiRef.createDedicatedTalkSession(channelForCamera, {
|
|
41976
|
+
deviceId: deviceIdRef,
|
|
41977
|
+
logger: loggerRef
|
|
41978
|
+
}),
|
|
41979
|
+
logger: loggerRef
|
|
41980
|
+
});
|
|
41981
|
+
const recordStart = Date.now();
|
|
41982
|
+
this.logger.info?.(
|
|
41983
|
+
`[BaichuanRtspBackchannelServer] RECORD opening TalkSession client=${clientId} session=${session.sessionId} channel=${channelForCamera} deviceId="${deviceIdRef}"`
|
|
41984
|
+
);
|
|
41985
|
+
try {
|
|
41986
|
+
const talk = await handler.start();
|
|
41987
|
+
this.logger.info?.(
|
|
41988
|
+
`[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}`
|
|
41989
|
+
);
|
|
41990
|
+
} catch (e) {
|
|
41991
|
+
this.logger.warn?.(
|
|
41992
|
+
`[BaichuanRtspBackchannelServer] RECORD failed to open TalkSession client=${clientId} session=${session.sessionId} setupMs=${Date.now() - recordStart} error="${e.message}"`
|
|
41993
|
+
);
|
|
41994
|
+
send(503, "Service Unavailable");
|
|
41995
|
+
return;
|
|
41996
|
+
}
|
|
41997
|
+
session.handler = handler;
|
|
41998
|
+
send(200, "OK", { Session: session.sessionId });
|
|
41999
|
+
return;
|
|
42000
|
+
}
|
|
42001
|
+
case "TEARDOWN": {
|
|
42002
|
+
const session = this.sessionByClient.get(clientId);
|
|
42003
|
+
if (session) {
|
|
42004
|
+
this.sessionByClient.delete(clientId);
|
|
42005
|
+
if (session.handler) {
|
|
42006
|
+
const stats = session.handler.stats;
|
|
42007
|
+
this.logger.info?.(
|
|
42008
|
+
`[BaichuanRtspBackchannelServer] TEARDOWN client=${clientId} session=${session.sessionId} rtpRx=${stats.rtpPacketsReceived} rtpDropped=${stats.rtpPacketsDropped} adpcmBlocks=${stats.adpcmBlocksSent}`
|
|
42009
|
+
);
|
|
42010
|
+
void session.handler.stop();
|
|
42011
|
+
} else {
|
|
42012
|
+
this.logger.info?.(
|
|
42013
|
+
`[BaichuanRtspBackchannelServer] TEARDOWN client=${clientId} session=${session.sessionId} (no RECORD)`
|
|
42014
|
+
);
|
|
42015
|
+
}
|
|
42016
|
+
} else {
|
|
42017
|
+
this.logger.info?.(
|
|
42018
|
+
`[BaichuanRtspBackchannelServer] TEARDOWN client=${clientId} (no active session)`
|
|
42019
|
+
);
|
|
42020
|
+
}
|
|
42021
|
+
send(200, "OK", { Session: sessionId ?? "" });
|
|
42022
|
+
try {
|
|
42023
|
+
socket.end();
|
|
42024
|
+
} catch {
|
|
42025
|
+
}
|
|
42026
|
+
return;
|
|
42027
|
+
}
|
|
42028
|
+
default:
|
|
42029
|
+
this.logger.warn?.(
|
|
42030
|
+
`[BaichuanRtspBackchannelServer] unknown method ${method} from ${clientId} \u2014 replying 501`
|
|
42031
|
+
);
|
|
42032
|
+
send(501, "Not Implemented");
|
|
42033
|
+
}
|
|
42034
|
+
}
|
|
42035
|
+
buildSdp() {
|
|
42036
|
+
return `v=0\r
|
|
42037
|
+
o=- ${Date.now()} ${Date.now()} IN IP4 ${this.listenHost}\r
|
|
42038
|
+
s=Baichuan Backchannel\r
|
|
42039
|
+
c=IN IP4 ${this.listenHost}\r
|
|
42040
|
+
t=0 0\r
|
|
42041
|
+
a=control:*\r
|
|
42042
|
+
m=audio 0 RTP/AVP 0\r
|
|
42043
|
+
a=rtpmap:0 PCMU/8000\r
|
|
42044
|
+
a=sendonly\r
|
|
42045
|
+
a=control:audiobackchannel\r
|
|
42046
|
+
`;
|
|
42047
|
+
}
|
|
42048
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
42049
|
+
// Digest auth helpers (mirror BaichuanRtspServer's behaviour so
|
|
42050
|
+
// operators can re-use the same credentials store across both servers).
|
|
42051
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
42052
|
+
wwwAuthenticate(clientId) {
|
|
42053
|
+
const nonce = this.nonceFor(clientId);
|
|
42054
|
+
return `Digest realm="${this.authRealm}", nonce="${nonce}"`;
|
|
42055
|
+
}
|
|
42056
|
+
nonceFor(clientId) {
|
|
42057
|
+
const now = Date.now();
|
|
42058
|
+
const existing = this.nonces.get(clientId);
|
|
42059
|
+
if (existing && now - existing.ts < _BaichuanRtspBackchannelServer.NONCE_TTL_MS) {
|
|
42060
|
+
return existing.nonce;
|
|
42061
|
+
}
|
|
42062
|
+
const nonce = crypto3.randomBytes(16).toString("hex");
|
|
42063
|
+
this.nonces.set(clientId, { nonce, ts: now });
|
|
42064
|
+
return nonce;
|
|
42065
|
+
}
|
|
42066
|
+
validateDigest(header, method, uri, clientId) {
|
|
42067
|
+
const params = parseDigestHeader(header);
|
|
42068
|
+
if (!params) return false;
|
|
42069
|
+
if (params.realm && params.realm !== this.authRealm) return false;
|
|
42070
|
+
const nonce = this.nonces.get(clientId);
|
|
42071
|
+
if (!nonce || nonce.nonce !== params.nonce) return false;
|
|
42072
|
+
if (Date.now() - nonce.ts > _BaichuanRtspBackchannelServer.NONCE_TTL_MS)
|
|
42073
|
+
return false;
|
|
42074
|
+
const cred = this.authCredentials.find((c) => c.username === params.username);
|
|
42075
|
+
if (!cred) return false;
|
|
42076
|
+
const ha1 = cred.ha1 ?? (cred.password !== void 0 ? md5Hex(`${cred.username}:${this.authRealm}:${cred.password}`) : void 0);
|
|
42077
|
+
if (!ha1) return false;
|
|
42078
|
+
const ha2 = md5Hex(`${method}:${params.uri || uri}`);
|
|
42079
|
+
const expected = md5Hex(`${ha1}:${params.nonce}:${ha2}`);
|
|
42080
|
+
return expected === params.response;
|
|
42081
|
+
}
|
|
42082
|
+
};
|
|
42083
|
+
function newSessionId() {
|
|
42084
|
+
return `talk_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
42085
|
+
}
|
|
42086
|
+
function parseInterleavedChannels(transport) {
|
|
42087
|
+
const m = transport.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
|
|
42088
|
+
if (!m) return void 0;
|
|
42089
|
+
const rtp = parseInt(m[1] ?? "", 10);
|
|
42090
|
+
const rtcp = parseInt(m[2] ?? "", 10);
|
|
42091
|
+
if (!Number.isFinite(rtp) || !Number.isFinite(rtcp)) return void 0;
|
|
42092
|
+
return { rtp, rtcp };
|
|
42093
|
+
}
|
|
42094
|
+
function parseDigestHeader(header) {
|
|
42095
|
+
if (!/^digest\s+/i.test(header)) return null;
|
|
42096
|
+
const params = {};
|
|
42097
|
+
const re = /(\w+)\s*=\s*("([^"]*)"|([^,\s]+))/g;
|
|
42098
|
+
let match;
|
|
42099
|
+
while ((match = re.exec(header)) !== null) {
|
|
42100
|
+
const key = (match[1] ?? "").toLowerCase();
|
|
42101
|
+
const value = match[3] ?? match[4] ?? "";
|
|
42102
|
+
params[key] = value;
|
|
42103
|
+
}
|
|
42104
|
+
if (!params.username || !params.nonce || !params.uri || !params.response) {
|
|
42105
|
+
return null;
|
|
42106
|
+
}
|
|
42107
|
+
return {
|
|
42108
|
+
username: params.username,
|
|
42109
|
+
realm: params.realm ?? "",
|
|
42110
|
+
nonce: params.nonce,
|
|
42111
|
+
uri: params.uri,
|
|
42112
|
+
response: params.response
|
|
42113
|
+
};
|
|
42114
|
+
}
|
|
42115
|
+
|
|
41138
42116
|
// src/emailPush/server.ts
|
|
41139
42117
|
var import_node_util2 = require("util");
|
|
41140
42118
|
var import_smtp_server = require("smtp-server");
|
|
@@ -41598,6 +42576,7 @@ function buildInitialStatus(config) {
|
|
|
41598
42576
|
BaichuanHlsServer,
|
|
41599
42577
|
BaichuanHttpStreamServer,
|
|
41600
42578
|
BaichuanMjpegServer,
|
|
42579
|
+
BaichuanRtspBackchannelServer,
|
|
41601
42580
|
BaichuanRtspServer,
|
|
41602
42581
|
BaichuanVideoStream,
|
|
41603
42582
|
BaichuanWebRTCServer,
|
|
@@ -41623,10 +42602,12 @@ function buildInitialStatus(config) {
|
|
|
41623
42602
|
ReolinkCgiApi,
|
|
41624
42603
|
ReolinkHttpClient,
|
|
41625
42604
|
Rfc4571Muxer,
|
|
42605
|
+
RtspBackchannel,
|
|
41626
42606
|
_resetEmailPushBusForTests,
|
|
41627
42607
|
abilitiesHasAny,
|
|
41628
42608
|
aesDecrypt,
|
|
41629
42609
|
aesEncrypt,
|
|
42610
|
+
alawToPcm16,
|
|
41630
42611
|
applyStreamPatch,
|
|
41631
42612
|
applyXmlTagPatch,
|
|
41632
42613
|
asLogger,
|
|
@@ -41699,6 +42680,7 @@ function buildInitialStatus(config) {
|
|
|
41699
42680
|
discoverViaUdpDirect,
|
|
41700
42681
|
emitEmailPushEvent,
|
|
41701
42682
|
encodeHeader,
|
|
42683
|
+
encodeImaAdpcm,
|
|
41702
42684
|
encodeMotionScopeBitmap,
|
|
41703
42685
|
encodeMotionSensitivityListXml,
|
|
41704
42686
|
encodeShelterCoord,
|
|
@@ -41741,6 +42723,7 @@ function buildInitialStatus(config) {
|
|
|
41741
42723
|
maskUid,
|
|
41742
42724
|
md5HexUpper,
|
|
41743
42725
|
md5StrModern,
|
|
42726
|
+
mulawToPcm16,
|
|
41744
42727
|
normalizeDayNightMode,
|
|
41745
42728
|
normalizeOpenClose,
|
|
41746
42729
|
normalizeUid,
|
|
@@ -41752,6 +42735,7 @@ function buildInitialStatus(config) {
|
|
|
41752
42735
|
parseAdtsHeader,
|
|
41753
42736
|
parseBcMedia,
|
|
41754
42737
|
parseRecordingFileName,
|
|
42738
|
+
parseRtpPacket,
|
|
41755
42739
|
parseSupportXml,
|
|
41756
42740
|
patchAiDetectCfgXml,
|
|
41757
42741
|
patchMotionSensitivityListXml,
|
|
@@ -41769,6 +42753,7 @@ function buildInitialStatus(config) {
|
|
|
41769
42753
|
splitAnnexBToNals,
|
|
41770
42754
|
splitH265AnnexBToNalPayloads,
|
|
41771
42755
|
testChannelStreams,
|
|
42756
|
+
upsamplePcm16,
|
|
41772
42757
|
upsertXmlTag,
|
|
41773
42758
|
xmlEscape,
|
|
41774
42759
|
xmlIndicatesFloodlight,
|