@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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-BshC0NZR.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-Dqmrb990.mjs";
|
|
4
4
|
import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
-
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-COoPqx93.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os$1 from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -3081,6 +3081,68 @@ var RtpPacketizer = class {
|
|
|
3081
3081
|
}
|
|
3082
3082
|
};
|
|
3083
3083
|
//#endregion
|
|
3084
|
+
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3085
|
+
/**
|
|
3086
|
+
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
3087
|
+
*
|
|
3088
|
+
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
3089
|
+
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
3090
|
+
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
3091
|
+
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
3092
|
+
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
3093
|
+
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
3094
|
+
* packet that happened to be allocated beside it. On the hub that held
|
|
3095
|
+
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
3096
|
+
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
3097
|
+
*
|
|
3098
|
+
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
3099
|
+
* now pass every entry through {@link detachFromPool} on the way in and
|
|
3100
|
+
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
3101
|
+
* entry costs. Short-lived traffic stays pooled.
|
|
3102
|
+
*/
|
|
3103
|
+
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
3104
|
+
function ownsBackingStore(buf) {
|
|
3105
|
+
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* What retaining `buf` really costs: its own length when it owns its
|
|
3109
|
+
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
3110
|
+
*/
|
|
3111
|
+
function backingStoreBytes(buf) {
|
|
3112
|
+
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
3116
|
+
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
3117
|
+
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
3118
|
+
* already unpooled is returned as-is.
|
|
3119
|
+
*/
|
|
3120
|
+
function detachFromPool(buf) {
|
|
3121
|
+
if (ownsBackingStore(buf)) return buf;
|
|
3122
|
+
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
3123
|
+
buf.copy(owned);
|
|
3124
|
+
return owned;
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* A copy of `src[start, end)` that OWNS its backing store — the shape a reader
|
|
3128
|
+
* hands out when the packet it cuts from its socket buffer is going to be
|
|
3129
|
+
* retained anyway.
|
|
3130
|
+
*
|
|
3131
|
+
* Every source video packet is: `captureRtpForPreBuffer` keeps it for the life
|
|
3132
|
+
* of a GOP, and the restreamer's keyframe cache keeps the keyframe ones. With
|
|
3133
|
+
* the readers cutting packets as `Buffer.from(subarray)` — a pooled view — the
|
|
3134
|
+
* ring then had to copy each one a second time to own it: two copies of every
|
|
3135
|
+
* packet on the ingest path (measured 15.1 + 14.8 MB/s on the hub's load).
|
|
3136
|
+
* Cutting the packet owned up front makes the ring's and the cache's
|
|
3137
|
+
* `detachFromPool` an identity, so the packet is copied once and shared by the
|
|
3138
|
+
* reader's fan-out, the ring, the cache and every session's write queue.
|
|
3139
|
+
*/
|
|
3140
|
+
function copyOwned(src, start, end) {
|
|
3141
|
+
const owned = Buffer.allocUnsafeSlow(end - start);
|
|
3142
|
+
src.copy(owned, 0, start, end);
|
|
3143
|
+
return owned;
|
|
3144
|
+
}
|
|
3145
|
+
//#endregion
|
|
3084
3146
|
//#region src/stream-broker/rtsp/sdp-builder.ts
|
|
3085
3147
|
function buildSdp(options) {
|
|
3086
3148
|
const { codec, parameterSets, streamName } = options;
|
|
@@ -3727,7 +3789,12 @@ var RtspRestreamer = class {
|
|
|
3727
3789
|
* Forwarded directly to all connected RTSP sessions — zero re-packetization.
|
|
3728
3790
|
*
|
|
3729
3791
|
* @param rtpData Complete RTP packet (header + payload)
|
|
3730
|
-
* @param isKeyframe True if this packet is part of a keyframe
|
|
3792
|
+
* @param isKeyframe True if this packet is part of a keyframe ACCESS UNIT —
|
|
3793
|
+
* judged per picture by the broker's `RtpAccessUnitTracker`: every
|
|
3794
|
+
* fragment of a fragmented IDR and a prefix SEI inside the picture carry
|
|
3795
|
+
* it, not only the packet that completed an IRAP NAL. The keyframe cache
|
|
3796
|
+
* below collects exactly that run, and the transition into it is the
|
|
3797
|
+
* once-per-picture `keyframeAuStart` the sessions resume and count on.
|
|
3731
3798
|
* @param isParameterSet True if this packet contains SPS/PPS/VPS
|
|
3732
3799
|
*/
|
|
3733
3800
|
pushRtpPassthrough(rtpData, isKeyframe, isParameterSet) {
|
|
@@ -3743,14 +3810,14 @@ var RtspRestreamer = class {
|
|
|
3743
3810
|
this.lastKeyframeRtpBytes = 0;
|
|
3744
3811
|
}
|
|
3745
3812
|
}
|
|
3813
|
+
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3746
3814
|
if (this.collectingKeyframeRtp) {
|
|
3747
3815
|
this.lastKeyframeRtpPackets.push({
|
|
3748
|
-
data:
|
|
3816
|
+
data: detachFromPool(rtpData),
|
|
3749
3817
|
marker: rtpPacket.marker
|
|
3750
3818
|
});
|
|
3751
3819
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
3752
|
-
if (
|
|
3753
|
-
else if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3820
|
+
if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
3754
3821
|
this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
|
|
3755
3822
|
this.lastKeyframeRtpPackets = [];
|
|
3756
3823
|
this.lastKeyframeRtpBytes = 0;
|
|
@@ -3763,17 +3830,16 @@ var RtspRestreamer = class {
|
|
|
3763
3830
|
if (!session.isPlaying()) continue;
|
|
3764
3831
|
this.pendingKeyframe.delete(session.getSessionId());
|
|
3765
3832
|
}
|
|
3766
|
-
const keyframeAu = isKeyframe || isParameterSet;
|
|
3767
3833
|
for (const session of this.sessions.values()) {
|
|
3768
3834
|
const sessionId = session.getSessionId();
|
|
3769
3835
|
if (this.activeReplays.get(sessionId)?.enqueue({
|
|
3770
3836
|
packet: rtpPacket,
|
|
3771
3837
|
prime: false,
|
|
3772
|
-
keyframeAuStart
|
|
3838
|
+
keyframeAuStart
|
|
3773
3839
|
})) continue;
|
|
3774
3840
|
if (this.pendingKeyframe.has(sessionId)) continue;
|
|
3775
3841
|
if (!session.isPlaying()) continue;
|
|
3776
|
-
this.recordLiveOutcome(session.sendRtp(rtpPacket, false,
|
|
3842
|
+
this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAuStart));
|
|
3777
3843
|
}
|
|
3778
3844
|
}
|
|
3779
3845
|
/**
|
|
@@ -4349,6 +4415,19 @@ var RtspSession = class {
|
|
|
4349
4415
|
lastRtpAt = 0;
|
|
4350
4416
|
/** Total video RTP bytes written to the socket. */
|
|
4351
4417
|
bytesSent = 0;
|
|
4418
|
+
/**
|
|
4419
|
+
* The socket is corked for the access unit in progress — see
|
|
4420
|
+
* {@link writeInterleaved}. Cleared by the packet that ends the picture (RTP
|
|
4421
|
+
* marker, or any audio packet) or by the next-tick safety net, whichever
|
|
4422
|
+
* comes first.
|
|
4423
|
+
*/
|
|
4424
|
+
accessUnitCorked = false;
|
|
4425
|
+
/** Flush the access unit in progress. Idempotent: the marker and the safety net both call it. */
|
|
4426
|
+
uncorkAccessUnit = () => {
|
|
4427
|
+
if (!this.accessUnitCorked) return;
|
|
4428
|
+
this.accessUnitCorked = false;
|
|
4429
|
+
this.socket.uncork();
|
|
4430
|
+
};
|
|
4352
4431
|
/** Client `User-Agent` (from the first request that carries one). Lets the
|
|
4353
4432
|
* broker panel identify a consumer by purpose — e.g. the recorder sets
|
|
4354
4433
|
* `-user_agent recorder` on its RTSP pull — instead of a bare address. */
|
|
@@ -4596,7 +4675,7 @@ var RtspSession = class {
|
|
|
4596
4675
|
return "dropped-backlog";
|
|
4597
4676
|
}
|
|
4598
4677
|
}
|
|
4599
|
-
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
|
|
4678
|
+
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
|
|
4600
4679
|
this.missedKeyframes = 0;
|
|
4601
4680
|
return "written";
|
|
4602
4681
|
}
|
|
@@ -4605,7 +4684,7 @@ var RtspSession = class {
|
|
|
4605
4684
|
if (!this.playing || this.closed || this.muted) return "not-playing";
|
|
4606
4685
|
if (this.tracks.length < 2) return "not-playing";
|
|
4607
4686
|
if (this.socket.writableLength > 2097152) return "dropped-backlog";
|
|
4608
|
-
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
|
|
4687
|
+
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
|
|
4609
4688
|
return "written";
|
|
4610
4689
|
}
|
|
4611
4690
|
/**
|
|
@@ -4627,14 +4706,13 @@ var RtspSession = class {
|
|
|
4627
4706
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
4628
4707
|
*
|
|
4629
4708
|
* The payload buffer handed here is a private allocation per packet —
|
|
4630
|
-
* `
|
|
4631
|
-
*
|
|
4632
|
-
*
|
|
4633
|
-
*
|
|
4634
|
-
*
|
|
4635
|
-
*
|
|
4636
|
-
*
|
|
4637
|
-
* to: a pending write holds its own reference, and the rule below is about
|
|
4709
|
+
* `copyOwned(readBuffer, …)` in the readers' `onData`, for video and audio
|
|
4710
|
+
* alike — never a view of the reader's socket buffer, which advances on the
|
|
4711
|
+
* next read. Nothing in the broker writes into a captured packet: every
|
|
4712
|
+
* mutation in this path targets a freshly allocated buffer. (The same object
|
|
4713
|
+
* is what the broker's `rtpRing` and the restreamer's keyframe cache retain —
|
|
4714
|
+
* it owns its backing store, so `detachFromPool` keeps it as is — and that is
|
|
4715
|
+
* fine: a pending write holds its own reference, and the rule below is about
|
|
4638
4716
|
* mutation, not retention.)
|
|
4639
4717
|
*
|
|
4640
4718
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
@@ -4644,18 +4722,36 @@ var RtspSession = class {
|
|
|
4644
4722
|
* queued. Four bytes from the Buffer pool is a pointer bump; the copy this
|
|
4645
4723
|
* replaces was ~1.4 KB.
|
|
4646
4724
|
*
|
|
4647
|
-
*
|
|
4648
|
-
*
|
|
4725
|
+
* ## One cork per ACCESS UNIT, not per packet
|
|
4726
|
+
*
|
|
4727
|
+
* The pair used to be corked and uncorked on its own: one `writev` per RTP
|
|
4728
|
+
* packet per session, so a 4K keyframe fragmented into a hundred packets and
|
|
4729
|
+
* fanned out to three sessions was three hundred syscalls for one picture no
|
|
4730
|
+
* decoder can use before it is whole. The socket is corked on the first write
|
|
4731
|
+
* of an access unit and uncorked by the packet that ENDS it
|
|
4732
|
+
* (`endOfAccessUnit`: the RTP marker on video, every audio packet), so the
|
|
4733
|
+
* picture leaves in one `writev`. A `process.nextTick` safety net uncorks
|
|
4734
|
+
* whatever is still open before the tick yields to I/O — a marker-less camera
|
|
4735
|
+
* or a lost marker degrades to one `writev` per tick, never to a socket held
|
|
4736
|
+
* corked across a read.
|
|
4737
|
+
*
|
|
4738
|
+
* Backpressure sees through the cork: `writableLength` counts corked bytes,
|
|
4739
|
+
* so the `MAX_WRITE_BUFFER` gate, the latch and the missed-keyframe close
|
|
4740
|
+
* (D447) judge exactly what they judged per packet.
|
|
4649
4741
|
*/
|
|
4650
|
-
writeInterleaved(channel, payload) {
|
|
4742
|
+
writeInterleaved(channel, payload, endOfAccessUnit) {
|
|
4651
4743
|
const header = Buffer.allocUnsafe(4);
|
|
4652
4744
|
header[0] = 36;
|
|
4653
4745
|
header[1] = channel;
|
|
4654
4746
|
header.writeUInt16BE(payload.length, 2);
|
|
4655
|
-
this.
|
|
4747
|
+
if (!this.accessUnitCorked) {
|
|
4748
|
+
this.accessUnitCorked = true;
|
|
4749
|
+
this.socket.cork();
|
|
4750
|
+
process.nextTick(this.uncorkAccessUnit);
|
|
4751
|
+
}
|
|
4656
4752
|
this.socket.write(header);
|
|
4657
4753
|
this.socket.write(payload);
|
|
4658
|
-
this.
|
|
4754
|
+
if (endOfAccessUnit) this.uncorkAccessUnit();
|
|
4659
4755
|
this.lastRtpAt = Date.now();
|
|
4660
4756
|
this.bytesSent += 4 + payload.length;
|
|
4661
4757
|
}
|
|
@@ -10079,7 +10175,7 @@ var Rfc4571Reader = class {
|
|
|
10079
10175
|
while (this.readBuffer.length >= 2) {
|
|
10080
10176
|
const length = this.readBuffer.readUInt16BE(0);
|
|
10081
10177
|
if (this.readBuffer.length < 2 + length) return;
|
|
10082
|
-
const rtpPacket =
|
|
10178
|
+
const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
|
|
10083
10179
|
this.readBuffer = this.readBuffer.subarray(2 + length);
|
|
10084
10180
|
this.dispatchRtp(rtpPacket);
|
|
10085
10181
|
}
|
|
@@ -10124,12 +10220,109 @@ function allocAnnexBNal(byteLength) {
|
|
|
10124
10220
|
buf[3] = 1;
|
|
10125
10221
|
return buf;
|
|
10126
10222
|
}
|
|
10223
|
+
var NO_PAYLOAD_CLASS = {
|
|
10224
|
+
keyframe: false,
|
|
10225
|
+
parameterSet: false
|
|
10226
|
+
};
|
|
10227
|
+
function isH265IrapType(type) {
|
|
10228
|
+
return type >= H265_NAL_IRAP_MIN && type <= H265_NAL_IRAP_MAX;
|
|
10229
|
+
}
|
|
10230
|
+
function isH265ParameterSetType(type) {
|
|
10231
|
+
return type === H265_NAL_VPS || type === H265_NAL_SPS || type === H265_NAL_PPS;
|
|
10232
|
+
}
|
|
10127
10233
|
/** Total byte length of the reassembly fragments, without allocating a closure. */
|
|
10128
10234
|
function totalFragmentBytes(fragments) {
|
|
10129
10235
|
let total = 0;
|
|
10130
10236
|
for (const fragment of fragments) total += fragment.length;
|
|
10131
10237
|
return total;
|
|
10132
10238
|
}
|
|
10239
|
+
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10240
|
+
function classifyH264Payload(payload) {
|
|
10241
|
+
const nalType = payload[0] & 31;
|
|
10242
|
+
if (nalType === H264_NAL_STAP_A) {
|
|
10243
|
+
let keyframe = false;
|
|
10244
|
+
let parameterSet = false;
|
|
10245
|
+
let offset = 1;
|
|
10246
|
+
while (offset + 2 <= payload.length) {
|
|
10247
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10248
|
+
offset += 2;
|
|
10249
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10250
|
+
const type = payload[offset] & 31;
|
|
10251
|
+
keyframe ||= type === H264_NAL_IDR;
|
|
10252
|
+
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10253
|
+
offset += nalSize;
|
|
10254
|
+
}
|
|
10255
|
+
return {
|
|
10256
|
+
keyframe,
|
|
10257
|
+
parameterSet
|
|
10258
|
+
};
|
|
10259
|
+
}
|
|
10260
|
+
if (nalType === 28) {
|
|
10261
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10262
|
+
return {
|
|
10263
|
+
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10264
|
+
parameterSet: false
|
|
10265
|
+
};
|
|
10266
|
+
}
|
|
10267
|
+
return {
|
|
10268
|
+
keyframe: nalType === H264_NAL_IDR,
|
|
10269
|
+
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10270
|
+
};
|
|
10271
|
+
}
|
|
10272
|
+
/** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
|
|
10273
|
+
function classifyH265Payload(payload) {
|
|
10274
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10275
|
+
const nalType = payload[0] >> 1 & 63;
|
|
10276
|
+
if (nalType === H265_NAL_AP) {
|
|
10277
|
+
let keyframe = false;
|
|
10278
|
+
let parameterSet = false;
|
|
10279
|
+
let offset = 2;
|
|
10280
|
+
while (offset + 2 <= payload.length) {
|
|
10281
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10282
|
+
offset += 2;
|
|
10283
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10284
|
+
const type = payload[offset] >> 1 & 63;
|
|
10285
|
+
keyframe ||= isH265IrapType(type);
|
|
10286
|
+
parameterSet ||= isH265ParameterSetType(type);
|
|
10287
|
+
offset += nalSize;
|
|
10288
|
+
}
|
|
10289
|
+
return {
|
|
10290
|
+
keyframe,
|
|
10291
|
+
parameterSet
|
|
10292
|
+
};
|
|
10293
|
+
}
|
|
10294
|
+
if (nalType === 49) {
|
|
10295
|
+
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10296
|
+
return {
|
|
10297
|
+
keyframe: isH265IrapType(payload[2] & 63),
|
|
10298
|
+
parameterSet: false
|
|
10299
|
+
};
|
|
10300
|
+
}
|
|
10301
|
+
return {
|
|
10302
|
+
keyframe: isH265IrapType(nalType),
|
|
10303
|
+
parameterSet: isH265ParameterSetType(nalType)
|
|
10304
|
+
};
|
|
10305
|
+
}
|
|
10306
|
+
/**
|
|
10307
|
+
* Classify one raw RTP packet (12-byte header + payload) by what it carries —
|
|
10308
|
+
* the per-packet half of {@link RtpDepacketizer.processPacket}, without the
|
|
10309
|
+
* reassembly half. Pure: reads the headers, allocates nothing, keeps no state.
|
|
10310
|
+
* `null` for a packet too short to be RTP with a payload.
|
|
10311
|
+
*/
|
|
10312
|
+
function classifyRtpPacket(codec, rtpData) {
|
|
10313
|
+
if (rtpData.length < 12) return null;
|
|
10314
|
+
const marker = (rtpData[1] & 128) !== 0;
|
|
10315
|
+
const timestamp = rtpData.readUInt32BE(4);
|
|
10316
|
+
const payload = rtpData.subarray(12);
|
|
10317
|
+
if (payload.length === 0) return null;
|
|
10318
|
+
const carried = codec === "h264" ? classifyH264Payload(payload) : classifyH265Payload(payload);
|
|
10319
|
+
return {
|
|
10320
|
+
timestamp,
|
|
10321
|
+
marker,
|
|
10322
|
+
keyframe: carried.keyframe,
|
|
10323
|
+
parameterSet: carried.parameterSet
|
|
10324
|
+
};
|
|
10325
|
+
}
|
|
10133
10326
|
/**
|
|
10134
10327
|
* RTP → Annex-B NAL depacketizer.
|
|
10135
10328
|
*
|
|
@@ -10169,16 +10362,27 @@ var RtpDepacketizer = class {
|
|
|
10169
10362
|
}
|
|
10170
10363
|
/**
|
|
10171
10364
|
* Process a raw RTP packet (including 12-byte RTP header).
|
|
10172
|
-
* Emits zero or more complete NAL units via the callback
|
|
10365
|
+
* Emits zero or more complete NAL units via the callback and returns what
|
|
10366
|
+
* THIS packet carried ({@link RtpPacketClass}) — `null` for a packet too
|
|
10367
|
+
* short to be RTP with a payload.
|
|
10173
10368
|
*/
|
|
10174
10369
|
processPacket(rtpData) {
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
const timestamp = rtpData.readUInt32BE(4);
|
|
10370
|
+
const carried = this.classifyPacket(rtpData);
|
|
10371
|
+
if (carried === null) return null;
|
|
10178
10372
|
const payload = rtpData.subarray(12);
|
|
10179
|
-
if (
|
|
10180
|
-
|
|
10181
|
-
|
|
10373
|
+
if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
|
|
10374
|
+
else this.processH265(payload, carried.timestamp, carried.marker);
|
|
10375
|
+
return carried;
|
|
10376
|
+
}
|
|
10377
|
+
/**
|
|
10378
|
+
* What this packet CARRIES, and nothing else: no NAL is reassembled, no buffer
|
|
10379
|
+
* allocated, no reassembly state touched. The same verdict `processPacket`
|
|
10380
|
+
* returns, for a broker whose Annex-B plane has no consumer — the source-RTP
|
|
10381
|
+
* ring, the restreamer's keyframe cache and the stats need exactly this and
|
|
10382
|
+
* none of the copies.
|
|
10383
|
+
*/
|
|
10384
|
+
classifyPacket(rtpData) {
|
|
10385
|
+
return classifyRtpPacket(this.codec, rtpData);
|
|
10182
10386
|
}
|
|
10183
10387
|
processH264(payload, timestamp, marker) {
|
|
10184
10388
|
const nalType = payload[0] & 31;
|
|
@@ -10324,6 +10528,140 @@ var RtpDepacketizer = class {
|
|
|
10324
10528
|
}
|
|
10325
10529
|
};
|
|
10326
10530
|
//#endregion
|
|
10531
|
+
//#region src/stream-broker/rtsp/rtp-access-unit-tracker.ts
|
|
10532
|
+
/**
|
|
10533
|
+
* Tracks access-unit membership across the source RTP packets of ONE stream.
|
|
10534
|
+
*
|
|
10535
|
+
* The RTSP restreamer forwards source RTP untouched and must know, per
|
|
10536
|
+
* packet, whether it is part of a keyframe picture: that is what its keyframe
|
|
10537
|
+
* cache collects and what a backlog-latched session resumes on. The
|
|
10538
|
+
* depacketizer can only say so about a NAL that COMPLETED, so the fragments of
|
|
10539
|
+
* a fragmented IDR before the last one, and a prefix SEI between the parameter
|
|
10540
|
+
* sets and the slice, were reported as plain packets: the cache latch closed
|
|
10541
|
+
* on them and re-opened on the final fragment, and a backlogged session
|
|
10542
|
+
* counted every IRAP slice as a missed keyframe. Here a packet is classified
|
|
10543
|
+
* by what it carries ({@link RtpPacketClass}) and the verdict is carried
|
|
10544
|
+
* across the whole picture, bounded by the RTP timestamp and marker bit.
|
|
10545
|
+
*
|
|
10546
|
+
* A parameter-set-only run is not a picture: a camera that stamps SPS/PPS
|
|
10547
|
+
* with the previous timestamp still announces ONE keyframe, so the keyframe
|
|
10548
|
+
* verdict carries over into the slice that follows instead of starting twice.
|
|
10549
|
+
*/
|
|
10550
|
+
var RtpAccessUnitTracker = class {
|
|
10551
|
+
timestamp = -1;
|
|
10552
|
+
previousMarker = true;
|
|
10553
|
+
keyframeAu = false;
|
|
10554
|
+
/** The picture in progress has received something other than a parameter set. */
|
|
10555
|
+
pictureSeen = false;
|
|
10556
|
+
classify(packet) {
|
|
10557
|
+
const auStart = this.previousMarker || packet.timestamp !== this.timestamp;
|
|
10558
|
+
if (auStart && this.pictureSeen) this.keyframeAu = false;
|
|
10559
|
+
if (auStart) this.pictureSeen = false;
|
|
10560
|
+
this.timestamp = packet.timestamp;
|
|
10561
|
+
this.previousMarker = packet.marker;
|
|
10562
|
+
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10563
|
+
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10564
|
+
if (keyframeContent) this.keyframeAu = true;
|
|
10565
|
+
const pictureStart = !packet.parameterSet && !this.pictureSeen;
|
|
10566
|
+
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10567
|
+
return {
|
|
10568
|
+
auStart,
|
|
10569
|
+
keyframeAu: this.keyframeAu,
|
|
10570
|
+
keyframeAuStart,
|
|
10571
|
+
pictureStart,
|
|
10572
|
+
parameterSet: packet.parameterSet
|
|
10573
|
+
};
|
|
10574
|
+
}
|
|
10575
|
+
/** A redial re-bases the clock; nothing carries over. */
|
|
10576
|
+
reset() {
|
|
10577
|
+
this.timestamp = -1;
|
|
10578
|
+
this.previousMarker = true;
|
|
10579
|
+
this.keyframeAu = false;
|
|
10580
|
+
this.pictureSeen = false;
|
|
10581
|
+
}
|
|
10582
|
+
};
|
|
10583
|
+
//#endregion
|
|
10584
|
+
//#region src/stream-broker/stream-broker/access-unit-assembler.ts
|
|
10585
|
+
/**
|
|
10586
|
+
* Groups depacketized NALs into ACCESS UNITS — one per picture.
|
|
10587
|
+
*
|
|
10588
|
+
* The broker's `EncodedPacket` is one access unit, and every consumer judges it
|
|
10589
|
+
* that way: `keyframe` gates where a clip ring widens to, where a late joiner
|
|
10590
|
+
* can start, how the pre-roll window is cut; the stats count it as one picture
|
|
10591
|
+
* and time the GOP from one `keyframe` to the next. A multi-slice camera (a
|
|
10592
|
+
* Hikvision G2 main stream is 3 slices per picture) used to hand each slice —
|
|
10593
|
+
* and each prefix SEI — down as its own "frame": 75 input fps on a 25 fps
|
|
10594
|
+
* camera, an 8–34 ms "GOP", a clip ring that widened to the LAST IRAP slice
|
|
10595
|
+
* (a third of the picture, without its parameter sets), and a prefix SEI that
|
|
10596
|
+
* inherited the welded VPS/SPS/PPS instead of the IDR.
|
|
10597
|
+
*
|
|
10598
|
+
* Boundary rule (RFC 3550 / 6184 / 7798): the RTP marker bit closes the
|
|
10599
|
+
* picture it ends — flushed on the spot, so a single-slice stream pays no
|
|
10600
|
+
* latency — and a timestamp change closes the one before it, for cameras that
|
|
10601
|
+
* never set the marker. Parameter sets are held and welded onto the picture
|
|
10602
|
+
* that follows them, so a keyframe access unit is self-contained
|
|
10603
|
+
* (VPS/SPS/PPS + IDR) exactly as before; a prefix SEI stays inside its
|
|
10604
|
+
* picture and is never a frame of its own.
|
|
10605
|
+
*/
|
|
10606
|
+
var AccessUnitAssembler = class {
|
|
10607
|
+
onAccessUnit;
|
|
10608
|
+
/** Parameter sets seen since the last picture closed; welded onto the next one. */
|
|
10609
|
+
pendingParamNals = [];
|
|
10610
|
+
/** NALs of the picture in progress (parameter sets first when welded). */
|
|
10611
|
+
pictureNals = [];
|
|
10612
|
+
pictureTimestamp = 0;
|
|
10613
|
+
pictureKeyframe = false;
|
|
10614
|
+
constructor(onAccessUnit) {
|
|
10615
|
+
this.onAccessUnit = onAccessUnit;
|
|
10616
|
+
}
|
|
10617
|
+
push(depacketized) {
|
|
10618
|
+
const { annexB, keyframe, parameterSet, timestamp, marker } = depacketized;
|
|
10619
|
+
if (this.pictureNals.length > 0 && timestamp !== this.pictureTimestamp) this.flush();
|
|
10620
|
+
if (parameterSet) {
|
|
10621
|
+
this.pendingParamNals.push(annexB);
|
|
10622
|
+
return;
|
|
10623
|
+
}
|
|
10624
|
+
if (this.pictureNals.length === 0) {
|
|
10625
|
+
this.pictureTimestamp = timestamp;
|
|
10626
|
+
this.pictureKeyframe = false;
|
|
10627
|
+
}
|
|
10628
|
+
if (this.pendingParamNals.length > 0) {
|
|
10629
|
+
for (const paramNal of this.pendingParamNals) this.pictureNals.push(paramNal);
|
|
10630
|
+
this.pendingParamNals = [];
|
|
10631
|
+
this.pictureKeyframe = true;
|
|
10632
|
+
}
|
|
10633
|
+
this.pictureNals.push(annexB);
|
|
10634
|
+
if (keyframe) this.pictureKeyframe = true;
|
|
10635
|
+
if (marker) this.flush();
|
|
10636
|
+
}
|
|
10637
|
+
/**
|
|
10638
|
+
* Emit the picture in progress, if any. Called on the marker bit, on a
|
|
10639
|
+
* timestamp change, and by the broker when the stream stops or redials so
|
|
10640
|
+
* the last picture of a marker-less stream is not lost.
|
|
10641
|
+
*/
|
|
10642
|
+
flush() {
|
|
10643
|
+
if (this.pictureNals.length === 0) return;
|
|
10644
|
+
const nals = this.pictureNals;
|
|
10645
|
+
const timestamp = this.pictureTimestamp;
|
|
10646
|
+
const keyframe = this.pictureKeyframe;
|
|
10647
|
+
this.pictureNals = [];
|
|
10648
|
+
this.pictureKeyframe = false;
|
|
10649
|
+
const annexB = nals.length === 1 ? nals[0] : Buffer.concat(nals);
|
|
10650
|
+
this.onAccessUnit({
|
|
10651
|
+
annexB,
|
|
10652
|
+
timestamp,
|
|
10653
|
+
keyframe
|
|
10654
|
+
});
|
|
10655
|
+
}
|
|
10656
|
+
/** Drop everything held — a redial re-bases the clock, nothing carries over. */
|
|
10657
|
+
reset() {
|
|
10658
|
+
this.pendingParamNals = [];
|
|
10659
|
+
this.pictureNals = [];
|
|
10660
|
+
this.pictureTimestamp = 0;
|
|
10661
|
+
this.pictureKeyframe = false;
|
|
10662
|
+
}
|
|
10663
|
+
};
|
|
10664
|
+
//#endregion
|
|
10327
10665
|
//#region src/stream-broker/rtsp/rtsp-client.ts
|
|
10328
10666
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
10329
10667
|
var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
|
|
@@ -10556,7 +10894,7 @@ var NativeRtspClient = class {
|
|
|
10556
10894
|
const channel = this.readBuffer[1];
|
|
10557
10895
|
const length = this.readBuffer.readUInt16BE(2);
|
|
10558
10896
|
if (this.readBuffer.length < 4 + length) return;
|
|
10559
|
-
const payload =
|
|
10897
|
+
const payload = copyOwned(this.readBuffer, 4, 4 + length);
|
|
10560
10898
|
this.readBuffer = this.readBuffer.subarray(4 + length);
|
|
10561
10899
|
this.handleInterleavedPacket(channel, payload);
|
|
10562
10900
|
} else {
|
|
@@ -11292,49 +11630,6 @@ var AudioCodecSession = class {
|
|
|
11292
11630
|
}
|
|
11293
11631
|
};
|
|
11294
11632
|
//#endregion
|
|
11295
|
-
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
11296
|
-
/**
|
|
11297
|
-
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
11298
|
-
*
|
|
11299
|
-
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
11300
|
-
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
11301
|
-
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
11302
|
-
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
11303
|
-
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
11304
|
-
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
11305
|
-
* packet that happened to be allocated beside it. On the hub that held
|
|
11306
|
-
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
11307
|
-
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
11308
|
-
*
|
|
11309
|
-
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
11310
|
-
* now pass every entry through {@link detachFromPool} on the way in and
|
|
11311
|
-
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
11312
|
-
* entry costs. Short-lived traffic stays pooled.
|
|
11313
|
-
*/
|
|
11314
|
-
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
11315
|
-
function ownsBackingStore(buf) {
|
|
11316
|
-
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
11317
|
-
}
|
|
11318
|
-
/**
|
|
11319
|
-
* What retaining `buf` really costs: its own length when it owns its
|
|
11320
|
-
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
11321
|
-
*/
|
|
11322
|
-
function backingStoreBytes(buf) {
|
|
11323
|
-
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
11324
|
-
}
|
|
11325
|
-
/**
|
|
11326
|
-
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
11327
|
-
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
11328
|
-
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
11329
|
-
* already unpooled is returned as-is.
|
|
11330
|
-
*/
|
|
11331
|
-
function detachFromPool(buf) {
|
|
11332
|
-
if (ownsBackingStore(buf)) return buf;
|
|
11333
|
-
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
11334
|
-
buf.copy(owned);
|
|
11335
|
-
return owned;
|
|
11336
|
-
}
|
|
11337
|
-
//#endregion
|
|
11338
11633
|
//#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
|
|
11339
11634
|
/**
|
|
11340
11635
|
* Time-based ring buffer for encoded video packets.
|
|
@@ -12274,9 +12569,6 @@ var StreamBroker = class StreamBroker {
|
|
|
12274
12569
|
*/
|
|
12275
12570
|
ffmpegBinaryPathSource = () => null;
|
|
12276
12571
|
rtpDepacketizer = null;
|
|
12277
|
-
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12278
|
-
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
12279
|
-
pushRtpFlagDepacketizer = null;
|
|
12280
12572
|
audioRtpDecoder = null;
|
|
12281
12573
|
audioCodecSession = null;
|
|
12282
12574
|
/**
|
|
@@ -12644,10 +12936,24 @@ var StreamBroker = class StreamBroker {
|
|
|
12644
12936
|
* log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
|
|
12645
12937
|
* back but never video is observable from the structured logs. */
|
|
12646
12938
|
videoRtpSeen = 0;
|
|
12647
|
-
/**
|
|
12939
|
+
/**
|
|
12940
|
+
* What the source RTP packet being handled CARRIES (keyframe / parameter-set
|
|
12941
|
+
* content, read from its payload headers by `classifyRtpPacket`) — the flags
|
|
12942
|
+
* `captureRtpForPreBuffer` reads. Per packet, not per completed NAL: every
|
|
12943
|
+
* fragment of a fragmented IDR carries them, and the ring marks one keyframe
|
|
12944
|
+
* AU for the lot.
|
|
12945
|
+
*/
|
|
12648
12946
|
_lastNalKeyframe = false;
|
|
12649
12947
|
_lastNalParamSet = false;
|
|
12650
12948
|
/**
|
|
12949
|
+
* Whether the Annex-B plane (NAL reassembly + access-unit assembly →
|
|
12950
|
+
* `EncodedPacket`) is being BUILT for this stream. False while nothing
|
|
12951
|
+
* consumes it: the source-RTP path then classifies each packet and copies
|
|
12952
|
+
* nothing. Armed at the next keyframe access unit once a consumer attaches
|
|
12953
|
+
* (`needsAnnexBPlane`), so the first packet it emits is decodable.
|
|
12954
|
+
*/
|
|
12955
|
+
annexBPlaneActive = false;
|
|
12956
|
+
/**
|
|
12651
12957
|
* Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
|
|
12652
12958
|
* begin at the most recent keyframe access unit — i.e. the current GOP so
|
|
12653
12959
|
* far. A late-joining WebRTC viewer on the repacketizer path replays this
|
|
@@ -13149,7 +13455,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13149
13455
|
this.rtspRestreamer.destroy();
|
|
13150
13456
|
this.preBuffer.clear();
|
|
13151
13457
|
this.clipBuffer?.clear();
|
|
13152
|
-
this.
|
|
13458
|
+
this.annexBAssembler.reset();
|
|
13153
13459
|
this.encodedCallbacks.clear();
|
|
13154
13460
|
this.audioChunkPlane.dispose();
|
|
13155
13461
|
}
|
|
@@ -13612,7 +13918,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13612
13918
|
this.reconcilePrestreamWindow();
|
|
13613
13919
|
this.preBuffer.clear();
|
|
13614
13920
|
this.clipBuffer?.clear();
|
|
13615
|
-
this.
|
|
13921
|
+
this.annexBAssembler.reset();
|
|
13616
13922
|
this.rtspRestreamer.resetStreamState();
|
|
13617
13923
|
this.totalBytes = 0;
|
|
13618
13924
|
this.bytesInWindow = 0;
|
|
@@ -13723,26 +14029,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13723
14029
|
return;
|
|
13724
14030
|
}
|
|
13725
14031
|
}
|
|
13726
|
-
if (packet.type === "video")
|
|
13727
|
-
this.packetCount++;
|
|
13728
|
-
this.totalBytes += packet.data.length;
|
|
13729
|
-
this.bytesInWindow += packet.data.length;
|
|
13730
|
-
this.packetsInWindow++;
|
|
13731
|
-
this.lastPacketAt = Date.now();
|
|
13732
|
-
const now = Date.now();
|
|
13733
|
-
const windowMs = now - this.windowStartMs;
|
|
13734
|
-
if (windowMs >= 2e3) {
|
|
13735
|
-
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
13736
|
-
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
13737
|
-
this.bytesInWindow = 0;
|
|
13738
|
-
this.packetsInWindow = 0;
|
|
13739
|
-
this.windowStartMs = now;
|
|
13740
|
-
}
|
|
13741
|
-
if (packet.keyframe) {
|
|
13742
|
-
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
13743
|
-
this.lastKeyframeMs = now;
|
|
13744
|
-
}
|
|
13745
|
-
}
|
|
14032
|
+
if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
|
|
13746
14033
|
if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
|
|
13747
14034
|
if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
|
|
13748
14035
|
this.fanOutEncoded(packet);
|
|
@@ -13933,8 +14220,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13933
14220
|
index: auStart,
|
|
13934
14221
|
ts: auPacket.readUInt32BE(4)
|
|
13935
14222
|
});
|
|
14223
|
+
this.trimRtpRingToPreRollWindow();
|
|
13936
14224
|
}
|
|
13937
|
-
this.trimRtpRingToPreRollWindow();
|
|
13938
14225
|
this.rtpRingHasKeyframe = true;
|
|
13939
14226
|
}
|
|
13940
14227
|
this.rtpRingPrevMarker = marker;
|
|
@@ -14203,10 +14490,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14203
14490
|
*
|
|
14204
14491
|
* `push-rtp` providers deliver AnnexB-equivalents separately via
|
|
14205
14492
|
* `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
|
|
14206
|
-
* does NOT re-emit EncodedPackets; it only
|
|
14207
|
-
* keyframe/param-set flags the pre-buffer capture needs
|
|
14208
|
-
*
|
|
14209
|
-
*
|
|
14493
|
+
* does NOT re-emit EncodedPackets; it only classifies each packet for the
|
|
14494
|
+
* keyframe/param-set flags the pre-buffer capture needs
|
|
14495
|
+
* (`classifyRtpPacket`, no reassembly), keeping the AnnexB consumers
|
|
14496
|
+
* (pipe, restreamer) single-fed by the provider.
|
|
14210
14497
|
*
|
|
14211
14498
|
* The pre-buffer is captured even with zero subscribers attached — a
|
|
14212
14499
|
* viewer that connects mid-GOP must be able to replay the GOP that
|
|
@@ -14216,9 +14503,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14216
14503
|
pushVideoRtp(rtpData) {
|
|
14217
14504
|
if (this.stopping) return;
|
|
14218
14505
|
this.kickRtpActivityWatchdog();
|
|
14219
|
-
this.
|
|
14220
|
-
this.
|
|
14221
|
-
this.
|
|
14506
|
+
const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
|
|
14507
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14508
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14222
14509
|
this.captureRtpForPreBuffer(rtpData);
|
|
14223
14510
|
if (this.rtpVideoCallbacks.size === 0) return;
|
|
14224
14511
|
for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -14228,27 +14515,6 @@ var StreamBroker = class StreamBroker {
|
|
|
14228
14515
|
this.logger?.warn("rtp video subscriber threw", { meta: { error: errMsg(err) } });
|
|
14229
14516
|
}
|
|
14230
14517
|
}
|
|
14231
|
-
/**
|
|
14232
|
-
* Lazily build a flag-only RTP depacketizer for the `push-rtp` source
|
|
14233
|
-
* path. Unlike the native reader's depacketizer (which emits AnnexB
|
|
14234
|
-
* EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
|
|
14235
|
-
* `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
|
|
14236
|
-
* AnnexB plane through `pushEncodedPacket`, so re-emitting here would
|
|
14237
|
-
* double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
|
|
14238
|
-
* so the codec comes from the source declaration (`detectedCodec`, seeded
|
|
14239
|
-
* in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
|
|
14240
|
-
* starts with clean FU reassembly state.
|
|
14241
|
-
*/
|
|
14242
|
-
ensurePushRtpFlagDepacketizer() {
|
|
14243
|
-
if (!this.pushRtpFlagDepacketizer) {
|
|
14244
|
-
const codec = this.detectedCodec === "h265" ? "h265" : "h264";
|
|
14245
|
-
this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
|
|
14246
|
-
if (keyframe) this._lastNalKeyframe = true;
|
|
14247
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14248
|
-
});
|
|
14249
|
-
}
|
|
14250
|
-
return this.pushRtpFlagDepacketizer;
|
|
14251
|
-
}
|
|
14252
14518
|
/** Toggle the per-device streaming debug flag. */
|
|
14253
14519
|
setStreamingDebug(enabled) {
|
|
14254
14520
|
this.streamingDebug = enabled;
|
|
@@ -14691,10 +14957,18 @@ var StreamBroker = class StreamBroker {
|
|
|
14691
14957
|
seq: this.videoRtpSeen,
|
|
14692
14958
|
bytes: rtpData.length
|
|
14693
14959
|
} });
|
|
14694
|
-
|
|
14695
|
-
this.
|
|
14696
|
-
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14697
|
-
this.
|
|
14960
|
+
const wantPlane = this.needsAnnexBPlane();
|
|
14961
|
+
if (!wantPlane && this.annexBPlaneActive) this.idleAnnexBPlane();
|
|
14962
|
+
const carried = this.annexBPlaneActive ? this.rtpDepacketizer?.processPacket(rtpData) ?? null : this.rtpDepacketizer?.classifyPacket(rtpData) ?? null;
|
|
14963
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14964
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14965
|
+
const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
|
|
14966
|
+
if (wantPlane && !this.annexBPlaneActive && auFlags?.keyframeAuStart === true) {
|
|
14967
|
+
this.armAnnexBPlane();
|
|
14968
|
+
this.rtpDepacketizer?.processPacket(rtpData);
|
|
14969
|
+
}
|
|
14970
|
+
if (auFlags) this.recordVideoStats(rtpData.length, auFlags.pictureStart, auFlags.keyframeAu);
|
|
14971
|
+
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14698
14972
|
this.captureRtpForPreBuffer(rtpData);
|
|
14699
14973
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
14700
14974
|
cb(rtpData);
|
|
@@ -14890,51 +15164,116 @@ var StreamBroker = class StreamBroker {
|
|
|
14890
15164
|
* RTSP restreamer's pushRtpPassthrough.
|
|
14891
15165
|
*/
|
|
14892
15166
|
/**
|
|
14893
|
-
*
|
|
14894
|
-
*
|
|
14895
|
-
*
|
|
14896
|
-
*
|
|
14897
|
-
* with a single `EncodedPacket` that contains SPS+PPS+IDR together.
|
|
15167
|
+
* Groups the depacketizer's NALs into access units — ONE `EncodedPacket`
|
|
15168
|
+
* per picture, parameter sets welded onto the keyframe picture they
|
|
15169
|
+
* precede. See `access-unit-assembler.ts` for the boundary rule and the
|
|
15170
|
+
* per-NAL emit it replaced.
|
|
14898
15171
|
*/
|
|
14899
|
-
|
|
15172
|
+
annexBAssembler = new AccessUnitAssembler((accessUnit) => this.emitAccessUnit(accessUnit));
|
|
15173
|
+
/** Access-unit membership of the source RTP packets, for the restreamer's flags. */
|
|
15174
|
+
rtpAuTracker = new RtpAccessUnitTracker();
|
|
14900
15175
|
/**
|
|
14901
15176
|
* 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).
|
|
15177
|
+
* and hands them to the access-unit assembler, which emits one
|
|
15178
|
+
* `EncodedPacket` per PICTURE (`emitAccessUnit`) — never per NAL. A
|
|
15179
|
+
* multi-slice IDR, its parameter sets and its prefix SEI are one keyframe
|
|
15180
|
+
* packet; three P slices are one P packet.
|
|
14909
15181
|
*
|
|
14910
|
-
* The RTSP restreamer
|
|
14911
|
-
* not
|
|
15182
|
+
* The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
|
|
15183
|
+
* not fed from here.
|
|
14912
15184
|
*/
|
|
14913
15185
|
handleDepacketizedNal(depacketized) {
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
15186
|
+
this.annexBAssembler.push(depacketized);
|
|
15187
|
+
}
|
|
15188
|
+
/**
|
|
15189
|
+
* Does anything consume the Annex-B plane right now? An encoded subscriber
|
|
15190
|
+
* (WebRTC Annex-B, fragments, a decode session), a pipe client, a clip ring
|
|
15191
|
+
* (it retains access units continuously, so it needs them built even with
|
|
15192
|
+
* nobody attached), or a restreamer still in Annex-B mode (no camera SDP yet).
|
|
15193
|
+
* The RTSP restream and the WebRTC RTP path are served from the source RTP
|
|
15194
|
+
* and never count. Cheap — four field reads — because it runs per packet.
|
|
15195
|
+
*/
|
|
15196
|
+
needsAnnexBPlane() {
|
|
15197
|
+
return this.encodedCallbacks.size > 0 || this.clipBuffer !== null || this.pipeServer.getClientCount() > 0 || !this.rtspRestreamer.rtpPassthroughMode;
|
|
15198
|
+
}
|
|
15199
|
+
/** The consumers that want the plane, for the arm/idle lines. */
|
|
15200
|
+
annexBPlaneConsumers() {
|
|
15201
|
+
return {
|
|
15202
|
+
encodedSubscribers: this.encodedCallbacks.size,
|
|
15203
|
+
clipRing: this.clipBuffer !== null,
|
|
15204
|
+
pipeClients: this.pipeServer.getClientCount(),
|
|
15205
|
+
restreamerAnnexB: !this.rtspRestreamer.rtpPassthroughMode
|
|
15206
|
+
};
|
|
15207
|
+
}
|
|
15208
|
+
/**
|
|
15209
|
+
* Start building access units from the keyframe AU the current packet opens.
|
|
15210
|
+
* The depacketizer and the assembler are clean here (reset when the plane
|
|
15211
|
+
* went idle, or never used), so nothing stale precedes that keyframe.
|
|
15212
|
+
*/
|
|
15213
|
+
armAnnexBPlane() {
|
|
15214
|
+
this.annexBPlaneActive = true;
|
|
15215
|
+
this.logger?.info("annex-b plane armed at a keyframe access unit", {
|
|
15216
|
+
tags: { deviceId: this.numericDeviceId },
|
|
15217
|
+
meta: this.annexBPlaneConsumers()
|
|
15218
|
+
});
|
|
15219
|
+
}
|
|
15220
|
+
/**
|
|
15221
|
+
* Stop building access units: the last consumer left. The picture in
|
|
15222
|
+
* progress is dropped, not flushed — there is nobody to hand it to, and a
|
|
15223
|
+
* partial picture must never reach the next consumer's clean start. Logged,
|
|
15224
|
+
* once per transition: a plane that is idle on purpose must not read as a
|
|
15225
|
+
* stream that stopped.
|
|
15226
|
+
*/
|
|
15227
|
+
idleAnnexBPlane() {
|
|
15228
|
+
this.annexBPlaneActive = false;
|
|
15229
|
+
this.rtpDepacketizer?.reset();
|
|
15230
|
+
this.annexBAssembler.reset();
|
|
15231
|
+
this.logger?.info("annex-b plane idle — no consumer, NAL reassembly skipped", { tags: { deviceId: this.numericDeviceId } });
|
|
15232
|
+
}
|
|
15233
|
+
/**
|
|
15234
|
+
* One place for the input stats, fed from whichever path owns the pictures:
|
|
15235
|
+
* the source RTP packets while the native reader is up (`onVideoRtp`), the
|
|
15236
|
+
* `EncodedPacket`s otherwise (`pushEncodedPacket`). `bytes` accumulate per
|
|
15237
|
+
* call; `inputFps`, `packetCount` and the IDR interval advance once per
|
|
15238
|
+
* picture — the window is closed on a picture start so a window of N frames
|
|
15239
|
+
* reads N, exactly as it did when every call was one access unit.
|
|
15240
|
+
*/
|
|
15241
|
+
recordVideoStats(bytes, pictureStart, keyframe) {
|
|
15242
|
+
this.totalBytes += bytes;
|
|
15243
|
+
this.bytesInWindow += bytes;
|
|
15244
|
+
if (!pictureStart) return;
|
|
15245
|
+
this.packetCount++;
|
|
15246
|
+
this.packetsInWindow++;
|
|
15247
|
+
const now = Date.now();
|
|
15248
|
+
this.lastPacketAt = now;
|
|
15249
|
+
const windowMs = now - this.windowStartMs;
|
|
15250
|
+
if (windowMs >= 2e3) {
|
|
15251
|
+
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
15252
|
+
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
15253
|
+
this.bytesInWindow = 0;
|
|
15254
|
+
this.packetsInWindow = 0;
|
|
15255
|
+
this.windowStartMs = now;
|
|
14923
15256
|
}
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
15257
|
+
if (keyframe) {
|
|
15258
|
+
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
15259
|
+
this.lastKeyframeMs = now;
|
|
15260
|
+
}
|
|
15261
|
+
}
|
|
15262
|
+
/**
|
|
15263
|
+
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15264
|
+
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15265
|
+
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
15266
|
+
* initialises from and what a clip or a pre-roll window may start at.
|
|
15267
|
+
*/
|
|
15268
|
+
emitAccessUnit(accessUnit) {
|
|
15269
|
+
const codec = this.detectedCodec ?? "h264";
|
|
15270
|
+
const pts = Math.floor(accessUnit.timestamp / 90);
|
|
14932
15271
|
const packet = {
|
|
14933
15272
|
type: "video",
|
|
14934
|
-
data:
|
|
15273
|
+
data: accessUnit.annexB,
|
|
14935
15274
|
pts,
|
|
14936
15275
|
dts: pts,
|
|
14937
|
-
keyframe: keyframe
|
|
15276
|
+
keyframe: accessUnit.keyframe,
|
|
14938
15277
|
codec
|
|
14939
15278
|
};
|
|
14940
15279
|
this.pushEncodedPacket(packet);
|
|
@@ -15338,10 +15677,10 @@ var StreamBroker = class StreamBroker {
|
|
|
15338
15677
|
this.rtpDepacketizer.reset();
|
|
15339
15678
|
this.rtpDepacketizer = null;
|
|
15340
15679
|
}
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15680
|
+
this.annexBAssembler.flush();
|
|
15681
|
+
this.annexBAssembler.reset();
|
|
15682
|
+
this.rtpAuTracker.reset();
|
|
15683
|
+
this.annexBPlaneActive = false;
|
|
15345
15684
|
this.resetRtpPreBuffer();
|
|
15346
15685
|
if (this.audioRtpDecoder) {
|
|
15347
15686
|
this.audioRtpDecoder.flush();
|