@camstack/addon-pipeline 1.2.364 → 1.2.366
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 +641 -206
- package/dist/stream-broker/index.mjs +641 -206
- 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,262 @@ var RtpPacketizer = class {
|
|
|
3081
3081
|
}
|
|
3082
3082
|
};
|
|
3083
3083
|
//#endregion
|
|
3084
|
+
//#region src/stream-broker/stream-broker/broker-mem-probe.ts
|
|
3085
|
+
/** Cadence of the `[broker-mem]` line — aligned with the runner's `[mem]`. */
|
|
3086
|
+
var BROKER_MEM_INTERVAL_MS = 5 * 6e4;
|
|
3087
|
+
var copiedPackets = 0;
|
|
3088
|
+
var copiedBytes = 0;
|
|
3089
|
+
/** O(1) per packet: called by `copyOwned`. No allocation. */
|
|
3090
|
+
function recordCopyOwned(bytes) {
|
|
3091
|
+
copiedPackets += 1;
|
|
3092
|
+
copiedBytes += bytes;
|
|
3093
|
+
}
|
|
3094
|
+
function readCopyOwnedTotals() {
|
|
3095
|
+
return {
|
|
3096
|
+
packets: copiedPackets,
|
|
3097
|
+
bytes: copiedBytes
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
/** `RssAnon:` / `RssFile:` out of `/proc/self/status` text; null when absent. */
|
|
3101
|
+
function parseProcStatus(text) {
|
|
3102
|
+
const anon = /^RssAnon:\s+(\d+)\s*kB/m.exec(text);
|
|
3103
|
+
const file = /^RssFile:\s+(\d+)\s*kB/m.exec(text);
|
|
3104
|
+
if (anon === null || file === null) return null;
|
|
3105
|
+
return {
|
|
3106
|
+
rssAnonKb: Number(anon[1]),
|
|
3107
|
+
rssFileKb: Number(file[1])
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Summarise `/proc/self/maps`. Cheaper than `smaps_rollup`'s kernel page walk:
|
|
3112
|
+
* the file is one line per mapping and needs no per-page accounting. The
|
|
3113
|
+
* `[heap]` figure is the brk extent (virtual), which is the number glibc's
|
|
3114
|
+
* main arena grows when fragmented.
|
|
3115
|
+
*
|
|
3116
|
+
* Line shape: `start-end perms offset dev inode [path]` — an anonymous mapping
|
|
3117
|
+
* has no sixth field.
|
|
3118
|
+
*/
|
|
3119
|
+
function parseProcMaps(text) {
|
|
3120
|
+
let heapBytes = 0;
|
|
3121
|
+
let anonMappings = 0;
|
|
3122
|
+
let anonBytes = 0;
|
|
3123
|
+
let from = 0;
|
|
3124
|
+
while (from < text.length) {
|
|
3125
|
+
let to = text.indexOf("\n", from);
|
|
3126
|
+
if (to === -1) to = text.length;
|
|
3127
|
+
const line = text.slice(from, to);
|
|
3128
|
+
from = to + 1;
|
|
3129
|
+
const dash = line.indexOf("-");
|
|
3130
|
+
const space = line.indexOf(" ");
|
|
3131
|
+
if (dash <= 0 || space <= dash) continue;
|
|
3132
|
+
const size = Number.parseInt(line.slice(dash + 1, space), 16) - Number.parseInt(line.slice(0, dash), 16);
|
|
3133
|
+
if (!Number.isFinite(size)) continue;
|
|
3134
|
+
const path = line.split(/\s+/, 6)[5];
|
|
3135
|
+
if (path === void 0 || path === "") {
|
|
3136
|
+
anonMappings += 1;
|
|
3137
|
+
anonBytes += size;
|
|
3138
|
+
} else if (path === "[heap]") heapBytes += size;
|
|
3139
|
+
}
|
|
3140
|
+
return {
|
|
3141
|
+
heapBytes,
|
|
3142
|
+
anonMappings,
|
|
3143
|
+
anonBytes
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
/** Opened/closed consumers since the last reading; `drain()` resets. */
|
|
3147
|
+
var ConsumerChurn = class {
|
|
3148
|
+
opened = 0;
|
|
3149
|
+
closed = 0;
|
|
3150
|
+
openedByKind = /* @__PURE__ */ new Map();
|
|
3151
|
+
closedByKind = /* @__PURE__ */ new Map();
|
|
3152
|
+
recordOpen(kind) {
|
|
3153
|
+
this.opened += 1;
|
|
3154
|
+
this.openedByKind.set(kind, (this.openedByKind.get(kind) ?? 0) + 1);
|
|
3155
|
+
}
|
|
3156
|
+
recordClose(kind) {
|
|
3157
|
+
this.closed += 1;
|
|
3158
|
+
this.closedByKind.set(kind, (this.closedByKind.get(kind) ?? 0) + 1);
|
|
3159
|
+
}
|
|
3160
|
+
drain() {
|
|
3161
|
+
const reading = {
|
|
3162
|
+
opened: this.opened,
|
|
3163
|
+
closed: this.closed,
|
|
3164
|
+
openedByKind: Object.fromEntries(this.openedByKind),
|
|
3165
|
+
closedByKind: Object.fromEntries(this.closedByKind)
|
|
3166
|
+
};
|
|
3167
|
+
this.opened = 0;
|
|
3168
|
+
this.closed = 0;
|
|
3169
|
+
this.openedByKind = /* @__PURE__ */ new Map();
|
|
3170
|
+
this.closedByKind = /* @__PURE__ */ new Map();
|
|
3171
|
+
return reading;
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
/** The one churn counter of this process (the broker runner). */
|
|
3175
|
+
var consumerChurn = new ConsumerChurn();
|
|
3176
|
+
/**
|
|
3177
|
+
* Sliding-window limiter for the attach/detach lines. `admit(now)` answers
|
|
3178
|
+
* whether this line may be written; the number of lines swallowed since the
|
|
3179
|
+
* last admitted one is returned with it so the next line can say so (a branch
|
|
3180
|
+
* that drops work names it).
|
|
3181
|
+
*/
|
|
3182
|
+
function createLineLimiter(maxPerWindow, windowMs) {
|
|
3183
|
+
let windowStart = 0;
|
|
3184
|
+
let inWindow = 0;
|
|
3185
|
+
let suppressed = 0;
|
|
3186
|
+
return (now) => {
|
|
3187
|
+
if (now - windowStart >= windowMs) {
|
|
3188
|
+
windowStart = now;
|
|
3189
|
+
inWindow = 0;
|
|
3190
|
+
}
|
|
3191
|
+
if (inWindow >= maxPerWindow) {
|
|
3192
|
+
suppressed += 1;
|
|
3193
|
+
return {
|
|
3194
|
+
admit: false,
|
|
3195
|
+
suppressedBefore: 0
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
inWindow += 1;
|
|
3199
|
+
const suppressedBefore = suppressed;
|
|
3200
|
+
suppressed = 0;
|
|
3201
|
+
return {
|
|
3202
|
+
admit: true,
|
|
3203
|
+
suppressedBefore
|
|
3204
|
+
};
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
var consumerLineLimiter = createLineLimiter(40, 6e4);
|
|
3208
|
+
/**
|
|
3209
|
+
* One consumer attached to / detached from a broker: counted always (the
|
|
3210
|
+
* `[broker-mem]` line reports opened/closed since the last line), written at
|
|
3211
|
+
* info under a rate limit so a step in RSS can be matched to an event. The
|
|
3212
|
+
* line after a suppressed burst says how many were swallowed.
|
|
3213
|
+
*/
|
|
3214
|
+
function reportConsumerEvent(logger, event, kind, deviceId, total, now = Date.now()) {
|
|
3215
|
+
if (event === "attach") consumerChurn.recordOpen(kind);
|
|
3216
|
+
else consumerChurn.recordClose(kind);
|
|
3217
|
+
if (logger === void 0 || logger === null) return;
|
|
3218
|
+
const verdict = consumerLineLimiter(now);
|
|
3219
|
+
if (!verdict.admit) return;
|
|
3220
|
+
const suppressed = verdict.suppressedBefore > 0 ? ` suppressedBefore=${verdict.suppressedBefore}` : "";
|
|
3221
|
+
logger.info(`consumer ${event} kind=${kind} total=${total}${suppressed}`, { ...deviceId !== void 0 ? { tags: { deviceId } } : {} });
|
|
3222
|
+
}
|
|
3223
|
+
var MB = 1024 * 1024;
|
|
3224
|
+
function ringBytesOf(s) {
|
|
3225
|
+
return s.preBufferBytes + s.clipBytes + s.rtpRingBytes + s.keyframeCacheBytes;
|
|
3226
|
+
}
|
|
3227
|
+
function mb(bytes) {
|
|
3228
|
+
return (bytes / MB).toFixed(1);
|
|
3229
|
+
}
|
|
3230
|
+
function kindList(counts) {
|
|
3231
|
+
const keys = Object.keys(counts).sort();
|
|
3232
|
+
return keys.length === 0 ? "none" : keys.map((k) => `${k}:${counts[k]}`).join(",");
|
|
3233
|
+
}
|
|
3234
|
+
/** One greppable `key=value` line; no per-device breakdown beyond the top N. */
|
|
3235
|
+
function formatBrokerMem(input) {
|
|
3236
|
+
const { samples, proc, churn, copied } = input;
|
|
3237
|
+
const consumers = {};
|
|
3238
|
+
let rtsp = 0;
|
|
3239
|
+
let audio = 0;
|
|
3240
|
+
let pipe = 0;
|
|
3241
|
+
let ingesting = 0;
|
|
3242
|
+
let planes = 0;
|
|
3243
|
+
let pre = 0;
|
|
3244
|
+
let clip = 0;
|
|
3245
|
+
let rtp = 0;
|
|
3246
|
+
let kf = 0;
|
|
3247
|
+
let qSum = 0;
|
|
3248
|
+
let qMax = 0;
|
|
3249
|
+
for (const s of samples) {
|
|
3250
|
+
if (s.ingesting) ingesting += 1;
|
|
3251
|
+
if (s.annexBPlaneActive) planes += 1;
|
|
3252
|
+
rtsp += s.rtspSessions;
|
|
3253
|
+
audio += s.audioSubscribers;
|
|
3254
|
+
pipe += s.pipeClients;
|
|
3255
|
+
pre += s.preBufferBytes;
|
|
3256
|
+
clip += s.clipBytes;
|
|
3257
|
+
rtp += s.rtpRingBytes;
|
|
3258
|
+
kf += s.keyframeCacheBytes;
|
|
3259
|
+
qSum += s.writeQueueSumBytes;
|
|
3260
|
+
if (s.writeQueueMaxBytes > qMax) qMax = s.writeQueueMaxBytes;
|
|
3261
|
+
for (const [kind, n] of Object.entries(s.encodedByKind)) consumers[kind] = (consumers[kind] ?? 0) + n;
|
|
3262
|
+
}
|
|
3263
|
+
const top = [...samples].filter((s) => s.deviceId !== void 0).sort((a, b) => ringBytesOf(b) - ringBytesOf(a)).slice(0, 5).map((s) => `${s.deviceId}:${mb(ringBytesOf(s))}`).join(",");
|
|
3264
|
+
const parts = ["[broker-mem]"];
|
|
3265
|
+
if (proc.status !== null) parts.push(`rssAnonMb=${(proc.status.rssAnonKb / 1024).toFixed(0)}`, `rssFileMb=${(proc.status.rssFileKb / 1024).toFixed(0)}`);
|
|
3266
|
+
if (proc.maps !== null) parts.push(`heapMapMb=${mb(proc.maps.heapBytes)}`, `anonMaps=${proc.maps.anonMappings}`, `anonMapsMb=${mb(proc.maps.anonBytes)}`);
|
|
3267
|
+
parts.push(`brokers=${samples.length}`, `ingesting=${ingesting}`, `annexBPlanes=${planes}`, `rtsp=${rtsp}`, `webrtc=${input.webrtcSessions}`, `encoded=${kindList(consumers)}`, `audioSubs=${audio}`, `pipeClients=${pipe}`, `egress=${input.egressTranscodes}`, `children=${input.childProcesses}`, `opened=${churn.opened}`, `closed=${churn.closed}`, `openedKinds=${kindList(churn.openedByKind)}`, `closedKinds=${kindList(churn.closedByKind)}`, `preBufferMb=${mb(pre)}`, `clipMb=${mb(clip)}`, `rtpRingMb=${mb(rtp)}`, `keyframeMb=${mb(kf)}`, `writeQueueSumMb=${mb(qSum)}`, `writeQueueMaxMb=${mb(qMax)}`, `copiedPackets=${copied.packets}`, `copiedMb=${mb(copied.bytes)}`, `topRingMb=${top === "" ? "none" : top}`);
|
|
3268
|
+
return parts.join(" ");
|
|
3269
|
+
}
|
|
3270
|
+
/** Count entries named `name` in `process.getActiveResourcesInfo()`. */
|
|
3271
|
+
function countActiveResources(resources, name) {
|
|
3272
|
+
let n = 0;
|
|
3273
|
+
for (const r of resources) if (r === name) n += 1;
|
|
3274
|
+
return n;
|
|
3275
|
+
}
|
|
3276
|
+
//#endregion
|
|
3277
|
+
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3278
|
+
/**
|
|
3279
|
+
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
3280
|
+
*
|
|
3281
|
+
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
3282
|
+
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
3283
|
+
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
3284
|
+
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
3285
|
+
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
3286
|
+
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
3287
|
+
* packet that happened to be allocated beside it. On the hub that held
|
|
3288
|
+
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
3289
|
+
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
3290
|
+
*
|
|
3291
|
+
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
3292
|
+
* now pass every entry through {@link detachFromPool} on the way in and
|
|
3293
|
+
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
3294
|
+
* entry costs. Short-lived traffic stays pooled.
|
|
3295
|
+
*/
|
|
3296
|
+
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
3297
|
+
function ownsBackingStore(buf) {
|
|
3298
|
+
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* What retaining `buf` really costs: its own length when it owns its
|
|
3302
|
+
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
3303
|
+
*/
|
|
3304
|
+
function backingStoreBytes(buf) {
|
|
3305
|
+
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
3309
|
+
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
3310
|
+
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
3311
|
+
* already unpooled is returned as-is.
|
|
3312
|
+
*/
|
|
3313
|
+
function detachFromPool(buf) {
|
|
3314
|
+
if (ownsBackingStore(buf)) return buf;
|
|
3315
|
+
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
3316
|
+
buf.copy(owned);
|
|
3317
|
+
return owned;
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* A copy of `src[start, end)` that OWNS its backing store — the shape a reader
|
|
3321
|
+
* hands out when the packet it cuts from its socket buffer is going to be
|
|
3322
|
+
* retained anyway.
|
|
3323
|
+
*
|
|
3324
|
+
* Every source video packet is: `captureRtpForPreBuffer` keeps it for the life
|
|
3325
|
+
* of a GOP, and the restreamer's keyframe cache keeps the keyframe ones. With
|
|
3326
|
+
* the readers cutting packets as `Buffer.from(subarray)` — a pooled view — the
|
|
3327
|
+
* ring then had to copy each one a second time to own it: two copies of every
|
|
3328
|
+
* packet on the ingest path (measured 15.1 + 14.8 MB/s on the hub's load).
|
|
3329
|
+
* Cutting the packet owned up front makes the ring's and the cache's
|
|
3330
|
+
* `detachFromPool` an identity, so the packet is copied once and shared by the
|
|
3331
|
+
* reader's fan-out, the ring, the cache and every session's write queue.
|
|
3332
|
+
*/
|
|
3333
|
+
function copyOwned(src, start, end) {
|
|
3334
|
+
const owned = Buffer.allocUnsafeSlow(end - start);
|
|
3335
|
+
src.copy(owned, 0, start, end);
|
|
3336
|
+
recordCopyOwned(end - start);
|
|
3337
|
+
return owned;
|
|
3338
|
+
}
|
|
3339
|
+
//#endregion
|
|
3084
3340
|
//#region src/stream-broker/rtsp/sdp-builder.ts
|
|
3085
3341
|
function buildSdp(options) {
|
|
3086
3342
|
const { codec, parameterSets, streamName } = options;
|
|
@@ -3690,6 +3946,26 @@ var RtspRestreamer = class {
|
|
|
3690
3946
|
for (const session of this.sessions.values()) if (session.isPrestreamIntent()) n += 1;
|
|
3691
3947
|
return n;
|
|
3692
3948
|
}
|
|
3949
|
+
/**
|
|
3950
|
+
* What this restreamer holds, for the `[broker-mem]` probe: live sessions,
|
|
3951
|
+
* their socket write queues (sum and max) and the keyframe caches. One pass
|
|
3952
|
+
* over the sessions, no allocation beyond the result.
|
|
3953
|
+
*/
|
|
3954
|
+
getMemProbe() {
|
|
3955
|
+
let sum = 0;
|
|
3956
|
+
let max = 0;
|
|
3957
|
+
for (const s of this.sessions.values()) {
|
|
3958
|
+
const q = s.getWriteQueueBytes();
|
|
3959
|
+
sum += q;
|
|
3960
|
+
if (q > max) max = q;
|
|
3961
|
+
}
|
|
3962
|
+
return {
|
|
3963
|
+
sessions: this.sessions.size,
|
|
3964
|
+
writeQueueSumBytes: sum,
|
|
3965
|
+
writeQueueMaxBytes: max,
|
|
3966
|
+
keyframeCacheBytes: this.lastKeyframeNalBytes + this.lastKeyframeRtpBytes
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3693
3969
|
/** Per-session diagnostic snapshots consumed by `StreamBroker.listClients`. */
|
|
3694
3970
|
listSessionInfos() {
|
|
3695
3971
|
return [...this.sessions.values()].map((s) => s.getInfo());
|
|
@@ -3701,6 +3977,7 @@ var RtspRestreamer = class {
|
|
|
3701
3977
|
streamName: this.streamName,
|
|
3702
3978
|
total: this.sessions.size
|
|
3703
3979
|
} });
|
|
3980
|
+
reportConsumerEvent(this.logger, "attach", "rtsp", this.deviceId, this.sessions.size);
|
|
3704
3981
|
this._onSessionCountChanged?.();
|
|
3705
3982
|
this.reconcileSentryAudio();
|
|
3706
3983
|
if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
|
|
@@ -3751,7 +4028,7 @@ var RtspRestreamer = class {
|
|
|
3751
4028
|
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3752
4029
|
if (this.collectingKeyframeRtp) {
|
|
3753
4030
|
this.lastKeyframeRtpPackets.push({
|
|
3754
|
-
data:
|
|
4031
|
+
data: detachFromPool(rtpData),
|
|
3755
4032
|
marker: rtpPacket.marker
|
|
3756
4033
|
});
|
|
3757
4034
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
@@ -4165,6 +4442,7 @@ var RtspRestreamer = class {
|
|
|
4165
4442
|
streamName: this.streamName,
|
|
4166
4443
|
total: this.sessions.size
|
|
4167
4444
|
} });
|
|
4445
|
+
reportConsumerEvent(this.logger, "detach", "rtsp", this.deviceId, this.sessions.size);
|
|
4168
4446
|
this._onSessionCountChanged?.();
|
|
4169
4447
|
this.reconcileSentryAudio();
|
|
4170
4448
|
}
|
|
@@ -4353,6 +4631,19 @@ var RtspSession = class {
|
|
|
4353
4631
|
lastRtpAt = 0;
|
|
4354
4632
|
/** Total video RTP bytes written to the socket. */
|
|
4355
4633
|
bytesSent = 0;
|
|
4634
|
+
/**
|
|
4635
|
+
* The socket is corked for the access unit in progress — see
|
|
4636
|
+
* {@link writeInterleaved}. Cleared by the packet that ends the picture (RTP
|
|
4637
|
+
* marker, or any audio packet) or by the next-tick safety net, whichever
|
|
4638
|
+
* comes first.
|
|
4639
|
+
*/
|
|
4640
|
+
accessUnitCorked = false;
|
|
4641
|
+
/** Flush the access unit in progress. Idempotent: the marker and the safety net both call it. */
|
|
4642
|
+
uncorkAccessUnit = () => {
|
|
4643
|
+
if (!this.accessUnitCorked) return;
|
|
4644
|
+
this.accessUnitCorked = false;
|
|
4645
|
+
this.socket.uncork();
|
|
4646
|
+
};
|
|
4356
4647
|
/** Client `User-Agent` (from the first request that carries one). Lets the
|
|
4357
4648
|
* broker panel identify a consumer by purpose — e.g. the recorder sets
|
|
4358
4649
|
* `-user_agent recorder` on its RTSP pull — instead of a bare address. */
|
|
@@ -4421,6 +4712,10 @@ var RtspSession = class {
|
|
|
4421
4712
|
this.onClose?.();
|
|
4422
4713
|
});
|
|
4423
4714
|
}
|
|
4715
|
+
/** Bytes the socket has accepted but not yet flushed (corked bytes included). */
|
|
4716
|
+
getWriteQueueBytes() {
|
|
4717
|
+
return this.socket.writableLength;
|
|
4718
|
+
}
|
|
4424
4719
|
getSessionId() {
|
|
4425
4720
|
return this.sessionId;
|
|
4426
4721
|
}
|
|
@@ -4600,7 +4895,7 @@ var RtspSession = class {
|
|
|
4600
4895
|
return "dropped-backlog";
|
|
4601
4896
|
}
|
|
4602
4897
|
}
|
|
4603
|
-
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
|
|
4898
|
+
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
|
|
4604
4899
|
this.missedKeyframes = 0;
|
|
4605
4900
|
return "written";
|
|
4606
4901
|
}
|
|
@@ -4609,7 +4904,7 @@ var RtspSession = class {
|
|
|
4609
4904
|
if (!this.playing || this.closed || this.muted) return "not-playing";
|
|
4610
4905
|
if (this.tracks.length < 2) return "not-playing";
|
|
4611
4906
|
if (this.socket.writableLength > 2097152) return "dropped-backlog";
|
|
4612
|
-
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
|
|
4907
|
+
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
|
|
4613
4908
|
return "written";
|
|
4614
4909
|
}
|
|
4615
4910
|
/**
|
|
@@ -4631,14 +4926,13 @@ var RtspSession = class {
|
|
|
4631
4926
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
4632
4927
|
*
|
|
4633
4928
|
* 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
|
|
4929
|
+
* `copyOwned(readBuffer, …)` in the readers' `onData`, for video and audio
|
|
4930
|
+
* alike — never a view of the reader's socket buffer, which advances on the
|
|
4931
|
+
* next read. Nothing in the broker writes into a captured packet: every
|
|
4932
|
+
* mutation in this path targets a freshly allocated buffer. (The same object
|
|
4933
|
+
* is what the broker's `rtpRing` and the restreamer's keyframe cache retain —
|
|
4934
|
+
* it owns its backing store, so `detachFromPool` keeps it as is — and that is
|
|
4935
|
+
* fine: a pending write holds its own reference, and the rule below is about
|
|
4642
4936
|
* mutation, not retention.)
|
|
4643
4937
|
*
|
|
4644
4938
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
@@ -4648,18 +4942,36 @@ var RtspSession = class {
|
|
|
4648
4942
|
* queued. Four bytes from the Buffer pool is a pointer bump; the copy this
|
|
4649
4943
|
* replaces was ~1.4 KB.
|
|
4650
4944
|
*
|
|
4651
|
-
*
|
|
4652
|
-
*
|
|
4945
|
+
* ## One cork per ACCESS UNIT, not per packet
|
|
4946
|
+
*
|
|
4947
|
+
* The pair used to be corked and uncorked on its own: one `writev` per RTP
|
|
4948
|
+
* packet per session, so a 4K keyframe fragmented into a hundred packets and
|
|
4949
|
+
* fanned out to three sessions was three hundred syscalls for one picture no
|
|
4950
|
+
* decoder can use before it is whole. The socket is corked on the first write
|
|
4951
|
+
* of an access unit and uncorked by the packet that ENDS it
|
|
4952
|
+
* (`endOfAccessUnit`: the RTP marker on video, every audio packet), so the
|
|
4953
|
+
* picture leaves in one `writev`. A `process.nextTick` safety net uncorks
|
|
4954
|
+
* whatever is still open before the tick yields to I/O — a marker-less camera
|
|
4955
|
+
* or a lost marker degrades to one `writev` per tick, never to a socket held
|
|
4956
|
+
* corked across a read.
|
|
4957
|
+
*
|
|
4958
|
+
* Backpressure sees through the cork: `writableLength` counts corked bytes,
|
|
4959
|
+
* so the `MAX_WRITE_BUFFER` gate, the latch and the missed-keyframe close
|
|
4960
|
+
* (D447) judge exactly what they judged per packet.
|
|
4653
4961
|
*/
|
|
4654
|
-
writeInterleaved(channel, payload) {
|
|
4962
|
+
writeInterleaved(channel, payload, endOfAccessUnit) {
|
|
4655
4963
|
const header = Buffer.allocUnsafe(4);
|
|
4656
4964
|
header[0] = 36;
|
|
4657
4965
|
header[1] = channel;
|
|
4658
4966
|
header.writeUInt16BE(payload.length, 2);
|
|
4659
|
-
this.
|
|
4967
|
+
if (!this.accessUnitCorked) {
|
|
4968
|
+
this.accessUnitCorked = true;
|
|
4969
|
+
this.socket.cork();
|
|
4970
|
+
process.nextTick(this.uncorkAccessUnit);
|
|
4971
|
+
}
|
|
4660
4972
|
this.socket.write(header);
|
|
4661
4973
|
this.socket.write(payload);
|
|
4662
|
-
this.
|
|
4974
|
+
if (endOfAccessUnit) this.uncorkAccessUnit();
|
|
4663
4975
|
this.lastRtpAt = Date.now();
|
|
4664
4976
|
this.bytesSent += 4 + payload.length;
|
|
4665
4977
|
}
|
|
@@ -6357,6 +6669,10 @@ var EgressTranscodeManager = class {
|
|
|
6357
6669
|
/** In-flight starts, keyed by sharing key — concurrent acquires share one. */
|
|
6358
6670
|
pending = /* @__PURE__ */ new Map();
|
|
6359
6671
|
releaseGraceMs;
|
|
6672
|
+
/** Live egress transcodes (ffmpeg children), for the `[broker-mem]` probe. */
|
|
6673
|
+
activeCount() {
|
|
6674
|
+
return this.entries.size;
|
|
6675
|
+
}
|
|
6360
6676
|
constructor(opts) {
|
|
6361
6677
|
this.opts = opts;
|
|
6362
6678
|
this.releaseGraceMs = opts.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS;
|
|
@@ -10083,7 +10399,7 @@ var Rfc4571Reader = class {
|
|
|
10083
10399
|
while (this.readBuffer.length >= 2) {
|
|
10084
10400
|
const length = this.readBuffer.readUInt16BE(0);
|
|
10085
10401
|
if (this.readBuffer.length < 2 + length) return;
|
|
10086
|
-
const rtpPacket =
|
|
10402
|
+
const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
|
|
10087
10403
|
this.readBuffer = this.readBuffer.subarray(2 + length);
|
|
10088
10404
|
this.dispatchRtp(rtpPacket);
|
|
10089
10405
|
}
|
|
@@ -10144,6 +10460,93 @@ function totalFragmentBytes(fragments) {
|
|
|
10144
10460
|
for (const fragment of fragments) total += fragment.length;
|
|
10145
10461
|
return total;
|
|
10146
10462
|
}
|
|
10463
|
+
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10464
|
+
function classifyH264Payload(payload) {
|
|
10465
|
+
const nalType = payload[0] & 31;
|
|
10466
|
+
if (nalType === H264_NAL_STAP_A) {
|
|
10467
|
+
let keyframe = false;
|
|
10468
|
+
let parameterSet = false;
|
|
10469
|
+
let offset = 1;
|
|
10470
|
+
while (offset + 2 <= payload.length) {
|
|
10471
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10472
|
+
offset += 2;
|
|
10473
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10474
|
+
const type = payload[offset] & 31;
|
|
10475
|
+
keyframe ||= type === H264_NAL_IDR;
|
|
10476
|
+
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10477
|
+
offset += nalSize;
|
|
10478
|
+
}
|
|
10479
|
+
return {
|
|
10480
|
+
keyframe,
|
|
10481
|
+
parameterSet
|
|
10482
|
+
};
|
|
10483
|
+
}
|
|
10484
|
+
if (nalType === 28) {
|
|
10485
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10486
|
+
return {
|
|
10487
|
+
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10488
|
+
parameterSet: false
|
|
10489
|
+
};
|
|
10490
|
+
}
|
|
10491
|
+
return {
|
|
10492
|
+
keyframe: nalType === H264_NAL_IDR,
|
|
10493
|
+
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10494
|
+
};
|
|
10495
|
+
}
|
|
10496
|
+
/** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
|
|
10497
|
+
function classifyH265Payload(payload) {
|
|
10498
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10499
|
+
const nalType = payload[0] >> 1 & 63;
|
|
10500
|
+
if (nalType === H265_NAL_AP) {
|
|
10501
|
+
let keyframe = false;
|
|
10502
|
+
let parameterSet = false;
|
|
10503
|
+
let offset = 2;
|
|
10504
|
+
while (offset + 2 <= payload.length) {
|
|
10505
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10506
|
+
offset += 2;
|
|
10507
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10508
|
+
const type = payload[offset] >> 1 & 63;
|
|
10509
|
+
keyframe ||= isH265IrapType(type);
|
|
10510
|
+
parameterSet ||= isH265ParameterSetType(type);
|
|
10511
|
+
offset += nalSize;
|
|
10512
|
+
}
|
|
10513
|
+
return {
|
|
10514
|
+
keyframe,
|
|
10515
|
+
parameterSet
|
|
10516
|
+
};
|
|
10517
|
+
}
|
|
10518
|
+
if (nalType === 49) {
|
|
10519
|
+
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10520
|
+
return {
|
|
10521
|
+
keyframe: isH265IrapType(payload[2] & 63),
|
|
10522
|
+
parameterSet: false
|
|
10523
|
+
};
|
|
10524
|
+
}
|
|
10525
|
+
return {
|
|
10526
|
+
keyframe: isH265IrapType(nalType),
|
|
10527
|
+
parameterSet: isH265ParameterSetType(nalType)
|
|
10528
|
+
};
|
|
10529
|
+
}
|
|
10530
|
+
/**
|
|
10531
|
+
* Classify one raw RTP packet (12-byte header + payload) by what it carries —
|
|
10532
|
+
* the per-packet half of {@link RtpDepacketizer.processPacket}, without the
|
|
10533
|
+
* reassembly half. Pure: reads the headers, allocates nothing, keeps no state.
|
|
10534
|
+
* `null` for a packet too short to be RTP with a payload.
|
|
10535
|
+
*/
|
|
10536
|
+
function classifyRtpPacket(codec, rtpData) {
|
|
10537
|
+
if (rtpData.length < 12) return null;
|
|
10538
|
+
const marker = (rtpData[1] & 128) !== 0;
|
|
10539
|
+
const timestamp = rtpData.readUInt32BE(4);
|
|
10540
|
+
const payload = rtpData.subarray(12);
|
|
10541
|
+
if (payload.length === 0) return null;
|
|
10542
|
+
const carried = codec === "h264" ? classifyH264Payload(payload) : classifyH265Payload(payload);
|
|
10543
|
+
return {
|
|
10544
|
+
timestamp,
|
|
10545
|
+
marker,
|
|
10546
|
+
keyframe: carried.keyframe,
|
|
10547
|
+
parameterSet: carried.parameterSet
|
|
10548
|
+
};
|
|
10549
|
+
}
|
|
10147
10550
|
/**
|
|
10148
10551
|
* RTP → Annex-B NAL depacketizer.
|
|
10149
10552
|
*
|
|
@@ -10188,87 +10591,22 @@ var RtpDepacketizer = class {
|
|
|
10188
10591
|
* short to be RTP with a payload.
|
|
10189
10592
|
*/
|
|
10190
10593
|
processPacket(rtpData) {
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
const timestamp = rtpData.readUInt32BE(4);
|
|
10594
|
+
const carried = this.classifyPacket(rtpData);
|
|
10595
|
+
if (carried === null) return null;
|
|
10194
10596
|
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
|
-
};
|
|
10205
|
-
}
|
|
10206
|
-
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10207
|
-
classifyH264Payload(payload) {
|
|
10208
|
-
const nalType = payload[0] & 31;
|
|
10209
|
-
if (nalType === H264_NAL_STAP_A) {
|
|
10210
|
-
let keyframe = false;
|
|
10211
|
-
let parameterSet = false;
|
|
10212
|
-
let offset = 1;
|
|
10213
|
-
while (offset + 2 <= payload.length) {
|
|
10214
|
-
const nalSize = payload.readUInt16BE(offset);
|
|
10215
|
-
offset += 2;
|
|
10216
|
-
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10217
|
-
const type = payload[offset] & 31;
|
|
10218
|
-
keyframe ||= type === H264_NAL_IDR;
|
|
10219
|
-
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10220
|
-
offset += nalSize;
|
|
10221
|
-
}
|
|
10222
|
-
return {
|
|
10223
|
-
keyframe,
|
|
10224
|
-
parameterSet
|
|
10225
|
-
};
|
|
10226
|
-
}
|
|
10227
|
-
if (nalType === 28) {
|
|
10228
|
-
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10229
|
-
return {
|
|
10230
|
-
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10231
|
-
parameterSet: false
|
|
10232
|
-
};
|
|
10233
|
-
}
|
|
10234
|
-
return {
|
|
10235
|
-
keyframe: nalType === H264_NAL_IDR,
|
|
10236
|
-
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10237
|
-
};
|
|
10597
|
+
if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
|
|
10598
|
+
else this.processH265(payload, carried.timestamp, carried.marker);
|
|
10599
|
+
return carried;
|
|
10238
10600
|
}
|
|
10239
|
-
/**
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
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
|
-
};
|
|
10601
|
+
/**
|
|
10602
|
+
* What this packet CARRIES, and nothing else: no NAL is reassembled, no buffer
|
|
10603
|
+
* allocated, no reassembly state touched. The same verdict `processPacket`
|
|
10604
|
+
* returns, for a broker whose Annex-B plane has no consumer — the source-RTP
|
|
10605
|
+
* ring, the restreamer's keyframe cache and the stats need exactly this and
|
|
10606
|
+
* none of the copies.
|
|
10607
|
+
*/
|
|
10608
|
+
classifyPacket(rtpData) {
|
|
10609
|
+
return classifyRtpPacket(this.codec, rtpData);
|
|
10272
10610
|
}
|
|
10273
10611
|
processH264(payload, timestamp, marker) {
|
|
10274
10612
|
const nalType = payload[0] & 31;
|
|
@@ -10448,11 +10786,13 @@ var RtpAccessUnitTracker = class {
|
|
|
10448
10786
|
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10449
10787
|
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10450
10788
|
if (keyframeContent) this.keyframeAu = true;
|
|
10789
|
+
const pictureStart = !packet.parameterSet && !this.pictureSeen;
|
|
10451
10790
|
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10452
10791
|
return {
|
|
10453
10792
|
auStart,
|
|
10454
10793
|
keyframeAu: this.keyframeAu,
|
|
10455
10794
|
keyframeAuStart,
|
|
10795
|
+
pictureStart,
|
|
10456
10796
|
parameterSet: packet.parameterSet
|
|
10457
10797
|
};
|
|
10458
10798
|
}
|
|
@@ -10778,7 +11118,7 @@ var NativeRtspClient = class {
|
|
|
10778
11118
|
const channel = this.readBuffer[1];
|
|
10779
11119
|
const length = this.readBuffer.readUInt16BE(2);
|
|
10780
11120
|
if (this.readBuffer.length < 4 + length) return;
|
|
10781
|
-
const payload =
|
|
11121
|
+
const payload = copyOwned(this.readBuffer, 4, 4 + length);
|
|
10782
11122
|
this.readBuffer = this.readBuffer.subarray(4 + length);
|
|
10783
11123
|
this.handleInterleavedPacket(channel, payload);
|
|
10784
11124
|
} else {
|
|
@@ -11514,49 +11854,6 @@ var AudioCodecSession = class {
|
|
|
11514
11854
|
}
|
|
11515
11855
|
};
|
|
11516
11856
|
//#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
11857
|
//#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
|
|
11561
11858
|
/**
|
|
11562
11859
|
* Time-based ring buffer for encoded video packets.
|
|
@@ -12496,9 +12793,6 @@ var StreamBroker = class StreamBroker {
|
|
|
12496
12793
|
*/
|
|
12497
12794
|
ffmpegBinaryPathSource = () => null;
|
|
12498
12795
|
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
12796
|
audioRtpDecoder = null;
|
|
12503
12797
|
audioCodecSession = null;
|
|
12504
12798
|
/**
|
|
@@ -12742,6 +13036,8 @@ var StreamBroker = class StreamBroker {
|
|
|
12742
13036
|
* timeout was chosen against.
|
|
12743
13037
|
*/
|
|
12744
13038
|
static RTP_ACTIVITY_SWEEP_MS = 1e3;
|
|
13039
|
+
/** A source counts as ingesting when its last video packet is younger than this. */
|
|
13040
|
+
static INGESTING_WITHIN_MS = 1e4;
|
|
12745
13041
|
manualStop = false;
|
|
12746
13042
|
stopping = false;
|
|
12747
13043
|
pipeServer;
|
|
@@ -12866,10 +13162,24 @@ var StreamBroker = class StreamBroker {
|
|
|
12866
13162
|
* log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
|
|
12867
13163
|
* back but never video is observable from the structured logs. */
|
|
12868
13164
|
videoRtpSeen = 0;
|
|
12869
|
-
/**
|
|
13165
|
+
/**
|
|
13166
|
+
* What the source RTP packet being handled CARRIES (keyframe / parameter-set
|
|
13167
|
+
* content, read from its payload headers by `classifyRtpPacket`) — the flags
|
|
13168
|
+
* `captureRtpForPreBuffer` reads. Per packet, not per completed NAL: every
|
|
13169
|
+
* fragment of a fragmented IDR carries them, and the ring marks one keyframe
|
|
13170
|
+
* AU for the lot.
|
|
13171
|
+
*/
|
|
12870
13172
|
_lastNalKeyframe = false;
|
|
12871
13173
|
_lastNalParamSet = false;
|
|
12872
13174
|
/**
|
|
13175
|
+
* Whether the Annex-B plane (NAL reassembly + access-unit assembly →
|
|
13176
|
+
* `EncodedPacket`) is being BUILT for this stream. False while nothing
|
|
13177
|
+
* consumes it: the source-RTP path then classifies each packet and copies
|
|
13178
|
+
* nothing. Armed at the next keyframe access unit once a consumer attaches
|
|
13179
|
+
* (`needsAnnexBPlane`), so the first packet it emits is decodable.
|
|
13180
|
+
*/
|
|
13181
|
+
annexBPlaneActive = false;
|
|
13182
|
+
/**
|
|
12873
13183
|
* Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
|
|
12874
13184
|
* begin at the most recent keyframe access unit — i.e. the current GOP so
|
|
12875
13185
|
* far. A late-joining WebRTC viewer on the repacketizer path replays this
|
|
@@ -13002,6 +13312,33 @@ var StreamBroker = class StreamBroker {
|
|
|
13002
13312
|
return this.lastPacketAt;
|
|
13003
13313
|
}
|
|
13004
13314
|
/**
|
|
13315
|
+
* What this broker holds, for the `[broker-mem]` probe (`broker-mem-probe.ts`).
|
|
13316
|
+
* Ring sizes are the rings' own counters (backing-store bytes), so this is
|
|
13317
|
+
* O(sessions + subscribers) and allocates only the result.
|
|
13318
|
+
*/
|
|
13319
|
+
getMemSample(now) {
|
|
13320
|
+
const encodedByKind = {};
|
|
13321
|
+
for (const entry of this.encodedCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
|
|
13322
|
+
for (const entry of this.rtpVideoCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
|
|
13323
|
+
const rtsp = this.rtspRestreamer.getMemProbe();
|
|
13324
|
+
return {
|
|
13325
|
+
deviceId: this.numericDeviceId,
|
|
13326
|
+
brokerId: this.deviceId,
|
|
13327
|
+
ingesting: this.lastPacketAt > 0 && now - this.lastPacketAt < StreamBroker.INGESTING_WITHIN_MS,
|
|
13328
|
+
preBufferBytes: this.preBuffer.getBufferedBytes(),
|
|
13329
|
+
clipBytes: this.clipBuffer?.getBufferedBytes() ?? 0,
|
|
13330
|
+
rtpRingBytes: this.rtpRingBytes,
|
|
13331
|
+
keyframeCacheBytes: rtsp.keyframeCacheBytes,
|
|
13332
|
+
rtspSessions: rtsp.sessions,
|
|
13333
|
+
writeQueueSumBytes: rtsp.writeQueueSumBytes,
|
|
13334
|
+
writeQueueMaxBytes: rtsp.writeQueueMaxBytes,
|
|
13335
|
+
encodedByKind,
|
|
13336
|
+
audioSubscribers: this.audioChunkPlane.subscriberCount,
|
|
13337
|
+
pipeClients: this.pipeServer.getClientCount(),
|
|
13338
|
+
annexBPlaneActive: this.annexBPlaneActive
|
|
13339
|
+
};
|
|
13340
|
+
}
|
|
13341
|
+
/**
|
|
13005
13342
|
* Whether a LAZY publisher is being built right now.
|
|
13006
13343
|
*
|
|
13007
13344
|
* A consumer deciding whether it is worth waiting for this source needs to
|
|
@@ -13945,26 +14282,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13945
14282
|
return;
|
|
13946
14283
|
}
|
|
13947
14284
|
}
|
|
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
|
-
}
|
|
14285
|
+
if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
|
|
13968
14286
|
if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
|
|
13969
14287
|
if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
|
|
13970
14288
|
this.fanOutEncoded(packet);
|
|
@@ -13983,6 +14301,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13983
14301
|
packetsDelivered: 0
|
|
13984
14302
|
};
|
|
13985
14303
|
this.encodedCallbacks.set(callback, entry);
|
|
14304
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
13986
14305
|
if (this.streamingDebug) this.logger?.info("encoded subscriber added", { meta: {
|
|
13987
14306
|
total: this.encodedCallbacks.size,
|
|
13988
14307
|
subscriberId: entry.id,
|
|
@@ -13993,6 +14312,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13993
14312
|
this.checkDemand();
|
|
13994
14313
|
return () => {
|
|
13995
14314
|
this.encodedCallbacks.delete(callback);
|
|
14315
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
13996
14316
|
if (this.streamingDebug) this.logger?.info("encoded subscriber removed", { meta: {
|
|
13997
14317
|
total: this.encodedCallbacks.size,
|
|
13998
14318
|
subscriberId: entry.id
|
|
@@ -14087,6 +14407,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14087
14407
|
packetsDelivered: 0
|
|
14088
14408
|
};
|
|
14089
14409
|
this.rtpVideoCallbacks.set(callback, entry);
|
|
14410
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14090
14411
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber added", { meta: {
|
|
14091
14412
|
total: this.rtpVideoCallbacks.size,
|
|
14092
14413
|
subscriberId: entry.id,
|
|
@@ -14096,6 +14417,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14096
14417
|
this.checkDemand();
|
|
14097
14418
|
return () => {
|
|
14098
14419
|
this.rtpVideoCallbacks.delete(callback);
|
|
14420
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14099
14421
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber removed", { meta: {
|
|
14100
14422
|
total: this.rtpVideoCallbacks.size,
|
|
14101
14423
|
subscriberId: entry.id
|
|
@@ -14155,8 +14477,8 @@ var StreamBroker = class StreamBroker {
|
|
|
14155
14477
|
index: auStart,
|
|
14156
14478
|
ts: auPacket.readUInt32BE(4)
|
|
14157
14479
|
});
|
|
14480
|
+
this.trimRtpRingToPreRollWindow();
|
|
14158
14481
|
}
|
|
14159
|
-
this.trimRtpRingToPreRollWindow();
|
|
14160
14482
|
this.rtpRingHasKeyframe = true;
|
|
14161
14483
|
}
|
|
14162
14484
|
this.rtpRingPrevMarker = marker;
|
|
@@ -14425,10 +14747,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14425
14747
|
*
|
|
14426
14748
|
* `push-rtp` providers deliver AnnexB-equivalents separately via
|
|
14427
14749
|
* `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
|
-
*
|
|
14750
|
+
* does NOT re-emit EncodedPackets; it only classifies each packet for the
|
|
14751
|
+
* keyframe/param-set flags the pre-buffer capture needs
|
|
14752
|
+
* (`classifyRtpPacket`, no reassembly), keeping the AnnexB consumers
|
|
14753
|
+
* (pipe, restreamer) single-fed by the provider.
|
|
14432
14754
|
*
|
|
14433
14755
|
* The pre-buffer is captured even with zero subscribers attached — a
|
|
14434
14756
|
* viewer that connects mid-GOP must be able to replay the GOP that
|
|
@@ -14438,9 +14760,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14438
14760
|
pushVideoRtp(rtpData) {
|
|
14439
14761
|
if (this.stopping) return;
|
|
14440
14762
|
this.kickRtpActivityWatchdog();
|
|
14441
|
-
this.
|
|
14442
|
-
this.
|
|
14443
|
-
this.
|
|
14763
|
+
const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
|
|
14764
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14765
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14444
14766
|
this.captureRtpForPreBuffer(rtpData);
|
|
14445
14767
|
if (this.rtpVideoCallbacks.size === 0) return;
|
|
14446
14768
|
for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -14450,27 +14772,6 @@ var StreamBroker = class StreamBroker {
|
|
|
14450
14772
|
this.logger?.warn("rtp video subscriber threw", { meta: { error: errMsg(err) } });
|
|
14451
14773
|
}
|
|
14452
14774
|
}
|
|
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
14775
|
/** Toggle the per-device streaming debug flag. */
|
|
14475
14776
|
setStreamingDebug(enabled) {
|
|
14476
14777
|
this.streamingDebug = enabled;
|
|
@@ -14517,6 +14818,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14517
14818
|
tag,
|
|
14518
14819
|
accept: options?.accept
|
|
14519
14820
|
});
|
|
14821
|
+
reportConsumerEvent(this.logger, "attach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14520
14822
|
this.checkDemand();
|
|
14521
14823
|
return result;
|
|
14522
14824
|
}
|
|
@@ -14527,7 +14829,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14527
14829
|
/** Release an audio-chunk subscription. Returns `true` when it existed. */
|
|
14528
14830
|
unsubscribeAudioChunks(subscriptionId) {
|
|
14529
14831
|
const released = this.audioChunkPlane.unsubscribe(subscriptionId);
|
|
14530
|
-
if (released)
|
|
14832
|
+
if (released) {
|
|
14833
|
+
reportConsumerEvent(this.logger, "detach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14834
|
+
this.checkDemand();
|
|
14835
|
+
}
|
|
14531
14836
|
return released;
|
|
14532
14837
|
}
|
|
14533
14838
|
getStats() {
|
|
@@ -14913,10 +15218,17 @@ var StreamBroker = class StreamBroker {
|
|
|
14913
15218
|
seq: this.videoRtpSeen,
|
|
14914
15219
|
bytes: rtpData.length
|
|
14915
15220
|
} });
|
|
14916
|
-
|
|
14917
|
-
this.
|
|
14918
|
-
const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
|
|
15221
|
+
const wantPlane = this.needsAnnexBPlane();
|
|
15222
|
+
if (!wantPlane && this.annexBPlaneActive) this.idleAnnexBPlane();
|
|
15223
|
+
const carried = this.annexBPlaneActive ? this.rtpDepacketizer?.processPacket(rtpData) ?? null : this.rtpDepacketizer?.classifyPacket(rtpData) ?? null;
|
|
15224
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
15225
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14919
15226
|
const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
|
|
15227
|
+
if (wantPlane && !this.annexBPlaneActive && auFlags?.keyframeAuStart === true) {
|
|
15228
|
+
this.armAnnexBPlane();
|
|
15229
|
+
this.rtpDepacketizer?.processPacket(rtpData);
|
|
15230
|
+
}
|
|
15231
|
+
if (auFlags) this.recordVideoStats(rtpData.length, auFlags.pictureStart, auFlags.keyframeAu);
|
|
14920
15232
|
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14921
15233
|
this.captureRtpForPreBuffer(rtpData);
|
|
14922
15234
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -15132,11 +15444,83 @@ var StreamBroker = class StreamBroker {
|
|
|
15132
15444
|
* not fed from here.
|
|
15133
15445
|
*/
|
|
15134
15446
|
handleDepacketizedNal(depacketized) {
|
|
15135
|
-
if (depacketized.keyframe) this._lastNalKeyframe = true;
|
|
15136
|
-
if (depacketized.parameterSet) this._lastNalParamSet = true;
|
|
15137
15447
|
this.annexBAssembler.push(depacketized);
|
|
15138
15448
|
}
|
|
15139
15449
|
/**
|
|
15450
|
+
* Does anything consume the Annex-B plane right now? An encoded subscriber
|
|
15451
|
+
* (WebRTC Annex-B, fragments, a decode session), a pipe client, a clip ring
|
|
15452
|
+
* (it retains access units continuously, so it needs them built even with
|
|
15453
|
+
* nobody attached), or a restreamer still in Annex-B mode (no camera SDP yet).
|
|
15454
|
+
* The RTSP restream and the WebRTC RTP path are served from the source RTP
|
|
15455
|
+
* and never count. Cheap — four field reads — because it runs per packet.
|
|
15456
|
+
*/
|
|
15457
|
+
needsAnnexBPlane() {
|
|
15458
|
+
return this.encodedCallbacks.size > 0 || this.clipBuffer !== null || this.pipeServer.getClientCount() > 0 || !this.rtspRestreamer.rtpPassthroughMode;
|
|
15459
|
+
}
|
|
15460
|
+
/** The consumers that want the plane, for the arm/idle lines. */
|
|
15461
|
+
annexBPlaneConsumers() {
|
|
15462
|
+
return {
|
|
15463
|
+
encodedSubscribers: this.encodedCallbacks.size,
|
|
15464
|
+
clipRing: this.clipBuffer !== null,
|
|
15465
|
+
pipeClients: this.pipeServer.getClientCount(),
|
|
15466
|
+
restreamerAnnexB: !this.rtspRestreamer.rtpPassthroughMode
|
|
15467
|
+
};
|
|
15468
|
+
}
|
|
15469
|
+
/**
|
|
15470
|
+
* Start building access units from the keyframe AU the current packet opens.
|
|
15471
|
+
* The depacketizer and the assembler are clean here (reset when the plane
|
|
15472
|
+
* went idle, or never used), so nothing stale precedes that keyframe.
|
|
15473
|
+
*/
|
|
15474
|
+
armAnnexBPlane() {
|
|
15475
|
+
this.annexBPlaneActive = true;
|
|
15476
|
+
this.logger?.info("annex-b plane armed at a keyframe access unit", {
|
|
15477
|
+
tags: { deviceId: this.numericDeviceId },
|
|
15478
|
+
meta: this.annexBPlaneConsumers()
|
|
15479
|
+
});
|
|
15480
|
+
}
|
|
15481
|
+
/**
|
|
15482
|
+
* Stop building access units: the last consumer left. The picture in
|
|
15483
|
+
* progress is dropped, not flushed — there is nobody to hand it to, and a
|
|
15484
|
+
* partial picture must never reach the next consumer's clean start. Logged,
|
|
15485
|
+
* once per transition: a plane that is idle on purpose must not read as a
|
|
15486
|
+
* stream that stopped.
|
|
15487
|
+
*/
|
|
15488
|
+
idleAnnexBPlane() {
|
|
15489
|
+
this.annexBPlaneActive = false;
|
|
15490
|
+
this.rtpDepacketizer?.reset();
|
|
15491
|
+
this.annexBAssembler.reset();
|
|
15492
|
+
this.logger?.info("annex-b plane idle — no consumer, NAL reassembly skipped", { tags: { deviceId: this.numericDeviceId } });
|
|
15493
|
+
}
|
|
15494
|
+
/**
|
|
15495
|
+
* One place for the input stats, fed from whichever path owns the pictures:
|
|
15496
|
+
* the source RTP packets while the native reader is up (`onVideoRtp`), the
|
|
15497
|
+
* `EncodedPacket`s otherwise (`pushEncodedPacket`). `bytes` accumulate per
|
|
15498
|
+
* call; `inputFps`, `packetCount` and the IDR interval advance once per
|
|
15499
|
+
* picture — the window is closed on a picture start so a window of N frames
|
|
15500
|
+
* reads N, exactly as it did when every call was one access unit.
|
|
15501
|
+
*/
|
|
15502
|
+
recordVideoStats(bytes, pictureStart, keyframe) {
|
|
15503
|
+
this.totalBytes += bytes;
|
|
15504
|
+
this.bytesInWindow += bytes;
|
|
15505
|
+
if (!pictureStart) return;
|
|
15506
|
+
this.packetCount++;
|
|
15507
|
+
this.packetsInWindow++;
|
|
15508
|
+
const now = Date.now();
|
|
15509
|
+
this.lastPacketAt = now;
|
|
15510
|
+
const windowMs = now - this.windowStartMs;
|
|
15511
|
+
if (windowMs >= 2e3) {
|
|
15512
|
+
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
15513
|
+
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
15514
|
+
this.bytesInWindow = 0;
|
|
15515
|
+
this.packetsInWindow = 0;
|
|
15516
|
+
this.windowStartMs = now;
|
|
15517
|
+
}
|
|
15518
|
+
if (keyframe) {
|
|
15519
|
+
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
15520
|
+
this.lastKeyframeMs = now;
|
|
15521
|
+
}
|
|
15522
|
+
}
|
|
15523
|
+
/**
|
|
15140
15524
|
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15141
15525
|
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15142
15526
|
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
@@ -15557,10 +15941,7 @@ var StreamBroker = class StreamBroker {
|
|
|
15557
15941
|
this.annexBAssembler.flush();
|
|
15558
15942
|
this.annexBAssembler.reset();
|
|
15559
15943
|
this.rtpAuTracker.reset();
|
|
15560
|
-
|
|
15561
|
-
this.pushRtpFlagDepacketizer.reset();
|
|
15562
|
-
this.pushRtpFlagDepacketizer = null;
|
|
15563
|
-
}
|
|
15944
|
+
this.annexBPlaneActive = false;
|
|
15564
15945
|
this.resetRtpPreBuffer();
|
|
15565
15946
|
if (this.audioRtpDecoder) {
|
|
15566
15947
|
this.audioRtpDecoder.flush();
|
|
@@ -19278,6 +19659,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19278
19659
|
return { killed: broker.killClient(input.channel, input.handle) };
|
|
19279
19660
|
}
|
|
19280
19661
|
/** Raw broker instances — internal use only. */
|
|
19662
|
+
/** Live egress transcodes (ffmpeg children) — the `[broker-mem]` probe. */
|
|
19663
|
+
getEgressTranscodeCount() {
|
|
19664
|
+
return this.egressTranscodeManager?.activeCount() ?? 0;
|
|
19665
|
+
}
|
|
19281
19666
|
getBrokerInstances() {
|
|
19282
19667
|
return [...this.brokers.values()];
|
|
19283
19668
|
}
|
|
@@ -35150,6 +35535,7 @@ var BrokerWebrtcServer = class {
|
|
|
35150
35535
|
}
|
|
35151
35536
|
};
|
|
35152
35537
|
this.sessions.set(sessionId, entry);
|
|
35538
|
+
reportConsumerEvent(this.logger, "attach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
35153
35539
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35154
35540
|
const locateRecorded = this.locateRecordedSegment;
|
|
35155
35541
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
@@ -36367,6 +36753,7 @@ var BrokerWebrtcServer = class {
|
|
|
36367
36753
|
const entry = this.sessions.get(sessionId);
|
|
36368
36754
|
if (!entry) return;
|
|
36369
36755
|
this.sessions.delete(sessionId);
|
|
36756
|
+
reportConsumerEvent(this.logger, "detach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
36370
36757
|
entry.pendingSwitch?.cancel();
|
|
36371
36758
|
entry.pendingSwitch = void 0;
|
|
36372
36759
|
let brokerStillServed = false;
|
|
@@ -37088,6 +37475,45 @@ function dialAnswerFromError(err) {
|
|
|
37088
37475
|
};
|
|
37089
37476
|
}
|
|
37090
37477
|
//#endregion
|
|
37478
|
+
//#region src/stream-broker/broker-mem-report.ts
|
|
37479
|
+
/** Linux only: elsewhere (or on a read error) the native view is simply absent. */
|
|
37480
|
+
async function readProc() {
|
|
37481
|
+
if (process.platform !== "linux") return {
|
|
37482
|
+
status: null,
|
|
37483
|
+
maps: null
|
|
37484
|
+
};
|
|
37485
|
+
const [status, maps] = await Promise.all([readFile("/proc/self/status", "utf8").then(parseProcStatus, () => null), readFile("/proc/self/maps", "utf8").then(parseProcMaps, () => null)]);
|
|
37486
|
+
return {
|
|
37487
|
+
status,
|
|
37488
|
+
maps
|
|
37489
|
+
};
|
|
37490
|
+
}
|
|
37491
|
+
async function emitOnce(deps) {
|
|
37492
|
+
const now = Date.now();
|
|
37493
|
+
const proc = await readProc();
|
|
37494
|
+
const resources = process.getActiveResourcesInfo();
|
|
37495
|
+
const line = formatBrokerMem({
|
|
37496
|
+
samples: deps.manager.getBrokerInstances().map((b) => b.getMemSample(now)),
|
|
37497
|
+
webrtcSessions: deps.webrtcSessions(),
|
|
37498
|
+
egressTranscodes: deps.manager.getEgressTranscodeCount(),
|
|
37499
|
+
childProcesses: countActiveResources(resources, "ProcessWrap"),
|
|
37500
|
+
proc,
|
|
37501
|
+
churn: consumerChurn.drain(),
|
|
37502
|
+
copied: readCopyOwnedTotals()
|
|
37503
|
+
});
|
|
37504
|
+
deps.logger.info(line);
|
|
37505
|
+
}
|
|
37506
|
+
/** Start the five-minute `[broker-mem]` heartbeat; returns its stop function. */
|
|
37507
|
+
function startBrokerMemReport(deps) {
|
|
37508
|
+
const timer = setInterval(() => {
|
|
37509
|
+
emitOnce(deps).catch((err) => {
|
|
37510
|
+
deps.logger.warn("broker-mem probe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
37511
|
+
});
|
|
37512
|
+
}, BROKER_MEM_INTERVAL_MS);
|
|
37513
|
+
timer.unref();
|
|
37514
|
+
return () => clearInterval(timer);
|
|
37515
|
+
}
|
|
37516
|
+
//#endregion
|
|
37091
37517
|
//#region src/stream-broker/event-loop-stall-monitor.ts
|
|
37092
37518
|
var DEFAULT_INTERVAL_MS = 100;
|
|
37093
37519
|
var DEFAULT_THRESHOLD_MS = 250;
|
|
@@ -37262,6 +37688,8 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37262
37688
|
* leaks those sockets and wedges the next runner's werift ICE agent
|
|
37263
37689
|
* (DTLS never connects until a full hub restart). */
|
|
37264
37690
|
webrtcServer = null;
|
|
37691
|
+
/** Stops the five-minute `[broker-mem]` attribution heartbeat. */
|
|
37692
|
+
stopBrokerMemReport = null;
|
|
37265
37693
|
/** Names this process's event-loop stalls — see onInitialize. */
|
|
37266
37694
|
loopStallMonitor = null;
|
|
37267
37695
|
/** The `log-channels` provider for this process. Stopped on shutdown so a
|
|
@@ -37619,6 +38047,11 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37619
38047
|
this.webrtcServer = webrtcServer;
|
|
37620
38048
|
const webrtcSessionProvider = new WebrtcSessionProvider(webrtcServer, this.brokerManager);
|
|
37621
38049
|
this.metricsSnapshotTimer = setInterval(() => this.emitBrokerMetricsSnapshot(), BROKER_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
38050
|
+
this.stopBrokerMemReport = startBrokerMemReport({
|
|
38051
|
+
logger: this.ctx.logger,
|
|
38052
|
+
manager: this.brokerManager,
|
|
38053
|
+
webrtcSessions: () => webrtcServer.getSessionCount()
|
|
38054
|
+
});
|
|
37622
38055
|
const widgetsProvider = { listWidgets: async () => [{
|
|
37623
38056
|
tab: "device-tab",
|
|
37624
38057
|
label: "Stream Brokers",
|
|
@@ -37723,6 +38156,8 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37723
38156
|
async onShutdown() {
|
|
37724
38157
|
this.logChannels?.stop();
|
|
37725
38158
|
this.logChannels = null;
|
|
38159
|
+
this.stopBrokerMemReport?.();
|
|
38160
|
+
this.stopBrokerMemReport = null;
|
|
37726
38161
|
this.loopStallMonitor?.stop();
|
|
37727
38162
|
this.loopStallMonitor = null;
|
|
37728
38163
|
this.flushProbeSnapshots(Date.now(), true);
|