@camstack/addon-pipeline 1.2.364 → 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 +325 -205
- package/dist/stream-broker/index.mjs +325 -205
- 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;
|
|
@@ -3751,7 +3813,7 @@ var RtspRestreamer = class {
|
|
|
3751
3813
|
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3752
3814
|
if (this.collectingKeyframeRtp) {
|
|
3753
3815
|
this.lastKeyframeRtpPackets.push({
|
|
3754
|
-
data:
|
|
3816
|
+
data: detachFromPool(rtpData),
|
|
3755
3817
|
marker: rtpPacket.marker
|
|
3756
3818
|
});
|
|
3757
3819
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
@@ -4353,6 +4415,19 @@ var RtspSession = class {
|
|
|
4353
4415
|
lastRtpAt = 0;
|
|
4354
4416
|
/** Total video RTP bytes written to the socket. */
|
|
4355
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
|
+
};
|
|
4356
4431
|
/** Client `User-Agent` (from the first request that carries one). Lets the
|
|
4357
4432
|
* broker panel identify a consumer by purpose — e.g. the recorder sets
|
|
4358
4433
|
* `-user_agent recorder` on its RTSP pull — instead of a bare address. */
|
|
@@ -4600,7 +4675,7 @@ var RtspSession = class {
|
|
|
4600
4675
|
return "dropped-backlog";
|
|
4601
4676
|
}
|
|
4602
4677
|
}
|
|
4603
|
-
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
|
|
4678
|
+
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
|
|
4604
4679
|
this.missedKeyframes = 0;
|
|
4605
4680
|
return "written";
|
|
4606
4681
|
}
|
|
@@ -4609,7 +4684,7 @@ var RtspSession = class {
|
|
|
4609
4684
|
if (!this.playing || this.closed || this.muted) return "not-playing";
|
|
4610
4685
|
if (this.tracks.length < 2) return "not-playing";
|
|
4611
4686
|
if (this.socket.writableLength > 2097152) return "dropped-backlog";
|
|
4612
|
-
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
|
|
4687
|
+
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
|
|
4613
4688
|
return "written";
|
|
4614
4689
|
}
|
|
4615
4690
|
/**
|
|
@@ -4631,14 +4706,13 @@ var RtspSession = class {
|
|
|
4631
4706
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
4632
4707
|
*
|
|
4633
4708
|
* The payload buffer handed here is a private allocation per packet —
|
|
4634
|
-
* `
|
|
4635
|
-
*
|
|
4636
|
-
*
|
|
4637
|
-
*
|
|
4638
|
-
*
|
|
4639
|
-
*
|
|
4640
|
-
*
|
|
4641
|
-
* to: a pending write holds its own reference, and the rule below is about
|
|
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
|
|
4642
4716
|
* mutation, not retention.)
|
|
4643
4717
|
*
|
|
4644
4718
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
@@ -4648,18 +4722,36 @@ var RtspSession = class {
|
|
|
4648
4722
|
* queued. Four bytes from the Buffer pool is a pointer bump; the copy this
|
|
4649
4723
|
* replaces was ~1.4 KB.
|
|
4650
4724
|
*
|
|
4651
|
-
*
|
|
4652
|
-
*
|
|
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.
|
|
4653
4741
|
*/
|
|
4654
|
-
writeInterleaved(channel, payload) {
|
|
4742
|
+
writeInterleaved(channel, payload, endOfAccessUnit) {
|
|
4655
4743
|
const header = Buffer.allocUnsafe(4);
|
|
4656
4744
|
header[0] = 36;
|
|
4657
4745
|
header[1] = channel;
|
|
4658
4746
|
header.writeUInt16BE(payload.length, 2);
|
|
4659
|
-
this.
|
|
4747
|
+
if (!this.accessUnitCorked) {
|
|
4748
|
+
this.accessUnitCorked = true;
|
|
4749
|
+
this.socket.cork();
|
|
4750
|
+
process.nextTick(this.uncorkAccessUnit);
|
|
4751
|
+
}
|
|
4660
4752
|
this.socket.write(header);
|
|
4661
4753
|
this.socket.write(payload);
|
|
4662
|
-
this.
|
|
4754
|
+
if (endOfAccessUnit) this.uncorkAccessUnit();
|
|
4663
4755
|
this.lastRtpAt = Date.now();
|
|
4664
4756
|
this.bytesSent += 4 + payload.length;
|
|
4665
4757
|
}
|
|
@@ -10083,7 +10175,7 @@ var Rfc4571Reader = class {
|
|
|
10083
10175
|
while (this.readBuffer.length >= 2) {
|
|
10084
10176
|
const length = this.readBuffer.readUInt16BE(0);
|
|
10085
10177
|
if (this.readBuffer.length < 2 + length) return;
|
|
10086
|
-
const rtpPacket =
|
|
10178
|
+
const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
|
|
10087
10179
|
this.readBuffer = this.readBuffer.subarray(2 + length);
|
|
10088
10180
|
this.dispatchRtp(rtpPacket);
|
|
10089
10181
|
}
|
|
@@ -10144,6 +10236,93 @@ function totalFragmentBytes(fragments) {
|
|
|
10144
10236
|
for (const fragment of fragments) total += fragment.length;
|
|
10145
10237
|
return total;
|
|
10146
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
|
+
}
|
|
10147
10326
|
/**
|
|
10148
10327
|
* RTP → Annex-B NAL depacketizer.
|
|
10149
10328
|
*
|
|
@@ -10188,87 +10367,22 @@ var RtpDepacketizer = class {
|
|
|
10188
10367
|
* short to be RTP with a payload.
|
|
10189
10368
|
*/
|
|
10190
10369
|
processPacket(rtpData) {
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
const timestamp = rtpData.readUInt32BE(4);
|
|
10370
|
+
const carried = this.classifyPacket(rtpData);
|
|
10371
|
+
if (carried === null) return null;
|
|
10194
10372
|
const payload = rtpData.subarray(12);
|
|
10195
|
-
if (
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
else this.processH265(payload, timestamp, marker);
|
|
10199
|
-
return {
|
|
10200
|
-
timestamp,
|
|
10201
|
-
marker,
|
|
10202
|
-
keyframe: carried.keyframe,
|
|
10203
|
-
parameterSet: carried.parameterSet
|
|
10204
|
-
};
|
|
10373
|
+
if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
|
|
10374
|
+
else this.processH265(payload, carried.timestamp, carried.marker);
|
|
10375
|
+
return carried;
|
|
10205
10376
|
}
|
|
10206
|
-
/**
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
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
|
-
};
|
|
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);
|
|
10272
10386
|
}
|
|
10273
10387
|
processH264(payload, timestamp, marker) {
|
|
10274
10388
|
const nalType = payload[0] & 31;
|
|
@@ -10448,11 +10562,13 @@ var RtpAccessUnitTracker = class {
|
|
|
10448
10562
|
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10449
10563
|
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10450
10564
|
if (keyframeContent) this.keyframeAu = true;
|
|
10565
|
+
const pictureStart = !packet.parameterSet && !this.pictureSeen;
|
|
10451
10566
|
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10452
10567
|
return {
|
|
10453
10568
|
auStart,
|
|
10454
10569
|
keyframeAu: this.keyframeAu,
|
|
10455
10570
|
keyframeAuStart,
|
|
10571
|
+
pictureStart,
|
|
10456
10572
|
parameterSet: packet.parameterSet
|
|
10457
10573
|
};
|
|
10458
10574
|
}
|
|
@@ -10778,7 +10894,7 @@ var NativeRtspClient = class {
|
|
|
10778
10894
|
const channel = this.readBuffer[1];
|
|
10779
10895
|
const length = this.readBuffer.readUInt16BE(2);
|
|
10780
10896
|
if (this.readBuffer.length < 4 + length) return;
|
|
10781
|
-
const payload =
|
|
10897
|
+
const payload = copyOwned(this.readBuffer, 4, 4 + length);
|
|
10782
10898
|
this.readBuffer = this.readBuffer.subarray(4 + length);
|
|
10783
10899
|
this.handleInterleavedPacket(channel, payload);
|
|
10784
10900
|
} else {
|
|
@@ -11514,49 +11630,6 @@ var AudioCodecSession = class {
|
|
|
11514
11630
|
}
|
|
11515
11631
|
};
|
|
11516
11632
|
//#endregion
|
|
11517
|
-
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
11518
|
-
/**
|
|
11519
|
-
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
11520
|
-
*
|
|
11521
|
-
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
11522
|
-
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
11523
|
-
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
11524
|
-
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
11525
|
-
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
11526
|
-
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
11527
|
-
* packet that happened to be allocated beside it. On the hub that held
|
|
11528
|
-
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
11529
|
-
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
11530
|
-
*
|
|
11531
|
-
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
11532
|
-
* now pass every entry through {@link detachFromPool} on the way in and
|
|
11533
|
-
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
11534
|
-
* entry costs. Short-lived traffic stays pooled.
|
|
11535
|
-
*/
|
|
11536
|
-
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
11537
|
-
function ownsBackingStore(buf) {
|
|
11538
|
-
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
11539
|
-
}
|
|
11540
|
-
/**
|
|
11541
|
-
* What retaining `buf` really costs: its own length when it owns its
|
|
11542
|
-
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
11543
|
-
*/
|
|
11544
|
-
function backingStoreBytes(buf) {
|
|
11545
|
-
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
11546
|
-
}
|
|
11547
|
-
/**
|
|
11548
|
-
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
11549
|
-
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
11550
|
-
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
11551
|
-
* already unpooled is returned as-is.
|
|
11552
|
-
*/
|
|
11553
|
-
function detachFromPool(buf) {
|
|
11554
|
-
if (ownsBackingStore(buf)) return buf;
|
|
11555
|
-
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
11556
|
-
buf.copy(owned);
|
|
11557
|
-
return owned;
|
|
11558
|
-
}
|
|
11559
|
-
//#endregion
|
|
11560
11633
|
//#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
|
|
11561
11634
|
/**
|
|
11562
11635
|
* Time-based ring buffer for encoded video packets.
|
|
@@ -12496,9 +12569,6 @@ var StreamBroker = class StreamBroker {
|
|
|
12496
12569
|
*/
|
|
12497
12570
|
ffmpegBinaryPathSource = () => null;
|
|
12498
12571
|
rtpDepacketizer = null;
|
|
12499
|
-
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12500
|
-
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
12501
|
-
pushRtpFlagDepacketizer = null;
|
|
12502
12572
|
audioRtpDecoder = null;
|
|
12503
12573
|
audioCodecSession = null;
|
|
12504
12574
|
/**
|
|
@@ -12866,10 +12936,24 @@ var StreamBroker = class StreamBroker {
|
|
|
12866
12936
|
* log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
|
|
12867
12937
|
* back but never video is observable from the structured logs. */
|
|
12868
12938
|
videoRtpSeen = 0;
|
|
12869
|
-
/**
|
|
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
|
+
*/
|
|
12870
12946
|
_lastNalKeyframe = false;
|
|
12871
12947
|
_lastNalParamSet = false;
|
|
12872
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
|
+
/**
|
|
12873
12957
|
* Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
|
|
12874
12958
|
* begin at the most recent keyframe access unit — i.e. the current GOP so
|
|
12875
12959
|
* far. A late-joining WebRTC viewer on the repacketizer path replays this
|
|
@@ -13945,26 +14029,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13945
14029
|
return;
|
|
13946
14030
|
}
|
|
13947
14031
|
}
|
|
13948
|
-
if (packet.type === "video")
|
|
13949
|
-
this.packetCount++;
|
|
13950
|
-
this.totalBytes += packet.data.length;
|
|
13951
|
-
this.bytesInWindow += packet.data.length;
|
|
13952
|
-
this.packetsInWindow++;
|
|
13953
|
-
this.lastPacketAt = Date.now();
|
|
13954
|
-
const now = Date.now();
|
|
13955
|
-
const windowMs = now - this.windowStartMs;
|
|
13956
|
-
if (windowMs >= 2e3) {
|
|
13957
|
-
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
13958
|
-
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
13959
|
-
this.bytesInWindow = 0;
|
|
13960
|
-
this.packetsInWindow = 0;
|
|
13961
|
-
this.windowStartMs = now;
|
|
13962
|
-
}
|
|
13963
|
-
if (packet.keyframe) {
|
|
13964
|
-
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
13965
|
-
this.lastKeyframeMs = now;
|
|
13966
|
-
}
|
|
13967
|
-
}
|
|
14032
|
+
if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
|
|
13968
14033
|
if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
|
|
13969
14034
|
if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
|
|
13970
14035
|
this.fanOutEncoded(packet);
|
|
@@ -14155,8 +14220,8 @@ var StreamBroker = class StreamBroker {
|
|
|
14155
14220
|
index: auStart,
|
|
14156
14221
|
ts: auPacket.readUInt32BE(4)
|
|
14157
14222
|
});
|
|
14223
|
+
this.trimRtpRingToPreRollWindow();
|
|
14158
14224
|
}
|
|
14159
|
-
this.trimRtpRingToPreRollWindow();
|
|
14160
14225
|
this.rtpRingHasKeyframe = true;
|
|
14161
14226
|
}
|
|
14162
14227
|
this.rtpRingPrevMarker = marker;
|
|
@@ -14425,10 +14490,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14425
14490
|
*
|
|
14426
14491
|
* `push-rtp` providers deliver AnnexB-equivalents separately via
|
|
14427
14492
|
* `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
|
|
14428
|
-
* does NOT re-emit EncodedPackets; it only
|
|
14429
|
-
* keyframe/param-set flags the pre-buffer capture needs
|
|
14430
|
-
*
|
|
14431
|
-
*
|
|
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.
|
|
14432
14497
|
*
|
|
14433
14498
|
* The pre-buffer is captured even with zero subscribers attached — a
|
|
14434
14499
|
* viewer that connects mid-GOP must be able to replay the GOP that
|
|
@@ -14438,9 +14503,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14438
14503
|
pushVideoRtp(rtpData) {
|
|
14439
14504
|
if (this.stopping) return;
|
|
14440
14505
|
this.kickRtpActivityWatchdog();
|
|
14441
|
-
this.
|
|
14442
|
-
this.
|
|
14443
|
-
this.
|
|
14506
|
+
const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
|
|
14507
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14508
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14444
14509
|
this.captureRtpForPreBuffer(rtpData);
|
|
14445
14510
|
if (this.rtpVideoCallbacks.size === 0) return;
|
|
14446
14511
|
for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -14450,27 +14515,6 @@ var StreamBroker = class StreamBroker {
|
|
|
14450
14515
|
this.logger?.warn("rtp video subscriber threw", { meta: { error: errMsg(err) } });
|
|
14451
14516
|
}
|
|
14452
14517
|
}
|
|
14453
|
-
/**
|
|
14454
|
-
* Lazily build a flag-only RTP depacketizer for the `push-rtp` source
|
|
14455
|
-
* path. Unlike the native reader's depacketizer (which emits AnnexB
|
|
14456
|
-
* EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
|
|
14457
|
-
* `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
|
|
14458
|
-
* AnnexB plane through `pushEncodedPacket`, so re-emitting here would
|
|
14459
|
-
* double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
|
|
14460
|
-
* so the codec comes from the source declaration (`detectedCodec`, seeded
|
|
14461
|
-
* in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
|
|
14462
|
-
* starts with clean FU reassembly state.
|
|
14463
|
-
*/
|
|
14464
|
-
ensurePushRtpFlagDepacketizer() {
|
|
14465
|
-
if (!this.pushRtpFlagDepacketizer) {
|
|
14466
|
-
const codec = this.detectedCodec === "h265" ? "h265" : "h264";
|
|
14467
|
-
this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
|
|
14468
|
-
if (keyframe) this._lastNalKeyframe = true;
|
|
14469
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14470
|
-
});
|
|
14471
|
-
}
|
|
14472
|
-
return this.pushRtpFlagDepacketizer;
|
|
14473
|
-
}
|
|
14474
14518
|
/** Toggle the per-device streaming debug flag. */
|
|
14475
14519
|
setStreamingDebug(enabled) {
|
|
14476
14520
|
this.streamingDebug = enabled;
|
|
@@ -14913,10 +14957,17 @@ var StreamBroker = class StreamBroker {
|
|
|
14913
14957
|
seq: this.videoRtpSeen,
|
|
14914
14958
|
bytes: rtpData.length
|
|
14915
14959
|
} });
|
|
14916
|
-
|
|
14917
|
-
this.
|
|
14918
|
-
const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
|
|
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;
|
|
14919
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);
|
|
14920
14971
|
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14921
14972
|
this.captureRtpForPreBuffer(rtpData);
|
|
14922
14973
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -15132,11 +15183,83 @@ var StreamBroker = class StreamBroker {
|
|
|
15132
15183
|
* not fed from here.
|
|
15133
15184
|
*/
|
|
15134
15185
|
handleDepacketizedNal(depacketized) {
|
|
15135
|
-
if (depacketized.keyframe) this._lastNalKeyframe = true;
|
|
15136
|
-
if (depacketized.parameterSet) this._lastNalParamSet = true;
|
|
15137
15186
|
this.annexBAssembler.push(depacketized);
|
|
15138
15187
|
}
|
|
15139
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;
|
|
15256
|
+
}
|
|
15257
|
+
if (keyframe) {
|
|
15258
|
+
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
15259
|
+
this.lastKeyframeMs = now;
|
|
15260
|
+
}
|
|
15261
|
+
}
|
|
15262
|
+
/**
|
|
15140
15263
|
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15141
15264
|
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15142
15265
|
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
@@ -15557,10 +15680,7 @@ var StreamBroker = class StreamBroker {
|
|
|
15557
15680
|
this.annexBAssembler.flush();
|
|
15558
15681
|
this.annexBAssembler.reset();
|
|
15559
15682
|
this.rtpAuTracker.reset();
|
|
15560
|
-
|
|
15561
|
-
this.pushRtpFlagDepacketizer.reset();
|
|
15562
|
-
this.pushRtpFlagDepacketizer = null;
|
|
15563
|
-
}
|
|
15683
|
+
this.annexBPlaneActive = false;
|
|
15564
15684
|
this.resetRtpPreBuffer();
|
|
15565
15685
|
if (this.audioRtpDecoder) {
|
|
15566
15686
|
this.audioRtpDecoder.flush();
|