@apocaliss92/nodelink-js 0.2.2 → 0.2.4
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/{DiagnosticsTools-FNLGCOVA.js → DiagnosticsTools-2JQRV5FE.js} +2 -2
- package/dist/{chunk-NLTB7GTA.js → chunk-APEEZ4UN.js} +10 -9
- package/dist/{chunk-NLTB7GTA.js.map → chunk-APEEZ4UN.js.map} +1 -1
- package/dist/{chunk-MN7GUZT7.js → chunk-EG5IY3CM.js} +159 -20
- package/dist/chunk-EG5IY3CM.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +164 -24
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +226 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +64 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MN7GUZT7.js.map +0 -1
- /package/dist/{DiagnosticsTools-FNLGCOVA.js.map → DiagnosticsTools-2JQRV5FE.js.map} +0 -0
package/dist/cli/rtsp-server.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
BaichuanRtspServer,
|
|
4
4
|
ReolinkBaichuanApi,
|
|
5
5
|
autoDetectDeviceType
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-EG5IY3CM.js";
|
|
7
7
|
import {
|
|
8
8
|
__require
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-APEEZ4UN.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/rtsp-server.ts
|
|
12
12
|
function parseArgs() {
|
package/dist/index.cjs
CHANGED
|
@@ -3000,14 +3000,6 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3000
3000
|
this.profile,
|
|
3001
3001
|
{ variant: this.variant, client: this.client }
|
|
3002
3002
|
);
|
|
3003
|
-
if (this.client.getTransport?.() === "udp") {
|
|
3004
|
-
await startPromise;
|
|
3005
|
-
} else {
|
|
3006
|
-
await Promise.race([
|
|
3007
|
-
startPromise,
|
|
3008
|
-
new Promise((resolve) => setTimeout(resolve, 400))
|
|
3009
|
-
]);
|
|
3010
|
-
}
|
|
3011
3003
|
const updateActiveMsgNum = () => {
|
|
3012
3004
|
try {
|
|
3013
3005
|
const getMsgNum = this.api.getActiveVideoMsgNumWithVariant;
|
|
@@ -3017,6 +3009,15 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3017
3009
|
}
|
|
3018
3010
|
};
|
|
3019
3011
|
updateActiveMsgNum();
|
|
3012
|
+
if (this.client.getTransport?.() === "udp") {
|
|
3013
|
+
await startPromise;
|
|
3014
|
+
} else {
|
|
3015
|
+
await Promise.race([
|
|
3016
|
+
startPromise,
|
|
3017
|
+
new Promise((resolve) => setTimeout(resolve, 400))
|
|
3018
|
+
]);
|
|
3019
|
+
}
|
|
3020
|
+
updateActiveMsgNum();
|
|
3020
3021
|
void startPromise.then(() => updateActiveMsgNum()).catch((e) => {
|
|
3021
3022
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
3022
3023
|
this.emitSafeError(err);
|
|
@@ -12542,8 +12543,24 @@ var BaichuanEventEmitter = class {
|
|
|
12542
12543
|
}
|
|
12543
12544
|
};
|
|
12544
12545
|
async function* createNativeStream(api, channel, profile, options) {
|
|
12546
|
+
let client = options?.client;
|
|
12547
|
+
let dedicatedRelease;
|
|
12548
|
+
if (!client) {
|
|
12549
|
+
const variantSuffix = options?.variant && options.variant !== "default" ? `:${options.variant}` : "";
|
|
12550
|
+
const sessionKey = `live:native${variantSuffix}:ch${channel}:${profile}`;
|
|
12551
|
+
try {
|
|
12552
|
+
api.logger?.info?.(`[createNativeStream] acquiring dedicated session key=${sessionKey}`);
|
|
12553
|
+
const session = await api.createDedicatedSession(sessionKey);
|
|
12554
|
+
client = session.client;
|
|
12555
|
+
dedicatedRelease = session.release;
|
|
12556
|
+
api.logger?.info?.(`[createNativeStream] dedicated session acquired key=${sessionKey}`);
|
|
12557
|
+
} catch (e) {
|
|
12558
|
+
api.logger?.warn?.(`[createNativeStream] dedicated session failed, using shared client: ${e instanceof Error ? e.message : e}`);
|
|
12559
|
+
client = api.client;
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12545
12562
|
const videoStream = new BaichuanVideoStream({
|
|
12546
|
-
client
|
|
12563
|
+
client,
|
|
12547
12564
|
api,
|
|
12548
12565
|
channel,
|
|
12549
12566
|
profile,
|
|
@@ -12557,9 +12574,15 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
12557
12574
|
let closed = false;
|
|
12558
12575
|
const onError = (_error) => {
|
|
12559
12576
|
closed = true;
|
|
12577
|
+
api.logger?.warn?.(
|
|
12578
|
+
`[createNativeStream] stream error \u2192 closed channel=${channel} profile=${profile} error=${_error?.message ?? _error}`
|
|
12579
|
+
);
|
|
12560
12580
|
};
|
|
12561
12581
|
const onClose = () => {
|
|
12562
12582
|
closed = true;
|
|
12583
|
+
api.logger?.warn?.(
|
|
12584
|
+
`[createNativeStream] stream close \u2192 closed channel=${channel} profile=${profile}`
|
|
12585
|
+
);
|
|
12563
12586
|
};
|
|
12564
12587
|
try {
|
|
12565
12588
|
videoStream.on("error", onError);
|
|
@@ -12670,6 +12693,10 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
12670
12693
|
}
|
|
12671
12694
|
videoStream.removeListener("error", onError);
|
|
12672
12695
|
videoStream.removeListener("close", onClose);
|
|
12696
|
+
if (dedicatedRelease) {
|
|
12697
|
+
dedicatedRelease().catch(() => {
|
|
12698
|
+
});
|
|
12699
|
+
}
|
|
12673
12700
|
}
|
|
12674
12701
|
}
|
|
12675
12702
|
|
|
@@ -12909,6 +12936,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
12909
12936
|
tcpRtpFraming;
|
|
12910
12937
|
active = false;
|
|
12911
12938
|
flow;
|
|
12939
|
+
deviceId;
|
|
12940
|
+
dedicatedSessionRelease;
|
|
12912
12941
|
// Authentication
|
|
12913
12942
|
authCredentials = [];
|
|
12914
12943
|
requireAuth;
|
|
@@ -12951,6 +12980,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
12951
12980
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
12952
12981
|
nativeFanout = null;
|
|
12953
12982
|
noClientAutoStopTimer;
|
|
12983
|
+
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
12984
|
+
// When a new client connects while the stream is already running it does not need
|
|
12985
|
+
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
12986
|
+
// from the last IDR in the prebuffer immediately.
|
|
12987
|
+
PREBUFFER_MAX_MS = 3e3;
|
|
12988
|
+
prebuffer = [];
|
|
12954
12989
|
static isAdtsAacFrame(b) {
|
|
12955
12990
|
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
12956
12991
|
}
|
|
@@ -12985,6 +13020,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
12985
13020
|
);
|
|
12986
13021
|
return { sampleRate, channels, configHex };
|
|
12987
13022
|
}
|
|
13023
|
+
/** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
|
|
13024
|
+
isRawFrameKeyframe(frame) {
|
|
13025
|
+
try {
|
|
13026
|
+
if (frame.videoType === "H264") {
|
|
13027
|
+
const nals = _BaichuanRtspServer.splitAnnexBNals(
|
|
13028
|
+
convertToAnnexB(frame.data)
|
|
13029
|
+
);
|
|
13030
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
13031
|
+
}
|
|
13032
|
+
if (frame.videoType === "H265") {
|
|
13033
|
+
const nals = splitAnnexBToNalPayloads2(convertToAnnexB2(frame.data));
|
|
13034
|
+
return nals.some(
|
|
13035
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
13036
|
+
);
|
|
13037
|
+
}
|
|
13038
|
+
} catch {
|
|
13039
|
+
}
|
|
13040
|
+
return false;
|
|
13041
|
+
}
|
|
12988
13042
|
static parseInterleavedChannels(transportHeader) {
|
|
12989
13043
|
const m = transportHeader.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
|
|
12990
13044
|
if (!m) return null;
|
|
@@ -13050,6 +13104,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13050
13104
|
this.path = options.path ?? `/stream/${this.profile}`;
|
|
13051
13105
|
this.logger = options.logger ?? console;
|
|
13052
13106
|
this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
|
|
13107
|
+
this.deviceId = options.deviceId;
|
|
13053
13108
|
this.authCredentials = options.credentials ?? [];
|
|
13054
13109
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
13055
13110
|
const transport = this.api.client.getTransport();
|
|
@@ -13379,7 +13434,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13379
13434
|
}
|
|
13380
13435
|
const { hasParamSets: hasParamSets2 } = this.flow.getFmtp();
|
|
13381
13436
|
if (!hasParamSets2) {
|
|
13382
|
-
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 :
|
|
13437
|
+
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 3e3;
|
|
13438
|
+
const primingStart = Date.now();
|
|
13439
|
+
this.logger.info(
|
|
13440
|
+
`[rebroadcast] DESCRIBE priming: waiting up to ${primingMs}ms for SPS/PPS client=${clientId} path=${this.path}`
|
|
13441
|
+
);
|
|
13383
13442
|
try {
|
|
13384
13443
|
await Promise.race([
|
|
13385
13444
|
this.firstFramePromise || Promise.resolve(),
|
|
@@ -13387,6 +13446,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13387
13446
|
]);
|
|
13388
13447
|
} catch {
|
|
13389
13448
|
}
|
|
13449
|
+
const primingElapsed = Date.now() - primingStart;
|
|
13450
|
+
const { hasParamSets: hasParamSetsAfter } = this.flow.getFmtp();
|
|
13451
|
+
if (hasParamSetsAfter) {
|
|
13452
|
+
this.logger.info(
|
|
13453
|
+
`[rebroadcast] DESCRIBE priming: SPS/PPS received after ${primingElapsed}ms client=${clientId} path=${this.path}`
|
|
13454
|
+
);
|
|
13455
|
+
} else {
|
|
13456
|
+
this.logger.warn(
|
|
13457
|
+
`[rebroadcast] DESCRIBE priming: timed out after ${primingElapsed}ms without SPS/PPS \u2014 SDP will lack sprop-parameter-sets, downstream decoder may hang client=${clientId} path=${this.path}`
|
|
13458
|
+
);
|
|
13459
|
+
}
|
|
13390
13460
|
}
|
|
13391
13461
|
}
|
|
13392
13462
|
{
|
|
@@ -13395,11 +13465,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13395
13465
|
this.logger.info(
|
|
13396
13466
|
`[BaichuanRtspServer] DESCRIBE SDP for ${clientId} path=${this.path} codec=${this.flow.sdpCodec} hasParamSets=${hasParamSets2} fmtp=${fmtpPreview}`
|
|
13397
13467
|
);
|
|
13398
|
-
if (!hasParamSets2) {
|
|
13399
|
-
this.rtspDebugLog(
|
|
13400
|
-
`DESCRIBE responding without parameter sets yet (client=${clientId}, path=${this.path}, flow=${this.flow.key})`
|
|
13401
|
-
);
|
|
13402
|
-
}
|
|
13403
13468
|
}
|
|
13404
13469
|
const sdp = this.generateSdp();
|
|
13405
13470
|
sendResponse(
|
|
@@ -13604,10 +13669,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13604
13669
|
sdp += `a=control:track1\r
|
|
13605
13670
|
`;
|
|
13606
13671
|
}
|
|
13607
|
-
sdp += `a=setup:passive\r
|
|
13608
|
-
`;
|
|
13609
|
-
sdp += `a=connection:new\r
|
|
13610
|
-
`;
|
|
13611
13672
|
return sdp;
|
|
13612
13673
|
}
|
|
13613
13674
|
/**
|
|
@@ -13678,6 +13739,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13678
13739
|
return false;
|
|
13679
13740
|
if (channel === audioRtpChannel && !resources2?.setupTrack1)
|
|
13680
13741
|
return false;
|
|
13742
|
+
const buffered = rtspSocket.writableLength;
|
|
13743
|
+
if (buffered > 10 * 1024 * 1024) {
|
|
13744
|
+
this.logger.warn(
|
|
13745
|
+
`[rebroadcast] backpressure: ${Math.round(buffered / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
|
|
13746
|
+
);
|
|
13747
|
+
rtspSocket.destroy();
|
|
13748
|
+
return false;
|
|
13749
|
+
}
|
|
13681
13750
|
try {
|
|
13682
13751
|
return rtspSocket.write(frameRtpOverTcp(channel, msg));
|
|
13683
13752
|
} catch (error) {
|
|
@@ -14107,6 +14176,24 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14107
14176
|
let frameCount = 0;
|
|
14108
14177
|
let lastFrameTime = Date.now();
|
|
14109
14178
|
const targetFrameInterval = streamMetadata && streamMetadata.frameRate > 0 ? 1e3 / streamMetadata.frameRate : 40;
|
|
14179
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
14180
|
+
let lastIdrIdx = -1;
|
|
14181
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
14182
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
14183
|
+
lastIdrIdx = i;
|
|
14184
|
+
break;
|
|
14185
|
+
}
|
|
14186
|
+
}
|
|
14187
|
+
const prebufferFrames = lastIdrIdx >= 0 ? prebufferSnap.slice(lastIdrIdx) : [];
|
|
14188
|
+
if (prebufferFrames.length > 0) {
|
|
14189
|
+
this.logger.info(
|
|
14190
|
+
`[rebroadcast] prebuffer replay client=${clientId} frames=${prebufferFrames.length} starting from IDR`
|
|
14191
|
+
);
|
|
14192
|
+
}
|
|
14193
|
+
const combined = async function* () {
|
|
14194
|
+
for (const entry of prebufferFrames) yield entry.frame;
|
|
14195
|
+
for await (const f of clientGenerator) yield f;
|
|
14196
|
+
};
|
|
14110
14197
|
const feedFrames = async () => {
|
|
14111
14198
|
try {
|
|
14112
14199
|
this.rtspDebugLog(
|
|
@@ -14118,7 +14205,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14118
14205
|
let firstVideoFrameSeenLogged = false;
|
|
14119
14206
|
let h265WaitParamSetsLogged = false;
|
|
14120
14207
|
let h265WaitIrapLogged = false;
|
|
14121
|
-
for await (const frame of
|
|
14208
|
+
for await (const frame of combined()) {
|
|
14122
14209
|
if (!this.connectedClients.has(clientId)) {
|
|
14123
14210
|
this.rtspDebugLog(
|
|
14124
14211
|
`Client ${clientId} disconnected, stopping frame feed`
|
|
@@ -14403,14 +14490,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14403
14490
|
this.firstAudioPromise = new Promise((resolve) => {
|
|
14404
14491
|
this.firstAudioResolve = resolve;
|
|
14405
14492
|
});
|
|
14493
|
+
let dedicatedClient;
|
|
14494
|
+
const variantSuffix = this.variant && this.variant !== "default" ? `:${this.variant}` : "";
|
|
14495
|
+
const deviceIdPart = this.deviceId ?? "rtsp-server";
|
|
14496
|
+
const sessionKey = `live:${deviceIdPart}:ch${this.channel}:${this.profile}${variantSuffix}`;
|
|
14497
|
+
try {
|
|
14498
|
+
const session = await this.api.createDedicatedSession(sessionKey, this.logger);
|
|
14499
|
+
dedicatedClient = session.client;
|
|
14500
|
+
this.dedicatedSessionRelease = session.release;
|
|
14501
|
+
this.logger.info(
|
|
14502
|
+
`[rebroadcast] dedicated session acquired sessionKey=${sessionKey}`
|
|
14503
|
+
);
|
|
14504
|
+
} catch (e) {
|
|
14505
|
+
this.logger.warn(
|
|
14506
|
+
`[rebroadcast] failed to acquire dedicated session, falling back to shared socket: ${e}`
|
|
14507
|
+
);
|
|
14508
|
+
}
|
|
14406
14509
|
this.logger.info(
|
|
14407
|
-
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
14510
|
+
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size} dedicated=${!!dedicatedClient}`
|
|
14408
14511
|
);
|
|
14409
14512
|
await this.flow.startKeepAlive(this.api);
|
|
14410
14513
|
this.nativeFanout = new NativeStreamFanout({
|
|
14411
14514
|
maxQueueItems: 200,
|
|
14412
14515
|
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
14413
|
-
variant: this.variant
|
|
14516
|
+
variant: this.variant,
|
|
14517
|
+
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
14414
14518
|
}),
|
|
14415
14519
|
onFrame: (frame) => {
|
|
14416
14520
|
if (frame.audio) {
|
|
@@ -14442,6 +14546,18 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14442
14546
|
if (hasParamSets2) {
|
|
14443
14547
|
this.markFirstFrameReceived();
|
|
14444
14548
|
}
|
|
14549
|
+
const isKeyframe = this.isRawFrameKeyframe(frame);
|
|
14550
|
+
this.prebuffer.push({
|
|
14551
|
+
frame: { ...frame, data: Buffer.from(frame.data) },
|
|
14552
|
+
time: Date.now(),
|
|
14553
|
+
isKeyframe
|
|
14554
|
+
});
|
|
14555
|
+
const cutoff = Date.now() - this.PREBUFFER_MAX_MS;
|
|
14556
|
+
let trimIdx = 0;
|
|
14557
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
14558
|
+
trimIdx++;
|
|
14559
|
+
}
|
|
14560
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
14445
14561
|
},
|
|
14446
14562
|
onError: (error) => {
|
|
14447
14563
|
this.logger.warn(
|
|
@@ -14455,6 +14571,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14455
14571
|
this.firstFramePromise = null;
|
|
14456
14572
|
this.firstFrameResolve = null;
|
|
14457
14573
|
this.nativeFanout = null;
|
|
14574
|
+
this.prebuffer = [];
|
|
14575
|
+
if (this.dedicatedSessionRelease) {
|
|
14576
|
+
const release = this.dedicatedSessionRelease;
|
|
14577
|
+
this.dedicatedSessionRelease = void 0;
|
|
14578
|
+
release().catch(() => {
|
|
14579
|
+
});
|
|
14580
|
+
}
|
|
14458
14581
|
this.logger.info(
|
|
14459
14582
|
`[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
14460
14583
|
);
|
|
@@ -14523,6 +14646,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14523
14646
|
this.nativeFanout = null;
|
|
14524
14647
|
await fanout.stop();
|
|
14525
14648
|
}
|
|
14649
|
+
this.prebuffer = [];
|
|
14650
|
+
if (this.dedicatedSessionRelease) {
|
|
14651
|
+
const release = this.dedicatedSessionRelease;
|
|
14652
|
+
this.dedicatedSessionRelease = void 0;
|
|
14653
|
+
try {
|
|
14654
|
+
await release();
|
|
14655
|
+
} catch {
|
|
14656
|
+
}
|
|
14657
|
+
}
|
|
14526
14658
|
if (this.tempStreamGenerator) {
|
|
14527
14659
|
try {
|
|
14528
14660
|
await this.tempStreamGenerator.return(void 0);
|
|
@@ -14532,14 +14664,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14532
14664
|
}
|
|
14533
14665
|
}
|
|
14534
14666
|
/**
|
|
14535
|
-
* Remove a client and
|
|
14667
|
+
* Remove a client and schedule native stream stop if no clients remain.
|
|
14668
|
+
* Uses a grace period so rapid reconnects (e.g. Frigate polling) reuse the running stream
|
|
14669
|
+
* and benefit from the prebuffer instead of waiting for a fresh keyframe.
|
|
14536
14670
|
*/
|
|
14537
14671
|
removeClient(clientId) {
|
|
14538
14672
|
if (this.connectedClients.has(clientId)) {
|
|
14539
14673
|
this.connectedClients.delete(clientId);
|
|
14540
14674
|
this.emit("clientDisconnected", clientId);
|
|
14541
14675
|
if (this.connectedClients.size === 0) {
|
|
14542
|
-
|
|
14676
|
+
this.clearNoClientAutoStopTimer();
|
|
14677
|
+
this.noClientAutoStopTimer = setTimeout(() => {
|
|
14678
|
+
if (this.connectedClients.size === 0) {
|
|
14679
|
+
void this.stopNativeStream();
|
|
14680
|
+
}
|
|
14681
|
+
}, 3e4);
|
|
14682
|
+
this.noClientAutoStopTimer?.unref?.();
|
|
14543
14683
|
}
|
|
14544
14684
|
}
|
|
14545
14685
|
}
|
|
@@ -34111,6 +34251,7 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
|
|
|
34111
34251
|
segmentDuration;
|
|
34112
34252
|
playlistSize;
|
|
34113
34253
|
ffmpegPath;
|
|
34254
|
+
externalVideoStream;
|
|
34114
34255
|
log;
|
|
34115
34256
|
outputDir = null;
|
|
34116
34257
|
createdTempDir = false;
|
|
@@ -34137,6 +34278,7 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
|
|
|
34137
34278
|
this.outputDir = options.outputDir;
|
|
34138
34279
|
this.createdTempDir = false;
|
|
34139
34280
|
}
|
|
34281
|
+
this.externalVideoStream = options.externalVideoStream;
|
|
34140
34282
|
this.log = options.logger ?? (() => {
|
|
34141
34283
|
});
|
|
34142
34284
|
}
|
|
@@ -34161,12 +34303,16 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
|
|
|
34161
34303
|
this.playlistPath = import_node_path3.default.join(this.outputDir, "playlist.m3u8");
|
|
34162
34304
|
this.segmentPattern = import_node_path3.default.join(this.outputDir, "segment_%05d.ts");
|
|
34163
34305
|
this.log("info", `Starting HLS stream to ${this.outputDir}`);
|
|
34164
|
-
this.
|
|
34165
|
-
this.
|
|
34166
|
-
|
|
34167
|
-
this.
|
|
34168
|
-
|
|
34169
|
-
|
|
34306
|
+
if (this.externalVideoStream) {
|
|
34307
|
+
this.nativeStream = this.wrapVideoStreamAsGenerator(this.externalVideoStream);
|
|
34308
|
+
} else {
|
|
34309
|
+
this.nativeStream = createNativeStream(
|
|
34310
|
+
this.api,
|
|
34311
|
+
this.channel,
|
|
34312
|
+
this.profile,
|
|
34313
|
+
this.variant ? { variant: this.variant } : void 0
|
|
34314
|
+
);
|
|
34315
|
+
}
|
|
34170
34316
|
this.pumpPromise = this.pumpNativeToFfmpeg();
|
|
34171
34317
|
this.startedAt = /* @__PURE__ */ new Date();
|
|
34172
34318
|
this.state = "running";
|
|
@@ -34285,6 +34431,56 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
|
|
|
34285
34431
|
// ============================================================================
|
|
34286
34432
|
// Private Methods
|
|
34287
34433
|
// ============================================================================
|
|
34434
|
+
/**
|
|
34435
|
+
* Wrap a BaichuanVideoStream's videoAccessUnit events into an async generator
|
|
34436
|
+
* compatible with createNativeStream's output format.
|
|
34437
|
+
*/
|
|
34438
|
+
async *wrapVideoStreamAsGenerator(videoStream) {
|
|
34439
|
+
const queue = [];
|
|
34440
|
+
let resolve = null;
|
|
34441
|
+
let done = false;
|
|
34442
|
+
const onFrame = (au) => {
|
|
34443
|
+
queue.push(au);
|
|
34444
|
+
resolve?.();
|
|
34445
|
+
resolve = null;
|
|
34446
|
+
};
|
|
34447
|
+
const onClose = () => {
|
|
34448
|
+
done = true;
|
|
34449
|
+
resolve?.();
|
|
34450
|
+
resolve = null;
|
|
34451
|
+
};
|
|
34452
|
+
const onError = () => {
|
|
34453
|
+
done = true;
|
|
34454
|
+
resolve?.();
|
|
34455
|
+
resolve = null;
|
|
34456
|
+
};
|
|
34457
|
+
videoStream.on("videoAccessUnit", onFrame);
|
|
34458
|
+
videoStream.on("close", onClose);
|
|
34459
|
+
videoStream.on("error", onError);
|
|
34460
|
+
try {
|
|
34461
|
+
while (!done) {
|
|
34462
|
+
if (queue.length === 0) {
|
|
34463
|
+
await new Promise((r) => {
|
|
34464
|
+
resolve = r;
|
|
34465
|
+
});
|
|
34466
|
+
}
|
|
34467
|
+
while (queue.length > 0) {
|
|
34468
|
+
const frame = queue.shift();
|
|
34469
|
+
yield {
|
|
34470
|
+
audio: false,
|
|
34471
|
+
data: frame.data,
|
|
34472
|
+
videoType: frame.videoType,
|
|
34473
|
+
isKeyframe: frame.isKeyframe,
|
|
34474
|
+
microseconds: frame.microseconds
|
|
34475
|
+
};
|
|
34476
|
+
}
|
|
34477
|
+
}
|
|
34478
|
+
} finally {
|
|
34479
|
+
videoStream.removeListener("videoAccessUnit", onFrame);
|
|
34480
|
+
videoStream.removeListener("close", onClose);
|
|
34481
|
+
videoStream.removeListener("error", onError);
|
|
34482
|
+
}
|
|
34483
|
+
}
|
|
34288
34484
|
async pumpNativeToFfmpeg() {
|
|
34289
34485
|
if (!this.nativeStream || !this.playlistPath || !this.segmentPattern) {
|
|
34290
34486
|
return;
|