@apocaliss92/nodelink-js 0.3.4 → 0.3.9
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/README.md +48 -82
- package/dist/{DiagnosticsTools-2JQRV5FE.js → DiagnosticsTools-DQDDBRM6.js} +4 -2
- package/dist/{chunk-APEEZ4UN.js → chunk-6Q6MK4WG.js} +172 -1
- package/dist/chunk-6Q6MK4WG.js.map +1 -0
- package/dist/{chunk-YSEFEQYV.js → chunk-OGIKBDON.js} +49 -8
- package/dist/chunk-OGIKBDON.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +215 -4
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +756 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +184 -1
- package/dist/index.d.ts +165 -0
- package/dist/index.js +541 -17
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/chunk-APEEZ4UN.js.map +0 -1
- package/dist/chunk-YSEFEQYV.js.map +0 -1
- /package/dist/{DiagnosticsTools-2JQRV5FE.js.map → DiagnosticsTools-DQDDBRM6.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -41,8 +41,9 @@ import {
|
|
|
41
41
|
maskUid,
|
|
42
42
|
normalizeUid,
|
|
43
43
|
parseSupportXml,
|
|
44
|
-
setGlobalLogger
|
|
45
|
-
|
|
44
|
+
setGlobalLogger,
|
|
45
|
+
xmlIndicatesFloodlight
|
|
46
|
+
} from "./chunk-OGIKBDON.js";
|
|
46
47
|
import {
|
|
47
48
|
AesStreamDecryptor,
|
|
48
49
|
BC_AES_IV,
|
|
@@ -184,6 +185,7 @@ import {
|
|
|
184
185
|
buildSirenTimesXml,
|
|
185
186
|
buildStartZoomFocusXml,
|
|
186
187
|
buildWhiteLedStateXml,
|
|
188
|
+
captureModelFixtures,
|
|
187
189
|
collectCgiDiagnostics,
|
|
188
190
|
collectMultifocalDiagnostics,
|
|
189
191
|
collectNativeDiagnostics,
|
|
@@ -219,7 +221,7 @@ import {
|
|
|
219
221
|
testChannelStreams,
|
|
220
222
|
xmlEscape,
|
|
221
223
|
zipDirectory
|
|
222
|
-
} from "./chunk-
|
|
224
|
+
} from "./chunk-6Q6MK4WG.js";
|
|
223
225
|
|
|
224
226
|
// src/reolink/baichuan/HlsSessionManager.ts
|
|
225
227
|
var withTimeout = async (p, ms, label) => {
|
|
@@ -4585,8 +4587,527 @@ async function createReplayHttpServer(options) {
|
|
|
4585
4587
|
};
|
|
4586
4588
|
}
|
|
4587
4589
|
|
|
4588
|
-
// src/baichuan/stream/
|
|
4590
|
+
// src/baichuan/stream/Go2rtcTcpServer.ts
|
|
4589
4591
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
4592
|
+
import * as net from "net";
|
|
4593
|
+
var AsyncBoundedQueue = class {
|
|
4594
|
+
maxItems;
|
|
4595
|
+
queue = [];
|
|
4596
|
+
waiting;
|
|
4597
|
+
closed = false;
|
|
4598
|
+
constructor(maxItems) {
|
|
4599
|
+
this.maxItems = Math.max(1, maxItems | 0);
|
|
4600
|
+
}
|
|
4601
|
+
push(item) {
|
|
4602
|
+
if (this.closed) return;
|
|
4603
|
+
if (this.waiting) {
|
|
4604
|
+
const { resolve } = this.waiting;
|
|
4605
|
+
this.waiting = void 0;
|
|
4606
|
+
resolve({ value: item, done: false });
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
this.queue.push(item);
|
|
4610
|
+
if (this.queue.length > this.maxItems) {
|
|
4611
|
+
this.queue.splice(0, this.queue.length - this.maxItems);
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
close() {
|
|
4615
|
+
if (this.closed) return;
|
|
4616
|
+
this.closed = true;
|
|
4617
|
+
if (this.waiting) {
|
|
4618
|
+
const { resolve } = this.waiting;
|
|
4619
|
+
this.waiting = void 0;
|
|
4620
|
+
resolve({ value: void 0, done: true });
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
async next() {
|
|
4624
|
+
if (this.closed) return { value: void 0, done: true };
|
|
4625
|
+
const item = this.queue.shift();
|
|
4626
|
+
if (item !== void 0) return { value: item, done: false };
|
|
4627
|
+
return await new Promise((resolve) => {
|
|
4628
|
+
this.waiting = { resolve };
|
|
4629
|
+
});
|
|
4630
|
+
}
|
|
4631
|
+
};
|
|
4632
|
+
var NativeStreamFanout = class {
|
|
4633
|
+
opts;
|
|
4634
|
+
queues = /* @__PURE__ */ new Map();
|
|
4635
|
+
source = null;
|
|
4636
|
+
running = false;
|
|
4637
|
+
pumpPromise = null;
|
|
4638
|
+
constructor(opts) {
|
|
4639
|
+
this.opts = opts;
|
|
4640
|
+
}
|
|
4641
|
+
start() {
|
|
4642
|
+
if (this.running) return;
|
|
4643
|
+
this.running = true;
|
|
4644
|
+
this.source = this.opts.createSource();
|
|
4645
|
+
this.pumpPromise = (async () => {
|
|
4646
|
+
try {
|
|
4647
|
+
for await (const frame of this.source) {
|
|
4648
|
+
try {
|
|
4649
|
+
this.opts.onFrame?.(frame);
|
|
4650
|
+
} catch {
|
|
4651
|
+
}
|
|
4652
|
+
for (const q of this.queues.values()) {
|
|
4653
|
+
q.push(frame);
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
} catch (e) {
|
|
4657
|
+
this.opts.onError?.(e);
|
|
4658
|
+
} finally {
|
|
4659
|
+
for (const q of this.queues.values()) q.close();
|
|
4660
|
+
this.queues.clear();
|
|
4661
|
+
this.running = false;
|
|
4662
|
+
this.opts.onEnd?.();
|
|
4663
|
+
}
|
|
4664
|
+
})();
|
|
4665
|
+
}
|
|
4666
|
+
subscribe(id) {
|
|
4667
|
+
const q = new AsyncBoundedQueue(this.opts.maxQueueItems);
|
|
4668
|
+
this.queues.set(id, q);
|
|
4669
|
+
const self = this;
|
|
4670
|
+
return (async function* () {
|
|
4671
|
+
try {
|
|
4672
|
+
while (true) {
|
|
4673
|
+
const r = await q.next();
|
|
4674
|
+
if (r.done) return;
|
|
4675
|
+
yield r.value;
|
|
4676
|
+
}
|
|
4677
|
+
} finally {
|
|
4678
|
+
q.close();
|
|
4679
|
+
self.queues.delete(id);
|
|
4680
|
+
}
|
|
4681
|
+
})();
|
|
4682
|
+
}
|
|
4683
|
+
async stop() {
|
|
4684
|
+
if (!this.running) return;
|
|
4685
|
+
this.running = false;
|
|
4686
|
+
const src = this.source;
|
|
4687
|
+
this.source = null;
|
|
4688
|
+
for (const q of this.queues.values()) q.close();
|
|
4689
|
+
this.queues.clear();
|
|
4690
|
+
try {
|
|
4691
|
+
await src?.return(void 0);
|
|
4692
|
+
} catch {
|
|
4693
|
+
}
|
|
4694
|
+
try {
|
|
4695
|
+
await this.pumpPromise;
|
|
4696
|
+
} catch {
|
|
4697
|
+
}
|
|
4698
|
+
this.pumpPromise = null;
|
|
4699
|
+
}
|
|
4700
|
+
};
|
|
4701
|
+
var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
4702
|
+
api;
|
|
4703
|
+
channel;
|
|
4704
|
+
profile;
|
|
4705
|
+
variant;
|
|
4706
|
+
listenHost;
|
|
4707
|
+
listenPort;
|
|
4708
|
+
logger;
|
|
4709
|
+
deviceId;
|
|
4710
|
+
gracePeriodMs;
|
|
4711
|
+
prebufferMaxMs;
|
|
4712
|
+
maxBufferBytes;
|
|
4713
|
+
prestartStream;
|
|
4714
|
+
active = false;
|
|
4715
|
+
server;
|
|
4716
|
+
resolvedPort;
|
|
4717
|
+
// Native stream
|
|
4718
|
+
nativeFanout = null;
|
|
4719
|
+
nativeStreamActive = false;
|
|
4720
|
+
dedicatedSessionRelease;
|
|
4721
|
+
detectedVideoType;
|
|
4722
|
+
// Client tracking
|
|
4723
|
+
connectedClients = /* @__PURE__ */ new Set();
|
|
4724
|
+
clientSockets = /* @__PURE__ */ new Map();
|
|
4725
|
+
stopGraceTimer;
|
|
4726
|
+
// Prebuffer
|
|
4727
|
+
prebuffer = [];
|
|
4728
|
+
constructor(options) {
|
|
4729
|
+
super();
|
|
4730
|
+
this.api = options.api;
|
|
4731
|
+
this.channel = options.channel;
|
|
4732
|
+
this.profile = options.profile;
|
|
4733
|
+
this.variant = options.variant ?? "default";
|
|
4734
|
+
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
4735
|
+
this.listenPort = options.listenPort ?? 0;
|
|
4736
|
+
this.logger = options.logger ?? console;
|
|
4737
|
+
this.deviceId = options.deviceId;
|
|
4738
|
+
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
4739
|
+
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
4740
|
+
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
4741
|
+
this.prestartStream = options.prestartStream ?? true;
|
|
4742
|
+
}
|
|
4743
|
+
// -----------------------------------------------------------------------
|
|
4744
|
+
// Public API
|
|
4745
|
+
// -----------------------------------------------------------------------
|
|
4746
|
+
/** Start listening. Resolves once the TCP server is bound. */
|
|
4747
|
+
async start() {
|
|
4748
|
+
if (this.active) return;
|
|
4749
|
+
this.active = true;
|
|
4750
|
+
this.server = net.createServer((socket) => this.handleClient(socket));
|
|
4751
|
+
this.server.on("error", (err) => {
|
|
4752
|
+
this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
|
|
4753
|
+
this.emit("error", err);
|
|
4754
|
+
});
|
|
4755
|
+
await new Promise((resolve, reject) => {
|
|
4756
|
+
this.server.listen(this.listenPort, this.listenHost, () => {
|
|
4757
|
+
const addr = this.server.address();
|
|
4758
|
+
this.resolvedPort = addr.port;
|
|
4759
|
+
this.logger.info?.(
|
|
4760
|
+
`[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
|
|
4761
|
+
);
|
|
4762
|
+
this.emit("listening", { host: addr.address, port: addr.port });
|
|
4763
|
+
resolve();
|
|
4764
|
+
});
|
|
4765
|
+
this.server.once("error", reject);
|
|
4766
|
+
});
|
|
4767
|
+
if (this.prestartStream) {
|
|
4768
|
+
this.logger.info?.(
|
|
4769
|
+
`[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
|
|
4770
|
+
);
|
|
4771
|
+
this.startNativeStream();
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
/** Stop the server and all active streams. */
|
|
4775
|
+
async stop() {
|
|
4776
|
+
if (!this.active) return;
|
|
4777
|
+
this.active = false;
|
|
4778
|
+
clearTimeout(this.stopGraceTimer);
|
|
4779
|
+
for (const [id, sock] of this.clientSockets) {
|
|
4780
|
+
sock.destroy();
|
|
4781
|
+
this.connectedClients.delete(id);
|
|
4782
|
+
}
|
|
4783
|
+
this.clientSockets.clear();
|
|
4784
|
+
await this.stopNativeStream();
|
|
4785
|
+
if (this.server) {
|
|
4786
|
+
await new Promise((resolve) => {
|
|
4787
|
+
this.server.close(() => resolve());
|
|
4788
|
+
});
|
|
4789
|
+
this.server = void 0;
|
|
4790
|
+
}
|
|
4791
|
+
this.prebuffer = [];
|
|
4792
|
+
this.resolvedPort = void 0;
|
|
4793
|
+
this.emit("close");
|
|
4794
|
+
}
|
|
4795
|
+
/** The actual port the server is listening on (available after start()). */
|
|
4796
|
+
get port() {
|
|
4797
|
+
return this.resolvedPort;
|
|
4798
|
+
}
|
|
4799
|
+
/** The go2rtc-compatible source URL. */
|
|
4800
|
+
get go2rtcSourceUrl() {
|
|
4801
|
+
if (this.resolvedPort == null) return void 0;
|
|
4802
|
+
return `tcp://127.0.0.1:${this.resolvedPort}`;
|
|
4803
|
+
}
|
|
4804
|
+
/** Number of currently connected clients. */
|
|
4805
|
+
get clientCount() {
|
|
4806
|
+
return this.connectedClients.size;
|
|
4807
|
+
}
|
|
4808
|
+
// -----------------------------------------------------------------------
|
|
4809
|
+
// Client handling
|
|
4810
|
+
// -----------------------------------------------------------------------
|
|
4811
|
+
handleClient(socket) {
|
|
4812
|
+
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
4813
|
+
socket.setNoDelay(true);
|
|
4814
|
+
this.connectedClients.add(clientId);
|
|
4815
|
+
this.clientSockets.set(clientId, socket);
|
|
4816
|
+
this.logger.info?.(
|
|
4817
|
+
`[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
|
|
4818
|
+
);
|
|
4819
|
+
this.emit("client", clientId);
|
|
4820
|
+
if (this.stopGraceTimer) {
|
|
4821
|
+
clearTimeout(this.stopGraceTimer);
|
|
4822
|
+
this.stopGraceTimer = void 0;
|
|
4823
|
+
}
|
|
4824
|
+
if (!this.nativeStreamActive) {
|
|
4825
|
+
this.startNativeStream();
|
|
4826
|
+
}
|
|
4827
|
+
this.feedClient(clientId, socket).catch((err) => {
|
|
4828
|
+
this.logger.warn?.(
|
|
4829
|
+
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
4830
|
+
);
|
|
4831
|
+
});
|
|
4832
|
+
const cleanup = () => {
|
|
4833
|
+
this.removeClient(clientId);
|
|
4834
|
+
socket.destroy();
|
|
4835
|
+
};
|
|
4836
|
+
socket.on("error", cleanup);
|
|
4837
|
+
socket.on("close", cleanup);
|
|
4838
|
+
}
|
|
4839
|
+
async feedClient(clientId, socket) {
|
|
4840
|
+
const fanoutDeadline = Date.now() + 3e4;
|
|
4841
|
+
while (this.active && !this.nativeFanout) {
|
|
4842
|
+
if (socket.destroyed) return;
|
|
4843
|
+
if (Date.now() > fanoutDeadline) {
|
|
4844
|
+
this.logger.warn?.(
|
|
4845
|
+
`[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
|
|
4846
|
+
);
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4849
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
4850
|
+
}
|
|
4851
|
+
if (!this.active || !this.nativeFanout) return;
|
|
4852
|
+
const subscription = this.nativeFanout.subscribe(clientId);
|
|
4853
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
4854
|
+
let lastIdrIdx = -1;
|
|
4855
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
4856
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
4857
|
+
lastIdrIdx = i;
|
|
4858
|
+
break;
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
if (lastIdrIdx >= 0) {
|
|
4862
|
+
const replay = prebufferSnap.slice(lastIdrIdx);
|
|
4863
|
+
this.logger.info?.(
|
|
4864
|
+
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
4865
|
+
);
|
|
4866
|
+
for (const entry of replay) {
|
|
4867
|
+
if (socket.destroyed) return;
|
|
4868
|
+
socket.write(entry.data);
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
let seenKeyframe = lastIdrIdx >= 0;
|
|
4872
|
+
let liveFrameCount = 0;
|
|
4873
|
+
let liveVideoWritten = 0;
|
|
4874
|
+
let lastLogAt = Date.now();
|
|
4875
|
+
try {
|
|
4876
|
+
this.logger.info?.(
|
|
4877
|
+
`[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
|
|
4878
|
+
);
|
|
4879
|
+
for await (const frame of subscription) {
|
|
4880
|
+
if (socket.destroyed || !this.active) {
|
|
4881
|
+
this.logger.info?.(
|
|
4882
|
+
`[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
|
|
4883
|
+
);
|
|
4884
|
+
break;
|
|
4885
|
+
}
|
|
4886
|
+
liveFrameCount++;
|
|
4887
|
+
const annexB = this.convertFrame(frame);
|
|
4888
|
+
if (!annexB) continue;
|
|
4889
|
+
if (!seenKeyframe) {
|
|
4890
|
+
if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
|
|
4891
|
+
seenKeyframe = true;
|
|
4892
|
+
this.logger.info?.(
|
|
4893
|
+
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
4894
|
+
);
|
|
4895
|
+
}
|
|
4896
|
+
socket.write(annexB);
|
|
4897
|
+
liveVideoWritten++;
|
|
4898
|
+
if (Date.now() - lastLogAt > 1e4) {
|
|
4899
|
+
this.logger.info?.(
|
|
4900
|
+
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
4901
|
+
);
|
|
4902
|
+
lastLogAt = Date.now();
|
|
4903
|
+
}
|
|
4904
|
+
if (socket.writableLength > this.maxBufferBytes) {
|
|
4905
|
+
this.logger.warn?.(
|
|
4906
|
+
`[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
|
|
4907
|
+
);
|
|
4908
|
+
socket.destroy();
|
|
4909
|
+
break;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
this.logger.info?.(
|
|
4913
|
+
`[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
|
|
4914
|
+
);
|
|
4915
|
+
} finally {
|
|
4916
|
+
await subscription.return(void 0).catch(() => {
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
// -----------------------------------------------------------------------
|
|
4921
|
+
// Frame conversion
|
|
4922
|
+
// -----------------------------------------------------------------------
|
|
4923
|
+
/**
|
|
4924
|
+
* Convert a native frame to wire-ready Annex-B.
|
|
4925
|
+
* Audio frames are skipped — raw TCP carries only video (Annex-B).
|
|
4926
|
+
* go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
|
|
4927
|
+
*/
|
|
4928
|
+
convertFrame(frame) {
|
|
4929
|
+
if (frame.audio) {
|
|
4930
|
+
return null;
|
|
4931
|
+
}
|
|
4932
|
+
if (frame.data.length === 0) return null;
|
|
4933
|
+
try {
|
|
4934
|
+
if (frame.videoType === "H264") {
|
|
4935
|
+
return convertToAnnexB(frame.data);
|
|
4936
|
+
}
|
|
4937
|
+
if (frame.videoType === "H265") {
|
|
4938
|
+
return convertToAnnexB2(frame.data);
|
|
4939
|
+
}
|
|
4940
|
+
} catch {
|
|
4941
|
+
}
|
|
4942
|
+
return frame.data;
|
|
4943
|
+
}
|
|
4944
|
+
/** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
|
|
4945
|
+
isAnnexBKeyframe(annexB, videoType) {
|
|
4946
|
+
try {
|
|
4947
|
+
if (videoType === "H264") {
|
|
4948
|
+
const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
|
|
4949
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
4950
|
+
}
|
|
4951
|
+
if (videoType === "H265") {
|
|
4952
|
+
const nals = splitAnnexBToNalPayloads2(annexB);
|
|
4953
|
+
return nals.some(
|
|
4954
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4957
|
+
} catch {
|
|
4958
|
+
}
|
|
4959
|
+
return false;
|
|
4960
|
+
}
|
|
4961
|
+
/** Split Annex-B byte stream into individual NAL units. */
|
|
4962
|
+
static splitAnnexBNals(buf) {
|
|
4963
|
+
const nals = [];
|
|
4964
|
+
let i = 0;
|
|
4965
|
+
while (i < buf.length) {
|
|
4966
|
+
if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
|
|
4967
|
+
let scLen;
|
|
4968
|
+
if (buf[i + 2] === 1) {
|
|
4969
|
+
scLen = 3;
|
|
4970
|
+
} else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
|
|
4971
|
+
scLen = 4;
|
|
4972
|
+
} else {
|
|
4973
|
+
i++;
|
|
4974
|
+
continue;
|
|
4975
|
+
}
|
|
4976
|
+
const nalStart = i + scLen;
|
|
4977
|
+
let nalEnd = buf.length;
|
|
4978
|
+
for (let j = nalStart; j < buf.length - 2; j++) {
|
|
4979
|
+
if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
|
|
4980
|
+
nalEnd = j;
|
|
4981
|
+
break;
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
if (nalEnd > nalStart) {
|
|
4985
|
+
nals.push(buf.subarray(nalStart, nalEnd));
|
|
4986
|
+
}
|
|
4987
|
+
i = nalEnd;
|
|
4988
|
+
} else {
|
|
4989
|
+
i++;
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
return nals;
|
|
4993
|
+
}
|
|
4994
|
+
// -----------------------------------------------------------------------
|
|
4995
|
+
// Native stream management
|
|
4996
|
+
// -----------------------------------------------------------------------
|
|
4997
|
+
async startNativeStream() {
|
|
4998
|
+
if (this.nativeStreamActive) return;
|
|
4999
|
+
this.nativeStreamActive = true;
|
|
5000
|
+
let dedicatedClient;
|
|
5001
|
+
if (this.deviceId) {
|
|
5002
|
+
try {
|
|
5003
|
+
const session = await this.api.createDedicatedSession(
|
|
5004
|
+
`live:${this.deviceId}:ch${this.channel}:${this.profile}`
|
|
5005
|
+
);
|
|
5006
|
+
dedicatedClient = session.client;
|
|
5007
|
+
this.dedicatedSessionRelease = session.release;
|
|
5008
|
+
} catch (e) {
|
|
5009
|
+
this.logger.warn?.(
|
|
5010
|
+
`[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
|
|
5011
|
+
);
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
this.logger.info?.(
|
|
5015
|
+
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
5016
|
+
);
|
|
5017
|
+
this.nativeFanout = new NativeStreamFanout({
|
|
5018
|
+
maxQueueItems: 200,
|
|
5019
|
+
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
5020
|
+
variant: this.variant,
|
|
5021
|
+
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
5022
|
+
}),
|
|
5023
|
+
onFrame: (frame) => {
|
|
5024
|
+
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
5025
|
+
this.detectedVideoType = frame.videoType;
|
|
5026
|
+
}
|
|
5027
|
+
const wireData = this.convertFrame(frame);
|
|
5028
|
+
if (!wireData || wireData.length === 0) return;
|
|
5029
|
+
const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
|
|
5030
|
+
this.prebuffer.push({
|
|
5031
|
+
data: Buffer.from(wireData),
|
|
5032
|
+
time: Date.now(),
|
|
5033
|
+
isKeyframe,
|
|
5034
|
+
audio: frame.audio
|
|
5035
|
+
});
|
|
5036
|
+
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
5037
|
+
let trimIdx = 0;
|
|
5038
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
5039
|
+
trimIdx++;
|
|
5040
|
+
}
|
|
5041
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
5042
|
+
},
|
|
5043
|
+
onError: (error) => {
|
|
5044
|
+
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
5045
|
+
},
|
|
5046
|
+
onEnd: () => {
|
|
5047
|
+
if (!this.nativeStreamActive) return;
|
|
5048
|
+
this.nativeStreamActive = false;
|
|
5049
|
+
this.nativeFanout = null;
|
|
5050
|
+
if (this.dedicatedSessionRelease) {
|
|
5051
|
+
this.dedicatedSessionRelease().catch(() => {
|
|
5052
|
+
});
|
|
5053
|
+
this.dedicatedSessionRelease = void 0;
|
|
5054
|
+
}
|
|
5055
|
+
if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
|
|
5056
|
+
this.logger.info?.(
|
|
5057
|
+
`[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
5058
|
+
);
|
|
5059
|
+
this.startNativeStream();
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
});
|
|
5063
|
+
this.nativeFanout.start();
|
|
5064
|
+
}
|
|
5065
|
+
async stopNativeStream() {
|
|
5066
|
+
this.nativeStreamActive = false;
|
|
5067
|
+
const fanout = this.nativeFanout;
|
|
5068
|
+
this.nativeFanout = null;
|
|
5069
|
+
if (fanout) {
|
|
5070
|
+
await fanout.stop();
|
|
5071
|
+
}
|
|
5072
|
+
this.prebuffer = [];
|
|
5073
|
+
if (this.dedicatedSessionRelease) {
|
|
5074
|
+
await this.dedicatedSessionRelease().catch(() => {
|
|
5075
|
+
});
|
|
5076
|
+
this.dedicatedSessionRelease = void 0;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
// -----------------------------------------------------------------------
|
|
5080
|
+
// Client lifecycle
|
|
5081
|
+
// -----------------------------------------------------------------------
|
|
5082
|
+
removeClient(clientId) {
|
|
5083
|
+
if (!this.connectedClients.has(clientId)) return;
|
|
5084
|
+
this.connectedClients.delete(clientId);
|
|
5085
|
+
this.clientSockets.delete(clientId);
|
|
5086
|
+
this.logger.info?.(
|
|
5087
|
+
`[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
|
|
5088
|
+
);
|
|
5089
|
+
this.emit("clientDisconnected", clientId);
|
|
5090
|
+
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
5091
|
+
this.scheduleStop();
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
scheduleStop() {
|
|
5095
|
+
if (this.stopGraceTimer) return;
|
|
5096
|
+
this.logger.info?.(
|
|
5097
|
+
`[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
|
|
5098
|
+
);
|
|
5099
|
+
this.stopGraceTimer = setTimeout(async () => {
|
|
5100
|
+
this.stopGraceTimer = void 0;
|
|
5101
|
+
if (this.connectedClients.size === 0 && this.nativeStreamActive) {
|
|
5102
|
+
this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
|
|
5103
|
+
await this.stopNativeStream();
|
|
5104
|
+
}
|
|
5105
|
+
}, this.gracePeriodMs);
|
|
5106
|
+
}
|
|
5107
|
+
};
|
|
5108
|
+
|
|
5109
|
+
// src/baichuan/stream/BaichuanHttpStreamServer.ts
|
|
5110
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
4590
5111
|
import { spawn as spawn5 } from "child_process";
|
|
4591
5112
|
import * as http4 from "http";
|
|
4592
5113
|
var NAL_START_CODE_4B = Buffer.from([0, 0, 0, 1]);
|
|
@@ -4633,7 +5154,7 @@ function isH264KeyframeFromAnnexB(annexB) {
|
|
|
4633
5154
|
}
|
|
4634
5155
|
return false;
|
|
4635
5156
|
}
|
|
4636
|
-
var BaichuanHttpStreamServer = class extends
|
|
5157
|
+
var BaichuanHttpStreamServer = class extends EventEmitter3 {
|
|
4637
5158
|
videoStream;
|
|
4638
5159
|
listenPort;
|
|
4639
5160
|
path;
|
|
@@ -4900,15 +5421,15 @@ var BaichuanHttpStreamServer = class extends EventEmitter2 {
|
|
|
4900
5421
|
};
|
|
4901
5422
|
|
|
4902
5423
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
4903
|
-
import { EventEmitter as
|
|
5424
|
+
import { EventEmitter as EventEmitter5 } from "events";
|
|
4904
5425
|
import * as http5 from "http";
|
|
4905
5426
|
|
|
4906
5427
|
// src/baichuan/stream/MjpegTransformer.ts
|
|
4907
|
-
import { EventEmitter as
|
|
5428
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
4908
5429
|
import { spawn as spawn6 } from "child_process";
|
|
4909
5430
|
var JPEG_SOI = Buffer.from([255, 216]);
|
|
4910
5431
|
var JPEG_EOI = Buffer.from([255, 217]);
|
|
4911
|
-
var MjpegTransformer = class extends
|
|
5432
|
+
var MjpegTransformer = class extends EventEmitter4 {
|
|
4912
5433
|
options;
|
|
4913
5434
|
ffmpeg = null;
|
|
4914
5435
|
started = false;
|
|
@@ -5105,7 +5626,7 @@ Content-Length: ${frame.length}\r
|
|
|
5105
5626
|
}
|
|
5106
5627
|
|
|
5107
5628
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
5108
|
-
var BaichuanMjpegServer = class extends
|
|
5629
|
+
var BaichuanMjpegServer = class extends EventEmitter5 {
|
|
5109
5630
|
options;
|
|
5110
5631
|
clients = /* @__PURE__ */ new Map();
|
|
5111
5632
|
httpServer = null;
|
|
@@ -5386,7 +5907,7 @@ var BaichuanMjpegServer = class extends EventEmitter4 {
|
|
|
5386
5907
|
};
|
|
5387
5908
|
|
|
5388
5909
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
5389
|
-
import { EventEmitter as
|
|
5910
|
+
import { EventEmitter as EventEmitter6 } from "events";
|
|
5390
5911
|
function parseAnnexBNalUnits(annexB) {
|
|
5391
5912
|
const nalUnits = [];
|
|
5392
5913
|
let offset = 0;
|
|
@@ -5421,7 +5942,7 @@ function getH264NalType(nalUnit) {
|
|
|
5421
5942
|
function getH265NalType2(nalUnit) {
|
|
5422
5943
|
return nalUnit[0] >> 1 & 63;
|
|
5423
5944
|
}
|
|
5424
|
-
var BaichuanWebRTCServer = class extends
|
|
5945
|
+
var BaichuanWebRTCServer = class extends EventEmitter6 {
|
|
5425
5946
|
options;
|
|
5426
5947
|
sessions = /* @__PURE__ */ new Map();
|
|
5427
5948
|
sessionIdCounter = 0;
|
|
@@ -6323,7 +6844,7 @@ Error: ${err}`
|
|
|
6323
6844
|
};
|
|
6324
6845
|
|
|
6325
6846
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
6326
|
-
import { EventEmitter as
|
|
6847
|
+
import { EventEmitter as EventEmitter7 } from "events";
|
|
6327
6848
|
import fs from "fs";
|
|
6328
6849
|
import fsp from "fs/promises";
|
|
6329
6850
|
import os from "os";
|
|
@@ -6400,7 +6921,7 @@ function getNalTypes(codec, annexB) {
|
|
|
6400
6921
|
}
|
|
6401
6922
|
});
|
|
6402
6923
|
}
|
|
6403
|
-
var BaichuanHlsServer = class extends
|
|
6924
|
+
var BaichuanHlsServer = class extends EventEmitter7 {
|
|
6404
6925
|
api;
|
|
6405
6926
|
channel;
|
|
6406
6927
|
profile;
|
|
@@ -6818,10 +7339,10 @@ var BaichuanHlsServer = class extends EventEmitter6 {
|
|
|
6818
7339
|
};
|
|
6819
7340
|
|
|
6820
7341
|
// src/multifocal/compositeRtspServer.ts
|
|
6821
|
-
import { EventEmitter as
|
|
7342
|
+
import { EventEmitter as EventEmitter8 } from "events";
|
|
6822
7343
|
import { spawn as spawn8 } from "child_process";
|
|
6823
|
-
import * as
|
|
6824
|
-
var CompositeRtspServer = class extends
|
|
7344
|
+
import * as net2 from "net";
|
|
7345
|
+
var CompositeRtspServer = class extends EventEmitter8 {
|
|
6825
7346
|
options;
|
|
6826
7347
|
compositeStream = null;
|
|
6827
7348
|
rtspServer = null;
|
|
@@ -6887,7 +7408,7 @@ var CompositeRtspServer = class extends EventEmitter7 {
|
|
|
6887
7408
|
const width = widerStreamInfo?.width ?? 1920;
|
|
6888
7409
|
const height = widerStreamInfo?.height ?? 1080;
|
|
6889
7410
|
const fps = widerStreamInfo?.frameRate ?? 25;
|
|
6890
|
-
this.rtspServer =
|
|
7411
|
+
this.rtspServer = net2.createServer((socket) => {
|
|
6891
7412
|
this.handleRtspConnection(socket);
|
|
6892
7413
|
});
|
|
6893
7414
|
await new Promise((resolve, reject) => {
|
|
@@ -7162,6 +7683,7 @@ export {
|
|
|
7162
7683
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
7163
7684
|
DUAL_LENS_MODELS,
|
|
7164
7685
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
7686
|
+
Go2rtcTcpServer,
|
|
7165
7687
|
H264RtpDepacketizer,
|
|
7166
7688
|
H265RtpDepacketizer,
|
|
7167
7689
|
HlsSessionManager,
|
|
@@ -7203,6 +7725,7 @@ export {
|
|
|
7203
7725
|
buildSirenTimesXml,
|
|
7204
7726
|
buildStartZoomFocusXml,
|
|
7205
7727
|
buildWhiteLedStateXml,
|
|
7728
|
+
captureModelFixtures,
|
|
7206
7729
|
collectCgiDiagnostics,
|
|
7207
7730
|
collectMultifocalDiagnostics,
|
|
7208
7731
|
collectNativeDiagnostics,
|
|
@@ -7284,6 +7807,7 @@ export {
|
|
|
7284
7807
|
splitAnnexBToNalPayloads2 as splitH265AnnexBToNalPayloads,
|
|
7285
7808
|
testChannelStreams,
|
|
7286
7809
|
xmlEscape,
|
|
7810
|
+
xmlIndicatesFloodlight,
|
|
7287
7811
|
zipDirectory
|
|
7288
7812
|
};
|
|
7289
7813
|
//# sourceMappingURL=index.js.map
|