@camstack/addon-pipeline 1.2.363 → 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/stream-broker/index.js +266 -47
- package/dist/stream-broker/index.mjs +266 -47
- package/package.json +1 -1
|
@@ -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
|
/**
|
|
@@ -10128,6 +10132,16 @@ function allocAnnexBNal(byteLength) {
|
|
|
10128
10132
|
buf[3] = 1;
|
|
10129
10133
|
return buf;
|
|
10130
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
|
+
}
|
|
10131
10145
|
/** Total byte length of the reassembly fragments, without allocating a closure. */
|
|
10132
10146
|
function totalFragmentBytes(fragments) {
|
|
10133
10147
|
let total = 0;
|
|
@@ -10173,16 +10187,92 @@ var RtpDepacketizer = class {
|
|
|
10173
10187
|
}
|
|
10174
10188
|
/**
|
|
10175
10189
|
* Process a raw RTP packet (including 12-byte RTP header).
|
|
10176
|
-
* 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.
|
|
10177
10193
|
*/
|
|
10178
10194
|
processPacket(rtpData) {
|
|
10179
|
-
if (rtpData.length < 12) return;
|
|
10195
|
+
if (rtpData.length < 12) return null;
|
|
10180
10196
|
const marker = (rtpData[1] & 128) !== 0;
|
|
10181
10197
|
const timestamp = rtpData.readUInt32BE(4);
|
|
10182
10198
|
const payload = rtpData.subarray(12);
|
|
10183
|
-
if (payload.length === 0) return;
|
|
10199
|
+
if (payload.length === 0) return null;
|
|
10200
|
+
const carried = this.codec === "h264" ? this.classifyH264Payload(payload) : this.classifyH265Payload(payload);
|
|
10184
10201
|
if (this.codec === "h264") this.processH264(payload, timestamp, marker);
|
|
10185
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
|
+
};
|
|
10186
10276
|
}
|
|
10187
10277
|
processH264(payload, timestamp, marker) {
|
|
10188
10278
|
const nalType = payload[0] & 31;
|
|
@@ -10328,6 +10418,138 @@ var RtpDepacketizer = class {
|
|
|
10328
10418
|
}
|
|
10329
10419
|
};
|
|
10330
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
|
|
10331
10553
|
//#region src/stream-broker/rtsp/rtsp-client.ts
|
|
10332
10554
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
10333
10555
|
var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
|
|
@@ -13153,7 +13375,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13153
13375
|
this.rtspRestreamer.destroy();
|
|
13154
13376
|
this.preBuffer.clear();
|
|
13155
13377
|
this.clipBuffer?.clear();
|
|
13156
|
-
this.
|
|
13378
|
+
this.annexBAssembler.reset();
|
|
13157
13379
|
this.encodedCallbacks.clear();
|
|
13158
13380
|
this.audioChunkPlane.dispose();
|
|
13159
13381
|
}
|
|
@@ -13616,7 +13838,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13616
13838
|
this.reconcilePrestreamWindow();
|
|
13617
13839
|
this.preBuffer.clear();
|
|
13618
13840
|
this.clipBuffer?.clear();
|
|
13619
|
-
this.
|
|
13841
|
+
this.annexBAssembler.reset();
|
|
13620
13842
|
this.rtspRestreamer.resetStreamState();
|
|
13621
13843
|
this.totalBytes = 0;
|
|
13622
13844
|
this.bytesInWindow = 0;
|
|
@@ -14697,8 +14919,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14697
14919
|
} });
|
|
14698
14920
|
this._lastNalKeyframe = false;
|
|
14699
14921
|
this._lastNalParamSet = false;
|
|
14700
|
-
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14701
|
-
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);
|
|
14702
14925
|
this.captureRtpForPreBuffer(rtpData);
|
|
14703
14926
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
14704
14927
|
cb(rtpData);
|
|
@@ -14894,51 +15117,44 @@ var StreamBroker = class StreamBroker {
|
|
|
14894
15117
|
* RTSP restreamer's pushRtpPassthrough.
|
|
14895
15118
|
*/
|
|
14896
15119
|
/**
|
|
14897
|
-
*
|
|
14898
|
-
*
|
|
14899
|
-
*
|
|
14900
|
-
*
|
|
14901
|
-
* 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.
|
|
14902
15124
|
*/
|
|
14903
|
-
|
|
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();
|
|
14904
15128
|
/**
|
|
14905
15129
|
* 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).
|
|
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.
|
|
14913
15134
|
*
|
|
14914
|
-
* The RTSP restreamer
|
|
14915
|
-
* not
|
|
15135
|
+
* The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
|
|
15136
|
+
* not fed from here.
|
|
14916
15137
|
*/
|
|
14917
15138
|
handleDepacketizedNal(depacketized) {
|
|
14918
|
-
|
|
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) {
|
|
14919
15150
|
const codec = this.detectedCodec ?? "h264";
|
|
14920
|
-
|
|
14921
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14922
|
-
const isH264 = codec === "h264";
|
|
14923
|
-
const nalType = isH264 ? nal[0] & 31 : (nal[0] & 126) >> 1;
|
|
14924
|
-
if (isH264 ? nalType === 7 || nalType === 8 : nalType === 32 || nalType === 33 || nalType === 34) {
|
|
14925
|
-
this.pendingParamNals.push(depacketized.annexB);
|
|
14926
|
-
return;
|
|
14927
|
-
}
|
|
14928
|
-
const hadParamSets = this.pendingParamNals.length > 0;
|
|
14929
|
-
let annexBData;
|
|
14930
|
-
if (hadParamSets) {
|
|
14931
|
-
this.pendingParamNals.push(depacketized.annexB);
|
|
14932
|
-
annexBData = Buffer.concat(this.pendingParamNals);
|
|
14933
|
-
this.pendingParamNals = [];
|
|
14934
|
-
} else annexBData = depacketized.annexB;
|
|
14935
|
-
const pts = Math.floor(timestamp / 90);
|
|
15151
|
+
const pts = Math.floor(accessUnit.timestamp / 90);
|
|
14936
15152
|
const packet = {
|
|
14937
15153
|
type: "video",
|
|
14938
|
-
data:
|
|
15154
|
+
data: accessUnit.annexB,
|
|
14939
15155
|
pts,
|
|
14940
15156
|
dts: pts,
|
|
14941
|
-
keyframe: keyframe
|
|
15157
|
+
keyframe: accessUnit.keyframe,
|
|
14942
15158
|
codec
|
|
14943
15159
|
};
|
|
14944
15160
|
this.pushEncodedPacket(packet);
|
|
@@ -15342,6 +15558,9 @@ var StreamBroker = class StreamBroker {
|
|
|
15342
15558
|
this.rtpDepacketizer.reset();
|
|
15343
15559
|
this.rtpDepacketizer = null;
|
|
15344
15560
|
}
|
|
15561
|
+
this.annexBAssembler.flush();
|
|
15562
|
+
this.annexBAssembler.reset();
|
|
15563
|
+
this.rtpAuTracker.reset();
|
|
15345
15564
|
if (this.pushRtpFlagDepacketizer) {
|
|
15346
15565
|
this.pushRtpFlagDepacketizer.reset();
|
|
15347
15566
|
this.pushRtpFlagDepacketizer = null;
|
|
@@ -3727,7 +3727,12 @@ var RtspRestreamer = class {
|
|
|
3727
3727
|
* Forwarded directly to all connected RTSP sessions — zero re-packetization.
|
|
3728
3728
|
*
|
|
3729
3729
|
* @param rtpData Complete RTP packet (header + payload)
|
|
3730
|
-
* @param isKeyframe True if this packet is part of a keyframe
|
|
3730
|
+
* @param isKeyframe True if this packet is part of a keyframe ACCESS UNIT —
|
|
3731
|
+
* judged per picture by the broker's `RtpAccessUnitTracker`: every
|
|
3732
|
+
* fragment of a fragmented IDR and a prefix SEI inside the picture carry
|
|
3733
|
+
* it, not only the packet that completed an IRAP NAL. The keyframe cache
|
|
3734
|
+
* below collects exactly that run, and the transition into it is the
|
|
3735
|
+
* once-per-picture `keyframeAuStart` the sessions resume and count on.
|
|
3731
3736
|
* @param isParameterSet True if this packet contains SPS/PPS/VPS
|
|
3732
3737
|
*/
|
|
3733
3738
|
pushRtpPassthrough(rtpData, isKeyframe, isParameterSet) {
|
|
@@ -3743,14 +3748,14 @@ var RtspRestreamer = class {
|
|
|
3743
3748
|
this.lastKeyframeRtpBytes = 0;
|
|
3744
3749
|
}
|
|
3745
3750
|
}
|
|
3751
|
+
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3746
3752
|
if (this.collectingKeyframeRtp) {
|
|
3747
3753
|
this.lastKeyframeRtpPackets.push({
|
|
3748
3754
|
data: Buffer.from(rtpData),
|
|
3749
3755
|
marker: rtpPacket.marker
|
|
3750
3756
|
});
|
|
3751
3757
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
3752
|
-
if (
|
|
3753
|
-
else if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3758
|
+
if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3754
3759
|
this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
|
|
3755
3760
|
this.lastKeyframeRtpPackets = [];
|
|
3756
3761
|
this.lastKeyframeRtpBytes = 0;
|
|
@@ -3763,17 +3768,16 @@ var RtspRestreamer = class {
|
|
|
3763
3768
|
if (!session.isPlaying()) continue;
|
|
3764
3769
|
this.pendingKeyframe.delete(session.getSessionId());
|
|
3765
3770
|
}
|
|
3766
|
-
const keyframeAu = isKeyframe || isParameterSet;
|
|
3767
3771
|
for (const session of this.sessions.values()) {
|
|
3768
3772
|
const sessionId = session.getSessionId();
|
|
3769
3773
|
if (this.activeReplays.get(sessionId)?.enqueue({
|
|
3770
3774
|
packet: rtpPacket,
|
|
3771
3775
|
prime: false,
|
|
3772
|
-
keyframeAuStart
|
|
3776
|
+
keyframeAuStart
|
|
3773
3777
|
})) continue;
|
|
3774
3778
|
if (this.pendingKeyframe.has(sessionId)) continue;
|
|
3775
3779
|
if (!session.isPlaying()) continue;
|
|
3776
|
-
this.recordLiveOutcome(session.sendRtp(rtpPacket, false,
|
|
3780
|
+
this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAuStart));
|
|
3777
3781
|
}
|
|
3778
3782
|
}
|
|
3779
3783
|
/**
|
|
@@ -10124,6 +10128,16 @@ function allocAnnexBNal(byteLength) {
|
|
|
10124
10128
|
buf[3] = 1;
|
|
10125
10129
|
return buf;
|
|
10126
10130
|
}
|
|
10131
|
+
var NO_PAYLOAD_CLASS = {
|
|
10132
|
+
keyframe: false,
|
|
10133
|
+
parameterSet: false
|
|
10134
|
+
};
|
|
10135
|
+
function isH265IrapType(type) {
|
|
10136
|
+
return type >= H265_NAL_IRAP_MIN && type <= H265_NAL_IRAP_MAX;
|
|
10137
|
+
}
|
|
10138
|
+
function isH265ParameterSetType(type) {
|
|
10139
|
+
return type === H265_NAL_VPS || type === H265_NAL_SPS || type === H265_NAL_PPS;
|
|
10140
|
+
}
|
|
10127
10141
|
/** Total byte length of the reassembly fragments, without allocating a closure. */
|
|
10128
10142
|
function totalFragmentBytes(fragments) {
|
|
10129
10143
|
let total = 0;
|
|
@@ -10169,16 +10183,92 @@ var RtpDepacketizer = class {
|
|
|
10169
10183
|
}
|
|
10170
10184
|
/**
|
|
10171
10185
|
* Process a raw RTP packet (including 12-byte RTP header).
|
|
10172
|
-
* Emits zero or more complete NAL units via the callback
|
|
10186
|
+
* Emits zero or more complete NAL units via the callback and returns what
|
|
10187
|
+
* THIS packet carried ({@link RtpPacketClass}) — `null` for a packet too
|
|
10188
|
+
* short to be RTP with a payload.
|
|
10173
10189
|
*/
|
|
10174
10190
|
processPacket(rtpData) {
|
|
10175
|
-
if (rtpData.length < 12) return;
|
|
10191
|
+
if (rtpData.length < 12) return null;
|
|
10176
10192
|
const marker = (rtpData[1] & 128) !== 0;
|
|
10177
10193
|
const timestamp = rtpData.readUInt32BE(4);
|
|
10178
10194
|
const payload = rtpData.subarray(12);
|
|
10179
|
-
if (payload.length === 0) return;
|
|
10195
|
+
if (payload.length === 0) return null;
|
|
10196
|
+
const carried = this.codec === "h264" ? this.classifyH264Payload(payload) : this.classifyH265Payload(payload);
|
|
10180
10197
|
if (this.codec === "h264") this.processH264(payload, timestamp, marker);
|
|
10181
10198
|
else this.processH265(payload, timestamp, marker);
|
|
10199
|
+
return {
|
|
10200
|
+
timestamp,
|
|
10201
|
+
marker,
|
|
10202
|
+
keyframe: carried.keyframe,
|
|
10203
|
+
parameterSet: carried.parameterSet
|
|
10204
|
+
};
|
|
10205
|
+
}
|
|
10206
|
+
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10207
|
+
classifyH264Payload(payload) {
|
|
10208
|
+
const nalType = payload[0] & 31;
|
|
10209
|
+
if (nalType === H264_NAL_STAP_A) {
|
|
10210
|
+
let keyframe = false;
|
|
10211
|
+
let parameterSet = false;
|
|
10212
|
+
let offset = 1;
|
|
10213
|
+
while (offset + 2 <= payload.length) {
|
|
10214
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10215
|
+
offset += 2;
|
|
10216
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10217
|
+
const type = payload[offset] & 31;
|
|
10218
|
+
keyframe ||= type === H264_NAL_IDR;
|
|
10219
|
+
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10220
|
+
offset += nalSize;
|
|
10221
|
+
}
|
|
10222
|
+
return {
|
|
10223
|
+
keyframe,
|
|
10224
|
+
parameterSet
|
|
10225
|
+
};
|
|
10226
|
+
}
|
|
10227
|
+
if (nalType === 28) {
|
|
10228
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10229
|
+
return {
|
|
10230
|
+
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10231
|
+
parameterSet: false
|
|
10232
|
+
};
|
|
10233
|
+
}
|
|
10234
|
+
return {
|
|
10235
|
+
keyframe: nalType === H264_NAL_IDR,
|
|
10236
|
+
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10237
|
+
};
|
|
10238
|
+
}
|
|
10239
|
+
/** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
|
|
10240
|
+
classifyH265Payload(payload) {
|
|
10241
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10242
|
+
const nalType = payload[0] >> 1 & 63;
|
|
10243
|
+
if (nalType === H265_NAL_AP) {
|
|
10244
|
+
let keyframe = false;
|
|
10245
|
+
let parameterSet = false;
|
|
10246
|
+
let offset = 2;
|
|
10247
|
+
while (offset + 2 <= payload.length) {
|
|
10248
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10249
|
+
offset += 2;
|
|
10250
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10251
|
+
const type = payload[offset] >> 1 & 63;
|
|
10252
|
+
keyframe ||= isH265IrapType(type);
|
|
10253
|
+
parameterSet ||= isH265ParameterSetType(type);
|
|
10254
|
+
offset += nalSize;
|
|
10255
|
+
}
|
|
10256
|
+
return {
|
|
10257
|
+
keyframe,
|
|
10258
|
+
parameterSet
|
|
10259
|
+
};
|
|
10260
|
+
}
|
|
10261
|
+
if (nalType === 49) {
|
|
10262
|
+
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10263
|
+
return {
|
|
10264
|
+
keyframe: isH265IrapType(payload[2] & 63),
|
|
10265
|
+
parameterSet: false
|
|
10266
|
+
};
|
|
10267
|
+
}
|
|
10268
|
+
return {
|
|
10269
|
+
keyframe: isH265IrapType(nalType),
|
|
10270
|
+
parameterSet: isH265ParameterSetType(nalType)
|
|
10271
|
+
};
|
|
10182
10272
|
}
|
|
10183
10273
|
processH264(payload, timestamp, marker) {
|
|
10184
10274
|
const nalType = payload[0] & 31;
|
|
@@ -10324,6 +10414,138 @@ var RtpDepacketizer = class {
|
|
|
10324
10414
|
}
|
|
10325
10415
|
};
|
|
10326
10416
|
//#endregion
|
|
10417
|
+
//#region src/stream-broker/rtsp/rtp-access-unit-tracker.ts
|
|
10418
|
+
/**
|
|
10419
|
+
* Tracks access-unit membership across the source RTP packets of ONE stream.
|
|
10420
|
+
*
|
|
10421
|
+
* The RTSP restreamer forwards source RTP untouched and must know, per
|
|
10422
|
+
* packet, whether it is part of a keyframe picture: that is what its keyframe
|
|
10423
|
+
* cache collects and what a backlog-latched session resumes on. The
|
|
10424
|
+
* depacketizer can only say so about a NAL that COMPLETED, so the fragments of
|
|
10425
|
+
* a fragmented IDR before the last one, and a prefix SEI between the parameter
|
|
10426
|
+
* sets and the slice, were reported as plain packets: the cache latch closed
|
|
10427
|
+
* on them and re-opened on the final fragment, and a backlogged session
|
|
10428
|
+
* counted every IRAP slice as a missed keyframe. Here a packet is classified
|
|
10429
|
+
* by what it carries ({@link RtpPacketClass}) and the verdict is carried
|
|
10430
|
+
* across the whole picture, bounded by the RTP timestamp and marker bit.
|
|
10431
|
+
*
|
|
10432
|
+
* A parameter-set-only run is not a picture: a camera that stamps SPS/PPS
|
|
10433
|
+
* with the previous timestamp still announces ONE keyframe, so the keyframe
|
|
10434
|
+
* verdict carries over into the slice that follows instead of starting twice.
|
|
10435
|
+
*/
|
|
10436
|
+
var RtpAccessUnitTracker = class {
|
|
10437
|
+
timestamp = -1;
|
|
10438
|
+
previousMarker = true;
|
|
10439
|
+
keyframeAu = false;
|
|
10440
|
+
/** The picture in progress has received something other than a parameter set. */
|
|
10441
|
+
pictureSeen = false;
|
|
10442
|
+
classify(packet) {
|
|
10443
|
+
const auStart = this.previousMarker || packet.timestamp !== this.timestamp;
|
|
10444
|
+
if (auStart && this.pictureSeen) this.keyframeAu = false;
|
|
10445
|
+
if (auStart) this.pictureSeen = false;
|
|
10446
|
+
this.timestamp = packet.timestamp;
|
|
10447
|
+
this.previousMarker = packet.marker;
|
|
10448
|
+
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10449
|
+
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10450
|
+
if (keyframeContent) this.keyframeAu = true;
|
|
10451
|
+
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10452
|
+
return {
|
|
10453
|
+
auStart,
|
|
10454
|
+
keyframeAu: this.keyframeAu,
|
|
10455
|
+
keyframeAuStart,
|
|
10456
|
+
parameterSet: packet.parameterSet
|
|
10457
|
+
};
|
|
10458
|
+
}
|
|
10459
|
+
/** A redial re-bases the clock; nothing carries over. */
|
|
10460
|
+
reset() {
|
|
10461
|
+
this.timestamp = -1;
|
|
10462
|
+
this.previousMarker = true;
|
|
10463
|
+
this.keyframeAu = false;
|
|
10464
|
+
this.pictureSeen = false;
|
|
10465
|
+
}
|
|
10466
|
+
};
|
|
10467
|
+
//#endregion
|
|
10468
|
+
//#region src/stream-broker/stream-broker/access-unit-assembler.ts
|
|
10469
|
+
/**
|
|
10470
|
+
* Groups depacketized NALs into ACCESS UNITS — one per picture.
|
|
10471
|
+
*
|
|
10472
|
+
* The broker's `EncodedPacket` is one access unit, and every consumer judges it
|
|
10473
|
+
* that way: `keyframe` gates where a clip ring widens to, where a late joiner
|
|
10474
|
+
* can start, how the pre-roll window is cut; the stats count it as one picture
|
|
10475
|
+
* and time the GOP from one `keyframe` to the next. A multi-slice camera (a
|
|
10476
|
+
* Hikvision G2 main stream is 3 slices per picture) used to hand each slice —
|
|
10477
|
+
* and each prefix SEI — down as its own "frame": 75 input fps on a 25 fps
|
|
10478
|
+
* camera, an 8–34 ms "GOP", a clip ring that widened to the LAST IRAP slice
|
|
10479
|
+
* (a third of the picture, without its parameter sets), and a prefix SEI that
|
|
10480
|
+
* inherited the welded VPS/SPS/PPS instead of the IDR.
|
|
10481
|
+
*
|
|
10482
|
+
* Boundary rule (RFC 3550 / 6184 / 7798): the RTP marker bit closes the
|
|
10483
|
+
* picture it ends — flushed on the spot, so a single-slice stream pays no
|
|
10484
|
+
* latency — and a timestamp change closes the one before it, for cameras that
|
|
10485
|
+
* never set the marker. Parameter sets are held and welded onto the picture
|
|
10486
|
+
* that follows them, so a keyframe access unit is self-contained
|
|
10487
|
+
* (VPS/SPS/PPS + IDR) exactly as before; a prefix SEI stays inside its
|
|
10488
|
+
* picture and is never a frame of its own.
|
|
10489
|
+
*/
|
|
10490
|
+
var AccessUnitAssembler = class {
|
|
10491
|
+
onAccessUnit;
|
|
10492
|
+
/** Parameter sets seen since the last picture closed; welded onto the next one. */
|
|
10493
|
+
pendingParamNals = [];
|
|
10494
|
+
/** NALs of the picture in progress (parameter sets first when welded). */
|
|
10495
|
+
pictureNals = [];
|
|
10496
|
+
pictureTimestamp = 0;
|
|
10497
|
+
pictureKeyframe = false;
|
|
10498
|
+
constructor(onAccessUnit) {
|
|
10499
|
+
this.onAccessUnit = onAccessUnit;
|
|
10500
|
+
}
|
|
10501
|
+
push(depacketized) {
|
|
10502
|
+
const { annexB, keyframe, parameterSet, timestamp, marker } = depacketized;
|
|
10503
|
+
if (this.pictureNals.length > 0 && timestamp !== this.pictureTimestamp) this.flush();
|
|
10504
|
+
if (parameterSet) {
|
|
10505
|
+
this.pendingParamNals.push(annexB);
|
|
10506
|
+
return;
|
|
10507
|
+
}
|
|
10508
|
+
if (this.pictureNals.length === 0) {
|
|
10509
|
+
this.pictureTimestamp = timestamp;
|
|
10510
|
+
this.pictureKeyframe = false;
|
|
10511
|
+
}
|
|
10512
|
+
if (this.pendingParamNals.length > 0) {
|
|
10513
|
+
for (const paramNal of this.pendingParamNals) this.pictureNals.push(paramNal);
|
|
10514
|
+
this.pendingParamNals = [];
|
|
10515
|
+
this.pictureKeyframe = true;
|
|
10516
|
+
}
|
|
10517
|
+
this.pictureNals.push(annexB);
|
|
10518
|
+
if (keyframe) this.pictureKeyframe = true;
|
|
10519
|
+
if (marker) this.flush();
|
|
10520
|
+
}
|
|
10521
|
+
/**
|
|
10522
|
+
* Emit the picture in progress, if any. Called on the marker bit, on a
|
|
10523
|
+
* timestamp change, and by the broker when the stream stops or redials so
|
|
10524
|
+
* the last picture of a marker-less stream is not lost.
|
|
10525
|
+
*/
|
|
10526
|
+
flush() {
|
|
10527
|
+
if (this.pictureNals.length === 0) return;
|
|
10528
|
+
const nals = this.pictureNals;
|
|
10529
|
+
const timestamp = this.pictureTimestamp;
|
|
10530
|
+
const keyframe = this.pictureKeyframe;
|
|
10531
|
+
this.pictureNals = [];
|
|
10532
|
+
this.pictureKeyframe = false;
|
|
10533
|
+
const annexB = nals.length === 1 ? nals[0] : Buffer.concat(nals);
|
|
10534
|
+
this.onAccessUnit({
|
|
10535
|
+
annexB,
|
|
10536
|
+
timestamp,
|
|
10537
|
+
keyframe
|
|
10538
|
+
});
|
|
10539
|
+
}
|
|
10540
|
+
/** Drop everything held — a redial re-bases the clock, nothing carries over. */
|
|
10541
|
+
reset() {
|
|
10542
|
+
this.pendingParamNals = [];
|
|
10543
|
+
this.pictureNals = [];
|
|
10544
|
+
this.pictureTimestamp = 0;
|
|
10545
|
+
this.pictureKeyframe = false;
|
|
10546
|
+
}
|
|
10547
|
+
};
|
|
10548
|
+
//#endregion
|
|
10327
10549
|
//#region src/stream-broker/rtsp/rtsp-client.ts
|
|
10328
10550
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
10329
10551
|
var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
|
|
@@ -13149,7 +13371,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13149
13371
|
this.rtspRestreamer.destroy();
|
|
13150
13372
|
this.preBuffer.clear();
|
|
13151
13373
|
this.clipBuffer?.clear();
|
|
13152
|
-
this.
|
|
13374
|
+
this.annexBAssembler.reset();
|
|
13153
13375
|
this.encodedCallbacks.clear();
|
|
13154
13376
|
this.audioChunkPlane.dispose();
|
|
13155
13377
|
}
|
|
@@ -13612,7 +13834,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13612
13834
|
this.reconcilePrestreamWindow();
|
|
13613
13835
|
this.preBuffer.clear();
|
|
13614
13836
|
this.clipBuffer?.clear();
|
|
13615
|
-
this.
|
|
13837
|
+
this.annexBAssembler.reset();
|
|
13616
13838
|
this.rtspRestreamer.resetStreamState();
|
|
13617
13839
|
this.totalBytes = 0;
|
|
13618
13840
|
this.bytesInWindow = 0;
|
|
@@ -14693,8 +14915,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14693
14915
|
} });
|
|
14694
14916
|
this._lastNalKeyframe = false;
|
|
14695
14917
|
this._lastNalParamSet = false;
|
|
14696
|
-
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14697
|
-
this.
|
|
14918
|
+
const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
|
|
14919
|
+
const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
|
|
14920
|
+
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14698
14921
|
this.captureRtpForPreBuffer(rtpData);
|
|
14699
14922
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
14700
14923
|
cb(rtpData);
|
|
@@ -14890,51 +15113,44 @@ var StreamBroker = class StreamBroker {
|
|
|
14890
15113
|
* RTSP restreamer's pushRtpPassthrough.
|
|
14891
15114
|
*/
|
|
14892
15115
|
/**
|
|
14893
|
-
*
|
|
14894
|
-
*
|
|
14895
|
-
*
|
|
14896
|
-
*
|
|
14897
|
-
* with a single `EncodedPacket` that contains SPS+PPS+IDR together.
|
|
15116
|
+
* Groups the depacketizer's NALs into access units — ONE `EncodedPacket`
|
|
15117
|
+
* per picture, parameter sets welded onto the keyframe picture they
|
|
15118
|
+
* precede. See `access-unit-assembler.ts` for the boundary rule and the
|
|
15119
|
+
* per-NAL emit it replaced.
|
|
14898
15120
|
*/
|
|
14899
|
-
|
|
15121
|
+
annexBAssembler = new AccessUnitAssembler((accessUnit) => this.emitAccessUnit(accessUnit));
|
|
15122
|
+
/** Access-unit membership of the source RTP packets, for the restreamer's flags. */
|
|
15123
|
+
rtpAuTracker = new RtpAccessUnitTracker();
|
|
14900
15124
|
/**
|
|
14901
15125
|
* handleDepacketizedNal receives individual NALs from the RTP depacketizer
|
|
14902
|
-
* and
|
|
14903
|
-
*
|
|
14904
|
-
*
|
|
14905
|
-
*
|
|
14906
|
-
* This guarantees every keyframe EncodedPacket is a self-contained access
|
|
14907
|
-
* unit that a decoder can initialise from — critical for WebRTC (Chrome
|
|
14908
|
-
* can't decode an IDR without preceding SPS/PPS).
|
|
15126
|
+
* and hands them to the access-unit assembler, which emits one
|
|
15127
|
+
* `EncodedPacket` per PICTURE (`emitAccessUnit`) — never per NAL. A
|
|
15128
|
+
* multi-slice IDR, its parameter sets and its prefix SEI are one keyframe
|
|
15129
|
+
* packet; three P slices are one P packet.
|
|
14909
15130
|
*
|
|
14910
|
-
* The RTSP restreamer
|
|
14911
|
-
* not
|
|
15131
|
+
* The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
|
|
15132
|
+
* not fed from here.
|
|
14912
15133
|
*/
|
|
14913
15134
|
handleDepacketizedNal(depacketized) {
|
|
14914
|
-
|
|
15135
|
+
if (depacketized.keyframe) this._lastNalKeyframe = true;
|
|
15136
|
+
if (depacketized.parameterSet) this._lastNalParamSet = true;
|
|
15137
|
+
this.annexBAssembler.push(depacketized);
|
|
15138
|
+
}
|
|
15139
|
+
/**
|
|
15140
|
+
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15141
|
+
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15142
|
+
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
15143
|
+
* initialises from and what a clip or a pre-roll window may start at.
|
|
15144
|
+
*/
|
|
15145
|
+
emitAccessUnit(accessUnit) {
|
|
14915
15146
|
const codec = this.detectedCodec ?? "h264";
|
|
14916
|
-
|
|
14917
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14918
|
-
const isH264 = codec === "h264";
|
|
14919
|
-
const nalType = isH264 ? nal[0] & 31 : (nal[0] & 126) >> 1;
|
|
14920
|
-
if (isH264 ? nalType === 7 || nalType === 8 : nalType === 32 || nalType === 33 || nalType === 34) {
|
|
14921
|
-
this.pendingParamNals.push(depacketized.annexB);
|
|
14922
|
-
return;
|
|
14923
|
-
}
|
|
14924
|
-
const hadParamSets = this.pendingParamNals.length > 0;
|
|
14925
|
-
let annexBData;
|
|
14926
|
-
if (hadParamSets) {
|
|
14927
|
-
this.pendingParamNals.push(depacketized.annexB);
|
|
14928
|
-
annexBData = Buffer.concat(this.pendingParamNals);
|
|
14929
|
-
this.pendingParamNals = [];
|
|
14930
|
-
} else annexBData = depacketized.annexB;
|
|
14931
|
-
const pts = Math.floor(timestamp / 90);
|
|
15147
|
+
const pts = Math.floor(accessUnit.timestamp / 90);
|
|
14932
15148
|
const packet = {
|
|
14933
15149
|
type: "video",
|
|
14934
|
-
data:
|
|
15150
|
+
data: accessUnit.annexB,
|
|
14935
15151
|
pts,
|
|
14936
15152
|
dts: pts,
|
|
14937
|
-
keyframe: keyframe
|
|
15153
|
+
keyframe: accessUnit.keyframe,
|
|
14938
15154
|
codec
|
|
14939
15155
|
};
|
|
14940
15156
|
this.pushEncodedPacket(packet);
|
|
@@ -15338,6 +15554,9 @@ var StreamBroker = class StreamBroker {
|
|
|
15338
15554
|
this.rtpDepacketizer.reset();
|
|
15339
15555
|
this.rtpDepacketizer = null;
|
|
15340
15556
|
}
|
|
15557
|
+
this.annexBAssembler.flush();
|
|
15558
|
+
this.annexBAssembler.reset();
|
|
15559
|
+
this.rtpAuTracker.reset();
|
|
15341
15560
|
if (this.pushRtpFlagDepacketizer) {
|
|
15342
15561
|
this.pushRtpFlagDepacketizer.reset();
|
|
15343
15562
|
this.pushRtpFlagDepacketizer = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.364",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|