@apocaliss92/nodelink-js 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -82
- package/dist/{chunk-YSEFEQYV.js → chunk-UDS2UR4S.js} +33 -2
- package/dist/chunk-UDS2UR4S.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +32 -1
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +569 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -1
- package/dist/index.d.ts +115 -0
- package/dist/index.js +535 -15
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/chunk-YSEFEQYV.js.map +0 -1
package/dist/cli/rtsp-server.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -7444,6 +7444,7 @@ __export(index_exports, {
|
|
|
7444
7444
|
DUAL_LENS_DUAL_MOTION_MODELS: () => DUAL_LENS_DUAL_MOTION_MODELS,
|
|
7445
7445
|
DUAL_LENS_MODELS: () => DUAL_LENS_MODELS,
|
|
7446
7446
|
DUAL_LENS_SINGLE_MOTION_MODELS: () => DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
7447
|
+
Go2rtcTcpServer: () => Go2rtcTcpServer,
|
|
7447
7448
|
H264RtpDepacketizer: () => H264RtpDepacketizer,
|
|
7448
7449
|
H265RtpDepacketizer: () => H265RtpDepacketizer,
|
|
7449
7450
|
HlsSessionManager: () => HlsSessionManager,
|
|
@@ -9261,6 +9262,22 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
9261
9262
|
static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
|
|
9262
9263
|
static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
|
|
9263
9264
|
static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
|
|
9265
|
+
/**
|
|
9266
|
+
* Per-client snapshot (cmd_id=109) serialization queue.
|
|
9267
|
+
*
|
|
9268
|
+
* WHY: On NVR/multi-camera devices sharing one socket, concurrent snapshot requests
|
|
9269
|
+
* can cause JPEG data to mix (even with per-request msgNum filtering):
|
|
9270
|
+
* - Camera A and B both send frames on same socket
|
|
9271
|
+
* - Frame listener is global per socket
|
|
9272
|
+
* - Timing quirks can cause chunk reordering or listener confusion
|
|
9273
|
+
*
|
|
9274
|
+
* FIX: Serialize all cmd_id=109 requests on THIS client instance.
|
|
9275
|
+
* Each snapshot waits for previous one to complete before starting.
|
|
9276
|
+
* This ensures clean frame sequences per request, zero data corruption.
|
|
9277
|
+
*
|
|
9278
|
+
* Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
|
|
9279
|
+
*/
|
|
9280
|
+
snapshotQueueTail = Promise.resolve();
|
|
9264
9281
|
opts;
|
|
9265
9282
|
debugCfg;
|
|
9266
9283
|
logger;
|
|
@@ -11742,6 +11759,20 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11742
11759
|
});
|
|
11743
11760
|
}
|
|
11744
11761
|
async sendBinarySnapshot109(params) {
|
|
11762
|
+
const prevTail = this.snapshotQueueTail;
|
|
11763
|
+
let resolve;
|
|
11764
|
+
const newTail = new Promise((r) => {
|
|
11765
|
+
resolve = r;
|
|
11766
|
+
});
|
|
11767
|
+
this.snapshotQueueTail = newTail;
|
|
11768
|
+
try {
|
|
11769
|
+
await prevTail;
|
|
11770
|
+
return await this.sendBinarySnapshot109Impl(params);
|
|
11771
|
+
} finally {
|
|
11772
|
+
resolve();
|
|
11773
|
+
}
|
|
11774
|
+
}
|
|
11775
|
+
async sendBinarySnapshot109Impl(params) {
|
|
11745
11776
|
await this.connect();
|
|
11746
11777
|
const channel = params.channel ?? this.opts.channel ?? 0;
|
|
11747
11778
|
const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
|
|
@@ -11801,7 +11832,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11801
11832
|
};
|
|
11802
11833
|
const onFrame = (frame) => {
|
|
11803
11834
|
if (frame.header.cmdId !== cmdId) return;
|
|
11804
|
-
if (frame.header.msgNum
|
|
11835
|
+
if (frame.header.msgNum !== msgNum) return;
|
|
11836
|
+
if (frame.header.responseCode >= 400) {
|
|
11805
11837
|
fail(
|
|
11806
11838
|
new Error(
|
|
11807
11839
|
`Baichuan snapshot request rejected (cmdId=${cmdId} msgNum=${msgNum} responseCode=${frame.header.responseCode})`
|
|
@@ -32724,8 +32756,529 @@ async function createReplayHttpServer(options) {
|
|
|
32724
32756
|
// src/index.ts
|
|
32725
32757
|
init_BaichuanVideoStream();
|
|
32726
32758
|
|
|
32727
|
-
// src/baichuan/stream/
|
|
32759
|
+
// src/baichuan/stream/Go2rtcTcpServer.ts
|
|
32728
32760
|
var import_node_events6 = require("events");
|
|
32761
|
+
var net4 = __toESM(require("net"), 1);
|
|
32762
|
+
init_H264Converter();
|
|
32763
|
+
init_H265Converter();
|
|
32764
|
+
var AsyncBoundedQueue2 = class {
|
|
32765
|
+
maxItems;
|
|
32766
|
+
queue = [];
|
|
32767
|
+
waiting;
|
|
32768
|
+
closed = false;
|
|
32769
|
+
constructor(maxItems) {
|
|
32770
|
+
this.maxItems = Math.max(1, maxItems | 0);
|
|
32771
|
+
}
|
|
32772
|
+
push(item) {
|
|
32773
|
+
if (this.closed) return;
|
|
32774
|
+
if (this.waiting) {
|
|
32775
|
+
const { resolve } = this.waiting;
|
|
32776
|
+
this.waiting = void 0;
|
|
32777
|
+
resolve({ value: item, done: false });
|
|
32778
|
+
return;
|
|
32779
|
+
}
|
|
32780
|
+
this.queue.push(item);
|
|
32781
|
+
if (this.queue.length > this.maxItems) {
|
|
32782
|
+
this.queue.splice(0, this.queue.length - this.maxItems);
|
|
32783
|
+
}
|
|
32784
|
+
}
|
|
32785
|
+
close() {
|
|
32786
|
+
if (this.closed) return;
|
|
32787
|
+
this.closed = true;
|
|
32788
|
+
if (this.waiting) {
|
|
32789
|
+
const { resolve } = this.waiting;
|
|
32790
|
+
this.waiting = void 0;
|
|
32791
|
+
resolve({ value: void 0, done: true });
|
|
32792
|
+
}
|
|
32793
|
+
}
|
|
32794
|
+
async next() {
|
|
32795
|
+
if (this.closed) return { value: void 0, done: true };
|
|
32796
|
+
const item = this.queue.shift();
|
|
32797
|
+
if (item !== void 0) return { value: item, done: false };
|
|
32798
|
+
return await new Promise((resolve) => {
|
|
32799
|
+
this.waiting = { resolve };
|
|
32800
|
+
});
|
|
32801
|
+
}
|
|
32802
|
+
};
|
|
32803
|
+
var NativeStreamFanout2 = class {
|
|
32804
|
+
opts;
|
|
32805
|
+
queues = /* @__PURE__ */ new Map();
|
|
32806
|
+
source = null;
|
|
32807
|
+
running = false;
|
|
32808
|
+
pumpPromise = null;
|
|
32809
|
+
constructor(opts) {
|
|
32810
|
+
this.opts = opts;
|
|
32811
|
+
}
|
|
32812
|
+
start() {
|
|
32813
|
+
if (this.running) return;
|
|
32814
|
+
this.running = true;
|
|
32815
|
+
this.source = this.opts.createSource();
|
|
32816
|
+
this.pumpPromise = (async () => {
|
|
32817
|
+
try {
|
|
32818
|
+
for await (const frame of this.source) {
|
|
32819
|
+
try {
|
|
32820
|
+
this.opts.onFrame?.(frame);
|
|
32821
|
+
} catch {
|
|
32822
|
+
}
|
|
32823
|
+
for (const q of this.queues.values()) {
|
|
32824
|
+
q.push(frame);
|
|
32825
|
+
}
|
|
32826
|
+
}
|
|
32827
|
+
} catch (e) {
|
|
32828
|
+
this.opts.onError?.(e);
|
|
32829
|
+
} finally {
|
|
32830
|
+
for (const q of this.queues.values()) q.close();
|
|
32831
|
+
this.queues.clear();
|
|
32832
|
+
this.running = false;
|
|
32833
|
+
this.opts.onEnd?.();
|
|
32834
|
+
}
|
|
32835
|
+
})();
|
|
32836
|
+
}
|
|
32837
|
+
subscribe(id) {
|
|
32838
|
+
const q = new AsyncBoundedQueue2(this.opts.maxQueueItems);
|
|
32839
|
+
this.queues.set(id, q);
|
|
32840
|
+
const self = this;
|
|
32841
|
+
return (async function* () {
|
|
32842
|
+
try {
|
|
32843
|
+
while (true) {
|
|
32844
|
+
const r = await q.next();
|
|
32845
|
+
if (r.done) return;
|
|
32846
|
+
yield r.value;
|
|
32847
|
+
}
|
|
32848
|
+
} finally {
|
|
32849
|
+
q.close();
|
|
32850
|
+
self.queues.delete(id);
|
|
32851
|
+
}
|
|
32852
|
+
})();
|
|
32853
|
+
}
|
|
32854
|
+
async stop() {
|
|
32855
|
+
if (!this.running) return;
|
|
32856
|
+
this.running = false;
|
|
32857
|
+
const src = this.source;
|
|
32858
|
+
this.source = null;
|
|
32859
|
+
for (const q of this.queues.values()) q.close();
|
|
32860
|
+
this.queues.clear();
|
|
32861
|
+
try {
|
|
32862
|
+
await src?.return(void 0);
|
|
32863
|
+
} catch {
|
|
32864
|
+
}
|
|
32865
|
+
try {
|
|
32866
|
+
await this.pumpPromise;
|
|
32867
|
+
} catch {
|
|
32868
|
+
}
|
|
32869
|
+
this.pumpPromise = null;
|
|
32870
|
+
}
|
|
32871
|
+
};
|
|
32872
|
+
var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEmitter {
|
|
32873
|
+
api;
|
|
32874
|
+
channel;
|
|
32875
|
+
profile;
|
|
32876
|
+
variant;
|
|
32877
|
+
listenHost;
|
|
32878
|
+
listenPort;
|
|
32879
|
+
logger;
|
|
32880
|
+
deviceId;
|
|
32881
|
+
gracePeriodMs;
|
|
32882
|
+
prebufferMaxMs;
|
|
32883
|
+
maxBufferBytes;
|
|
32884
|
+
prestartStream;
|
|
32885
|
+
active = false;
|
|
32886
|
+
server;
|
|
32887
|
+
resolvedPort;
|
|
32888
|
+
// Native stream
|
|
32889
|
+
nativeFanout = null;
|
|
32890
|
+
nativeStreamActive = false;
|
|
32891
|
+
dedicatedSessionRelease;
|
|
32892
|
+
detectedVideoType;
|
|
32893
|
+
// Client tracking
|
|
32894
|
+
connectedClients = /* @__PURE__ */ new Set();
|
|
32895
|
+
clientSockets = /* @__PURE__ */ new Map();
|
|
32896
|
+
stopGraceTimer;
|
|
32897
|
+
// Prebuffer
|
|
32898
|
+
prebuffer = [];
|
|
32899
|
+
constructor(options) {
|
|
32900
|
+
super();
|
|
32901
|
+
this.api = options.api;
|
|
32902
|
+
this.channel = options.channel;
|
|
32903
|
+
this.profile = options.profile;
|
|
32904
|
+
this.variant = options.variant ?? "default";
|
|
32905
|
+
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
32906
|
+
this.listenPort = options.listenPort ?? 0;
|
|
32907
|
+
this.logger = options.logger ?? console;
|
|
32908
|
+
this.deviceId = options.deviceId;
|
|
32909
|
+
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
32910
|
+
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
32911
|
+
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
32912
|
+
this.prestartStream = options.prestartStream ?? true;
|
|
32913
|
+
}
|
|
32914
|
+
// -----------------------------------------------------------------------
|
|
32915
|
+
// Public API
|
|
32916
|
+
// -----------------------------------------------------------------------
|
|
32917
|
+
/** Start listening. Resolves once the TCP server is bound. */
|
|
32918
|
+
async start() {
|
|
32919
|
+
if (this.active) return;
|
|
32920
|
+
this.active = true;
|
|
32921
|
+
this.server = net4.createServer((socket) => this.handleClient(socket));
|
|
32922
|
+
this.server.on("error", (err) => {
|
|
32923
|
+
this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
|
|
32924
|
+
this.emit("error", err);
|
|
32925
|
+
});
|
|
32926
|
+
await new Promise((resolve, reject) => {
|
|
32927
|
+
this.server.listen(this.listenPort, this.listenHost, () => {
|
|
32928
|
+
const addr = this.server.address();
|
|
32929
|
+
this.resolvedPort = addr.port;
|
|
32930
|
+
this.logger.info?.(
|
|
32931
|
+
`[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
|
|
32932
|
+
);
|
|
32933
|
+
this.emit("listening", { host: addr.address, port: addr.port });
|
|
32934
|
+
resolve();
|
|
32935
|
+
});
|
|
32936
|
+
this.server.once("error", reject);
|
|
32937
|
+
});
|
|
32938
|
+
if (this.prestartStream) {
|
|
32939
|
+
this.logger.info?.(
|
|
32940
|
+
`[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
|
|
32941
|
+
);
|
|
32942
|
+
this.startNativeStream();
|
|
32943
|
+
}
|
|
32944
|
+
}
|
|
32945
|
+
/** Stop the server and all active streams. */
|
|
32946
|
+
async stop() {
|
|
32947
|
+
if (!this.active) return;
|
|
32948
|
+
this.active = false;
|
|
32949
|
+
clearTimeout(this.stopGraceTimer);
|
|
32950
|
+
for (const [id, sock] of this.clientSockets) {
|
|
32951
|
+
sock.destroy();
|
|
32952
|
+
this.connectedClients.delete(id);
|
|
32953
|
+
}
|
|
32954
|
+
this.clientSockets.clear();
|
|
32955
|
+
await this.stopNativeStream();
|
|
32956
|
+
if (this.server) {
|
|
32957
|
+
await new Promise((resolve) => {
|
|
32958
|
+
this.server.close(() => resolve());
|
|
32959
|
+
});
|
|
32960
|
+
this.server = void 0;
|
|
32961
|
+
}
|
|
32962
|
+
this.prebuffer = [];
|
|
32963
|
+
this.resolvedPort = void 0;
|
|
32964
|
+
this.emit("close");
|
|
32965
|
+
}
|
|
32966
|
+
/** The actual port the server is listening on (available after start()). */
|
|
32967
|
+
get port() {
|
|
32968
|
+
return this.resolvedPort;
|
|
32969
|
+
}
|
|
32970
|
+
/** The go2rtc-compatible source URL. */
|
|
32971
|
+
get go2rtcSourceUrl() {
|
|
32972
|
+
if (this.resolvedPort == null) return void 0;
|
|
32973
|
+
return `tcp://127.0.0.1:${this.resolvedPort}`;
|
|
32974
|
+
}
|
|
32975
|
+
/** Number of currently connected clients. */
|
|
32976
|
+
get clientCount() {
|
|
32977
|
+
return this.connectedClients.size;
|
|
32978
|
+
}
|
|
32979
|
+
// -----------------------------------------------------------------------
|
|
32980
|
+
// Client handling
|
|
32981
|
+
// -----------------------------------------------------------------------
|
|
32982
|
+
handleClient(socket) {
|
|
32983
|
+
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
32984
|
+
socket.setNoDelay(true);
|
|
32985
|
+
this.connectedClients.add(clientId);
|
|
32986
|
+
this.clientSockets.set(clientId, socket);
|
|
32987
|
+
this.logger.info?.(
|
|
32988
|
+
`[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
|
|
32989
|
+
);
|
|
32990
|
+
this.emit("client", clientId);
|
|
32991
|
+
if (this.stopGraceTimer) {
|
|
32992
|
+
clearTimeout(this.stopGraceTimer);
|
|
32993
|
+
this.stopGraceTimer = void 0;
|
|
32994
|
+
}
|
|
32995
|
+
if (!this.nativeStreamActive) {
|
|
32996
|
+
this.startNativeStream();
|
|
32997
|
+
}
|
|
32998
|
+
this.feedClient(clientId, socket).catch((err) => {
|
|
32999
|
+
this.logger.warn?.(
|
|
33000
|
+
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
33001
|
+
);
|
|
33002
|
+
});
|
|
33003
|
+
const cleanup = () => {
|
|
33004
|
+
this.removeClient(clientId);
|
|
33005
|
+
socket.destroy();
|
|
33006
|
+
};
|
|
33007
|
+
socket.on("error", cleanup);
|
|
33008
|
+
socket.on("close", cleanup);
|
|
33009
|
+
}
|
|
33010
|
+
async feedClient(clientId, socket) {
|
|
33011
|
+
const fanoutDeadline = Date.now() + 3e4;
|
|
33012
|
+
while (this.active && !this.nativeFanout) {
|
|
33013
|
+
if (socket.destroyed) return;
|
|
33014
|
+
if (Date.now() > fanoutDeadline) {
|
|
33015
|
+
this.logger.warn?.(
|
|
33016
|
+
`[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
|
|
33017
|
+
);
|
|
33018
|
+
return;
|
|
33019
|
+
}
|
|
33020
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
33021
|
+
}
|
|
33022
|
+
if (!this.active || !this.nativeFanout) return;
|
|
33023
|
+
const subscription = this.nativeFanout.subscribe(clientId);
|
|
33024
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
33025
|
+
let lastIdrIdx = -1;
|
|
33026
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
33027
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
33028
|
+
lastIdrIdx = i;
|
|
33029
|
+
break;
|
|
33030
|
+
}
|
|
33031
|
+
}
|
|
33032
|
+
if (lastIdrIdx >= 0) {
|
|
33033
|
+
const replay = prebufferSnap.slice(lastIdrIdx);
|
|
33034
|
+
this.logger.info?.(
|
|
33035
|
+
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
33036
|
+
);
|
|
33037
|
+
for (const entry of replay) {
|
|
33038
|
+
if (socket.destroyed) return;
|
|
33039
|
+
socket.write(entry.data);
|
|
33040
|
+
}
|
|
33041
|
+
}
|
|
33042
|
+
let seenKeyframe = lastIdrIdx >= 0;
|
|
33043
|
+
let liveFrameCount = 0;
|
|
33044
|
+
let liveVideoWritten = 0;
|
|
33045
|
+
let lastLogAt = Date.now();
|
|
33046
|
+
try {
|
|
33047
|
+
this.logger.info?.(
|
|
33048
|
+
`[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
|
|
33049
|
+
);
|
|
33050
|
+
for await (const frame of subscription) {
|
|
33051
|
+
if (socket.destroyed || !this.active) {
|
|
33052
|
+
this.logger.info?.(
|
|
33053
|
+
`[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
|
|
33054
|
+
);
|
|
33055
|
+
break;
|
|
33056
|
+
}
|
|
33057
|
+
liveFrameCount++;
|
|
33058
|
+
const annexB = this.convertFrame(frame);
|
|
33059
|
+
if (!annexB) continue;
|
|
33060
|
+
if (!seenKeyframe) {
|
|
33061
|
+
if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
|
|
33062
|
+
seenKeyframe = true;
|
|
33063
|
+
this.logger.info?.(
|
|
33064
|
+
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
33065
|
+
);
|
|
33066
|
+
}
|
|
33067
|
+
socket.write(annexB);
|
|
33068
|
+
liveVideoWritten++;
|
|
33069
|
+
if (Date.now() - lastLogAt > 1e4) {
|
|
33070
|
+
this.logger.info?.(
|
|
33071
|
+
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
33072
|
+
);
|
|
33073
|
+
lastLogAt = Date.now();
|
|
33074
|
+
}
|
|
33075
|
+
if (socket.writableLength > this.maxBufferBytes) {
|
|
33076
|
+
this.logger.warn?.(
|
|
33077
|
+
`[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
|
|
33078
|
+
);
|
|
33079
|
+
socket.destroy();
|
|
33080
|
+
break;
|
|
33081
|
+
}
|
|
33082
|
+
}
|
|
33083
|
+
this.logger.info?.(
|
|
33084
|
+
`[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
|
|
33085
|
+
);
|
|
33086
|
+
} finally {
|
|
33087
|
+
await subscription.return(void 0).catch(() => {
|
|
33088
|
+
});
|
|
33089
|
+
}
|
|
33090
|
+
}
|
|
33091
|
+
// -----------------------------------------------------------------------
|
|
33092
|
+
// Frame conversion
|
|
33093
|
+
// -----------------------------------------------------------------------
|
|
33094
|
+
/**
|
|
33095
|
+
* Convert a native frame to wire-ready Annex-B.
|
|
33096
|
+
* Audio frames are skipped — raw TCP carries only video (Annex-B).
|
|
33097
|
+
* go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
|
|
33098
|
+
*/
|
|
33099
|
+
convertFrame(frame) {
|
|
33100
|
+
if (frame.audio) {
|
|
33101
|
+
return null;
|
|
33102
|
+
}
|
|
33103
|
+
if (frame.data.length === 0) return null;
|
|
33104
|
+
try {
|
|
33105
|
+
if (frame.videoType === "H264") {
|
|
33106
|
+
return convertToAnnexB(frame.data);
|
|
33107
|
+
}
|
|
33108
|
+
if (frame.videoType === "H265") {
|
|
33109
|
+
return convertToAnnexB2(frame.data);
|
|
33110
|
+
}
|
|
33111
|
+
} catch {
|
|
33112
|
+
}
|
|
33113
|
+
return frame.data;
|
|
33114
|
+
}
|
|
33115
|
+
/** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
|
|
33116
|
+
isAnnexBKeyframe(annexB, videoType) {
|
|
33117
|
+
try {
|
|
33118
|
+
if (videoType === "H264") {
|
|
33119
|
+
const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
|
|
33120
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
33121
|
+
}
|
|
33122
|
+
if (videoType === "H265") {
|
|
33123
|
+
const nals = splitAnnexBToNalPayloads2(annexB);
|
|
33124
|
+
return nals.some(
|
|
33125
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
33126
|
+
);
|
|
33127
|
+
}
|
|
33128
|
+
} catch {
|
|
33129
|
+
}
|
|
33130
|
+
return false;
|
|
33131
|
+
}
|
|
33132
|
+
/** Split Annex-B byte stream into individual NAL units. */
|
|
33133
|
+
static splitAnnexBNals(buf) {
|
|
33134
|
+
const nals = [];
|
|
33135
|
+
let i = 0;
|
|
33136
|
+
while (i < buf.length) {
|
|
33137
|
+
if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
|
|
33138
|
+
let scLen;
|
|
33139
|
+
if (buf[i + 2] === 1) {
|
|
33140
|
+
scLen = 3;
|
|
33141
|
+
} else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
|
|
33142
|
+
scLen = 4;
|
|
33143
|
+
} else {
|
|
33144
|
+
i++;
|
|
33145
|
+
continue;
|
|
33146
|
+
}
|
|
33147
|
+
const nalStart = i + scLen;
|
|
33148
|
+
let nalEnd = buf.length;
|
|
33149
|
+
for (let j = nalStart; j < buf.length - 2; j++) {
|
|
33150
|
+
if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
|
|
33151
|
+
nalEnd = j;
|
|
33152
|
+
break;
|
|
33153
|
+
}
|
|
33154
|
+
}
|
|
33155
|
+
if (nalEnd > nalStart) {
|
|
33156
|
+
nals.push(buf.subarray(nalStart, nalEnd));
|
|
33157
|
+
}
|
|
33158
|
+
i = nalEnd;
|
|
33159
|
+
} else {
|
|
33160
|
+
i++;
|
|
33161
|
+
}
|
|
33162
|
+
}
|
|
33163
|
+
return nals;
|
|
33164
|
+
}
|
|
33165
|
+
// -----------------------------------------------------------------------
|
|
33166
|
+
// Native stream management
|
|
33167
|
+
// -----------------------------------------------------------------------
|
|
33168
|
+
async startNativeStream() {
|
|
33169
|
+
if (this.nativeStreamActive) return;
|
|
33170
|
+
this.nativeStreamActive = true;
|
|
33171
|
+
let dedicatedClient;
|
|
33172
|
+
if (this.deviceId) {
|
|
33173
|
+
try {
|
|
33174
|
+
const session = await this.api.createDedicatedSession(
|
|
33175
|
+
`live:${this.deviceId}:ch${this.channel}:${this.profile}`
|
|
33176
|
+
);
|
|
33177
|
+
dedicatedClient = session.client;
|
|
33178
|
+
this.dedicatedSessionRelease = session.release;
|
|
33179
|
+
} catch (e) {
|
|
33180
|
+
this.logger.warn?.(
|
|
33181
|
+
`[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
|
|
33182
|
+
);
|
|
33183
|
+
}
|
|
33184
|
+
}
|
|
33185
|
+
this.logger.info?.(
|
|
33186
|
+
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
33187
|
+
);
|
|
33188
|
+
this.nativeFanout = new NativeStreamFanout2({
|
|
33189
|
+
maxQueueItems: 200,
|
|
33190
|
+
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
33191
|
+
variant: this.variant,
|
|
33192
|
+
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
33193
|
+
}),
|
|
33194
|
+
onFrame: (frame) => {
|
|
33195
|
+
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
33196
|
+
this.detectedVideoType = frame.videoType;
|
|
33197
|
+
}
|
|
33198
|
+
const wireData = this.convertFrame(frame);
|
|
33199
|
+
if (!wireData || wireData.length === 0) return;
|
|
33200
|
+
const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
|
|
33201
|
+
this.prebuffer.push({
|
|
33202
|
+
data: Buffer.from(wireData),
|
|
33203
|
+
time: Date.now(),
|
|
33204
|
+
isKeyframe,
|
|
33205
|
+
audio: frame.audio
|
|
33206
|
+
});
|
|
33207
|
+
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
33208
|
+
let trimIdx = 0;
|
|
33209
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
33210
|
+
trimIdx++;
|
|
33211
|
+
}
|
|
33212
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
33213
|
+
},
|
|
33214
|
+
onError: (error) => {
|
|
33215
|
+
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
33216
|
+
},
|
|
33217
|
+
onEnd: () => {
|
|
33218
|
+
if (!this.nativeStreamActive) return;
|
|
33219
|
+
this.nativeStreamActive = false;
|
|
33220
|
+
this.nativeFanout = null;
|
|
33221
|
+
if (this.dedicatedSessionRelease) {
|
|
33222
|
+
this.dedicatedSessionRelease().catch(() => {
|
|
33223
|
+
});
|
|
33224
|
+
this.dedicatedSessionRelease = void 0;
|
|
33225
|
+
}
|
|
33226
|
+
if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
|
|
33227
|
+
this.logger.info?.(
|
|
33228
|
+
`[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
33229
|
+
);
|
|
33230
|
+
this.startNativeStream();
|
|
33231
|
+
}
|
|
33232
|
+
}
|
|
33233
|
+
});
|
|
33234
|
+
this.nativeFanout.start();
|
|
33235
|
+
}
|
|
33236
|
+
async stopNativeStream() {
|
|
33237
|
+
this.nativeStreamActive = false;
|
|
33238
|
+
const fanout = this.nativeFanout;
|
|
33239
|
+
this.nativeFanout = null;
|
|
33240
|
+
if (fanout) {
|
|
33241
|
+
await fanout.stop();
|
|
33242
|
+
}
|
|
33243
|
+
this.prebuffer = [];
|
|
33244
|
+
if (this.dedicatedSessionRelease) {
|
|
33245
|
+
await this.dedicatedSessionRelease().catch(() => {
|
|
33246
|
+
});
|
|
33247
|
+
this.dedicatedSessionRelease = void 0;
|
|
33248
|
+
}
|
|
33249
|
+
}
|
|
33250
|
+
// -----------------------------------------------------------------------
|
|
33251
|
+
// Client lifecycle
|
|
33252
|
+
// -----------------------------------------------------------------------
|
|
33253
|
+
removeClient(clientId) {
|
|
33254
|
+
if (!this.connectedClients.has(clientId)) return;
|
|
33255
|
+
this.connectedClients.delete(clientId);
|
|
33256
|
+
this.clientSockets.delete(clientId);
|
|
33257
|
+
this.logger.info?.(
|
|
33258
|
+
`[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
|
|
33259
|
+
);
|
|
33260
|
+
this.emit("clientDisconnected", clientId);
|
|
33261
|
+
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
33262
|
+
this.scheduleStop();
|
|
33263
|
+
}
|
|
33264
|
+
}
|
|
33265
|
+
scheduleStop() {
|
|
33266
|
+
if (this.stopGraceTimer) return;
|
|
33267
|
+
this.logger.info?.(
|
|
33268
|
+
`[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
|
|
33269
|
+
);
|
|
33270
|
+
this.stopGraceTimer = setTimeout(async () => {
|
|
33271
|
+
this.stopGraceTimer = void 0;
|
|
33272
|
+
if (this.connectedClients.size === 0 && this.nativeStreamActive) {
|
|
33273
|
+
this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
|
|
33274
|
+
await this.stopNativeStream();
|
|
33275
|
+
}
|
|
33276
|
+
}, this.gracePeriodMs);
|
|
33277
|
+
}
|
|
33278
|
+
};
|
|
33279
|
+
|
|
33280
|
+
// src/baichuan/stream/BaichuanHttpStreamServer.ts
|
|
33281
|
+
var import_node_events7 = require("events");
|
|
32729
33282
|
var import_node_child_process9 = require("child_process");
|
|
32730
33283
|
var http4 = __toESM(require("http"), 1);
|
|
32731
33284
|
var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
|
|
@@ -32772,7 +33325,7 @@ function isH264KeyframeFromAnnexB(annexB) {
|
|
|
32772
33325
|
}
|
|
32773
33326
|
return false;
|
|
32774
33327
|
}
|
|
32775
|
-
var BaichuanHttpStreamServer = class extends
|
|
33328
|
+
var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
|
|
32776
33329
|
videoStream;
|
|
32777
33330
|
listenPort;
|
|
32778
33331
|
path;
|
|
@@ -33039,15 +33592,15 @@ var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
|
|
|
33039
33592
|
};
|
|
33040
33593
|
|
|
33041
33594
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
33042
|
-
var
|
|
33595
|
+
var import_node_events9 = require("events");
|
|
33043
33596
|
var http5 = __toESM(require("http"), 1);
|
|
33044
33597
|
|
|
33045
33598
|
// src/baichuan/stream/MjpegTransformer.ts
|
|
33046
|
-
var
|
|
33599
|
+
var import_node_events8 = require("events");
|
|
33047
33600
|
var import_node_child_process10 = require("child_process");
|
|
33048
33601
|
var JPEG_SOI = Buffer.from([255, 216]);
|
|
33049
33602
|
var JPEG_EOI = Buffer.from([255, 217]);
|
|
33050
|
-
var MjpegTransformer = class extends
|
|
33603
|
+
var MjpegTransformer = class extends import_node_events8.EventEmitter {
|
|
33051
33604
|
options;
|
|
33052
33605
|
ffmpeg = null;
|
|
33053
33606
|
started = false;
|
|
@@ -33246,7 +33799,7 @@ Content-Length: ${frame.length}\r
|
|
|
33246
33799
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
33247
33800
|
init_H264Converter();
|
|
33248
33801
|
init_H265Converter();
|
|
33249
|
-
var BaichuanMjpegServer = class extends
|
|
33802
|
+
var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
|
|
33250
33803
|
options;
|
|
33251
33804
|
clients = /* @__PURE__ */ new Map();
|
|
33252
33805
|
httpServer = null;
|
|
@@ -33527,7 +34080,7 @@ var BaichuanMjpegServer = class extends import_node_events8.EventEmitter {
|
|
|
33527
34080
|
};
|
|
33528
34081
|
|
|
33529
34082
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
33530
|
-
var
|
|
34083
|
+
var import_node_events10 = require("events");
|
|
33531
34084
|
init_BcMediaAnnexBDecoder();
|
|
33532
34085
|
init_H264Converter();
|
|
33533
34086
|
function parseAnnexBNalUnits(annexB) {
|
|
@@ -33564,7 +34117,7 @@ function getH264NalType(nalUnit) {
|
|
|
33564
34117
|
function getH265NalType2(nalUnit) {
|
|
33565
34118
|
return nalUnit[0] >> 1 & 63;
|
|
33566
34119
|
}
|
|
33567
|
-
var BaichuanWebRTCServer = class extends
|
|
34120
|
+
var BaichuanWebRTCServer = class extends import_node_events10.EventEmitter {
|
|
33568
34121
|
options;
|
|
33569
34122
|
sessions = /* @__PURE__ */ new Map();
|
|
33570
34123
|
sessionIdCounter = 0;
|
|
@@ -34466,7 +35019,7 @@ Error: ${err}`
|
|
|
34466
35019
|
};
|
|
34467
35020
|
|
|
34468
35021
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
34469
|
-
var
|
|
35022
|
+
var import_node_events11 = require("events");
|
|
34470
35023
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
34471
35024
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
34472
35025
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
@@ -34546,7 +35099,7 @@ function getNalTypes(codec, annexB) {
|
|
|
34546
35099
|
}
|
|
34547
35100
|
});
|
|
34548
35101
|
}
|
|
34549
|
-
var BaichuanHlsServer = class extends
|
|
35102
|
+
var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
|
|
34550
35103
|
api;
|
|
34551
35104
|
channel;
|
|
34552
35105
|
profile;
|
|
@@ -35547,10 +36100,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
35547
36100
|
}
|
|
35548
36101
|
|
|
35549
36102
|
// src/multifocal/compositeRtspServer.ts
|
|
35550
|
-
var
|
|
36103
|
+
var import_node_events12 = require("events");
|
|
35551
36104
|
var import_node_child_process12 = require("child_process");
|
|
35552
|
-
var
|
|
35553
|
-
var CompositeRtspServer = class extends
|
|
36105
|
+
var net5 = __toESM(require("net"), 1);
|
|
36106
|
+
var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
35554
36107
|
options;
|
|
35555
36108
|
compositeStream = null;
|
|
35556
36109
|
rtspServer = null;
|
|
@@ -35616,7 +36169,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
35616
36169
|
const width = widerStreamInfo?.width ?? 1920;
|
|
35617
36170
|
const height = widerStreamInfo?.height ?? 1080;
|
|
35618
36171
|
const fps = widerStreamInfo?.frameRate ?? 25;
|
|
35619
|
-
this.rtspServer =
|
|
36172
|
+
this.rtspServer = net5.createServer((socket) => {
|
|
35620
36173
|
this.handleRtspConnection(socket);
|
|
35621
36174
|
});
|
|
35622
36175
|
await new Promise((resolve, reject) => {
|
|
@@ -35892,6 +36445,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
35892
36445
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
35893
36446
|
DUAL_LENS_MODELS,
|
|
35894
36447
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
36448
|
+
Go2rtcTcpServer,
|
|
35895
36449
|
H264RtpDepacketizer,
|
|
35896
36450
|
H265RtpDepacketizer,
|
|
35897
36451
|
HlsSessionManager,
|