@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.
@@ -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 access unit
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 (!isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
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: keyframeAu
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, keyframeAu));
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.pendingParamNals = [];
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.pendingParamNals = [];
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.rtspRestreamer.pushRtpPassthrough(rtpData, this._lastNalKeyframe, this._lastNalParamSet);
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
- * Pending parameter-set NALs (SPS/PPS for H.264, VPS/SPS/PPS for H.265).
14898
- * Cameras emit these as separate RTP packets before the IDR/IRAP frame.
14899
- * We buffer them here and prepend to the next VCL NAL so downstream
14900
- * consumers (WebRTC, prebuffer, restream) receive complete access units
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
- pendingParamNals = [];
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 emits them as EncodedPackets.
14907
- *
14908
- * Parameter-set NALs (SPS/PPS type 7/8 for H.264, VPS/SPS/PPS type 32/33/34
14909
- * for H.265) are buffered and prepended to the next VCL NAL (slice/IDR).
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 handles its own RTP passthrough separately and is
14915
- * not affected by this merging.
15135
+ * The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
15136
+ * not fed from here.
14916
15137
  */
14917
15138
  handleDepacketizedNal(depacketized) {
14918
- const { nal, keyframe, parameterSet, timestamp } = depacketized;
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
- if (keyframe) this._lastNalKeyframe = true;
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: annexBData,
15154
+ data: accessUnit.annexB,
14939
15155
  pts,
14940
15156
  dts: pts,
14941
- keyframe: keyframe || hadParamSets,
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 access unit
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 (!isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
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: keyframeAu
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, keyframeAu));
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.pendingParamNals = [];
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.pendingParamNals = [];
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.rtspRestreamer.pushRtpPassthrough(rtpData, this._lastNalKeyframe, this._lastNalParamSet);
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
- * Pending parameter-set NALs (SPS/PPS for H.264, VPS/SPS/PPS for H.265).
14894
- * Cameras emit these as separate RTP packets before the IDR/IRAP frame.
14895
- * We buffer them here and prepend to the next VCL NAL so downstream
14896
- * consumers (WebRTC, prebuffer, restream) receive complete access units
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
- pendingParamNals = [];
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 emits them as EncodedPackets.
14903
- *
14904
- * Parameter-set NALs (SPS/PPS type 7/8 for H.264, VPS/SPS/PPS type 32/33/34
14905
- * for H.265) are buffered and prepended to the next VCL NAL (slice/IDR).
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 handles its own RTP passthrough separately and is
14911
- * not affected by this merging.
15131
+ * The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
15132
+ * not fed from here.
14912
15133
  */
14913
15134
  handleDepacketizedNal(depacketized) {
14914
- const { nal, keyframe, parameterSet, timestamp } = depacketized;
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
- if (keyframe) this._lastNalKeyframe = true;
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: annexBData,
15150
+ data: accessUnit.annexB,
14935
15151
  pts,
14936
15152
  dts: pts,
14937
- keyframe: keyframe || hadParamSets,
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.363",
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",