@camstack/addon-pipeline 1.2.363 → 1.2.365
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-nx2TXAGS.js → custom-model-catalog-C34VqcPo.js} +2 -2
- package/dist/{custom-model-catalog-DHFPSu0E.mjs → custom-model-catalog-CtpK3FKL.mjs} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-BX9VTm12.mjs → dist-BshC0NZR.mjs} +411 -291
- package/dist/{dist-CGOb--t-.js → dist-i4kS_3O7.js} +411 -291
- package/dist/{model-catalogs-DHV48cdY.js → model-catalogs-Cy7mZRWh.js} +1 -1
- package/dist/{model-catalogs-DNL4ydAg.mjs → model-catalogs-DNx5ATgm.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-C8bbmV3c.mjs → node-Dqmrb990.mjs} +1 -1
- package/dist/{node-UBMRK8SK.js → node-TzgGsCvW.js} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-1BwIq2Ks.js → process-memory-DLCPFTGC.js} +1 -1
- package/dist/{process-memory-BHG2uCeq.mjs → process-memory-DyfrbFY4.mjs} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/{segment-demux-js-PkDP-xQg.js → segment-demux-js-C9AYdKkD.js} +1 -1
- package/dist/{segment-demux-js-BhnT__dA.mjs → segment-demux-js-COoPqx93.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vdmrukBk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DVgnss_e.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DWI1BCfa.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUBmyDde.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-D_DBY5wu.mjs → hostInit-ZvA7Cl_1.mjs} +2 -2
- package/dist/stream-broker/index.js +513 -174
- package/dist/stream-broker/index.mjs +513 -174
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +4 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ScPS8ZWM.mjs +0 -26
- 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 +0 -26
|
@@ -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-i4kS_3O7.js");
|
|
7
|
+
const require_node = require("../node-TzgGsCvW.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-C9AYdKkD.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");
|
|
@@ -3085,6 +3085,68 @@ var RtpPacketizer = class {
|
|
|
3085
3085
|
}
|
|
3086
3086
|
};
|
|
3087
3087
|
//#endregion
|
|
3088
|
+
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3089
|
+
/**
|
|
3090
|
+
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
3091
|
+
*
|
|
3092
|
+
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
3093
|
+
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
3094
|
+
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
3095
|
+
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
3096
|
+
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
3097
|
+
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
3098
|
+
* packet that happened to be allocated beside it. On the hub that held
|
|
3099
|
+
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
3100
|
+
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
3101
|
+
*
|
|
3102
|
+
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
3103
|
+
* now pass every entry through {@link detachFromPool} on the way in and
|
|
3104
|
+
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
3105
|
+
* entry costs. Short-lived traffic stays pooled.
|
|
3106
|
+
*/
|
|
3107
|
+
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
3108
|
+
function ownsBackingStore(buf) {
|
|
3109
|
+
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* What retaining `buf` really costs: its own length when it owns its
|
|
3113
|
+
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
3114
|
+
*/
|
|
3115
|
+
function backingStoreBytes(buf) {
|
|
3116
|
+
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
3120
|
+
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
3121
|
+
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
3122
|
+
* already unpooled is returned as-is.
|
|
3123
|
+
*/
|
|
3124
|
+
function detachFromPool(buf) {
|
|
3125
|
+
if (ownsBackingStore(buf)) return buf;
|
|
3126
|
+
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
3127
|
+
buf.copy(owned);
|
|
3128
|
+
return owned;
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* A copy of `src[start, end)` that OWNS its backing store — the shape a reader
|
|
3132
|
+
* hands out when the packet it cuts from its socket buffer is going to be
|
|
3133
|
+
* retained anyway.
|
|
3134
|
+
*
|
|
3135
|
+
* Every source video packet is: `captureRtpForPreBuffer` keeps it for the life
|
|
3136
|
+
* of a GOP, and the restreamer's keyframe cache keeps the keyframe ones. With
|
|
3137
|
+
* the readers cutting packets as `Buffer.from(subarray)` — a pooled view — the
|
|
3138
|
+
* ring then had to copy each one a second time to own it: two copies of every
|
|
3139
|
+
* packet on the ingest path (measured 15.1 + 14.8 MB/s on the hub's load).
|
|
3140
|
+
* Cutting the packet owned up front makes the ring's and the cache's
|
|
3141
|
+
* `detachFromPool` an identity, so the packet is copied once and shared by the
|
|
3142
|
+
* reader's fan-out, the ring, the cache and every session's write queue.
|
|
3143
|
+
*/
|
|
3144
|
+
function copyOwned(src, start, end) {
|
|
3145
|
+
const owned = Buffer.allocUnsafeSlow(end - start);
|
|
3146
|
+
src.copy(owned, 0, start, end);
|
|
3147
|
+
return owned;
|
|
3148
|
+
}
|
|
3149
|
+
//#endregion
|
|
3088
3150
|
//#region src/stream-broker/rtsp/sdp-builder.ts
|
|
3089
3151
|
function buildSdp(options) {
|
|
3090
3152
|
const { codec, parameterSets, streamName } = options;
|
|
@@ -3731,7 +3793,12 @@ var RtspRestreamer = class {
|
|
|
3731
3793
|
* Forwarded directly to all connected RTSP sessions — zero re-packetization.
|
|
3732
3794
|
*
|
|
3733
3795
|
* @param rtpData Complete RTP packet (header + payload)
|
|
3734
|
-
* @param isKeyframe True if this packet is part of a keyframe
|
|
3796
|
+
* @param isKeyframe True if this packet is part of a keyframe ACCESS UNIT —
|
|
3797
|
+
* judged per picture by the broker's `RtpAccessUnitTracker`: every
|
|
3798
|
+
* fragment of a fragmented IDR and a prefix SEI inside the picture carry
|
|
3799
|
+
* it, not only the packet that completed an IRAP NAL. The keyframe cache
|
|
3800
|
+
* below collects exactly that run, and the transition into it is the
|
|
3801
|
+
* once-per-picture `keyframeAuStart` the sessions resume and count on.
|
|
3735
3802
|
* @param isParameterSet True if this packet contains SPS/PPS/VPS
|
|
3736
3803
|
*/
|
|
3737
3804
|
pushRtpPassthrough(rtpData, isKeyframe, isParameterSet) {
|
|
@@ -3747,14 +3814,14 @@ var RtspRestreamer = class {
|
|
|
3747
3814
|
this.lastKeyframeRtpBytes = 0;
|
|
3748
3815
|
}
|
|
3749
3816
|
}
|
|
3817
|
+
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3750
3818
|
if (this.collectingKeyframeRtp) {
|
|
3751
3819
|
this.lastKeyframeRtpPackets.push({
|
|
3752
|
-
data:
|
|
3820
|
+
data: detachFromPool(rtpData),
|
|
3753
3821
|
marker: rtpPacket.marker
|
|
3754
3822
|
});
|
|
3755
3823
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
3756
|
-
if (
|
|
3757
|
-
else if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3824
|
+
if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3758
3825
|
this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
|
|
3759
3826
|
this.lastKeyframeRtpPackets = [];
|
|
3760
3827
|
this.lastKeyframeRtpBytes = 0;
|
|
@@ -3767,17 +3834,16 @@ var RtspRestreamer = class {
|
|
|
3767
3834
|
if (!session.isPlaying()) continue;
|
|
3768
3835
|
this.pendingKeyframe.delete(session.getSessionId());
|
|
3769
3836
|
}
|
|
3770
|
-
const keyframeAu = isKeyframe || isParameterSet;
|
|
3771
3837
|
for (const session of this.sessions.values()) {
|
|
3772
3838
|
const sessionId = session.getSessionId();
|
|
3773
3839
|
if (this.activeReplays.get(sessionId)?.enqueue({
|
|
3774
3840
|
packet: rtpPacket,
|
|
3775
3841
|
prime: false,
|
|
3776
|
-
keyframeAuStart
|
|
3842
|
+
keyframeAuStart
|
|
3777
3843
|
})) continue;
|
|
3778
3844
|
if (this.pendingKeyframe.has(sessionId)) continue;
|
|
3779
3845
|
if (!session.isPlaying()) continue;
|
|
3780
|
-
this.recordLiveOutcome(session.sendRtp(rtpPacket, false,
|
|
3846
|
+
this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAuStart));
|
|
3781
3847
|
}
|
|
3782
3848
|
}
|
|
3783
3849
|
/**
|
|
@@ -4353,6 +4419,19 @@ var RtspSession = class {
|
|
|
4353
4419
|
lastRtpAt = 0;
|
|
4354
4420
|
/** Total video RTP bytes written to the socket. */
|
|
4355
4421
|
bytesSent = 0;
|
|
4422
|
+
/**
|
|
4423
|
+
* The socket is corked for the access unit in progress — see
|
|
4424
|
+
* {@link writeInterleaved}. Cleared by the packet that ends the picture (RTP
|
|
4425
|
+
* marker, or any audio packet) or by the next-tick safety net, whichever
|
|
4426
|
+
* comes first.
|
|
4427
|
+
*/
|
|
4428
|
+
accessUnitCorked = false;
|
|
4429
|
+
/** Flush the access unit in progress. Idempotent: the marker and the safety net both call it. */
|
|
4430
|
+
uncorkAccessUnit = () => {
|
|
4431
|
+
if (!this.accessUnitCorked) return;
|
|
4432
|
+
this.accessUnitCorked = false;
|
|
4433
|
+
this.socket.uncork();
|
|
4434
|
+
};
|
|
4356
4435
|
/** Client `User-Agent` (from the first request that carries one). Lets the
|
|
4357
4436
|
* broker panel identify a consumer by purpose — e.g. the recorder sets
|
|
4358
4437
|
* `-user_agent recorder` on its RTSP pull — instead of a bare address. */
|
|
@@ -4600,7 +4679,7 @@ var RtspSession = class {
|
|
|
4600
4679
|
return "dropped-backlog";
|
|
4601
4680
|
}
|
|
4602
4681
|
}
|
|
4603
|
-
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
|
|
4682
|
+
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
|
|
4604
4683
|
this.missedKeyframes = 0;
|
|
4605
4684
|
return "written";
|
|
4606
4685
|
}
|
|
@@ -4609,7 +4688,7 @@ var RtspSession = class {
|
|
|
4609
4688
|
if (!this.playing || this.closed || this.muted) return "not-playing";
|
|
4610
4689
|
if (this.tracks.length < 2) return "not-playing";
|
|
4611
4690
|
if (this.socket.writableLength > 2097152) return "dropped-backlog";
|
|
4612
|
-
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
|
|
4691
|
+
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
|
|
4613
4692
|
return "written";
|
|
4614
4693
|
}
|
|
4615
4694
|
/**
|
|
@@ -4631,14 +4710,13 @@ var RtspSession = class {
|
|
|
4631
4710
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
4632
4711
|
*
|
|
4633
4712
|
* The payload buffer handed here is a private allocation per packet —
|
|
4634
|
-
* `
|
|
4635
|
-
*
|
|
4636
|
-
*
|
|
4637
|
-
*
|
|
4638
|
-
*
|
|
4639
|
-
*
|
|
4640
|
-
*
|
|
4641
|
-
* to: a pending write holds its own reference, and the rule below is about
|
|
4713
|
+
* `copyOwned(readBuffer, …)` in the readers' `onData`, for video and audio
|
|
4714
|
+
* alike — never a view of the reader's socket buffer, which advances on the
|
|
4715
|
+
* next read. Nothing in the broker writes into a captured packet: every
|
|
4716
|
+
* mutation in this path targets a freshly allocated buffer. (The same object
|
|
4717
|
+
* is what the broker's `rtpRing` and the restreamer's keyframe cache retain —
|
|
4718
|
+
* it owns its backing store, so `detachFromPool` keeps it as is — and that is
|
|
4719
|
+
* fine: a pending write holds its own reference, and the rule below is about
|
|
4642
4720
|
* mutation, not retention.)
|
|
4643
4721
|
*
|
|
4644
4722
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
@@ -4648,18 +4726,36 @@ var RtspSession = class {
|
|
|
4648
4726
|
* queued. Four bytes from the Buffer pool is a pointer bump; the copy this
|
|
4649
4727
|
* replaces was ~1.4 KB.
|
|
4650
4728
|
*
|
|
4651
|
-
*
|
|
4652
|
-
*
|
|
4729
|
+
* ## One cork per ACCESS UNIT, not per packet
|
|
4730
|
+
*
|
|
4731
|
+
* The pair used to be corked and uncorked on its own: one `writev` per RTP
|
|
4732
|
+
* packet per session, so a 4K keyframe fragmented into a hundred packets and
|
|
4733
|
+
* fanned out to three sessions was three hundred syscalls for one picture no
|
|
4734
|
+
* decoder can use before it is whole. The socket is corked on the first write
|
|
4735
|
+
* of an access unit and uncorked by the packet that ENDS it
|
|
4736
|
+
* (`endOfAccessUnit`: the RTP marker on video, every audio packet), so the
|
|
4737
|
+
* picture leaves in one `writev`. A `process.nextTick` safety net uncorks
|
|
4738
|
+
* whatever is still open before the tick yields to I/O — a marker-less camera
|
|
4739
|
+
* or a lost marker degrades to one `writev` per tick, never to a socket held
|
|
4740
|
+
* corked across a read.
|
|
4741
|
+
*
|
|
4742
|
+
* Backpressure sees through the cork: `writableLength` counts corked bytes,
|
|
4743
|
+
* so the `MAX_WRITE_BUFFER` gate, the latch and the missed-keyframe close
|
|
4744
|
+
* (D447) judge exactly what they judged per packet.
|
|
4653
4745
|
*/
|
|
4654
|
-
writeInterleaved(channel, payload) {
|
|
4746
|
+
writeInterleaved(channel, payload, endOfAccessUnit) {
|
|
4655
4747
|
const header = Buffer.allocUnsafe(4);
|
|
4656
4748
|
header[0] = 36;
|
|
4657
4749
|
header[1] = channel;
|
|
4658
4750
|
header.writeUInt16BE(payload.length, 2);
|
|
4659
|
-
this.
|
|
4751
|
+
if (!this.accessUnitCorked) {
|
|
4752
|
+
this.accessUnitCorked = true;
|
|
4753
|
+
this.socket.cork();
|
|
4754
|
+
process.nextTick(this.uncorkAccessUnit);
|
|
4755
|
+
}
|
|
4660
4756
|
this.socket.write(header);
|
|
4661
4757
|
this.socket.write(payload);
|
|
4662
|
-
this.
|
|
4758
|
+
if (endOfAccessUnit) this.uncorkAccessUnit();
|
|
4663
4759
|
this.lastRtpAt = Date.now();
|
|
4664
4760
|
this.bytesSent += 4 + payload.length;
|
|
4665
4761
|
}
|
|
@@ -10083,7 +10179,7 @@ var Rfc4571Reader = class {
|
|
|
10083
10179
|
while (this.readBuffer.length >= 2) {
|
|
10084
10180
|
const length = this.readBuffer.readUInt16BE(0);
|
|
10085
10181
|
if (this.readBuffer.length < 2 + length) return;
|
|
10086
|
-
const rtpPacket =
|
|
10182
|
+
const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
|
|
10087
10183
|
this.readBuffer = this.readBuffer.subarray(2 + length);
|
|
10088
10184
|
this.dispatchRtp(rtpPacket);
|
|
10089
10185
|
}
|
|
@@ -10128,12 +10224,109 @@ function allocAnnexBNal(byteLength) {
|
|
|
10128
10224
|
buf[3] = 1;
|
|
10129
10225
|
return buf;
|
|
10130
10226
|
}
|
|
10227
|
+
var NO_PAYLOAD_CLASS = {
|
|
10228
|
+
keyframe: false,
|
|
10229
|
+
parameterSet: false
|
|
10230
|
+
};
|
|
10231
|
+
function isH265IrapType(type) {
|
|
10232
|
+
return type >= H265_NAL_IRAP_MIN && type <= H265_NAL_IRAP_MAX;
|
|
10233
|
+
}
|
|
10234
|
+
function isH265ParameterSetType(type) {
|
|
10235
|
+
return type === H265_NAL_VPS || type === H265_NAL_SPS || type === H265_NAL_PPS;
|
|
10236
|
+
}
|
|
10131
10237
|
/** Total byte length of the reassembly fragments, without allocating a closure. */
|
|
10132
10238
|
function totalFragmentBytes(fragments) {
|
|
10133
10239
|
let total = 0;
|
|
10134
10240
|
for (const fragment of fragments) total += fragment.length;
|
|
10135
10241
|
return total;
|
|
10136
10242
|
}
|
|
10243
|
+
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10244
|
+
function classifyH264Payload(payload) {
|
|
10245
|
+
const nalType = payload[0] & 31;
|
|
10246
|
+
if (nalType === H264_NAL_STAP_A) {
|
|
10247
|
+
let keyframe = false;
|
|
10248
|
+
let parameterSet = false;
|
|
10249
|
+
let offset = 1;
|
|
10250
|
+
while (offset + 2 <= payload.length) {
|
|
10251
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10252
|
+
offset += 2;
|
|
10253
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10254
|
+
const type = payload[offset] & 31;
|
|
10255
|
+
keyframe ||= type === H264_NAL_IDR;
|
|
10256
|
+
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10257
|
+
offset += nalSize;
|
|
10258
|
+
}
|
|
10259
|
+
return {
|
|
10260
|
+
keyframe,
|
|
10261
|
+
parameterSet
|
|
10262
|
+
};
|
|
10263
|
+
}
|
|
10264
|
+
if (nalType === 28) {
|
|
10265
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10266
|
+
return {
|
|
10267
|
+
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10268
|
+
parameterSet: false
|
|
10269
|
+
};
|
|
10270
|
+
}
|
|
10271
|
+
return {
|
|
10272
|
+
keyframe: nalType === H264_NAL_IDR,
|
|
10273
|
+
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10274
|
+
};
|
|
10275
|
+
}
|
|
10276
|
+
/** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
|
|
10277
|
+
function classifyH265Payload(payload) {
|
|
10278
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10279
|
+
const nalType = payload[0] >> 1 & 63;
|
|
10280
|
+
if (nalType === H265_NAL_AP) {
|
|
10281
|
+
let keyframe = false;
|
|
10282
|
+
let parameterSet = false;
|
|
10283
|
+
let offset = 2;
|
|
10284
|
+
while (offset + 2 <= payload.length) {
|
|
10285
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10286
|
+
offset += 2;
|
|
10287
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10288
|
+
const type = payload[offset] >> 1 & 63;
|
|
10289
|
+
keyframe ||= isH265IrapType(type);
|
|
10290
|
+
parameterSet ||= isH265ParameterSetType(type);
|
|
10291
|
+
offset += nalSize;
|
|
10292
|
+
}
|
|
10293
|
+
return {
|
|
10294
|
+
keyframe,
|
|
10295
|
+
parameterSet
|
|
10296
|
+
};
|
|
10297
|
+
}
|
|
10298
|
+
if (nalType === 49) {
|
|
10299
|
+
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10300
|
+
return {
|
|
10301
|
+
keyframe: isH265IrapType(payload[2] & 63),
|
|
10302
|
+
parameterSet: false
|
|
10303
|
+
};
|
|
10304
|
+
}
|
|
10305
|
+
return {
|
|
10306
|
+
keyframe: isH265IrapType(nalType),
|
|
10307
|
+
parameterSet: isH265ParameterSetType(nalType)
|
|
10308
|
+
};
|
|
10309
|
+
}
|
|
10310
|
+
/**
|
|
10311
|
+
* Classify one raw RTP packet (12-byte header + payload) by what it carries —
|
|
10312
|
+
* the per-packet half of {@link RtpDepacketizer.processPacket}, without the
|
|
10313
|
+
* reassembly half. Pure: reads the headers, allocates nothing, keeps no state.
|
|
10314
|
+
* `null` for a packet too short to be RTP with a payload.
|
|
10315
|
+
*/
|
|
10316
|
+
function classifyRtpPacket(codec, rtpData) {
|
|
10317
|
+
if (rtpData.length < 12) return null;
|
|
10318
|
+
const marker = (rtpData[1] & 128) !== 0;
|
|
10319
|
+
const timestamp = rtpData.readUInt32BE(4);
|
|
10320
|
+
const payload = rtpData.subarray(12);
|
|
10321
|
+
if (payload.length === 0) return null;
|
|
10322
|
+
const carried = codec === "h264" ? classifyH264Payload(payload) : classifyH265Payload(payload);
|
|
10323
|
+
return {
|
|
10324
|
+
timestamp,
|
|
10325
|
+
marker,
|
|
10326
|
+
keyframe: carried.keyframe,
|
|
10327
|
+
parameterSet: carried.parameterSet
|
|
10328
|
+
};
|
|
10329
|
+
}
|
|
10137
10330
|
/**
|
|
10138
10331
|
* RTP → Annex-B NAL depacketizer.
|
|
10139
10332
|
*
|
|
@@ -10173,16 +10366,27 @@ var RtpDepacketizer = class {
|
|
|
10173
10366
|
}
|
|
10174
10367
|
/**
|
|
10175
10368
|
* Process a raw RTP packet (including 12-byte RTP header).
|
|
10176
|
-
* Emits zero or more complete NAL units via the callback
|
|
10369
|
+
* Emits zero or more complete NAL units via the callback and returns what
|
|
10370
|
+
* THIS packet carried ({@link RtpPacketClass}) — `null` for a packet too
|
|
10371
|
+
* short to be RTP with a payload.
|
|
10177
10372
|
*/
|
|
10178
10373
|
processPacket(rtpData) {
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
const timestamp = rtpData.readUInt32BE(4);
|
|
10374
|
+
const carried = this.classifyPacket(rtpData);
|
|
10375
|
+
if (carried === null) return null;
|
|
10182
10376
|
const payload = rtpData.subarray(12);
|
|
10183
|
-
if (
|
|
10184
|
-
|
|
10185
|
-
|
|
10377
|
+
if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
|
|
10378
|
+
else this.processH265(payload, carried.timestamp, carried.marker);
|
|
10379
|
+
return carried;
|
|
10380
|
+
}
|
|
10381
|
+
/**
|
|
10382
|
+
* What this packet CARRIES, and nothing else: no NAL is reassembled, no buffer
|
|
10383
|
+
* allocated, no reassembly state touched. The same verdict `processPacket`
|
|
10384
|
+
* returns, for a broker whose Annex-B plane has no consumer — the source-RTP
|
|
10385
|
+
* ring, the restreamer's keyframe cache and the stats need exactly this and
|
|
10386
|
+
* none of the copies.
|
|
10387
|
+
*/
|
|
10388
|
+
classifyPacket(rtpData) {
|
|
10389
|
+
return classifyRtpPacket(this.codec, rtpData);
|
|
10186
10390
|
}
|
|
10187
10391
|
processH264(payload, timestamp, marker) {
|
|
10188
10392
|
const nalType = payload[0] & 31;
|
|
@@ -10328,6 +10532,140 @@ var RtpDepacketizer = class {
|
|
|
10328
10532
|
}
|
|
10329
10533
|
};
|
|
10330
10534
|
//#endregion
|
|
10535
|
+
//#region src/stream-broker/rtsp/rtp-access-unit-tracker.ts
|
|
10536
|
+
/**
|
|
10537
|
+
* Tracks access-unit membership across the source RTP packets of ONE stream.
|
|
10538
|
+
*
|
|
10539
|
+
* The RTSP restreamer forwards source RTP untouched and must know, per
|
|
10540
|
+
* packet, whether it is part of a keyframe picture: that is what its keyframe
|
|
10541
|
+
* cache collects and what a backlog-latched session resumes on. The
|
|
10542
|
+
* depacketizer can only say so about a NAL that COMPLETED, so the fragments of
|
|
10543
|
+
* a fragmented IDR before the last one, and a prefix SEI between the parameter
|
|
10544
|
+
* sets and the slice, were reported as plain packets: the cache latch closed
|
|
10545
|
+
* on them and re-opened on the final fragment, and a backlogged session
|
|
10546
|
+
* counted every IRAP slice as a missed keyframe. Here a packet is classified
|
|
10547
|
+
* by what it carries ({@link RtpPacketClass}) and the verdict is carried
|
|
10548
|
+
* across the whole picture, bounded by the RTP timestamp and marker bit.
|
|
10549
|
+
*
|
|
10550
|
+
* A parameter-set-only run is not a picture: a camera that stamps SPS/PPS
|
|
10551
|
+
* with the previous timestamp still announces ONE keyframe, so the keyframe
|
|
10552
|
+
* verdict carries over into the slice that follows instead of starting twice.
|
|
10553
|
+
*/
|
|
10554
|
+
var RtpAccessUnitTracker = class {
|
|
10555
|
+
timestamp = -1;
|
|
10556
|
+
previousMarker = true;
|
|
10557
|
+
keyframeAu = false;
|
|
10558
|
+
/** The picture in progress has received something other than a parameter set. */
|
|
10559
|
+
pictureSeen = false;
|
|
10560
|
+
classify(packet) {
|
|
10561
|
+
const auStart = this.previousMarker || packet.timestamp !== this.timestamp;
|
|
10562
|
+
if (auStart && this.pictureSeen) this.keyframeAu = false;
|
|
10563
|
+
if (auStart) this.pictureSeen = false;
|
|
10564
|
+
this.timestamp = packet.timestamp;
|
|
10565
|
+
this.previousMarker = packet.marker;
|
|
10566
|
+
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10567
|
+
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10568
|
+
if (keyframeContent) this.keyframeAu = true;
|
|
10569
|
+
const pictureStart = !packet.parameterSet && !this.pictureSeen;
|
|
10570
|
+
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10571
|
+
return {
|
|
10572
|
+
auStart,
|
|
10573
|
+
keyframeAu: this.keyframeAu,
|
|
10574
|
+
keyframeAuStart,
|
|
10575
|
+
pictureStart,
|
|
10576
|
+
parameterSet: packet.parameterSet
|
|
10577
|
+
};
|
|
10578
|
+
}
|
|
10579
|
+
/** A redial re-bases the clock; nothing carries over. */
|
|
10580
|
+
reset() {
|
|
10581
|
+
this.timestamp = -1;
|
|
10582
|
+
this.previousMarker = true;
|
|
10583
|
+
this.keyframeAu = false;
|
|
10584
|
+
this.pictureSeen = false;
|
|
10585
|
+
}
|
|
10586
|
+
};
|
|
10587
|
+
//#endregion
|
|
10588
|
+
//#region src/stream-broker/stream-broker/access-unit-assembler.ts
|
|
10589
|
+
/**
|
|
10590
|
+
* Groups depacketized NALs into ACCESS UNITS — one per picture.
|
|
10591
|
+
*
|
|
10592
|
+
* The broker's `EncodedPacket` is one access unit, and every consumer judges it
|
|
10593
|
+
* that way: `keyframe` gates where a clip ring widens to, where a late joiner
|
|
10594
|
+
* can start, how the pre-roll window is cut; the stats count it as one picture
|
|
10595
|
+
* and time the GOP from one `keyframe` to the next. A multi-slice camera (a
|
|
10596
|
+
* Hikvision G2 main stream is 3 slices per picture) used to hand each slice —
|
|
10597
|
+
* and each prefix SEI — down as its own "frame": 75 input fps on a 25 fps
|
|
10598
|
+
* camera, an 8–34 ms "GOP", a clip ring that widened to the LAST IRAP slice
|
|
10599
|
+
* (a third of the picture, without its parameter sets), and a prefix SEI that
|
|
10600
|
+
* inherited the welded VPS/SPS/PPS instead of the IDR.
|
|
10601
|
+
*
|
|
10602
|
+
* Boundary rule (RFC 3550 / 6184 / 7798): the RTP marker bit closes the
|
|
10603
|
+
* picture it ends — flushed on the spot, so a single-slice stream pays no
|
|
10604
|
+
* latency — and a timestamp change closes the one before it, for cameras that
|
|
10605
|
+
* never set the marker. Parameter sets are held and welded onto the picture
|
|
10606
|
+
* that follows them, so a keyframe access unit is self-contained
|
|
10607
|
+
* (VPS/SPS/PPS + IDR) exactly as before; a prefix SEI stays inside its
|
|
10608
|
+
* picture and is never a frame of its own.
|
|
10609
|
+
*/
|
|
10610
|
+
var AccessUnitAssembler = class {
|
|
10611
|
+
onAccessUnit;
|
|
10612
|
+
/** Parameter sets seen since the last picture closed; welded onto the next one. */
|
|
10613
|
+
pendingParamNals = [];
|
|
10614
|
+
/** NALs of the picture in progress (parameter sets first when welded). */
|
|
10615
|
+
pictureNals = [];
|
|
10616
|
+
pictureTimestamp = 0;
|
|
10617
|
+
pictureKeyframe = false;
|
|
10618
|
+
constructor(onAccessUnit) {
|
|
10619
|
+
this.onAccessUnit = onAccessUnit;
|
|
10620
|
+
}
|
|
10621
|
+
push(depacketized) {
|
|
10622
|
+
const { annexB, keyframe, parameterSet, timestamp, marker } = depacketized;
|
|
10623
|
+
if (this.pictureNals.length > 0 && timestamp !== this.pictureTimestamp) this.flush();
|
|
10624
|
+
if (parameterSet) {
|
|
10625
|
+
this.pendingParamNals.push(annexB);
|
|
10626
|
+
return;
|
|
10627
|
+
}
|
|
10628
|
+
if (this.pictureNals.length === 0) {
|
|
10629
|
+
this.pictureTimestamp = timestamp;
|
|
10630
|
+
this.pictureKeyframe = false;
|
|
10631
|
+
}
|
|
10632
|
+
if (this.pendingParamNals.length > 0) {
|
|
10633
|
+
for (const paramNal of this.pendingParamNals) this.pictureNals.push(paramNal);
|
|
10634
|
+
this.pendingParamNals = [];
|
|
10635
|
+
this.pictureKeyframe = true;
|
|
10636
|
+
}
|
|
10637
|
+
this.pictureNals.push(annexB);
|
|
10638
|
+
if (keyframe) this.pictureKeyframe = true;
|
|
10639
|
+
if (marker) this.flush();
|
|
10640
|
+
}
|
|
10641
|
+
/**
|
|
10642
|
+
* Emit the picture in progress, if any. Called on the marker bit, on a
|
|
10643
|
+
* timestamp change, and by the broker when the stream stops or redials so
|
|
10644
|
+
* the last picture of a marker-less stream is not lost.
|
|
10645
|
+
*/
|
|
10646
|
+
flush() {
|
|
10647
|
+
if (this.pictureNals.length === 0) return;
|
|
10648
|
+
const nals = this.pictureNals;
|
|
10649
|
+
const timestamp = this.pictureTimestamp;
|
|
10650
|
+
const keyframe = this.pictureKeyframe;
|
|
10651
|
+
this.pictureNals = [];
|
|
10652
|
+
this.pictureKeyframe = false;
|
|
10653
|
+
const annexB = nals.length === 1 ? nals[0] : Buffer.concat(nals);
|
|
10654
|
+
this.onAccessUnit({
|
|
10655
|
+
annexB,
|
|
10656
|
+
timestamp,
|
|
10657
|
+
keyframe
|
|
10658
|
+
});
|
|
10659
|
+
}
|
|
10660
|
+
/** Drop everything held — a redial re-bases the clock, nothing carries over. */
|
|
10661
|
+
reset() {
|
|
10662
|
+
this.pendingParamNals = [];
|
|
10663
|
+
this.pictureNals = [];
|
|
10664
|
+
this.pictureTimestamp = 0;
|
|
10665
|
+
this.pictureKeyframe = false;
|
|
10666
|
+
}
|
|
10667
|
+
};
|
|
10668
|
+
//#endregion
|
|
10331
10669
|
//#region src/stream-broker/rtsp/rtsp-client.ts
|
|
10332
10670
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
10333
10671
|
var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
|
|
@@ -10560,7 +10898,7 @@ var NativeRtspClient = class {
|
|
|
10560
10898
|
const channel = this.readBuffer[1];
|
|
10561
10899
|
const length = this.readBuffer.readUInt16BE(2);
|
|
10562
10900
|
if (this.readBuffer.length < 4 + length) return;
|
|
10563
|
-
const payload =
|
|
10901
|
+
const payload = copyOwned(this.readBuffer, 4, 4 + length);
|
|
10564
10902
|
this.readBuffer = this.readBuffer.subarray(4 + length);
|
|
10565
10903
|
this.handleInterleavedPacket(channel, payload);
|
|
10566
10904
|
} else {
|
|
@@ -11296,49 +11634,6 @@ var AudioCodecSession = class {
|
|
|
11296
11634
|
}
|
|
11297
11635
|
};
|
|
11298
11636
|
//#endregion
|
|
11299
|
-
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
11300
|
-
/**
|
|
11301
|
-
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
11302
|
-
*
|
|
11303
|
-
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
11304
|
-
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
11305
|
-
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
11306
|
-
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
11307
|
-
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
11308
|
-
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
11309
|
-
* packet that happened to be allocated beside it. On the hub that held
|
|
11310
|
-
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
11311
|
-
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
11312
|
-
*
|
|
11313
|
-
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
11314
|
-
* now pass every entry through {@link detachFromPool} on the way in and
|
|
11315
|
-
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
11316
|
-
* entry costs. Short-lived traffic stays pooled.
|
|
11317
|
-
*/
|
|
11318
|
-
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
11319
|
-
function ownsBackingStore(buf) {
|
|
11320
|
-
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
11321
|
-
}
|
|
11322
|
-
/**
|
|
11323
|
-
* What retaining `buf` really costs: its own length when it owns its
|
|
11324
|
-
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
11325
|
-
*/
|
|
11326
|
-
function backingStoreBytes(buf) {
|
|
11327
|
-
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
11328
|
-
}
|
|
11329
|
-
/**
|
|
11330
|
-
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
11331
|
-
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
11332
|
-
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
11333
|
-
* already unpooled is returned as-is.
|
|
11334
|
-
*/
|
|
11335
|
-
function detachFromPool(buf) {
|
|
11336
|
-
if (ownsBackingStore(buf)) return buf;
|
|
11337
|
-
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
11338
|
-
buf.copy(owned);
|
|
11339
|
-
return owned;
|
|
11340
|
-
}
|
|
11341
|
-
//#endregion
|
|
11342
11637
|
//#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
|
|
11343
11638
|
/**
|
|
11344
11639
|
* Time-based ring buffer for encoded video packets.
|
|
@@ -12278,9 +12573,6 @@ var StreamBroker = class StreamBroker {
|
|
|
12278
12573
|
*/
|
|
12279
12574
|
ffmpegBinaryPathSource = () => null;
|
|
12280
12575
|
rtpDepacketizer = null;
|
|
12281
|
-
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12282
|
-
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
12283
|
-
pushRtpFlagDepacketizer = null;
|
|
12284
12576
|
audioRtpDecoder = null;
|
|
12285
12577
|
audioCodecSession = null;
|
|
12286
12578
|
/**
|
|
@@ -12648,10 +12940,24 @@ var StreamBroker = class StreamBroker {
|
|
|
12648
12940
|
* log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
|
|
12649
12941
|
* back but never video is observable from the structured logs. */
|
|
12650
12942
|
videoRtpSeen = 0;
|
|
12651
|
-
/**
|
|
12943
|
+
/**
|
|
12944
|
+
* What the source RTP packet being handled CARRIES (keyframe / parameter-set
|
|
12945
|
+
* content, read from its payload headers by `classifyRtpPacket`) — the flags
|
|
12946
|
+
* `captureRtpForPreBuffer` reads. Per packet, not per completed NAL: every
|
|
12947
|
+
* fragment of a fragmented IDR carries them, and the ring marks one keyframe
|
|
12948
|
+
* AU for the lot.
|
|
12949
|
+
*/
|
|
12652
12950
|
_lastNalKeyframe = false;
|
|
12653
12951
|
_lastNalParamSet = false;
|
|
12654
12952
|
/**
|
|
12953
|
+
* Whether the Annex-B plane (NAL reassembly + access-unit assembly →
|
|
12954
|
+
* `EncodedPacket`) is being BUILT for this stream. False while nothing
|
|
12955
|
+
* consumes it: the source-RTP path then classifies each packet and copies
|
|
12956
|
+
* nothing. Armed at the next keyframe access unit once a consumer attaches
|
|
12957
|
+
* (`needsAnnexBPlane`), so the first packet it emits is decodable.
|
|
12958
|
+
*/
|
|
12959
|
+
annexBPlaneActive = false;
|
|
12960
|
+
/**
|
|
12655
12961
|
* Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
|
|
12656
12962
|
* begin at the most recent keyframe access unit — i.e. the current GOP so
|
|
12657
12963
|
* far. A late-joining WebRTC viewer on the repacketizer path replays this
|
|
@@ -13153,7 +13459,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13153
13459
|
this.rtspRestreamer.destroy();
|
|
13154
13460
|
this.preBuffer.clear();
|
|
13155
13461
|
this.clipBuffer?.clear();
|
|
13156
|
-
this.
|
|
13462
|
+
this.annexBAssembler.reset();
|
|
13157
13463
|
this.encodedCallbacks.clear();
|
|
13158
13464
|
this.audioChunkPlane.dispose();
|
|
13159
13465
|
}
|
|
@@ -13616,7 +13922,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13616
13922
|
this.reconcilePrestreamWindow();
|
|
13617
13923
|
this.preBuffer.clear();
|
|
13618
13924
|
this.clipBuffer?.clear();
|
|
13619
|
-
this.
|
|
13925
|
+
this.annexBAssembler.reset();
|
|
13620
13926
|
this.rtspRestreamer.resetStreamState();
|
|
13621
13927
|
this.totalBytes = 0;
|
|
13622
13928
|
this.bytesInWindow = 0;
|
|
@@ -13727,26 +14033,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13727
14033
|
return;
|
|
13728
14034
|
}
|
|
13729
14035
|
}
|
|
13730
|
-
if (packet.type === "video")
|
|
13731
|
-
this.packetCount++;
|
|
13732
|
-
this.totalBytes += packet.data.length;
|
|
13733
|
-
this.bytesInWindow += packet.data.length;
|
|
13734
|
-
this.packetsInWindow++;
|
|
13735
|
-
this.lastPacketAt = Date.now();
|
|
13736
|
-
const now = Date.now();
|
|
13737
|
-
const windowMs = now - this.windowStartMs;
|
|
13738
|
-
if (windowMs >= 2e3) {
|
|
13739
|
-
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
13740
|
-
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
13741
|
-
this.bytesInWindow = 0;
|
|
13742
|
-
this.packetsInWindow = 0;
|
|
13743
|
-
this.windowStartMs = now;
|
|
13744
|
-
}
|
|
13745
|
-
if (packet.keyframe) {
|
|
13746
|
-
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
13747
|
-
this.lastKeyframeMs = now;
|
|
13748
|
-
}
|
|
13749
|
-
}
|
|
14036
|
+
if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
|
|
13750
14037
|
if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
|
|
13751
14038
|
if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
|
|
13752
14039
|
this.fanOutEncoded(packet);
|
|
@@ -13937,8 +14224,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13937
14224
|
index: auStart,
|
|
13938
14225
|
ts: auPacket.readUInt32BE(4)
|
|
13939
14226
|
});
|
|
14227
|
+
this.trimRtpRingToPreRollWindow();
|
|
13940
14228
|
}
|
|
13941
|
-
this.trimRtpRingToPreRollWindow();
|
|
13942
14229
|
this.rtpRingHasKeyframe = true;
|
|
13943
14230
|
}
|
|
13944
14231
|
this.rtpRingPrevMarker = marker;
|
|
@@ -14207,10 +14494,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14207
14494
|
*
|
|
14208
14495
|
* `push-rtp` providers deliver AnnexB-equivalents separately via
|
|
14209
14496
|
* `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
|
|
14210
|
-
* does NOT re-emit EncodedPackets; it only
|
|
14211
|
-
* keyframe/param-set flags the pre-buffer capture needs
|
|
14212
|
-
*
|
|
14213
|
-
*
|
|
14497
|
+
* does NOT re-emit EncodedPackets; it only classifies each packet for the
|
|
14498
|
+
* keyframe/param-set flags the pre-buffer capture needs
|
|
14499
|
+
* (`classifyRtpPacket`, no reassembly), keeping the AnnexB consumers
|
|
14500
|
+
* (pipe, restreamer) single-fed by the provider.
|
|
14214
14501
|
*
|
|
14215
14502
|
* The pre-buffer is captured even with zero subscribers attached — a
|
|
14216
14503
|
* viewer that connects mid-GOP must be able to replay the GOP that
|
|
@@ -14220,9 +14507,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14220
14507
|
pushVideoRtp(rtpData) {
|
|
14221
14508
|
if (this.stopping) return;
|
|
14222
14509
|
this.kickRtpActivityWatchdog();
|
|
14223
|
-
this.
|
|
14224
|
-
this.
|
|
14225
|
-
this.
|
|
14510
|
+
const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
|
|
14511
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14512
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14226
14513
|
this.captureRtpForPreBuffer(rtpData);
|
|
14227
14514
|
if (this.rtpVideoCallbacks.size === 0) return;
|
|
14228
14515
|
for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -14232,27 +14519,6 @@ var StreamBroker = class StreamBroker {
|
|
|
14232
14519
|
this.logger?.warn("rtp video subscriber threw", { meta: { error: require_dist.errMsg(err) } });
|
|
14233
14520
|
}
|
|
14234
14521
|
}
|
|
14235
|
-
/**
|
|
14236
|
-
* Lazily build a flag-only RTP depacketizer for the `push-rtp` source
|
|
14237
|
-
* path. Unlike the native reader's depacketizer (which emits AnnexB
|
|
14238
|
-
* EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
|
|
14239
|
-
* `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
|
|
14240
|
-
* AnnexB plane through `pushEncodedPacket`, so re-emitting here would
|
|
14241
|
-
* double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
|
|
14242
|
-
* so the codec comes from the source declaration (`detectedCodec`, seeded
|
|
14243
|
-
* in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
|
|
14244
|
-
* starts with clean FU reassembly state.
|
|
14245
|
-
*/
|
|
14246
|
-
ensurePushRtpFlagDepacketizer() {
|
|
14247
|
-
if (!this.pushRtpFlagDepacketizer) {
|
|
14248
|
-
const codec = this.detectedCodec === "h265" ? "h265" : "h264";
|
|
14249
|
-
this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
|
|
14250
|
-
if (keyframe) this._lastNalKeyframe = true;
|
|
14251
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14252
|
-
});
|
|
14253
|
-
}
|
|
14254
|
-
return this.pushRtpFlagDepacketizer;
|
|
14255
|
-
}
|
|
14256
14522
|
/** Toggle the per-device streaming debug flag. */
|
|
14257
14523
|
setStreamingDebug(enabled) {
|
|
14258
14524
|
this.streamingDebug = enabled;
|
|
@@ -14695,10 +14961,18 @@ var StreamBroker = class StreamBroker {
|
|
|
14695
14961
|
seq: this.videoRtpSeen,
|
|
14696
14962
|
bytes: rtpData.length
|
|
14697
14963
|
} });
|
|
14698
|
-
|
|
14699
|
-
this.
|
|
14700
|
-
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14701
|
-
this.
|
|
14964
|
+
const wantPlane = this.needsAnnexBPlane();
|
|
14965
|
+
if (!wantPlane && this.annexBPlaneActive) this.idleAnnexBPlane();
|
|
14966
|
+
const carried = this.annexBPlaneActive ? this.rtpDepacketizer?.processPacket(rtpData) ?? null : this.rtpDepacketizer?.classifyPacket(rtpData) ?? null;
|
|
14967
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14968
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14969
|
+
const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
|
|
14970
|
+
if (wantPlane && !this.annexBPlaneActive && auFlags?.keyframeAuStart === true) {
|
|
14971
|
+
this.armAnnexBPlane();
|
|
14972
|
+
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14973
|
+
}
|
|
14974
|
+
if (auFlags) this.recordVideoStats(rtpData.length, auFlags.pictureStart, auFlags.keyframeAu);
|
|
14975
|
+
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14702
14976
|
this.captureRtpForPreBuffer(rtpData);
|
|
14703
14977
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
14704
14978
|
cb(rtpData);
|
|
@@ -14894,51 +15168,116 @@ var StreamBroker = class StreamBroker {
|
|
|
14894
15168
|
* RTSP restreamer's pushRtpPassthrough.
|
|
14895
15169
|
*/
|
|
14896
15170
|
/**
|
|
14897
|
-
*
|
|
14898
|
-
*
|
|
14899
|
-
*
|
|
14900
|
-
*
|
|
14901
|
-
* with a single `EncodedPacket` that contains SPS+PPS+IDR together.
|
|
15171
|
+
* Groups the depacketizer's NALs into access units — ONE `EncodedPacket`
|
|
15172
|
+
* per picture, parameter sets welded onto the keyframe picture they
|
|
15173
|
+
* precede. See `access-unit-assembler.ts` for the boundary rule and the
|
|
15174
|
+
* per-NAL emit it replaced.
|
|
14902
15175
|
*/
|
|
14903
|
-
|
|
15176
|
+
annexBAssembler = new AccessUnitAssembler((accessUnit) => this.emitAccessUnit(accessUnit));
|
|
15177
|
+
/** Access-unit membership of the source RTP packets, for the restreamer's flags. */
|
|
15178
|
+
rtpAuTracker = new RtpAccessUnitTracker();
|
|
14904
15179
|
/**
|
|
14905
15180
|
* handleDepacketizedNal receives individual NALs from the RTP depacketizer
|
|
14906
|
-
* and
|
|
14907
|
-
*
|
|
14908
|
-
*
|
|
14909
|
-
*
|
|
14910
|
-
* This guarantees every keyframe EncodedPacket is a self-contained access
|
|
14911
|
-
* unit that a decoder can initialise from — critical for WebRTC (Chrome
|
|
14912
|
-
* can't decode an IDR without preceding SPS/PPS).
|
|
15181
|
+
* and hands them to the access-unit assembler, which emits one
|
|
15182
|
+
* `EncodedPacket` per PICTURE (`emitAccessUnit`) — never per NAL. A
|
|
15183
|
+
* multi-slice IDR, its parameter sets and its prefix SEI are one keyframe
|
|
15184
|
+
* packet; three P slices are one P packet.
|
|
14913
15185
|
*
|
|
14914
|
-
* The RTSP restreamer
|
|
14915
|
-
* not
|
|
15186
|
+
* The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
|
|
15187
|
+
* not fed from here.
|
|
14916
15188
|
*/
|
|
14917
15189
|
handleDepacketizedNal(depacketized) {
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
15190
|
+
this.annexBAssembler.push(depacketized);
|
|
15191
|
+
}
|
|
15192
|
+
/**
|
|
15193
|
+
* Does anything consume the Annex-B plane right now? An encoded subscriber
|
|
15194
|
+
* (WebRTC Annex-B, fragments, a decode session), a pipe client, a clip ring
|
|
15195
|
+
* (it retains access units continuously, so it needs them built even with
|
|
15196
|
+
* nobody attached), or a restreamer still in Annex-B mode (no camera SDP yet).
|
|
15197
|
+
* The RTSP restream and the WebRTC RTP path are served from the source RTP
|
|
15198
|
+
* and never count. Cheap — four field reads — because it runs per packet.
|
|
15199
|
+
*/
|
|
15200
|
+
needsAnnexBPlane() {
|
|
15201
|
+
return this.encodedCallbacks.size > 0 || this.clipBuffer !== null || this.pipeServer.getClientCount() > 0 || !this.rtspRestreamer.rtpPassthroughMode;
|
|
15202
|
+
}
|
|
15203
|
+
/** The consumers that want the plane, for the arm/idle lines. */
|
|
15204
|
+
annexBPlaneConsumers() {
|
|
15205
|
+
return {
|
|
15206
|
+
encodedSubscribers: this.encodedCallbacks.size,
|
|
15207
|
+
clipRing: this.clipBuffer !== null,
|
|
15208
|
+
pipeClients: this.pipeServer.getClientCount(),
|
|
15209
|
+
restreamerAnnexB: !this.rtspRestreamer.rtpPassthroughMode
|
|
15210
|
+
};
|
|
15211
|
+
}
|
|
15212
|
+
/**
|
|
15213
|
+
* Start building access units from the keyframe AU the current packet opens.
|
|
15214
|
+
* The depacketizer and the assembler are clean here (reset when the plane
|
|
15215
|
+
* went idle, or never used), so nothing stale precedes that keyframe.
|
|
15216
|
+
*/
|
|
15217
|
+
armAnnexBPlane() {
|
|
15218
|
+
this.annexBPlaneActive = true;
|
|
15219
|
+
this.logger?.info("annex-b plane armed at a keyframe access unit", {
|
|
15220
|
+
tags: { deviceId: this.numericDeviceId },
|
|
15221
|
+
meta: this.annexBPlaneConsumers()
|
|
15222
|
+
});
|
|
15223
|
+
}
|
|
15224
|
+
/**
|
|
15225
|
+
* Stop building access units: the last consumer left. The picture in
|
|
15226
|
+
* progress is dropped, not flushed — there is nobody to hand it to, and a
|
|
15227
|
+
* partial picture must never reach the next consumer's clean start. Logged,
|
|
15228
|
+
* once per transition: a plane that is idle on purpose must not read as a
|
|
15229
|
+
* stream that stopped.
|
|
15230
|
+
*/
|
|
15231
|
+
idleAnnexBPlane() {
|
|
15232
|
+
this.annexBPlaneActive = false;
|
|
15233
|
+
this.rtpDepacketizer?.reset();
|
|
15234
|
+
this.annexBAssembler.reset();
|
|
15235
|
+
this.logger?.info("annex-b plane idle — no consumer, NAL reassembly skipped", { tags: { deviceId: this.numericDeviceId } });
|
|
15236
|
+
}
|
|
15237
|
+
/**
|
|
15238
|
+
* One place for the input stats, fed from whichever path owns the pictures:
|
|
15239
|
+
* the source RTP packets while the native reader is up (`onVideoRtp`), the
|
|
15240
|
+
* `EncodedPacket`s otherwise (`pushEncodedPacket`). `bytes` accumulate per
|
|
15241
|
+
* call; `inputFps`, `packetCount` and the IDR interval advance once per
|
|
15242
|
+
* picture — the window is closed on a picture start so a window of N frames
|
|
15243
|
+
* reads N, exactly as it did when every call was one access unit.
|
|
15244
|
+
*/
|
|
15245
|
+
recordVideoStats(bytes, pictureStart, keyframe) {
|
|
15246
|
+
this.totalBytes += bytes;
|
|
15247
|
+
this.bytesInWindow += bytes;
|
|
15248
|
+
if (!pictureStart) return;
|
|
15249
|
+
this.packetCount++;
|
|
15250
|
+
this.packetsInWindow++;
|
|
15251
|
+
const now = Date.now();
|
|
15252
|
+
this.lastPacketAt = now;
|
|
15253
|
+
const windowMs = now - this.windowStartMs;
|
|
15254
|
+
if (windowMs >= 2e3) {
|
|
15255
|
+
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
15256
|
+
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
15257
|
+
this.bytesInWindow = 0;
|
|
15258
|
+
this.packetsInWindow = 0;
|
|
15259
|
+
this.windowStartMs = now;
|
|
14927
15260
|
}
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
15261
|
+
if (keyframe) {
|
|
15262
|
+
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
15263
|
+
this.lastKeyframeMs = now;
|
|
15264
|
+
}
|
|
15265
|
+
}
|
|
15266
|
+
/**
|
|
15267
|
+
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15268
|
+
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15269
|
+
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
15270
|
+
* initialises from and what a clip or a pre-roll window may start at.
|
|
15271
|
+
*/
|
|
15272
|
+
emitAccessUnit(accessUnit) {
|
|
15273
|
+
const codec = this.detectedCodec ?? "h264";
|
|
15274
|
+
const pts = Math.floor(accessUnit.timestamp / 90);
|
|
14936
15275
|
const packet = {
|
|
14937
15276
|
type: "video",
|
|
14938
|
-
data:
|
|
15277
|
+
data: accessUnit.annexB,
|
|
14939
15278
|
pts,
|
|
14940
15279
|
dts: pts,
|
|
14941
|
-
keyframe: keyframe
|
|
15280
|
+
keyframe: accessUnit.keyframe,
|
|
14942
15281
|
codec
|
|
14943
15282
|
};
|
|
14944
15283
|
this.pushEncodedPacket(packet);
|
|
@@ -15342,10 +15681,10 @@ var StreamBroker = class StreamBroker {
|
|
|
15342
15681
|
this.rtpDepacketizer.reset();
|
|
15343
15682
|
this.rtpDepacketizer = null;
|
|
15344
15683
|
}
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15684
|
+
this.annexBAssembler.flush();
|
|
15685
|
+
this.annexBAssembler.reset();
|
|
15686
|
+
this.rtpAuTracker.reset();
|
|
15687
|
+
this.annexBPlaneActive = false;
|
|
15349
15688
|
this.resetRtpPreBuffer();
|
|
15350
15689
|
if (this.audioRtpDecoder) {
|
|
15351
15690
|
this.audioRtpDecoder.flush();
|