@camstack/addon-pipeline 1.2.362 → 1.2.364
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/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-C77XAnt9.mjs → custom-model-catalog-DHFPSu0E.mjs} +2 -2
- package/dist/{custom-model-catalog-CL9Tcn2Y.js → custom-model-catalog-nx2TXAGS.js} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-D629dz5x.mjs → dist-BX9VTm12.mjs} +177 -3
- package/dist/{dist-tclsRHCk.js → dist-CGOb--t-.js} +177 -3
- package/dist/{model-catalogs-Y1WJrGyS.js → model-catalogs-DHV48cdY.js} +1 -1
- package/dist/{model-catalogs-BxQBJBt4.mjs → model-catalogs-DNL4ydAg.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-D2D0N7_h.mjs → node-C8bbmV3c.mjs} +1 -1
- package/dist/{node-BYrYuZdh.js → node-UBMRK8SK.js} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-eM4qLFdC.js → process-memory-1BwIq2Ks.js} +1 -1
- package/dist/{process-memory-BR1e6q_Q.mjs → process-memory-BHG2uCeq.mjs} +1 -1
- package/dist/recorder/index.js +3 -3
- package/dist/recorder/index.mjs +3 -3
- package/dist/{segment-demux-js-C5IOs4lZ.mjs → segment-demux-js-BhnT__dA.mjs} +1 -1
- package/dist/{segment-demux-js-B9EqRfBi.js → segment-demux-js-PkDP-xQg.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C4Cp_OAY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vdmrukBk.mjs} +2 -2
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CICWhpXS.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ScPS8ZWM.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Cq5NPjBX.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-CZ_HBwki.mjs → hostInit-D_DBY5wu.mjs} +2 -2
- package/dist/stream-broker/index.js +357 -71
- package/dist/stream-broker/index.mjs +357 -71
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-7VuNrr9l.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
1
|
+
import { t as demuxSegmentFullJs } from "../segment-demux-js-BhnT__dA.mjs";
|
|
2
2
|
import { parentPort } from "node:worker_threads";
|
|
3
3
|
//#region src/stream-broker/recorded/demux-worker-child.ts
|
|
4
4
|
/**
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.360",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.284",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-CGOb--t-.js");
|
|
7
|
+
const require_node = require("../node-UBMRK8SK.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-PkDP-xQg.js");
|
|
12
12
|
let node_os = require("node:os");
|
|
13
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
@@ -3731,7 +3731,12 @@ var RtspRestreamer = class {
|
|
|
3731
3731
|
* Forwarded directly to all connected RTSP sessions — zero re-packetization.
|
|
3732
3732
|
*
|
|
3733
3733
|
* @param rtpData Complete RTP packet (header + payload)
|
|
3734
|
-
* @param isKeyframe True if this packet is part of a keyframe
|
|
3734
|
+
* @param isKeyframe True if this packet is part of a keyframe ACCESS UNIT —
|
|
3735
|
+
* judged per picture by the broker's `RtpAccessUnitTracker`: every
|
|
3736
|
+
* fragment of a fragmented IDR and a prefix SEI inside the picture carry
|
|
3737
|
+
* it, not only the packet that completed an IRAP NAL. The keyframe cache
|
|
3738
|
+
* below collects exactly that run, and the transition into it is the
|
|
3739
|
+
* once-per-picture `keyframeAuStart` the sessions resume and count on.
|
|
3735
3740
|
* @param isParameterSet True if this packet contains SPS/PPS/VPS
|
|
3736
3741
|
*/
|
|
3737
3742
|
pushRtpPassthrough(rtpData, isKeyframe, isParameterSet) {
|
|
@@ -3747,14 +3752,14 @@ var RtspRestreamer = class {
|
|
|
3747
3752
|
this.lastKeyframeRtpBytes = 0;
|
|
3748
3753
|
}
|
|
3749
3754
|
}
|
|
3755
|
+
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3750
3756
|
if (this.collectingKeyframeRtp) {
|
|
3751
3757
|
this.lastKeyframeRtpPackets.push({
|
|
3752
3758
|
data: Buffer.from(rtpData),
|
|
3753
3759
|
marker: rtpPacket.marker
|
|
3754
3760
|
});
|
|
3755
3761
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
3756
|
-
if (
|
|
3757
|
-
else if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3762
|
+
if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3758
3763
|
this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
|
|
3759
3764
|
this.lastKeyframeRtpPackets = [];
|
|
3760
3765
|
this.lastKeyframeRtpBytes = 0;
|
|
@@ -3767,17 +3772,16 @@ var RtspRestreamer = class {
|
|
|
3767
3772
|
if (!session.isPlaying()) continue;
|
|
3768
3773
|
this.pendingKeyframe.delete(session.getSessionId());
|
|
3769
3774
|
}
|
|
3770
|
-
const keyframeAu = isKeyframe || isParameterSet;
|
|
3771
3775
|
for (const session of this.sessions.values()) {
|
|
3772
3776
|
const sessionId = session.getSessionId();
|
|
3773
3777
|
if (this.activeReplays.get(sessionId)?.enqueue({
|
|
3774
3778
|
packet: rtpPacket,
|
|
3775
3779
|
prime: false,
|
|
3776
|
-
keyframeAuStart
|
|
3780
|
+
keyframeAuStart
|
|
3777
3781
|
})) continue;
|
|
3778
3782
|
if (this.pendingKeyframe.has(sessionId)) continue;
|
|
3779
3783
|
if (!session.isPlaying()) continue;
|
|
3780
|
-
this.recordLiveOutcome(session.sendRtp(rtpPacket, false,
|
|
3784
|
+
this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAuStart));
|
|
3781
3785
|
}
|
|
3782
3786
|
}
|
|
3783
3787
|
/**
|
|
@@ -4630,15 +4634,16 @@ var RtspSession = class {
|
|
|
4630
4634
|
*
|
|
4631
4635
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
4632
4636
|
*
|
|
4633
|
-
* The payload buffer handed here is
|
|
4634
|
-
* broker's `rtpRing` for the life of a GOP (`captureRtpForPreBuffer` pushes
|
|
4635
|
-
* the same object). A pending `socket.write` cannot outlive that, so the
|
|
4636
|
-
* lifetime requirement this adds is strictly weaker than one the broker
|
|
4637
|
-
* already meets. Its provenance is a private allocation per packet —
|
|
4637
|
+
* The payload buffer handed here is a private allocation per packet —
|
|
4638
4638
|
* `Buffer.from(readBuffer.subarray(...))` in `rtsp-client.ts`'s `onData`, for
|
|
4639
4639
|
* video and audio alike — never a view of the reader's socket buffer, which
|
|
4640
4640
|
* advances on the next read. Nothing in the broker writes into a captured
|
|
4641
4641
|
* packet: every mutation in this path targets a freshly allocated buffer.
|
|
4642
|
+
* (The broker's `rtpRing` keeps its OWN copy of a pooled packet —
|
|
4643
|
+
* `detachFromPool`, so a ring entry never pins a shared pool slab — so the
|
|
4644
|
+
* ring no longer vouches for THIS object's lifetime, and it does not need
|
|
4645
|
+
* to: a pending write holds its own reference, and the rule below is about
|
|
4646
|
+
* mutation, not retention.)
|
|
4642
4647
|
*
|
|
4643
4648
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
4644
4649
|
* completes, not retention.** So the header, which IS built per call, must
|
|
@@ -10127,6 +10132,16 @@ function allocAnnexBNal(byteLength) {
|
|
|
10127
10132
|
buf[3] = 1;
|
|
10128
10133
|
return buf;
|
|
10129
10134
|
}
|
|
10135
|
+
var NO_PAYLOAD_CLASS = {
|
|
10136
|
+
keyframe: false,
|
|
10137
|
+
parameterSet: false
|
|
10138
|
+
};
|
|
10139
|
+
function isH265IrapType(type) {
|
|
10140
|
+
return type >= H265_NAL_IRAP_MIN && type <= H265_NAL_IRAP_MAX;
|
|
10141
|
+
}
|
|
10142
|
+
function isH265ParameterSetType(type) {
|
|
10143
|
+
return type === H265_NAL_VPS || type === H265_NAL_SPS || type === H265_NAL_PPS;
|
|
10144
|
+
}
|
|
10130
10145
|
/** Total byte length of the reassembly fragments, without allocating a closure. */
|
|
10131
10146
|
function totalFragmentBytes(fragments) {
|
|
10132
10147
|
let total = 0;
|
|
@@ -10172,16 +10187,92 @@ var RtpDepacketizer = class {
|
|
|
10172
10187
|
}
|
|
10173
10188
|
/**
|
|
10174
10189
|
* Process a raw RTP packet (including 12-byte RTP header).
|
|
10175
|
-
* Emits zero or more complete NAL units via the callback
|
|
10190
|
+
* Emits zero or more complete NAL units via the callback and returns what
|
|
10191
|
+
* THIS packet carried ({@link RtpPacketClass}) — `null` for a packet too
|
|
10192
|
+
* short to be RTP with a payload.
|
|
10176
10193
|
*/
|
|
10177
10194
|
processPacket(rtpData) {
|
|
10178
|
-
if (rtpData.length < 12) return;
|
|
10195
|
+
if (rtpData.length < 12) return null;
|
|
10179
10196
|
const marker = (rtpData[1] & 128) !== 0;
|
|
10180
10197
|
const timestamp = rtpData.readUInt32BE(4);
|
|
10181
10198
|
const payload = rtpData.subarray(12);
|
|
10182
|
-
if (payload.length === 0) return;
|
|
10199
|
+
if (payload.length === 0) return null;
|
|
10200
|
+
const carried = this.codec === "h264" ? this.classifyH264Payload(payload) : this.classifyH265Payload(payload);
|
|
10183
10201
|
if (this.codec === "h264") this.processH264(payload, timestamp, marker);
|
|
10184
10202
|
else this.processH265(payload, timestamp, marker);
|
|
10203
|
+
return {
|
|
10204
|
+
timestamp,
|
|
10205
|
+
marker,
|
|
10206
|
+
keyframe: carried.keyframe,
|
|
10207
|
+
parameterSet: carried.parameterSet
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10211
|
+
classifyH264Payload(payload) {
|
|
10212
|
+
const nalType = payload[0] & 31;
|
|
10213
|
+
if (nalType === H264_NAL_STAP_A) {
|
|
10214
|
+
let keyframe = false;
|
|
10215
|
+
let parameterSet = false;
|
|
10216
|
+
let offset = 1;
|
|
10217
|
+
while (offset + 2 <= payload.length) {
|
|
10218
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10219
|
+
offset += 2;
|
|
10220
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10221
|
+
const type = payload[offset] & 31;
|
|
10222
|
+
keyframe ||= type === H264_NAL_IDR;
|
|
10223
|
+
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10224
|
+
offset += nalSize;
|
|
10225
|
+
}
|
|
10226
|
+
return {
|
|
10227
|
+
keyframe,
|
|
10228
|
+
parameterSet
|
|
10229
|
+
};
|
|
10230
|
+
}
|
|
10231
|
+
if (nalType === 28) {
|
|
10232
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10233
|
+
return {
|
|
10234
|
+
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10235
|
+
parameterSet: false
|
|
10236
|
+
};
|
|
10237
|
+
}
|
|
10238
|
+
return {
|
|
10239
|
+
keyframe: nalType === H264_NAL_IDR,
|
|
10240
|
+
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10241
|
+
};
|
|
10242
|
+
}
|
|
10243
|
+
/** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
|
|
10244
|
+
classifyH265Payload(payload) {
|
|
10245
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10246
|
+
const nalType = payload[0] >> 1 & 63;
|
|
10247
|
+
if (nalType === H265_NAL_AP) {
|
|
10248
|
+
let keyframe = false;
|
|
10249
|
+
let parameterSet = false;
|
|
10250
|
+
let offset = 2;
|
|
10251
|
+
while (offset + 2 <= payload.length) {
|
|
10252
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10253
|
+
offset += 2;
|
|
10254
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10255
|
+
const type = payload[offset] >> 1 & 63;
|
|
10256
|
+
keyframe ||= isH265IrapType(type);
|
|
10257
|
+
parameterSet ||= isH265ParameterSetType(type);
|
|
10258
|
+
offset += nalSize;
|
|
10259
|
+
}
|
|
10260
|
+
return {
|
|
10261
|
+
keyframe,
|
|
10262
|
+
parameterSet
|
|
10263
|
+
};
|
|
10264
|
+
}
|
|
10265
|
+
if (nalType === 49) {
|
|
10266
|
+
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10267
|
+
return {
|
|
10268
|
+
keyframe: isH265IrapType(payload[2] & 63),
|
|
10269
|
+
parameterSet: false
|
|
10270
|
+
};
|
|
10271
|
+
}
|
|
10272
|
+
return {
|
|
10273
|
+
keyframe: isH265IrapType(nalType),
|
|
10274
|
+
parameterSet: isH265ParameterSetType(nalType)
|
|
10275
|
+
};
|
|
10185
10276
|
}
|
|
10186
10277
|
processH264(payload, timestamp, marker) {
|
|
10187
10278
|
const nalType = payload[0] & 31;
|
|
@@ -10327,6 +10418,138 @@ var RtpDepacketizer = class {
|
|
|
10327
10418
|
}
|
|
10328
10419
|
};
|
|
10329
10420
|
//#endregion
|
|
10421
|
+
//#region src/stream-broker/rtsp/rtp-access-unit-tracker.ts
|
|
10422
|
+
/**
|
|
10423
|
+
* Tracks access-unit membership across the source RTP packets of ONE stream.
|
|
10424
|
+
*
|
|
10425
|
+
* The RTSP restreamer forwards source RTP untouched and must know, per
|
|
10426
|
+
* packet, whether it is part of a keyframe picture: that is what its keyframe
|
|
10427
|
+
* cache collects and what a backlog-latched session resumes on. The
|
|
10428
|
+
* depacketizer can only say so about a NAL that COMPLETED, so the fragments of
|
|
10429
|
+
* a fragmented IDR before the last one, and a prefix SEI between the parameter
|
|
10430
|
+
* sets and the slice, were reported as plain packets: the cache latch closed
|
|
10431
|
+
* on them and re-opened on the final fragment, and a backlogged session
|
|
10432
|
+
* counted every IRAP slice as a missed keyframe. Here a packet is classified
|
|
10433
|
+
* by what it carries ({@link RtpPacketClass}) and the verdict is carried
|
|
10434
|
+
* across the whole picture, bounded by the RTP timestamp and marker bit.
|
|
10435
|
+
*
|
|
10436
|
+
* A parameter-set-only run is not a picture: a camera that stamps SPS/PPS
|
|
10437
|
+
* with the previous timestamp still announces ONE keyframe, so the keyframe
|
|
10438
|
+
* verdict carries over into the slice that follows instead of starting twice.
|
|
10439
|
+
*/
|
|
10440
|
+
var RtpAccessUnitTracker = class {
|
|
10441
|
+
timestamp = -1;
|
|
10442
|
+
previousMarker = true;
|
|
10443
|
+
keyframeAu = false;
|
|
10444
|
+
/** The picture in progress has received something other than a parameter set. */
|
|
10445
|
+
pictureSeen = false;
|
|
10446
|
+
classify(packet) {
|
|
10447
|
+
const auStart = this.previousMarker || packet.timestamp !== this.timestamp;
|
|
10448
|
+
if (auStart && this.pictureSeen) this.keyframeAu = false;
|
|
10449
|
+
if (auStart) this.pictureSeen = false;
|
|
10450
|
+
this.timestamp = packet.timestamp;
|
|
10451
|
+
this.previousMarker = packet.marker;
|
|
10452
|
+
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10453
|
+
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10454
|
+
if (keyframeContent) this.keyframeAu = true;
|
|
10455
|
+
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10456
|
+
return {
|
|
10457
|
+
auStart,
|
|
10458
|
+
keyframeAu: this.keyframeAu,
|
|
10459
|
+
keyframeAuStart,
|
|
10460
|
+
parameterSet: packet.parameterSet
|
|
10461
|
+
};
|
|
10462
|
+
}
|
|
10463
|
+
/** A redial re-bases the clock; nothing carries over. */
|
|
10464
|
+
reset() {
|
|
10465
|
+
this.timestamp = -1;
|
|
10466
|
+
this.previousMarker = true;
|
|
10467
|
+
this.keyframeAu = false;
|
|
10468
|
+
this.pictureSeen = false;
|
|
10469
|
+
}
|
|
10470
|
+
};
|
|
10471
|
+
//#endregion
|
|
10472
|
+
//#region src/stream-broker/stream-broker/access-unit-assembler.ts
|
|
10473
|
+
/**
|
|
10474
|
+
* Groups depacketized NALs into ACCESS UNITS — one per picture.
|
|
10475
|
+
*
|
|
10476
|
+
* The broker's `EncodedPacket` is one access unit, and every consumer judges it
|
|
10477
|
+
* that way: `keyframe` gates where a clip ring widens to, where a late joiner
|
|
10478
|
+
* can start, how the pre-roll window is cut; the stats count it as one picture
|
|
10479
|
+
* and time the GOP from one `keyframe` to the next. A multi-slice camera (a
|
|
10480
|
+
* Hikvision G2 main stream is 3 slices per picture) used to hand each slice —
|
|
10481
|
+
* and each prefix SEI — down as its own "frame": 75 input fps on a 25 fps
|
|
10482
|
+
* camera, an 8–34 ms "GOP", a clip ring that widened to the LAST IRAP slice
|
|
10483
|
+
* (a third of the picture, without its parameter sets), and a prefix SEI that
|
|
10484
|
+
* inherited the welded VPS/SPS/PPS instead of the IDR.
|
|
10485
|
+
*
|
|
10486
|
+
* Boundary rule (RFC 3550 / 6184 / 7798): the RTP marker bit closes the
|
|
10487
|
+
* picture it ends — flushed on the spot, so a single-slice stream pays no
|
|
10488
|
+
* latency — and a timestamp change closes the one before it, for cameras that
|
|
10489
|
+
* never set the marker. Parameter sets are held and welded onto the picture
|
|
10490
|
+
* that follows them, so a keyframe access unit is self-contained
|
|
10491
|
+
* (VPS/SPS/PPS + IDR) exactly as before; a prefix SEI stays inside its
|
|
10492
|
+
* picture and is never a frame of its own.
|
|
10493
|
+
*/
|
|
10494
|
+
var AccessUnitAssembler = class {
|
|
10495
|
+
onAccessUnit;
|
|
10496
|
+
/** Parameter sets seen since the last picture closed; welded onto the next one. */
|
|
10497
|
+
pendingParamNals = [];
|
|
10498
|
+
/** NALs of the picture in progress (parameter sets first when welded). */
|
|
10499
|
+
pictureNals = [];
|
|
10500
|
+
pictureTimestamp = 0;
|
|
10501
|
+
pictureKeyframe = false;
|
|
10502
|
+
constructor(onAccessUnit) {
|
|
10503
|
+
this.onAccessUnit = onAccessUnit;
|
|
10504
|
+
}
|
|
10505
|
+
push(depacketized) {
|
|
10506
|
+
const { annexB, keyframe, parameterSet, timestamp, marker } = depacketized;
|
|
10507
|
+
if (this.pictureNals.length > 0 && timestamp !== this.pictureTimestamp) this.flush();
|
|
10508
|
+
if (parameterSet) {
|
|
10509
|
+
this.pendingParamNals.push(annexB);
|
|
10510
|
+
return;
|
|
10511
|
+
}
|
|
10512
|
+
if (this.pictureNals.length === 0) {
|
|
10513
|
+
this.pictureTimestamp = timestamp;
|
|
10514
|
+
this.pictureKeyframe = false;
|
|
10515
|
+
}
|
|
10516
|
+
if (this.pendingParamNals.length > 0) {
|
|
10517
|
+
for (const paramNal of this.pendingParamNals) this.pictureNals.push(paramNal);
|
|
10518
|
+
this.pendingParamNals = [];
|
|
10519
|
+
this.pictureKeyframe = true;
|
|
10520
|
+
}
|
|
10521
|
+
this.pictureNals.push(annexB);
|
|
10522
|
+
if (keyframe) this.pictureKeyframe = true;
|
|
10523
|
+
if (marker) this.flush();
|
|
10524
|
+
}
|
|
10525
|
+
/**
|
|
10526
|
+
* Emit the picture in progress, if any. Called on the marker bit, on a
|
|
10527
|
+
* timestamp change, and by the broker when the stream stops or redials so
|
|
10528
|
+
* the last picture of a marker-less stream is not lost.
|
|
10529
|
+
*/
|
|
10530
|
+
flush() {
|
|
10531
|
+
if (this.pictureNals.length === 0) return;
|
|
10532
|
+
const nals = this.pictureNals;
|
|
10533
|
+
const timestamp = this.pictureTimestamp;
|
|
10534
|
+
const keyframe = this.pictureKeyframe;
|
|
10535
|
+
this.pictureNals = [];
|
|
10536
|
+
this.pictureKeyframe = false;
|
|
10537
|
+
const annexB = nals.length === 1 ? nals[0] : Buffer.concat(nals);
|
|
10538
|
+
this.onAccessUnit({
|
|
10539
|
+
annexB,
|
|
10540
|
+
timestamp,
|
|
10541
|
+
keyframe
|
|
10542
|
+
});
|
|
10543
|
+
}
|
|
10544
|
+
/** Drop everything held — a redial re-bases the clock, nothing carries over. */
|
|
10545
|
+
reset() {
|
|
10546
|
+
this.pendingParamNals = [];
|
|
10547
|
+
this.pictureNals = [];
|
|
10548
|
+
this.pictureTimestamp = 0;
|
|
10549
|
+
this.pictureKeyframe = false;
|
|
10550
|
+
}
|
|
10551
|
+
};
|
|
10552
|
+
//#endregion
|
|
10330
10553
|
//#region src/stream-broker/rtsp/rtsp-client.ts
|
|
10331
10554
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
10332
10555
|
var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
|
|
@@ -11295,6 +11518,49 @@ var AudioCodecSession = class {
|
|
|
11295
11518
|
}
|
|
11296
11519
|
};
|
|
11297
11520
|
//#endregion
|
|
11521
|
+
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
11522
|
+
/**
|
|
11523
|
+
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
11524
|
+
*
|
|
11525
|
+
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
11526
|
+
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
11527
|
+
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
11528
|
+
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
11529
|
+
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
11530
|
+
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
11531
|
+
* packet that happened to be allocated beside it. On the hub that held
|
|
11532
|
+
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
11533
|
+
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
11534
|
+
*
|
|
11535
|
+
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
11536
|
+
* now pass every entry through {@link detachFromPool} on the way in and
|
|
11537
|
+
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
11538
|
+
* entry costs. Short-lived traffic stays pooled.
|
|
11539
|
+
*/
|
|
11540
|
+
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
11541
|
+
function ownsBackingStore(buf) {
|
|
11542
|
+
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
11543
|
+
}
|
|
11544
|
+
/**
|
|
11545
|
+
* What retaining `buf` really costs: its own length when it owns its
|
|
11546
|
+
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
11547
|
+
*/
|
|
11548
|
+
function backingStoreBytes(buf) {
|
|
11549
|
+
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
11550
|
+
}
|
|
11551
|
+
/**
|
|
11552
|
+
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
11553
|
+
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
11554
|
+
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
11555
|
+
* already unpooled is returned as-is.
|
|
11556
|
+
*/
|
|
11557
|
+
function detachFromPool(buf) {
|
|
11558
|
+
if (ownsBackingStore(buf)) return buf;
|
|
11559
|
+
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
11560
|
+
buf.copy(owned);
|
|
11561
|
+
return owned;
|
|
11562
|
+
}
|
|
11563
|
+
//#endregion
|
|
11298
11564
|
//#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
|
|
11299
11565
|
/**
|
|
11300
11566
|
* Time-based ring buffer for encoded video packets.
|
|
@@ -11305,6 +11571,12 @@ var AudioCodecSession = class {
|
|
|
11305
11571
|
*
|
|
11306
11572
|
* The buffer always starts from the most recent keyframe to ensure
|
|
11307
11573
|
* consumers can decode the returned data.
|
|
11574
|
+
*
|
|
11575
|
+
* Every entry OWNS its backing store (`owned-buffer.ts`): a sub-stream NAL
|
|
11576
|
+
* arrives as a view of a shared 8 KiB pool slab, and a ring that kept it for
|
|
11577
|
+
* 60 s pinned the slab and every short-lived packet beside it — the clip
|
|
11578
|
+
* rings of ~70 brokers were the largest holder of the hub broker's
|
|
11579
|
+
* 550–980 MB of ArrayBuffers. The byte budget charges what an entry pins.
|
|
11308
11580
|
*/
|
|
11309
11581
|
var EncodedRingBuffer = class {
|
|
11310
11582
|
packets = [];
|
|
@@ -11336,13 +11608,25 @@ var EncodedRingBuffer = class {
|
|
|
11336
11608
|
getDuration() {
|
|
11337
11609
|
return this.maxDurationMs / 1e3;
|
|
11338
11610
|
}
|
|
11339
|
-
/**
|
|
11611
|
+
/**
|
|
11612
|
+
* Push a new packet into the buffer and evict expired ones.
|
|
11613
|
+
*
|
|
11614
|
+
* A pooled payload is copied into its own allocation on the way in; one
|
|
11615
|
+
* that already owns its ArrayBuffer is stored as-is. The consumers fanned
|
|
11616
|
+
* out from the same packet keep the original, so the copy is the ring's
|
|
11617
|
+
* alone and is freed with it.
|
|
11618
|
+
*/
|
|
11340
11619
|
push(packet) {
|
|
11341
|
-
|
|
11342
|
-
|
|
11620
|
+
const data = detachFromPool(packet.data);
|
|
11621
|
+
const stored = data === packet.data ? packet : {
|
|
11622
|
+
...packet,
|
|
11623
|
+
data
|
|
11624
|
+
};
|
|
11625
|
+
this.packets.push(stored);
|
|
11626
|
+
this.bytes += backingStoreBytes(data);
|
|
11343
11627
|
this.evict();
|
|
11344
11628
|
}
|
|
11345
|
-
/** What the retained window currently costs, in bytes. */
|
|
11629
|
+
/** What the retained window currently costs, in bytes of backing store. */
|
|
11346
11630
|
getBufferedBytes() {
|
|
11347
11631
|
return this.bytes;
|
|
11348
11632
|
}
|
|
@@ -11420,14 +11704,14 @@ var EncodedRingBuffer = class {
|
|
|
11420
11704
|
let removeCount = 0;
|
|
11421
11705
|
while (removeCount < this.packets.length && (this.packets[removeCount].pts < cutoff || this.packets[removeCount].pts > newest)) removeCount++;
|
|
11422
11706
|
if (removeCount > 0) {
|
|
11423
|
-
for (let i = 0; i < removeCount; i++) this.bytes -= this.packets[i].data
|
|
11707
|
+
for (let i = 0; i < removeCount; i++) this.bytes -= backingStoreBytes(this.packets[i].data);
|
|
11424
11708
|
this.packets.splice(0, removeCount);
|
|
11425
11709
|
}
|
|
11426
11710
|
if (this.maxBytes > 0 && this.bytes > this.maxBytes) {
|
|
11427
11711
|
this.clampedByBytes = true;
|
|
11428
11712
|
let dropped = 0;
|
|
11429
11713
|
while (dropped < this.packets.length && this.bytes > this.maxBytes) {
|
|
11430
|
-
this.bytes -= this.packets[dropped].data
|
|
11714
|
+
this.bytes -= backingStoreBytes(this.packets[dropped].data);
|
|
11431
11715
|
dropped++;
|
|
11432
11716
|
}
|
|
11433
11717
|
if (dropped > 0) this.packets.splice(0, dropped);
|
|
@@ -13091,7 +13375,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13091
13375
|
this.rtspRestreamer.destroy();
|
|
13092
13376
|
this.preBuffer.clear();
|
|
13093
13377
|
this.clipBuffer?.clear();
|
|
13094
|
-
this.
|
|
13378
|
+
this.annexBAssembler.reset();
|
|
13095
13379
|
this.encodedCallbacks.clear();
|
|
13096
13380
|
this.audioChunkPlane.dispose();
|
|
13097
13381
|
}
|
|
@@ -13554,7 +13838,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13554
13838
|
this.reconcilePrestreamWindow();
|
|
13555
13839
|
this.preBuffer.clear();
|
|
13556
13840
|
this.clipBuffer?.clear();
|
|
13557
|
-
this.
|
|
13841
|
+
this.annexBAssembler.reset();
|
|
13558
13842
|
this.rtspRestreamer.resetStreamState();
|
|
13559
13843
|
this.totalBytes = 0;
|
|
13560
13844
|
this.bytesInWindow = 0;
|
|
@@ -13863,8 +14147,9 @@ var StreamBroker = class StreamBroker {
|
|
|
13863
14147
|
captureRtpForPreBuffer(rtpData) {
|
|
13864
14148
|
const marker = rtpData.length > 1 && (rtpData[1] & 128) !== 0;
|
|
13865
14149
|
if (this.rtpRingPrevMarker || this.rtpRing.length === 0) this.rtpRingCurAuStart = this.rtpRing.length;
|
|
13866
|
-
|
|
13867
|
-
this.
|
|
14150
|
+
const owned = detachFromPool(rtpData);
|
|
14151
|
+
this.rtpRing.push(owned);
|
|
14152
|
+
this.rtpRingBytes += backingStoreBytes(owned);
|
|
13868
14153
|
if (this._lastNalParamSet || this._lastNalKeyframe) {
|
|
13869
14154
|
const auStart = this.rtpRingCurAuStart;
|
|
13870
14155
|
const lastMark = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1];
|
|
@@ -14042,18 +14327,18 @@ var StreamBroker = class StreamBroker {
|
|
|
14042
14327
|
this.reportPreRollClamp(budget, retentionMs);
|
|
14043
14328
|
const historyEnd = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1].index;
|
|
14044
14329
|
let retained = 0;
|
|
14045
|
-
for (let i = cut; i < historyEnd; i++) retained += this.rtpRing[i]
|
|
14330
|
+
for (let i = cut; i < historyEnd; i++) retained += backingStoreBytes(this.rtpRing[i]);
|
|
14046
14331
|
let markIdx = this.rtpRingKeyframeMarks.findIndex((m) => m.index === cut);
|
|
14047
14332
|
while (retained > budget.bytes && markIdx >= 0 && markIdx < this.rtpRingKeyframeMarks.length - 1) {
|
|
14048
14333
|
const nextCut = this.rtpRingKeyframeMarks[markIdx + 1].index;
|
|
14049
|
-
for (let i = cut; i < nextCut; i++) retained -= this.rtpRing[i]
|
|
14334
|
+
for (let i = cut; i < nextCut; i++) retained -= backingStoreBytes(this.rtpRing[i]);
|
|
14050
14335
|
cut = nextCut;
|
|
14051
14336
|
markIdx++;
|
|
14052
14337
|
}
|
|
14053
14338
|
}
|
|
14054
14339
|
if (cut <= 0) return;
|
|
14055
14340
|
const dropped = this.rtpRing.splice(0, cut);
|
|
14056
|
-
for (const b of dropped) this.rtpRingBytes -= b
|
|
14341
|
+
for (const b of dropped) this.rtpRingBytes -= backingStoreBytes(b);
|
|
14057
14342
|
this.rtpRingKeyframeMarks = this.rtpRingKeyframeMarks.filter((mark) => mark.index >= cut).map((mark) => ({
|
|
14058
14343
|
index: mark.index - cut,
|
|
14059
14344
|
ts: mark.ts
|
|
@@ -14393,11 +14678,15 @@ var StreamBroker = class StreamBroker {
|
|
|
14393
14678
|
if (clamped === this.clipRetentionSec) return;
|
|
14394
14679
|
const previous = this.clipRetentionSec;
|
|
14395
14680
|
this.clipRetentionSec = clamped;
|
|
14396
|
-
this.logger?.info(clamped === 0 ? "Clip retention off" : "Clip retention set", {
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14681
|
+
this.logger?.info(clamped === 0 ? "Clip retention off" : "Clip retention set", {
|
|
14682
|
+
tags: { deviceId: this.numericDeviceId },
|
|
14683
|
+
meta: {
|
|
14684
|
+
seconds: clamped,
|
|
14685
|
+
requested: seconds,
|
|
14686
|
+
previousSeconds: previous,
|
|
14687
|
+
capBytes: StreamBroker.CLIP_RING_MAX_BYTES
|
|
14688
|
+
}
|
|
14689
|
+
});
|
|
14401
14690
|
if (clamped === 0) {
|
|
14402
14691
|
this.clipBuffer = null;
|
|
14403
14692
|
return;
|
|
@@ -14630,8 +14919,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14630
14919
|
} });
|
|
14631
14920
|
this._lastNalKeyframe = false;
|
|
14632
14921
|
this._lastNalParamSet = false;
|
|
14633
|
-
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14634
|
-
this.
|
|
14922
|
+
const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
|
|
14923
|
+
const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
|
|
14924
|
+
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14635
14925
|
this.captureRtpForPreBuffer(rtpData);
|
|
14636
14926
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
14637
14927
|
cb(rtpData);
|
|
@@ -14827,51 +15117,44 @@ var StreamBroker = class StreamBroker {
|
|
|
14827
15117
|
* RTSP restreamer's pushRtpPassthrough.
|
|
14828
15118
|
*/
|
|
14829
15119
|
/**
|
|
14830
|
-
*
|
|
14831
|
-
*
|
|
14832
|
-
*
|
|
14833
|
-
*
|
|
14834
|
-
* with a single `EncodedPacket` that contains SPS+PPS+IDR together.
|
|
15120
|
+
* Groups the depacketizer's NALs into access units — ONE `EncodedPacket`
|
|
15121
|
+
* per picture, parameter sets welded onto the keyframe picture they
|
|
15122
|
+
* precede. See `access-unit-assembler.ts` for the boundary rule and the
|
|
15123
|
+
* per-NAL emit it replaced.
|
|
14835
15124
|
*/
|
|
14836
|
-
|
|
15125
|
+
annexBAssembler = new AccessUnitAssembler((accessUnit) => this.emitAccessUnit(accessUnit));
|
|
15126
|
+
/** Access-unit membership of the source RTP packets, for the restreamer's flags. */
|
|
15127
|
+
rtpAuTracker = new RtpAccessUnitTracker();
|
|
14837
15128
|
/**
|
|
14838
15129
|
* handleDepacketizedNal receives individual NALs from the RTP depacketizer
|
|
14839
|
-
* and
|
|
14840
|
-
*
|
|
14841
|
-
*
|
|
14842
|
-
*
|
|
14843
|
-
* This guarantees every keyframe EncodedPacket is a self-contained access
|
|
14844
|
-
* unit that a decoder can initialise from — critical for WebRTC (Chrome
|
|
14845
|
-
* can't decode an IDR without preceding SPS/PPS).
|
|
15130
|
+
* and hands them to the access-unit assembler, which emits one
|
|
15131
|
+
* `EncodedPacket` per PICTURE (`emitAccessUnit`) — never per NAL. A
|
|
15132
|
+
* multi-slice IDR, its parameter sets and its prefix SEI are one keyframe
|
|
15133
|
+
* packet; three P slices are one P packet.
|
|
14846
15134
|
*
|
|
14847
|
-
* The RTSP restreamer
|
|
14848
|
-
* not
|
|
15135
|
+
* The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
|
|
15136
|
+
* not fed from here.
|
|
14849
15137
|
*/
|
|
14850
15138
|
handleDepacketizedNal(depacketized) {
|
|
14851
|
-
|
|
15139
|
+
if (depacketized.keyframe) this._lastNalKeyframe = true;
|
|
15140
|
+
if (depacketized.parameterSet) this._lastNalParamSet = true;
|
|
15141
|
+
this.annexBAssembler.push(depacketized);
|
|
15142
|
+
}
|
|
15143
|
+
/**
|
|
15144
|
+
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15145
|
+
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15146
|
+
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
15147
|
+
* initialises from and what a clip or a pre-roll window may start at.
|
|
15148
|
+
*/
|
|
15149
|
+
emitAccessUnit(accessUnit) {
|
|
14852
15150
|
const codec = this.detectedCodec ?? "h264";
|
|
14853
|
-
|
|
14854
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14855
|
-
const isH264 = codec === "h264";
|
|
14856
|
-
const nalType = isH264 ? nal[0] & 31 : (nal[0] & 126) >> 1;
|
|
14857
|
-
if (isH264 ? nalType === 7 || nalType === 8 : nalType === 32 || nalType === 33 || nalType === 34) {
|
|
14858
|
-
this.pendingParamNals.push(depacketized.annexB);
|
|
14859
|
-
return;
|
|
14860
|
-
}
|
|
14861
|
-
const hadParamSets = this.pendingParamNals.length > 0;
|
|
14862
|
-
let annexBData;
|
|
14863
|
-
if (hadParamSets) {
|
|
14864
|
-
this.pendingParamNals.push(depacketized.annexB);
|
|
14865
|
-
annexBData = Buffer.concat(this.pendingParamNals);
|
|
14866
|
-
this.pendingParamNals = [];
|
|
14867
|
-
} else annexBData = depacketized.annexB;
|
|
14868
|
-
const pts = Math.floor(timestamp / 90);
|
|
15151
|
+
const pts = Math.floor(accessUnit.timestamp / 90);
|
|
14869
15152
|
const packet = {
|
|
14870
15153
|
type: "video",
|
|
14871
|
-
data:
|
|
15154
|
+
data: accessUnit.annexB,
|
|
14872
15155
|
pts,
|
|
14873
15156
|
dts: pts,
|
|
14874
|
-
keyframe: keyframe
|
|
15157
|
+
keyframe: accessUnit.keyframe,
|
|
14875
15158
|
codec
|
|
14876
15159
|
};
|
|
14877
15160
|
this.pushEncodedPacket(packet);
|
|
@@ -15275,6 +15558,9 @@ var StreamBroker = class StreamBroker {
|
|
|
15275
15558
|
this.rtpDepacketizer.reset();
|
|
15276
15559
|
this.rtpDepacketizer = null;
|
|
15277
15560
|
}
|
|
15561
|
+
this.annexBAssembler.flush();
|
|
15562
|
+
this.annexBAssembler.reset();
|
|
15563
|
+
this.rtpAuTracker.reset();
|
|
15278
15564
|
if (this.pushRtpFlagDepacketizer) {
|
|
15279
15565
|
this.pushRtpFlagDepacketizer.reset();
|
|
15280
15566
|
this.pushRtpFlagDepacketizer = null;
|